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 all 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
116 changes: 109 additions & 7 deletions packages/credentials/src/V1/KiltCredentialV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,15 +327,83 @@ export function fromInput({

const cachingCTypeLoader = newCachingCTypeLoader()

async function loadNestedCTypeDefinitions(
cType: ICType,
cTypeLoader: (id: string) => Promise<ICType | undefined>
): Promise<Set<ICType>> {
const fetchedCTypeIds = new Set<string>()
const fetchedCTypeDefinitions = new Set<ICType>()

async function processValue(value: unknown): Promise<void> {
if (typeof value !== 'object' || value === null) {
return
}

if (Array.isArray(value)) {
await Promise.all(value.map(processValue))
return
}

// Check if value is an object with $ref
const objValue = value as Record<string, unknown>
if ('$ref' in objValue) {
const ref = objValue.$ref
if (typeof ref === 'string' && ref.startsWith('kilt:ctype:')) {
const cTypeId = ref.split('#/')[0]

if (!fetchedCTypeIds.has(cTypeId)) {
fetchedCTypeIds.add(cTypeId)
const referencedCType = await cTypeLoader(cTypeId)

if (referencedCType === undefined || referencedCType === null) {
throw new Error(`Failed to load referenced CType: ${cTypeId}`)
}

fetchedCTypeDefinitions.add(referencedCType)

const { properties } = referencedCType
if (properties !== undefined && properties !== null) {
await Promise.all(Object.values(properties).map(processValue))
}
}
}
return
}

// Process all values in the object
await Promise.all(Object.values(objValue).map(processValue))
}

const { properties } = cType
if (properties !== undefined && properties !== null) {
await Promise.all(Object.values(properties).map(processValue))
}

return fetchedCTypeDefinitions
}

/**
* 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 +415,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 +435,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 +451,42 @@ export async function validateSubject(
[key.substring(vocab.length)]: value,
}
}, {})
// validates against CType (also validates CType schema itself)
CType.verifyClaimAgainstSchema(claims, cType)

// Create a type-safe loader function
const effectiveLoader = async (id: string): Promise<ICType | undefined> => {
// Ensure the ID is in the correct format
if (!id.startsWith('kilt:ctype:0x')) {
throw new Error(`Invalid CType ID format: ${id}`)
}

const typedId = id as `kilt:ctype:0x${string}`

if (typeof loadCTypes === 'function') {
return loadCTypes(typedId)
}
const found = cTypes.find((ct) => ct.$id === typedId)
if (found) {
return found
}
throw new Error(
`CType ${id} not found in provided cTypes array and CType loading is disabled`
)
}
// Load all nested CTypes
const referencedCTypes = await loadNestedCTypeDefinitions(
cType,
effectiveLoader
)

// Convert Set to Array and filter out any undefined or null values
const validCTypes = Array.from(referencedCTypes).filter(
(ctype): ctype is ICType => ctype !== undefined && ctype !== null
)

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