Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: nested ctype val #919

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 72 additions & 7 deletions packages/credentials/src/V1/KiltCredentialV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,15 +327,61 @@ export function fromInput({

const cachingCTypeLoader = newCachingCTypeLoader()

// Single function to both check for references and extract them
function extractUniqueReferences(
cType: ICType,
references: Set<string> = new Set<string>()
): Set<string> {
if (typeof cType?.properties !== 'object' || cType.properties === null) {
return references
}

const objValue = cType.properties as Record<string, unknown>

if ('$ref' in objValue) {
const ref = objValue.$ref as string
if (ref.startsWith('kilt:ctype:')) {
references.add(ref.split('#/')[0])
}
}

if (Array.isArray(objValue)) {
objValue.forEach((item) =>
extractUniqueReferences(item as ICType, references)
)
} else {
Object.values(objValue).forEach((value) => {
if (typeof value === 'object' && value !== null) {
extractUniqueReferences(value as ICType, references)
}
})
}

return references
}

/**
* Validates the claims in the VC's `credentialSubject` against a CType definition.
* Supports both nested and non-nested CType validation.
* For non-nested CTypes:
* - Validates claims directly against the CType schema.
* For nested CTypes:
* - Automatically detects nested structure through $ref properties.
* - Fetches referenced CTypes from the blockchain.
* - Performs validation against the main CType and all referenced CTypes.
*
* @param credential A {@link KiltCredentialV1} type verifiable credential.
* @param credential.credentialSubject The credentialSubject to be validated.
* @param credential.type The credential's types.
* @param options Options map.
* @param options.cTypes One or more CType definitions to be used for validation. If `loadCTypes` is set to `false`, validation will fail if the definition of the credential's CType is not given.
* @param options.loadCTypes A function to load CType definitions that are not in `cTypes`. Defaults to using the {@link newCachingCTypeLoader | CachingCTypeLoader}. If set to `false` or `undefined`, no additional CTypes will be loaded.
* @param options.cTypes One or more CType definitions to be used for validation. If loadCTypes is set to false, validation will fail if the definition of the credential's CType is not given.
* @param options.loadCTypes A function to load CType definitions that are not in cTypes. Defaults to using the {@link newCachingCTypeLoader | CachingCTypeLoader}. If set to false or undefined, no additional CTypes will be loaded.
*
* @throws {Error} If the credential type does not contain a valid CType id.
* @throws {Error} If required CType definitions cannot be loaded.
* @throws {Error} If claims do not follow the expected CType format.
* @throws {Error} If referenced CTypes in nested structure cannot be fetched from the blockchain.
* @throws {Error} If validation fails against the CType schema.
*/
export async function validateSubject(
{
Expand All @@ -347,14 +393,13 @@ export async function validateSubject(
loadCTypes = cachingCTypeLoader,
}: { cTypes?: ICType[]; loadCTypes?: false | CTypeLoader } = {}
): Promise<void> {
// get CType id referenced in credential
const credentialsCTypeId = type.find((str) =>
str.startsWith('kilt:ctype:')
) as ICType['$id']
if (!credentialsCTypeId) {
throw new Error('credential type does not contain a valid CType id')
}
// check that we have access to the right schema

let cType = cTypes?.find(({ $id }) => $id === credentialsCTypeId)
if (!cType) {
if (typeof loadCTypes !== 'function') {
Expand All @@ -368,7 +413,6 @@ export async function validateSubject(
}
}

// normalize credential subject to form expected by CType schema
const expandedClaims: Record<string, unknown> =
jsonLdExpandCredentialSubject(credentialSubject)
delete expandedClaims['@id']
Expand All @@ -385,6 +429,27 @@ export async function validateSubject(
[key.substring(vocab.length)]: value,
}
}, {})
// validates against CType (also validates CType schema itself)
CType.verifyClaimAgainstSchema(claims, cType)

const references = extractUniqueReferences(cType)

const referencedCTypes = await Promise.all(
Array.from(references).map(async (ref) => {
if (typeof loadCTypes !== 'function') {
throw new Error(
`The definition for this credential's CType ${ref} has not been passed to the validator and CType loading has been disabled`
)
}
return loadCTypes(ref as any)
})
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry that I'm only realising that now, but we have two more issues here that need to be addressed:

  1. There is a maximum recursion depth for synchronous code in JS, which means that deeply nested structures could end up triggering a max recursion depth reached error
  2. The CTypes that we fetch could also contain references, so we need to check their definitions too and fetch any CTypes that they depend on

In light of these issues I think it's a good idea to refactor extractUniqueReferences so that it is async and also loads the ctype definitions directly. This function could be named loadNestedCTypeDefinitions and would

  1. Take a CType definition cType and a CTypeLoader cTypeLoader as parameters
  2. Initialise an empty Set fetchedCTypeIds
  3. Initialise an empty Set fetchedCTypeDefinitions
  4. Define a subroutine processCTypeProperties which accepts ICType["properties"] and:
    1. iterates over all properties in the object
    2. If the property value is an array, call and await processCTypeProperties on each item in it and continue
    3. If the property key is $ref, extract the CType id from it
    4. If the extracted CType id is not yet in fetchedCTypeIds, push the id to it, then call cTypeLoader passing the id as an argument
    5. await the Promise returned by the cTypeLoader; if it's a CType, call processCTypeProperties on its properties; else, throw an error
  5. calls processCTypeProperties on the CType properties and awaits its completion
  6. returns fetchedCTypeDefinitions


const validCTypes = referencedCTypes.filter(
(ctype): ctype is ICType => ctype !== undefined && ctype !== null
)

if (validCTypes.length === references.size) {
await CType.verifyClaimAgainstNestedSchemas(cType, validCTypes, claims)
} else {
throw new Error('Some referenced CTypes could not be fetched')
}
}
Loading