From 0992abc8fc1ac2ba8bb4fd18d866632f2acf9013 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 3 Feb 2026 13:18:00 +0100 Subject: [PATCH 1/2] docs(changeset): feat: support more than one certificate in the certificate chain when signing an mdoc Signed-off-by: Timo Glastra --- .changeset/bumpy-crabs-read.md | 5 +++ src/issuer.ts | 2 +- src/mdoc/builders/issuer-signed-builder.ts | 6 ++-- tests/builders/issuer-signed-builder.test.ts | 32 ++++++++++++++++++-- tests/issuing/issuance.test.ts | 4 +-- tests/models/issuer-signed.test.ts | 2 +- tests/verification/verify.test.ts | 12 ++++---- 7 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 .changeset/bumpy-crabs-read.md diff --git a/.changeset/bumpy-crabs-read.md b/.changeset/bumpy-crabs-read.md new file mode 100644 index 0000000..de4fb4d --- /dev/null +++ b/.changeset/bumpy-crabs-read.md @@ -0,0 +1,5 @@ +--- +"@animo-id/mdoc": minor +--- + +feat: support more than one certificate in the certificate chain when signing an mdoc diff --git a/src/issuer.ts b/src/issuer.ts index a853f3e..0c56b40 100644 --- a/src/issuer.ts +++ b/src/issuer.ts @@ -29,7 +29,7 @@ export class Issuer { digestAlgorithm: DigestAlgorithm validityInfo: ValidityInfo | ValidityInfoOptions deviceKeyInfo: DeviceKeyInfo | DeviceKeyInfoOptions - certificate: Uint8Array + certificates: [Uint8Array, ...Uint8Array[]] }): Promise { const signingKey = options.signingKey instanceof CoseKey ? options.signingKey : CoseKey.fromJwk(options.signingKey) return await this.isb.sign({ ...options, signingKey }) diff --git a/src/mdoc/builders/issuer-signed-builder.ts b/src/mdoc/builders/issuer-signed-builder.ts index 1593e96..c89e3f0 100644 --- a/src/mdoc/builders/issuer-signed-builder.ts +++ b/src/mdoc/builders/issuer-signed-builder.ts @@ -82,7 +82,7 @@ export class IssuerSignedBuilder { digestAlgorithm: DigestAlgorithm validityInfo: ValidityInfo | ValidityInfoOptions deviceKeyInfo: DeviceKeyInfo | DeviceKeyInfoOptions - certificate: Uint8Array + certificates: [Uint8Array, ...Uint8Array[]] }): Promise { const validityInfo = options.validityInfo instanceof ValidityInfo ? options.validityInfo : ValidityInfo.create(options.validityInfo) @@ -105,7 +105,9 @@ export class IssuerSignedBuilder { }) const unprotectedHeaders = UnprotectedHeaders.create({ - unprotectedHeaders: new Map([[Header.X5Chain, options.certificate]]), + unprotectedHeaders: new Map([ + [Header.X5Chain, options.certificates.length === 1 ? options.certificates[0] : options.certificates], + ]), }) if (options.signingKey.keyId) { diff --git a/tests/builders/issuer-signed-builder.test.ts b/tests/builders/issuer-signed-builder.test.ts index 0c5e615..7b8f952 100644 --- a/tests/builders/issuer-signed-builder.test.ts +++ b/tests/builders/issuer-signed-builder.test.ts @@ -1,6 +1,6 @@ import { X509Certificate } from '@peculiar/x509' import { describe, expect, test } from 'vitest' -import { CoseKey, DateOnly, DeviceKey, type IssuerSigned, SignatureAlgorithm } from '../../src' +import { CoseKey, DateOnly, DeviceKey, IssuerSigned, SignatureAlgorithm } from '../../src' import { IssuerSignedBuilder } from '../../src/mdoc/builders/issuer-signed-builder' import { DEVICE_JWK_PUBLIC, ISSUER_CERTIFICATE, ISSUER_PRIVATE_KEY_JWK } from '../config' import { mdocContext } from '../context' @@ -49,7 +49,7 @@ describe('issuer signed builder', () => { issuerSigned = await issuerSignedBuilder.sign({ signingKey: coseKey, - certificate: new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData), + certificates: [new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData)], algorithm: SignatureAlgorithm.ES256, digestAlgorithm: 'SHA-256', deviceKeyInfo: { deviceKey: DeviceKey.fromJwk(DEVICE_JWK_PUBLIC) }, @@ -101,4 +101,32 @@ describe('issuer signed builder', () => { const prettyClaims = issuerSigned.getPrettyClaims('org.iso.18013.5.1') expect(prettyClaims).toEqual(claims) }) + + test('should support certificate chain with multiple certificates', async () => { + const issuerSignedBuilder = new IssuerSignedBuilder('org.iso.18013.5.1.mDL', mdocContext).addIssuerNamespace( + 'org.iso.18013.5.1', + { family_name: 'Smith' } + ) + + const cert1 = new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData) + const cert2 = new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData) + + const issuerSignedWithChain = await issuerSignedBuilder.sign({ + signingKey: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), + certificates: [cert1, cert2], + algorithm: SignatureAlgorithm.ES256, + digestAlgorithm: 'SHA-256', + deviceKeyInfo: { deviceKey: DeviceKey.fromJwk(DEVICE_JWK_PUBLIC) }, + validityInfo: { signed, validFrom, validUntil }, + }) + + expect(issuerSignedWithChain.issuerAuth.certificateChain).toHaveLength(2) + expect(issuerSignedWithChain.issuerAuth.certificateChain[0]).toEqual(cert1) + expect(issuerSignedWithChain.issuerAuth.certificateChain[1]).toEqual(cert2) + + // Verify that the certificate chain can be decoded correctly + const encodedChain = issuerSignedWithChain.encode() + const decodedChain = IssuerSigned.decode(encodedChain) + expect(decodedChain.issuerAuth.certificateChain).toHaveLength(2) + }) }) diff --git a/tests/issuing/issuance.test.ts b/tests/issuing/issuance.test.ts index eec1864..9ce600d 100644 --- a/tests/issuing/issuance.test.ts +++ b/tests/issuing/issuance.test.ts @@ -21,7 +21,7 @@ suite('Issuance', () => { const issuerSigned = await issuer.sign({ signingKey: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), - certificate: new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData), + certificates: [new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData)], algorithm: SignatureAlgorithm.ES256, digestAlgorithm: 'SHA-256', deviceKeyInfo: { deviceKey: DeviceKey.fromJwk(DEVICE_JWK_PUBLIC) }, @@ -59,7 +59,7 @@ suite('Issuance', () => { const issuerSigned = await issuer.sign({ signingKey: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), - certificate: new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData), + certificates: [new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData)], algorithm: SignatureAlgorithm.ES256, digestAlgorithm: 'SHA-256', deviceKeyInfo: { deviceKey: DeviceKey.fromJwk(DEVICE_JWK_PUBLIC) }, diff --git a/tests/models/issuer-signed.test.ts b/tests/models/issuer-signed.test.ts index 2ed8601..a8b722e 100644 --- a/tests/models/issuer-signed.test.ts +++ b/tests/models/issuer-signed.test.ts @@ -22,7 +22,7 @@ describe('Issuer signed', () => { const issuerSigned = await isb.sign({ signingKey: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), - certificate: new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData), + certificates: [new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData)], algorithm: SignatureAlgorithm.ES256, digestAlgorithm: 'SHA-256', deviceKeyInfo: { deviceKey: DeviceKey.fromJwk(DEVICE_JWK_PUBLIC) }, diff --git a/tests/verification/verify.test.ts b/tests/verification/verify.test.ts index 3486f28..97c3ed9 100644 --- a/tests/verification/verify.test.ts +++ b/tests/verification/verify.test.ts @@ -36,7 +36,7 @@ suite('Verification', () => { const issuerSigned = await issuer.sign({ signingKey: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), - certificate: new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData), + certificates: [new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData)], algorithm: SignatureAlgorithm.ES256, digestAlgorithm: 'SHA-256', deviceKeyInfo: { deviceKey: DeviceKey.fromJwk(DEVICE_JWK_PUBLIC) }, @@ -122,7 +122,7 @@ suite('Verification', () => { const issuerSigned = await issuer.sign({ signingKey: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), - certificate: new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData), + certificates: [new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData)], algorithm: SignatureAlgorithm.ES256, digestAlgorithm: 'SHA-256', deviceKeyInfo: { deviceKey: DeviceKey.fromJwk(DEVICE_JWK_PUBLIC) }, @@ -214,7 +214,7 @@ suite('Verification', () => { const issuerSigned = await issuer.sign({ signingKey: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), - certificate: new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData), + certificates: [new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData)], algorithm: SignatureAlgorithm.ES256, digestAlgorithm: 'SHA-256', deviceKeyInfo: { deviceKey: DeviceKey.fromJwk(DEVICE_JWK_PUBLIC) }, @@ -301,7 +301,7 @@ suite('Verification', () => { const issuerSigned = await issuer.sign({ signingKey: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), - certificate: new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData), + certificates: [new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData)], algorithm: SignatureAlgorithm.ES256, digestAlgorithm: 'SHA-256', deviceKeyInfo: { deviceKey: DeviceKey.fromJwk(DEVICE_JWK_PUBLIC) }, @@ -378,7 +378,7 @@ suite('Verification', () => { const issuerSigned = await issuer.sign({ signingKey: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), - certificate: new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData), + certificates: [new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData)], algorithm: SignatureAlgorithm.ES256, digestAlgorithm: 'SHA-256', deviceKeyInfo: { deviceKey: DeviceKey.fromJwk(DEVICE_JWK_PUBLIC) }, @@ -424,7 +424,7 @@ suite('Verification', () => { const issuerSigned = await issuer.sign({ signingKey: CoseKey.fromJwk(ISSUER_PRIVATE_KEY_JWK), - certificate: new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData), + certificates: [new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData)], algorithm: SignatureAlgorithm.ES256, digestAlgorithm: 'SHA-256', deviceKeyInfo: { deviceKey: DeviceKey.fromJwk(DEVICE_JWK_PUBLIC) }, From dd98001dc7137ba6bd8136d19517bb65ef695514 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 3 Feb 2026 13:27:24 +0100 Subject: [PATCH 2/2] Update .changeset/bumpy-crabs-read.md --- .changeset/bumpy-crabs-read.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/bumpy-crabs-read.md b/.changeset/bumpy-crabs-read.md index de4fb4d..80e58a6 100644 --- a/.changeset/bumpy-crabs-read.md +++ b/.changeset/bumpy-crabs-read.md @@ -2,4 +2,4 @@ "@animo-id/mdoc": minor --- -feat: support more than one certificate in the certificate chain when signing an mdoc +feat: support more than one certificate in the certificate chain when signing an mdoc. The `certificate` parameter has been renamed to `certificates` and now expects an array with at least one certificate.