diff --git a/.changeset/violet-needles-own.md b/.changeset/violet-needles-own.md new file mode 100644 index 0000000..88eef0f --- /dev/null +++ b/.changeset/violet-needles-own.md @@ -0,0 +1,5 @@ +--- +"@animo-id/mdoc": patch +--- + +fix: always true statement throws error. In the DeviceResponse model there was an always true if statement that throws an error before allowing the creation of the response. diff --git a/src/context.ts b/src/context.ts index d037203..40fb2cf 100644 --- a/src/context.ts +++ b/src/context.ts @@ -50,9 +50,10 @@ export interface MdocContext { alg: string }) => MaybePromise - validateCertificateChain: (input: { + verifyCertificateChain: (input: { trustedCertificates: Uint8Array[] x5chain: Uint8Array[] + now?: Date }) => MaybePromise getCertificateData: (input: { certificate: Uint8Array }) => MaybePromise<{ diff --git a/src/cose/sign1.ts b/src/cose/sign1.ts index 605b3e9..540f6cb 100644 --- a/src/cose/sign1.ts +++ b/src/cose/sign1.ts @@ -156,7 +156,7 @@ export class Sign1 extends CborStructure { this.signature = signature } - public async verify(options: { key?: CoseKey }, ctx: Pick) { + public async verifySignature(options: { key?: CoseKey }, ctx: Pick) { const publicKey = options.key ?? (await ctx.x509.getPublicKey({ diff --git a/src/holder.ts b/src/holder.ts index c97b2d4..98cdfe8 100644 --- a/src/holder.ts +++ b/src/holder.ts @@ -2,6 +2,7 @@ import { base64url } from 'jose' import type { MdocContext } from './context' import type { CoseKey } from './cose' import { + type DeviceNamespaces, DeviceRequest, DeviceResponse, type Document, @@ -17,7 +18,7 @@ export class Holder { * string should be base64url encoded as defined in openid4vci Draft 15 * */ - public static async validateIssuerSigned( + public static async verifyIssuerSigned( options: { issuerSigned: Uint8Array | string | IssuerSigned verificationCallback?: VerificationCallback @@ -34,10 +35,10 @@ export class Holder { ? IssuerSigned.decode(options.issuerSigned) : options.issuerSigned - await issuerSigned.issuerAuth.validate(options, ctx) + await issuerSigned.issuerAuth.verify(options, ctx) } - public static async validateDeviceRequest( + public static async verifyDeviceRequest( options: { deviceRequest: Uint8Array | DeviceRequest sessionTranscript: Uint8Array | SessionTranscript @@ -56,7 +57,7 @@ export class Holder { : SessionTranscript.decode(options.sessionTranscript) for (const docRequest of deviceRequest.docRequests) { - await docRequest.readerAuth?.validate( + await docRequest.readerAuth?.verify( { readerAuthentication: { itemsRequest: docRequest.itemsRequest, @@ -74,6 +75,7 @@ export class Holder { deviceRequest: DeviceRequest sessionTranscript: SessionTranscript documents: Array + deviceNamespaces?: DeviceNamespaces mac?: { ephemeralKey: CoseKey signingKey: CoseKey @@ -92,6 +94,7 @@ export class Holder { presentationDefinition: PresentationDefinition sessionTranscript: SessionTranscript documents: Array + deviceNamespaces?: DeviceNamespaces mac?: { ephemeralKey: CoseKey signingKey: CoseKey diff --git a/src/mdoc/models/device-auth.ts b/src/mdoc/models/device-auth.ts index 15f319e..48c0baa 100644 --- a/src/mdoc/models/device-auth.ts +++ b/src/mdoc/models/device-auth.ts @@ -60,7 +60,7 @@ export class DeviceAuth extends CborStructure { throw new MdlError('unreachable') } - public async validate( + public async verify( options: { document: Document verificationCallback?: VerificationCallback diff --git a/src/mdoc/models/device-response.ts b/src/mdoc/models/device-response.ts index 66cf4af..d9e1039 100644 --- a/src/mdoc/models/device-response.ts +++ b/src/mdoc/models/device-response.ts @@ -6,14 +6,15 @@ import { EitherSignatureOrMacMustBeProvidedError } from '../errors' import { DeviceAuth, type DeviceAuthOptions } from './device-auth' import { DeviceAuthentication } from './device-authentication' import { DeviceMac } from './device-mac' -import type { DeviceNamespaces } from './device-namespaces' +import { DeviceNamespaces } from './device-namespaces' import type { DeviceRequest } from './device-request' import { DeviceSignature } from './device-signature' import { DeviceSigned } from './device-signed' import type { DocRequest } from './doc-request' import { Document, type DocumentStructure } from './document' import { DocumentError, type DocumentErrorStructure } from './document-error' -import type { IssuerSigned } from './issuer-signed' +import type { IssuerNamespace } from './issuer-namespace' +import { IssuerSigned } from './issuer-signed' import { findMdocMatchingDocType, limitDisclosureToDeviceRequestNameSpaces, @@ -90,7 +91,7 @@ export class DeviceResponse extends CborStructure { return DeviceResponse.fromEncodedStructure(structure) } - public async validate( + public async verify( options: { sessionTranscript: SessionTranscript | Uint8Array ephemeralReaderKey?: CoseKey @@ -116,7 +117,7 @@ export class DeviceResponse extends CborStructure { }) for (const document of this.documents ?? []) { - await document.issuerSigned.issuerAuth.validate( + await document.issuerSigned.issuerAuth.verify( { disableCertificateChainValidation: options.disableCertificateChainValidation, now: options.now, @@ -126,7 +127,7 @@ export class DeviceResponse extends CborStructure { ctx ) - await document.deviceSigned.deviceAuth.validate( + await document.deviceSigned.deviceAuth.verify( { document, ephemeralMacPrivateKey: options.ephemeralReaderKey, @@ -139,34 +140,35 @@ export class DeviceResponse extends CborStructure { ctx ) - await document.issuerSigned.validate({ verificationCallback: onCheck }, ctx) + await document.issuerSigned.verify({ verificationCallback: onCheck }, ctx) } } private static async create( limitDisclosureCb: - | ((issuerSigned: IssuerSigned, inputDescriptor: InputDescriptor) => DeviceNamespaces) - | ((issuerSigned: IssuerSigned, docRequest: DocRequest) => DeviceNamespaces), + | ((issuerSigned: IssuerSigned, inputDescriptor: InputDescriptor) => IssuerNamespace) + | ((issuerSigned: IssuerSigned, docRequest: DocRequest) => IssuerNamespace), options: { inputDescriptorsOrRequests: Array | Array sessionTranscript: SessionTranscript documents: Array - mac?: { - ephemeralKey: CoseKey + deviceNamespaces?: DeviceNamespaces + signature?: { signingKey: CoseKey } - signature?: { + mac?: { + ephemeralKey: CoseKey signingKey: CoseKey } }, ctx: Pick ) { - if (!(options.mac && options.signature) || (options.mac && options.signature)) { - throw new EitherSignatureOrMacMustBeProvidedError() - } - + const useMac = !!options.mac const useSignature = !!options.signature - const signingKey = useSignature ? options.mac.signingKey : options.signature.signingKey + if (useMac === useSignature) throw new EitherSignatureOrMacMustBeProvidedError() + + const signingKey = useSignature ? options.signature?.signingKey : options.mac?.signingKey + if (!signingKey) throw new Error('Signing key is missing') const documents = await Promise.all( options.inputDescriptorsOrRequests.map(async (idOrRequest) => { @@ -174,15 +176,17 @@ export class DeviceResponse extends CborStructure { options.documents, 'id' in idOrRequest ? idOrRequest.id : idOrRequest.itemsRequest.docType ) - const deviceNamespaces = limitDisclosureCb( + const disclosedIssuerNamespaces = limitDisclosureCb( document.issuerSigned, idOrRequest as unknown as InputDescriptor & DocRequest ) + const deviceNamespaces = options.deviceNamespaces ?? new DeviceNamespaces({ deviceNamespaces: new Map() }) + const deviceAuthenticationBytes = new DeviceAuthentication({ sessionTranscript: options.sessionTranscript, docType: document.docType, - deviceNamespaces: document.deviceSigned.deviceNamespaces, + deviceNamespaces, }).encode({ asDataItem: true }) const unprotectedHeaders = signingKey.keyId @@ -215,10 +219,13 @@ export class DeviceResponse extends CborStructure { detachedContent: deviceAuthenticationBytes, }) + const ephemeralKey = options.mac?.ephemeralKey + if (!ephemeralKey) throw new Error('Ephemeral key is missing') + await deviceMac.addTag( { privateKey: signingKey, - ephemeralKey: (options.mac as Required).ephemeralKey, + ephemeralKey: ephemeralKey, sessionTranscript: options.sessionTranscript, }, ctx @@ -229,7 +236,10 @@ export class DeviceResponse extends CborStructure { return new Document({ docType: document.docType, - issuerSigned: document.issuerSigned, + issuerSigned: new IssuerSigned({ + issuerNamespaces: disclosedIssuerNamespaces, + issuerAuth: document.issuerSigned.issuerAuth, + }), deviceSigned: new DeviceSigned({ deviceNamespaces, deviceAuth: new DeviceAuth(deviceAuthOptions), @@ -248,6 +258,7 @@ export class DeviceResponse extends CborStructure { deviceRequest: DeviceRequest sessionTranscript: SessionTranscript documents: Array + deviceNamespaces?: DeviceNamespaces mac?: { ephemeralKey: CoseKey signingKey: CoseKey @@ -270,6 +281,7 @@ export class DeviceResponse extends CborStructure { presentationDefinition: PresentationDefinition sessionTranscript: SessionTranscript documents: Array + deviceNamespaces?: DeviceNamespaces mac?: { ephemeralKey: CoseKey signingKey: CoseKey diff --git a/src/mdoc/models/issuer-auth.ts b/src/mdoc/models/issuer-auth.ts index 6607aa9..0fc685b 100644 --- a/src/mdoc/models/issuer-auth.ts +++ b/src/mdoc/models/issuer-auth.ts @@ -27,7 +27,7 @@ export class IssuerAuth extends Sign1 { return mso } - public async validate( + public async verify( options: { verificationCallback?: VerificationCallback now?: Date @@ -54,9 +54,10 @@ export class IssuerAuth extends Sign1 { throw new Error('No trusted certificates found. Cannot verify issuer signature.') } - await ctx.x509.validateCertificateChain({ + await ctx.x509.verifyCertificateChain({ trustedCertificates, x5chain: this.certificateChain, + now, }) onCheck({ @@ -72,7 +73,7 @@ export class IssuerAuth extends Sign1 { } } - const isSignatureValid = await this.verify({}, ctx) + const isSignatureValid = await this.verifySignature({}, ctx) onCheck({ status: isSignatureValid ? 'PASSED' : 'FAILED', @@ -86,7 +87,7 @@ export class IssuerAuth extends Sign1 { }) onCheck({ - status: validityInfo.validateSigned(notBefore, notAfter) ? 'FAILED' : 'PASSED', + status: validityInfo.verifySigned(notBefore, notAfter) ? 'FAILED' : 'PASSED', check: 'The MSO signed date must be within the validity period of the certificate', reason: `The MSO signed date (${validityInfo.signed.toUTCString()}) must be within the validity period of the certificate (${notBefore.toUTCString()} to ${notAfter.toUTCString()})`, }) diff --git a/src/mdoc/models/issuer-signed.ts b/src/mdoc/models/issuer-signed.ts index ba894f9..e65c4dd 100644 --- a/src/mdoc/models/issuer-signed.ts +++ b/src/mdoc/models/issuer-signed.ts @@ -48,7 +48,7 @@ export class IssuerSigned extends CborStructure { return IssuerSigned.decode(base64url.decode(encoded)) } - public async validate( + public async verify( options: { verificationCallback?: VerificationCallback }, ctx: Pick ) { diff --git a/src/mdoc/models/pex-limit-disclosure.ts b/src/mdoc/models/pex-limit-disclosure.ts index 008d5d2..a77c629 100644 --- a/src/mdoc/models/pex-limit-disclosure.ts +++ b/src/mdoc/models/pex-limit-disclosure.ts @@ -1,43 +1,31 @@ -import type { DataElementIdentifier } from './data-element-identifier.js' -import type { DataElementValue } from './data-element-value.js' -import { DeviceNamespaces } from './device-namespaces.js' -import { DeviceSignedItems } from './device-signed-items.js' -import type { DocRequest } from './doc-request.js' -import type { DocType } from './doctype.js' -import type { Document } from './document.js' -import type { IssuerNamespace } from './issuer-namespace.js' -import type { IssuerSignedItem } from './issuer-signed-item.js' -import type { IssuerSigned } from './issuer-signed.js' -import type { InputDescriptor } from './presentation-definition.js' +import type { DocRequest } from './doc-request' +import type { DocType } from './doctype' +import type { Document } from './document' +import { IssuerNamespace } from './issuer-namespace' +import type { IssuerSigned } from './issuer-signed' +import type { IssuerSignedItem } from './issuer-signed-item' +import type { Namespace } from './namespace' +import type { InputDescriptor } from './presentation-definition' export const limitDisclosureToDeviceRequestNameSpaces = ( issuerSigned: IssuerSigned, docRequest: DocRequest -): DeviceNamespaces => { - const deviceNamespaces: Map = new Map() - - for (const [nameSpace, nameSpaceFields] of docRequest.itemsRequest.namespaces.entries()) { - const nsAttrs = issuerSigned.issuerNamespaces?.issuerNamespaces.get(nameSpace) ?? [] +): IssuerNamespace => { + const issuerNamespaces = new Map>() + for (const [namespace, nameSpaceFields] of docRequest.itemsRequest.namespaces.entries()) { + const nsAttrs = issuerSigned.issuerNamespaces?.issuerNamespaces.get(namespace) ?? [] const issuerSignedItems = Array.from(nameSpaceFields.entries()).map(([elementIdentifier, _]) => { const issuerSignedItem = prepareIssuerSignedItem(elementIdentifier, nsAttrs) if (!issuerSignedItem) { throw new Error(`No matching field found for '${elementIdentifier}'`) } - return issuerSignedItem }) - - const deviceSignedItems = new Map() - - for (const issuerSignedItem of issuerSignedItems) { - deviceSignedItems.set(issuerSignedItem.elementIdentifier, issuerSignedItem.elementValue) - } - - deviceNamespaces.set(nameSpace, new DeviceSignedItems({ deviceSignedItems })) + issuerNamespaces.set(namespace, issuerSignedItems) } - return new DeviceNamespaces({ deviceNamespaces }) + return new IssuerNamespace({ issuerNamespaces }) } const prepareIssuerSignedItem = ( @@ -144,8 +132,8 @@ export const findMdocMatchingDocType = (documents: Array, docType: Doc export const limitDisclosureToInputDescriptor = ( issuerSigned: IssuerSigned, inputDescriptor: InputDescriptor -): DeviceNamespaces => { - const deviceNamespaces: Map = new Map() +): IssuerNamespace => { + const issuerNamespaces = new Map>() for (const field of inputDescriptor.constraints.fields) { const result = prepareDigestForInputDescriptor(field.path, issuerSigned.issuerNamespaces) @@ -161,18 +149,9 @@ export const limitDisclosureToInputDescriptor = ( } const { namespace, digest } = result - const entry = deviceNamespaces.get(namespace) - if (!entry) { - deviceNamespaces.set(namespace, issuerSignedItemToDeviceSignedItems(digest)) - } else { - entry.deviceSignedItems.set(digest.elementIdentifier, digest.elementValue) - } + const entry = issuerNamespaces.get(namespace) + if (!entry) issuerNamespaces.set(namespace, [digest]) + else entry.push(digest) } - - return new DeviceNamespaces({ deviceNamespaces }) + return new IssuerNamespace({ issuerNamespaces }) } - -const issuerSignedItemToDeviceSignedItems = (issuerSignedItem: IssuerSignedItem) => - new DeviceSignedItems({ - deviceSignedItems: new Map([[issuerSignedItem.elementIdentifier, issuerSignedItem.elementValue]]), - }) diff --git a/src/mdoc/models/reader-auth.ts b/src/mdoc/models/reader-auth.ts index ef525cb..b15a83b 100644 --- a/src/mdoc/models/reader-auth.ts +++ b/src/mdoc/models/reader-auth.ts @@ -21,7 +21,7 @@ export class ReaderAuth extends Sign1 { return ReaderAuth.fromEncodedStructure(data) } - public async validate( + public async verify( options: { readerAuthentication: ReaderAuthentication | ReaderAuthenticationOptions verificationCallback?: VerificationCallback @@ -39,7 +39,7 @@ export class ReaderAuth extends Sign1 { this.detachedContent = readerAuthentication.encode({ asDataItem: true }) - const isValid = await this.verify({}, ctx) + const isValid = await this.verifySignature({}, ctx) onCheck({ status: isValid ? 'PASSED' : 'FAILED', diff --git a/src/mdoc/models/validity-info.ts b/src/mdoc/models/validity-info.ts index e4631a6..1d1002c 100644 --- a/src/mdoc/models/validity-info.ts +++ b/src/mdoc/models/validity-info.ts @@ -28,16 +28,16 @@ export class ValidityInfo extends CborStructure { this.expectedUpdate = options.expectedUpdate } - public validateSigned(notBefore: Date, notAfter: Date): boolean { + public verifySigned(notBefore: Date, notAfter: Date): boolean { const isWithinRange = this.signed < notBefore || this.signed > notAfter return isWithinRange } - public validateValidUntil(now: Date = new Date()): boolean { + public verifyValidUntil(now: Date = new Date()): boolean { return this.validUntil < now } - public validateValidFrom(now: Date = new Date()): boolean { + public verifyValidFrom(now: Date = new Date()): boolean { return this.validFrom < now } diff --git a/src/verifier.ts b/src/verifier.ts index 4e49520..d9f558f 100644 --- a/src/verifier.ts +++ b/src/verifier.ts @@ -27,6 +27,6 @@ export class Verifier { ? options.deviceResponse : DeviceResponse.decode(options.deviceResponse) - await deviceResponse.validate(options, ctx) + await deviceResponse.verify(options, ctx) } } diff --git a/tests/builders/issuer-signed-builder.test.ts b/tests/builders/issuer-signed-builder.test.ts index 5b486cf..8407e4b 100644 --- a/tests/builders/issuer-signed-builder.test.ts +++ b/tests/builders/issuer-signed-builder.test.ts @@ -58,14 +58,14 @@ describe('issuer signed builder', () => { expect(issuerSigned.issuerNamespaces?.issuerNamespaces.has('org.iso.18013.5.1')).toBeTruthy() expect(issuerSigned.issuerAuth.signature).toBeDefined() - const verificationResult = await issuerSigned.issuerAuth.verify({}, mdocContext) + const verificationResult = await issuerSigned.issuerAuth.verifySignature({}, mdocContext) expect(verificationResult).toBeTruthy() }) test('verify issuer signature', async () => { await expect( - issuerSigned.issuerAuth.validate( + issuerSigned.issuerAuth.verify( { trustedCertificates: [new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData)], }, diff --git a/tests/context.ts b/tests/context.ts index 5fe8121..492bb2b 100644 --- a/tests/context.ts +++ b/tests/context.ts @@ -87,9 +87,10 @@ export const mdocContext: MdocContext = { return CoseKey.fromJwk((await exportJWK(key)) as unknown as Record) }, - validateCertificateChain: async (input: { + verifyCertificateChain: async (input: { trustedCertificates: Array x5chain: Array + now?: Date }) => { const { trustedCertificates, x5chain: certificateChain } = input if (certificateChain.length === 0) throw new Error('Certificate chain is empty') @@ -132,7 +133,7 @@ export const mdocContext: MdocContext = { const cert = parsedChain[i] const previousCertificate = parsedChain[i - 1] const publicKey = previousCertificate ? previousCertificate.publicKey : undefined - await cert?.verify({ publicKey, date: new Date() }) + await cert?.verify({ publicKey, date: input.now ?? new Date() }) } }, getCertificateData: async (input: { certificate: Uint8Array }) => { diff --git a/tests/cose/sign1.test.ts b/tests/cose/sign1.test.ts index a7762e3..ddd79fd 100644 --- a/tests/cose/sign1.test.ts +++ b/tests/cose/sign1.test.ts @@ -35,12 +35,12 @@ describe('sign1', () => { expect(tbsHex).toStrictEqual(testVector['sign1::sign'].tbsHex.cborHex) - const isValid = await sign1.verify({ key }, mdocContext) + const isValid = await sign1.verifySignature({ key }, mdocContext) expect(isValid).toBeTruthy() await sign1.addSignature({ signingKey: key }, mdocContext) - const isValidAfterResign = await sign1.verify({ key }, mdocContext) + const isValidAfterResign = await sign1.verifySignature({ key }, mdocContext) expect(isValidAfterResign).toBeTruthy() }) }) diff --git a/tests/examples/bdr/verify.test.ts b/tests/examples/bdr/verify.test.ts index 6ce4c08..a6538e8 100644 --- a/tests/examples/bdr/verify.test.ts +++ b/tests/examples/bdr/verify.test.ts @@ -8,7 +8,7 @@ describe('BDR mDL implementation', () => { it('should verify mDL IssuerSigned from BDR', async () => { const issuerSigned = IssuerSigned.decode(issuerSignedBytes) - await issuerSigned.issuerAuth.validate( + await issuerSigned.issuerAuth.verify( { trustedCertificates: [new Uint8Array(issuerCertificate.rawData)], disableCertificateChainValidation: false, diff --git a/tests/examples/france/verify.test.ts b/tests/examples/france/verify.test.ts index f1d253c..c0cc345 100644 --- a/tests/examples/france/verify.test.ts +++ b/tests/examples/france/verify.test.ts @@ -18,7 +18,7 @@ describe('French playground mdoc implementation', () => { await expect( async () => - await DeviceResponse.decode(deviceResponse).validate( + await DeviceResponse.decode(deviceResponse).verify( { trustedCertificates: [new Uint8Array(issuerCertificate.rawData)], sessionTranscript: await SessionTranscript.calculateSessionTranscriptBytesForOid4VpDraft18( diff --git a/tests/examples/google/verify.test.ts b/tests/examples/google/verify.test.ts index a3057f0..4eb023a 100644 --- a/tests/examples/google/verify.test.ts +++ b/tests/examples/google/verify.test.ts @@ -18,7 +18,7 @@ describe('Google CM Wallet mdoc implementation', () => { await expect( async () => - await DeviceResponse.decode(deviceResponse).validate( + await DeviceResponse.decode(deviceResponse).verify( { trustedCertificates: [ new Uint8Array(rootCertificate.rawData), diff --git a/tests/examples/ubique/verify.test.ts b/tests/examples/ubique/verify.test.ts index b0464cf..e90d4ef 100644 --- a/tests/examples/ubique/verify.test.ts +++ b/tests/examples/ubique/verify.test.ts @@ -18,7 +18,7 @@ describe('Ubique mdoc implementation', () => { await expect( async () => - await DeviceResponse.decode(deviceResponse).validate( + await DeviceResponse.decode(deviceResponse).verify( { trustedCertificates: [new Uint8Array(issuerCertificate.rawData)], sessionTranscript: await SessionTranscript.calculateSessionTranscriptBytesForOid4VpDraft18( diff --git a/tests/issueAndVerify.test.ts b/tests/issueAndVerify.test.ts index 79dcb7f..8a52ab4 100644 --- a/tests/issueAndVerify.test.ts +++ b/tests/issueAndVerify.test.ts @@ -36,7 +36,7 @@ describe('Issue And Verify', () => { family_name: 'Doe', }) - const isSignatureValid = await issuerSigned.issuerAuth.verify({}, mdocContext) + const isSignatureValid = await issuerSigned.issuerAuth.verifySignature({}, mdocContext) expect(isSignatureValid).toBeTruthy() }) @@ -44,7 +44,7 @@ describe('Issue And Verify', () => { test('receive mdoc', async () => { const issuerSigned = IssuerSigned.fromEncodedForOid4Vci(encodedIssuerSigned) - const isSignatureValid = await issuerSigned.issuerAuth.verify({}, mdocContext) + const isSignatureValid = await issuerSigned.issuerAuth.verifySignature({}, mdocContext) expect(isSignatureValid).toBeTruthy() }) })