diff --git a/__tests__/example/example5.tests.ts b/__tests__/example/example5.tests.ts index e29530e..84067ae 100644 --- a/__tests__/example/example5.tests.ts +++ b/__tests__/example/example5.tests.ts @@ -3,7 +3,7 @@ import * as crypto from 'crypto'; import fs from 'fs'; import { Verifier, Document, MDoc, DeviceResponse } from '../../src/index'; -describe('example 5: device response contains a partial x5chain of the issuer certificate', () => { +describe('example 5: device response (using presentationDefinition) contains a partial x5chain of the issuer certificate', () => { it('issuer signature should be valid', async () => { const devicePrivatePEM = '-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIKWuHzvetdYpe5cErlOrU1bipA0OFtbBpJBdXCzRIVbz\n-----END PRIVATE KEY-----'; const devicePrivateKey = await jose.exportJWK(crypto.createPrivateKey({ key: devicePrivatePEM })); diff --git a/__tests__/example/example6.tests.ts b/__tests__/example/example6.tests.ts new file mode 100644 index 0000000..cdf1b8c --- /dev/null +++ b/__tests__/example/example6.tests.ts @@ -0,0 +1,80 @@ +import * as jose from 'jose'; +import * as crypto from 'crypto'; +import fs from 'fs'; +import { Verifier, Document, MDoc, DeviceResponse } from '../../src/index'; +import { DcqlQuery } from '../../src/mdoc/model/DcqlQuery'; + +describe('example 6: device response (using dcqlQuery) contains a partial x5chain of the issuer certificate', () => { + it('issuer signature should be valid', async () => { + const devicePrivatePEM = '-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIKWuHzvetdYpe5cErlOrU1bipA0OFtbBpJBdXCzRIVbz\n-----END PRIVATE KEY-----'; + const devicePrivateKey = await jose.exportJWK(crypto.createPrivateKey({ key: devicePrivatePEM })); + const devicePublicKey: jose.JWK = { + kty: devicePrivateKey.kty, + crv: devicePrivateKey.crv, + x: devicePrivateKey.x, + }; + + // A test IACA Root Certificate that the Issuer has shared publicly + // The openssl command to generate this certificate can be found @ https://github.com/auth0-lab/mdl/issues/37#issuecomment-2618717656 + const issuerIacaRootCertificate = '-----BEGIN CERTIFICATE-----\nMIICuzCCAh2gAwIBAgIUS9ewqx43m6VHiP5koCeEd09JOIwwCgYIKoZIzj0EAwQw\nOzELMAkGA1UEBhMCVVMxDDAKBgNVBAoMA01ETDEeMBwGA1UEAwwVSUFDQSBSb290\nIENlcnRpZmljYXRlMB4XDTI1MDEyODEyNTUzNloXDTM1MDEyNjEyNTUzNlowOzEL\nMAkGA1UEBhMCVVMxDDAKBgNVBAoMA01ETDEeMBwGA1UEAwwVSUFDQSBSb290IENl\ncnRpZmljYXRlMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBBt6kxI26+zkCddEN\ntuCUddthenpnDC7yT2ZgNvisCl6ZXRYI+oljvxgky53SZ18qixN4jtvnUOx/UuE3\nYDo0pZMBjk7CX2aKE91tG3kMt0G0LRnHSnkioCTPcDd67pN5myw8oEGHF5PQC9ai\nRZd30V4faCz+kZGO0ilLWGL0EElWILKjgbswgbgwHQYDVR0OBBYEFLvAYj7DJGBy\nMlzxdrwHYrSRPb03MHYGA1UdIwRvMG2AFLvAYj7DJGByMlzxdrwHYrSRPb03oT+k\nPTA7MQswCQYDVQQGEwJVUzEMMAoGA1UECgwDTURMMR4wHAYDVQQDDBVJQUNBIFJv\nb3QgQ2VydGlmaWNhdGWCFEvXsKseN5ulR4j+ZKAnhHdPSTiMMA8GA1UdEwEB/wQF\nMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMEA4GLADCBhwJCAbauH2Uj\noaB7fcKJqBgXqWfSXm5wqo6tEECM4gjtmIKPJnVSBbfcTn2bw7zIl2eBHzRdPX85\nPliPBGxjzyAoQcZ3AkES0U2MiwkDdoyUrb3k3jGOo02ayiCmtfy7y1OVZQWaH2HF\nMgQBbITyFHAZ0HUNpWIT+8527nB7POnJkguMovTIcA==\n-----END CERTIFICATE-----\n'; + const anotherIssuerRootCertificate = fs.readFileSync(`${__dirname}/issuer.pem`, 'utf-8'); + + // A test Document Signing Certificate that has been signed by the IACA Root Certificate above + const issuerDocumentSigningCertificate = '-----BEGIN CERTIFICATE-----\nMIIB1zCCATigAwIBAgIURoTE4I1tg1T7wyVF6YJxJqu2SLgwCgYIKoZIzj0EAwIw\nOzELMAkGA1UEBhMCVVMxDDAKBgNVBAoMA01ETDEeMBwGA1UEAwwVSUFDQSBSb290\nIENlcnRpZmljYXRlMB4XDTI1MDEyODEyNTU0M1oXDTI2MDEyODEyNTU0M1owQjEL\nMAkGA1UEBhMCVVMxDDAKBgNVBAoMA01ETDElMCMGA1UEAwwcRG9jdW1lbnQgU2ln\nbmluZyBDZXJ0aWZpY2F0ZTAqMAUGAytlcAMhACis1tWdJ2MW+6zokonq9bxhsLO5\nR6E0bFiLnYEWR4t+o0IwQDAdBgNVHQ4EFgQUgxDu5pET///uNotJBKr1gZOZNogw\nHwYDVR0jBBgwFoAUu8BiPsMkYHIyXPF2vAditJE9vTcwCgYIKoZIzj0EAwIDgYwA\nMIGIAkIA4YMSiBuUGrfU1UKeCbYwzp0ZoQhcL+HNCEtgLFW6LtDB4tP+T9A/O5bS\nWV6P+e3mWti13BKCraPRUkKVQA1qyNcCQgFXTRr1Xt+ufVjl1XqnJo0KITN91TyL\n4GKJeBxGYWDFgyvbpCNUs5XeiGejkvhz/8E0fYzCqZmqEIlp6IcvgV8c0A==\n-----END CERTIFICATE-----\n'; + const issuerDocumentSigningKeyPem = '-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIEQLHY7fwKG6Av4VP3uByNKMyS7/sJKk4ntbzL8nSq0t\n-----END PRIVATE KEY-----\n'; + const issuerDocumentSigningKey = await jose.exportJWK(crypto.createPrivateKey({ key: issuerDocumentSigningKeyPem })); + + const document = new Document('org.iso.18013.5.1.mDL') + .addIssuerNameSpace('org.iso.18013.5.1', { + family_name: 'Jones', + given_name: 'Ava', + birth_date: '2007-03-25', + }) + .useDigestAlgorithm('SHA-256') + .addValidityInfo({ + signed: new Date(), + }) + .addDeviceKeyInfo({ deviceKey: devicePublicKey }); + + const signedDoc = await document.sign({ + issuerPrivateKey: issuerDocumentSigningKey, + issuerCertificate: issuerDocumentSigningCertificate, + alg: 'EdDSA', + }); + + const issuerMDoc = new MDoc([signedDoc]).encode(); + const dcqlQuery: DcqlQuery = { + credentials: [ + { + id: 'family_name_only', + format: 'mso_mdoc', + meta: { + doctype_value: 'org.iso.18013.5.1.mDL', + }, + claims: [ + { + path: ['org.iso.18013.5.1', 'family_name'], + intent_to_retain: false, + }, + ], + }, + ], + }; + + const deviceResponseMDoc = await DeviceResponse.from(issuerMDoc) + .usingDcqlQuery(dcqlQuery) + .usingSessionTranscriptForOID4VP('', '', '', '') + .authenticateWithSignature(devicePrivateKey, 'EdDSA') + .sign(); + + const verifier = new Verifier([issuerIacaRootCertificate, anotherIssuerRootCertificate]); + const diagnosticInfo = await verifier.getDiagnosticInformation(deviceResponseMDoc.encode(), {}); + + expect(diagnosticInfo.issuerSignature).toEqual({ + alg: 'EdDSA', + digests: { 'org.iso.18013.5.1': 3 }, + isValid: true, + reasons: [], + }); + }); +}); diff --git a/__tests__/issuing/config.ts b/__tests__/issuing/config.ts index 6285358..c703297 100644 --- a/__tests__/issuing/config.ts +++ b/__tests__/issuing/config.ts @@ -127,3 +127,29 @@ export const PRESENTATION_DEFINITION_2 = { }, ], }; + +export const DCQL_QUERY = { + credentials: [ + { + id: 'mdl-test-all-data-dcql', + format: 'mso_mdoc', + meta: { + doctype_value: 'org.iso.18013.5.1.mDL', + }, + claims: [ + { path: ["$['org.iso.18013.5.1']['family_name']"], intent_to_retain: false }, + { path: ["$['org.iso.18013.5.1']['given_name']"], intent_to_retain: false }, + { path: ["$['org.iso.18013.5.1']['birth_date']"], intent_to_retain: false }, + { path: ["$['org.iso.18013.5.1']['issue_date']"], intent_to_retain: false }, + { path: ["$['org.iso.18013.5.1']['expiry_date']"], intent_to_retain: false }, + { path: ["$['org.iso.18013.5.1']['issuing_country']"], intent_to_retain: false }, + { path: ["$['org.iso.18013.5.1']['issuing_authority']"], intent_to_retain: false }, + { path: ["$['org.iso.18013.5.1']['issuing_jurisdiction']"], intent_to_retain: false }, + { path: ["$['org.iso.18013.5.1']['document_number']"], intent_to_retain: false }, + { path: ["$['org.iso.18013.5.1']['portrait']"], intent_to_retain: false }, + { path: ["$['org.iso.18013.5.1']['driving_privileges']"], intent_to_retain: false }, + { path: ["$['org.iso.18013.5.1']['un_distinguishing_sign']"], intent_to_retain: false }, + ], + }, + ], +}; diff --git a/__tests__/issuing/deviceResponseWithDcql.tests.ts b/__tests__/issuing/deviceResponseWithDcql.tests.ts new file mode 100644 index 0000000..3544580 --- /dev/null +++ b/__tests__/issuing/deviceResponseWithDcql.tests.ts @@ -0,0 +1,238 @@ +import { randomFillSync } from 'node:crypto'; +import * as jose from 'jose'; +import { + MDoc, + Document, + Verifier, + parse, + DeviceResponse, + DeviceSignedDocument, +} from '../../src'; +import { DEVICE_JWK, ISSUER_CERTIFICATE, ISSUER_PRIVATE_KEY_JWK, DCQL_QUERY } from './config'; +import { DataItem, cborEncode } from '../../src/cbor'; + +const { d, ...publicKeyJWK } = DEVICE_JWK as jose.JWK; + +describe('issuing a device response', () => { + let encoded: Uint8Array; + let parsedDocument: DeviceSignedDocument; + let mdoc: MDoc; + + const signed = new Date('2023-10-24T14:55:18Z'); + const validUntil = new Date(signed); + validUntil.setFullYear(signed.getFullYear() + 30); + + beforeAll(async () => { + const issuerPrivateKey = ISSUER_PRIVATE_KEY_JWK; + + // this is the ISSUER side + { + const document = await new Document('org.iso.18013.5.1.mDL') + .addIssuerNameSpace('org.iso.18013.5.1', { + family_name: 'Jones', + given_name: 'Ava', + birth_date: '2007-03-25', + issue_date: '2023-09-01', + expiry_date: '2028-09-30', + issuing_country: 'US', + issuing_authority: 'NY DMV', + document_number: '01-856-5050', + portrait: 'bstr', + driving_privileges: [ + { + vehicle_category_code: 'C', + issue_date: '2022-09-02', + expiry_date: '2027-09-20', + }, + ], + un_distinguishing_sign: 'tbd-us.ny.dmv', + + sex: 'F', + height: '5\' 8"', + weight: '120lb', + eye_colour: 'brown', + hair_colour: 'brown', + resident_addres: '123 Street Rd', + resident_city: 'Brooklyn', + resident_state: 'NY', + resident_postal_code: '19001', + resident_country: 'US', + issuing_jurisdiction: 'New York', + }) + .useDigestAlgorithm('SHA-512') + .addValidityInfo({ + signed, + validUntil, + }) + .addDeviceKeyInfo({ deviceKey: publicKeyJWK }) + .sign({ + issuerPrivateKey, + issuerCertificate: ISSUER_CERTIFICATE, + alg: 'ES256', + }); + + mdoc = new MDoc([document]); + } + }); + + describe('using OID4VP handover', () => { + const verifierGeneratedNonce = 'abcdefg'; + const mdocGeneratedNonce = '123456'; + const clientId = 'Cq1anPb8vZU5j5C0d7hcsbuJLBpIawUJIDQRi2Ebwb4'; + const responseUri = 'http://localhost:4000/api/presentation_request/dc8999df-d6ea-4c84-9985-37a8b81a82ec/callback'; + + const getSessionTranscriptBytes = (clId: string, respUri: string, nonce: string, mdocNonce: string) => cborEncode( + DataItem.fromData([ + null, // DeviceEngagementBytes + null, // EReaderKeyBytes + [mdocNonce, clId, respUri, nonce], // Handover = OID4VPHandover + ]), + ); + + beforeAll(async () => { + // This is the Device side + const devicePrivateKey = DEVICE_JWK; + const deviceResponseMDoc = await DeviceResponse.from(mdoc) + .usingDcqlQuery(DCQL_QUERY) + .usingSessionTranscriptForOID4VP(mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce) + .authenticateWithSignature(devicePrivateKey, 'ES256') + .addDeviceNameSpace('com.foobar-device', { test: 1234 }) + .sign(); + + encoded = deviceResponseMDoc.encode(); + const parsedMDOC = parse(encoded); + [parsedDocument] = parsedMDOC.documents as DeviceSignedDocument[]; + }); + + it('should be verifiable', async () => { + const verifier = new Verifier([ISSUER_CERTIFICATE]); + await verifier.verify(encoded, { + encodedSessionTranscript: getSessionTranscriptBytes(clientId, responseUri, verifierGeneratedNonce, mdocGeneratedNonce), + }); + }); + + describe('should not be verifiable', () => { + [ + ['clientId', { clientId: 'wrong', responseUri, verifierGeneratedNonce, mdocGeneratedNonce }] as const, + ['responseUri', { clientId, responseUri: 'wrong', verifierGeneratedNonce, mdocGeneratedNonce }] as const, + ['verifierGeneratedNonce', { clientId, responseUri, verifierGeneratedNonce: 'wrong', mdocGeneratedNonce }] as const, + ['mdocGeneratedNonce', { clientId, responseUri, verifierGeneratedNonce, mdocGeneratedNonce: 'wrong' }] as const, + ].forEach(([name, values]) => { + it(`with a different ${name}`, async () => { + try { + const verifier = new Verifier([ISSUER_CERTIFICATE]); + await verifier.verify(encoded, { + encodedSessionTranscript: getSessionTranscriptBytes(values.clientId, values.responseUri, values.verifierGeneratedNonce, values.mdocGeneratedNonce), + }); + throw new Error('should not validate with different transcripts'); + } catch (error) { + expect(error.message).toMatch('Unable to verify deviceAuth signature (ECDSA/EdDSA): Device signature must be valid'); + } + }); + }); + }); + + it('should contain the validity info', () => { + const { validityInfo } = parsedDocument.issuerSigned.issuerAuth.decodedPayload; + expect(validityInfo).toBeDefined(); + expect(validityInfo.signed).toEqual(signed); + expect(validityInfo.validFrom).toEqual(signed); + expect(validityInfo.validUntil).toEqual(validUntil); + expect(validityInfo).not.toHaveProperty('expectedUpdate'); + }); + + it('should contain the device namespaces', () => { + expect(parsedDocument.getDeviceNameSpace('com.foobar-device')) + .toEqual({ test: 1234 }); + }); + + it('should generate the signature without payload', () => { + expect(parsedDocument.deviceSigned.deviceAuth.deviceSignature?.payload).toBeNull(); + }); + }); + + describe('using WebAPI handover', () => { + // The actual value for the engagements & the key do not matter, + // as long as the device and the reader agree on what value to use. + const eReaderKeyBytes: Buffer = randomFillSync(Buffer.alloc(32)); + const readerEngagementBytes = randomFillSync(Buffer.alloc(32)); + const deviceEngagementBytes = randomFillSync(Buffer.alloc(32)); + + const getSessionTranscriptBytes = ( + rdrEngtBytes: Buffer, + devEngtBytes: Buffer, + eRdrKeyBytes: Buffer, + ) => cborEncode( + DataItem.fromData([ + new DataItem({ buffer: devEngtBytes }), + new DataItem({ buffer: eRdrKeyBytes }), + rdrEngtBytes, + ]), + ); + + beforeAll(async () => { + // Nothing more to do on the verifier side. + + // This is the Device side + { + const devicePrivateKey = DEVICE_JWK; + const deviceResponseMDoc = await DeviceResponse.from(mdoc) + .usingDcqlQuery(DCQL_QUERY) + .usingSessionTranscriptForWebAPI(deviceEngagementBytes, readerEngagementBytes, eReaderKeyBytes) + .authenticateWithSignature(devicePrivateKey, 'ES256') + .addDeviceNameSpace('com.foobar-device', { test: 1234 }) + .sign(); + encoded = deviceResponseMDoc.encode(); + } + + const parsedMDOC = parse(encoded); + [parsedDocument] = parsedMDOC.documents as DeviceSignedDocument[]; + }); + + it('should be verifiable', async () => { + const verifier = new Verifier([ISSUER_CERTIFICATE]); + await verifier.verify(encoded, { + encodedSessionTranscript: getSessionTranscriptBytes(readerEngagementBytes, deviceEngagementBytes, eReaderKeyBytes), + }); + }); + + describe('should not be verifiable', () => { + const wrong = randomFillSync(Buffer.alloc(32)); + [ + ['readerEngagementBytes', { readerEngagementBytes: wrong, deviceEngagementBytes, eReaderKeyBytes }] as const, + ['deviceEngagementBytes', { readerEngagementBytes, deviceEngagementBytes: wrong, eReaderKeyBytes }] as const, + ['eReaderKeyBytes', { readerEngagementBytes, deviceEngagementBytes, eReaderKeyBytes: wrong }] as const, + ].forEach(([name, values]) => { + it(`with a different ${name}`, async () => { + const verifier = new Verifier([ISSUER_CERTIFICATE]); + try { + await verifier.verify(encoded, { + encodedSessionTranscript: getSessionTranscriptBytes(values.readerEngagementBytes, values.deviceEngagementBytes, values.eReaderKeyBytes), + }); + throw new Error('should not validate with different transcripts'); + } catch (error) { + expect(error.message).toMatch('Unable to verify deviceAuth signature (ECDSA/EdDSA): Device signature must be valid'); + } + }); + }); + }); + + it('should contain the validity info', () => { + const { validityInfo } = parsedDocument.issuerSigned.issuerAuth.decodedPayload; + expect(validityInfo).toBeDefined(); + expect(validityInfo.signed).toEqual(signed); + expect(validityInfo.validFrom).toEqual(signed); + expect(validityInfo.validUntil).toEqual(validUntil); + expect(validityInfo).not.toHaveProperty('expectedUpdate'); + }); + + it('should contain the device namespaces', () => { + expect(parsedDocument.getDeviceNameSpace('com.foobar-device')) + .toEqual({ test: 1234 }); + }); + + it('should generate the signature without payload', () => { + expect(parsedDocument.deviceSigned.deviceAuth.deviceSignature?.payload).toBeNull(); + }); + }); +}); diff --git a/src/mdoc/model/DcqlQuery.ts b/src/mdoc/model/DcqlQuery.ts new file mode 100644 index 0000000..f517180 --- /dev/null +++ b/src/mdoc/model/DcqlQuery.ts @@ -0,0 +1,32 @@ +export type DcqlQuery = { + credentials: DcqlQueryCredential[]; + credential_sets?: DcqlCredentialSetQuery[]; +}; + +export type DcqlQueryCredential = { + id: string; + format: string; + multiple?: boolean; + meta: { + vct_values?: string[]; + doctype_value?: string; + }; + trusted_authorities?: any; + require_cryptographic_holder_binding?: boolean; + claims: DcqlClaim[]; + claim_sets?: string[][]; +}; + +export type DcqlCredentialSetQuery = { + options: string[][]; + required?: boolean; +}; + +export type DcqlClaim = { + id: string; + path: string[]; + values?: (string | number | boolean)[]; + intent_to_retain?: boolean; +}; + + diff --git a/src/mdoc/model/DeviceResponse.ts b/src/mdoc/model/DeviceResponse.ts index bb9287a..3ae12b1 100644 --- a/src/mdoc/model/DeviceResponse.ts +++ b/src/mdoc/model/DeviceResponse.ts @@ -2,6 +2,7 @@ import * as jose from 'jose'; import { COSEKeyFromJWK, COSEKeyToJWK, Mac0, Sign1, importCOSEKey } from 'cose-kit'; import { Buffer } from 'buffer'; import { InputDescriptor, PresentationDefinition } from './PresentationDefinition'; +import { DcqlQuery, DcqlQueryCredential} from './DcqlQuery'; import { MDoc } from './MDoc'; import { DeviceAuth, DeviceSigned, MacSupportedAlgs, SupportedAlgs } from './types'; import { IssuerSignedDocument } from './IssuerSignedDocument'; @@ -17,7 +18,8 @@ import COSEKeyToRAW from '../../cose/coseKey'; */ export class DeviceResponse { private mdoc: MDoc; - private pd: PresentationDefinition; + private pd?: PresentationDefinition; + private dcqlQuery?: DcqlQuery; private sessionTranscriptBytes: Buffer; private useMac = true; private devicePrivateKey: Uint8Array; @@ -64,6 +66,25 @@ export class DeviceResponse { return this; } + /** + * + * @param dcqlQuery - The DCQL query to use for the device response. + * @returns {DeviceResponse} + */ + public usingDcqlQuery(dcqlQuery: DcqlQuery): DeviceResponse { + if (!dcqlQuery.credentials.length) { + throw new Error('The DCQL query must have at least one credential.'); + } + + const hasDuplicates = dcqlQuery.credentials.some((id1, idx) => dcqlQuery.credentials.findIndex((id2) => id2.id === id1.id) !== idx); + if (hasDuplicates) { + throw new Error('Each credential must have a unique id property.'); + } + + this.dcqlQuery = dcqlQuery; + return this; + } + /** * Set the session transcript data to use for the device response with the given handover data. * this is a shortcut to calling {@link usingSessionTranscriptBytes}(``), @@ -223,13 +244,39 @@ export class DeviceResponse { * @returns {Promise} - The device response as an MDoc. */ public async sign(): Promise { - if (!this.pd) throw new Error('Must provide a presentation definition with .usingPresentationDefinition()'); + if (!this.pd && !this.dcqlQuery) throw new Error('Must provide a presentation definition with .usingPresentationDefinition() or a DCQL query with .usingDcqlQuery()'); if (!this.sessionTranscriptBytes) throw new Error('Must provide the session transcript with either .usingSessionTranscriptForOID4VP, .usingSessionTranscriptForWebAPI or .usingSessionTranscriptBytes'); - const docs = await Promise.all(this.pd.input_descriptors.map((id) => this.handleInputDescriptor(id))); + let docs; + if (this.dcqlQuery) { + docs = await Promise.all(this.dcqlQuery.credentials.map((cred) => this.handleDcqlCredential(cred))); + } else if (this.pd) { + docs = await Promise.all(this.pd.input_descriptors.map((id) => this.handleInputDescriptor(id))); + } else { + throw new Error('No query or presentation definition provided'); + } return new MDoc(docs); } + private async handleDcqlCredential(cred: DcqlQueryCredential): Promise { + const document = (this.mdoc.documents || []).find((d) => d.docType === cred.meta.doctype_value); + if (!document) { + // TODO; probl need to create a DocumentError here, but let's just throw for now + throw new Error(`The mdoc does not have a document with DocType "${cred.id}"`); + } + + const nameSpaces = await this.prepareNamespaces(cred, document); + + return new DeviceSignedDocument( + document.docType, + { + nameSpaces, + issuerAuth: document.issuerSigned.issuerAuth, + }, + await this.getDeviceSigned(document.docType), + ); + } + private async handleInputDescriptor(id: InputDescriptor): Promise { const document = (this.mdoc.documents || []).find((d) => d.docType === id.id); if (!document) { @@ -302,8 +349,15 @@ export class DeviceResponse { return { deviceSignature }; } - private async prepareNamespaces(id: InputDescriptor, document: IssuerSignedDocument) { - const requestedFields = id.constraints.fields; + private async prepareNamespaces(id: InputDescriptor | DcqlQueryCredential, document: IssuerSignedDocument) { + // For InputDescriptor, use id.constraints.fields + // For DcqlQueryCredential, use id.claims (array of DcqlClaim) + let requestedFields; + if ('constraints' in id && id.constraints?.fields) { + requestedFields = id.constraints.fields; + } else if ('claims' in id && Array.isArray(id.claims)) { + requestedFields = id.claims.map(claim => ({ path: claim.path, intent_to_retain: claim.intent_to_retain || false })); + } const nameSpaces: { [ns: string]: any } = {}; for await (const field of requestedFields) { const result = await this.prepareDigest(field.path, document); @@ -326,16 +380,26 @@ export class DeviceResponse { document: IssuerSignedDocument, ): Promise<{ nameSpace: string; digest: IssuerSignedItem } | null> { /** - * path looks like this: "$['org.iso.18013.5.1']['family_name']" - * the regex creates two groups with contents between "['" and "']" - * the second entry in each group contains the result without the "'[" or "']" + * Supports two formats: + * 1. ["$['org.iso.18013.5.1']['family_name']"] + * 2. ["org.iso.18013.5.1", "family_name"] */ for (const path of paths) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [[_1, nameSpace], [_2, elementIdentifier]] = [...path.matchAll(/\['(.*?)'\]/g)]; - if (!nameSpace) throw new Error(`Failed to parse namespace from path "${path}"`); - if (!elementIdentifier) throw new Error(`Failed to parse elementIdentifier from path "${path}"`); - + let nameSpace: string | undefined; + let elementIdentifier: string | undefined; + if (typeof path === 'string' && path.startsWith("$['")) { + // JSONPath string format + const matches = [...path.matchAll(/\['(.*?)'\]/g)]; + nameSpace = matches[0]?.[1]; + elementIdentifier = matches[1]?.[1]; + } else if (Array.isArray(paths) && paths.length === 2 && paths.every(p => typeof p === 'string')) { + // Array format: [namespace, elementIdentifier] + nameSpace = paths[0]; + elementIdentifier = paths[1]; + } + if (!nameSpace || !elementIdentifier) { + throw new Error(`Failed to parse namespace/elementIdentifier from path "${JSON.stringify(path)}"`); + } const nsAttrs: IssuerSignedItem[] = document.issuerSigned.nameSpaces[nameSpace] || []; const digest = nsAttrs.find((d) => d.elementIdentifier === elementIdentifier);