From 24c8e2c280ca3b2c7ec0b0315b8d6b3494f5dd30 Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Fri, 4 Oct 2024 12:07:57 +0200 Subject: [PATCH 01/16] fix!: remove the deprecated .usingHandover method BREAKING CHANGE: remove the deprecated .usingHandover method --- src/mdoc/model/DeviceResponse.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/mdoc/model/DeviceResponse.ts b/src/mdoc/model/DeviceResponse.ts index dd28284..7f6e961 100644 --- a/src/mdoc/model/DeviceResponse.ts +++ b/src/mdoc/model/DeviceResponse.ts @@ -64,23 +64,6 @@ export class DeviceResponse { 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}(``), - * which is what the OID4VP protocol expects. - * - * @deprecated Use {@link usingSessionTranscriptForOID4VP} instead. - * @param {string[]} handover - The handover data to use in the session transcript. - * @returns {DeviceResponse} - */ - public usingHandover(handover: string[]): DeviceResponse { - this.usingSessionTranscriptBytes(cborEncode(DataItem.fromData([ - null, // deviceEngagementBytes - null, // eReaderKeyBytes - handover]))); - return this; - } - /** * Set the session transcript data to use for the device response. * From 1733c66a0ad6b58cb69ccad979315758c8d8d69c Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Fri, 4 Oct 2024 12:27:49 +0200 Subject: [PATCH 02/16] feat!: calculating the session transcripts as defined in dash7:2024 BREAKING CHANGE: session transcripts are calculated differently so signature and MAC auth break. --- __tests__/issuing/deviceResponse.tests.ts | 10 ++++++---- __tests__/issuing/deviceResponseWithMac.tests.ts | 10 +++++++--- src/mdoc/model/DeviceResponse.ts | 9 +++++++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/__tests__/issuing/deviceResponse.tests.ts b/__tests__/issuing/deviceResponse.tests.ts index 68924c4..682887d 100644 --- a/__tests__/issuing/deviceResponse.tests.ts +++ b/__tests__/issuing/deviceResponse.tests.ts @@ -1,6 +1,5 @@ import { createHash, randomFillSync } from 'node:crypto'; import * as jose from 'jose'; -import { COSEKeyFromJWK } from 'cose-kit'; import { MDoc, Document, @@ -11,7 +10,6 @@ import { } from '../../src'; import { DEVICE_JWK, ISSUER_CERTIFICATE, ISSUER_PRIVATE_KEY_JWK, PRESENTATION_DEFINITION_1 } from './config'; import { DataItem, cborEncode } from '../../src/cbor'; -import COSEKeyToRAW from '../../src/cose/coseKey'; const { d, ...publicKeyJWK } = DEVICE_JWK as jose.JWK; @@ -83,7 +81,11 @@ describe('issuing a device response', () => { DataItem.fromData([ null, // DeviceEngagementBytes null, // EReaderKeyBytes - [mdocNonce, clId, respUri, nonce], // Handover = OID4VPHandover + [ + createHash('sha256').update(cborEncode([clId, mdocNonce])).digest(), + createHash('sha256').update(cborEncode([respUri, mdocNonce])).digest(), + nonce, + ], // Handover = OID4VPHandover ]), ); @@ -163,7 +165,7 @@ describe('issuing a device response', () => { DataItem.fromData([ new DataItem({ buffer: devEngtBytes }), new DataItem({ buffer: eRdrKeyBytes }), - rdrEngtBytes, + createHash('sha256').update(rdrEngtBytes).digest(), ]), ); diff --git a/__tests__/issuing/deviceResponseWithMac.tests.ts b/__tests__/issuing/deviceResponseWithMac.tests.ts index f2a5a01..f63416f 100644 --- a/__tests__/issuing/deviceResponseWithMac.tests.ts +++ b/__tests__/issuing/deviceResponseWithMac.tests.ts @@ -1,4 +1,4 @@ -import { randomFillSync } from 'node:crypto'; +import { createHash, randomFillSync } from 'node:crypto'; import * as jose from 'jose'; import { COSEKeyFromJWK } from 'cose-kit'; import { @@ -93,7 +93,11 @@ describe('issuing a device response with MAC authentication', () => { DataItem.fromData([ null, // DeviceEngagementBytes null, // EReaderKeyBytes - [mdocNonce, clId, respUri, nonce], // Handover = OID4VPHandover + [ + createHash('sha256').update(cborEncode([clId, mdocNonce])).digest(), + createHash('sha256').update(cborEncode([respUri, mdocNonce])).digest(), + nonce, + ], // Handover = OID4VPHandover ]), ); @@ -174,7 +178,7 @@ describe('issuing a device response with MAC authentication', () => { DataItem.fromData([ new DataItem({ buffer: devEngtBytes }), new DataItem({ buffer: eRdrKeyBytes }), - rdrEngtBytes, + createHash('sha256').update(rdrEngtBytes).digest(), ]), ); diff --git a/src/mdoc/model/DeviceResponse.ts b/src/mdoc/model/DeviceResponse.ts index 7f6e961..575a6ad 100644 --- a/src/mdoc/model/DeviceResponse.ts +++ b/src/mdoc/model/DeviceResponse.ts @@ -1,3 +1,4 @@ +import { createHash } from 'node:crypto'; import * as jose from 'jose'; import { COSEKeyFromJWK, COSEKeyToJWK, Mac0, Sign1, importCOSEKey } from 'cose-kit'; import { Buffer } from 'buffer'; @@ -109,7 +110,11 @@ export class DeviceResponse { DataItem.fromData([ null, // deviceEngagementBytes null, // eReaderKeyBytes - [mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce], + [ + createHash('sha256').update(cborEncode([clientId, mdocGeneratedNonce])).digest(), + createHash('sha256').update(cborEncode([responseUri, mdocGeneratedNonce])).digest(), + verifierGeneratedNonce, + ], ]), ), ); @@ -136,7 +141,7 @@ export class DeviceResponse { DataItem.fromData([ new DataItem({ buffer: deviceEngagementBytes }), new DataItem({ buffer: eReaderKeyBytes }), - readerEngagementBytes, + createHash('sha256').update(readerEngagementBytes).digest(), ]), ), ); From 3d34245dff1023cc857cadf1d346950142003a9b Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Fri, 4 Oct 2024 14:58:42 +0200 Subject: [PATCH 03/16] docs: update README.MD --- README.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/README.md b/README.md index 6de2b02..cfbade9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [ISO 18013-5](https://www.iso.org/standard/69084.html) defines mDL (mobile Driver Licenses): an ISO standard for digital driver licenses. -This is a Node.js library to issue and verify mDL [CBOR encoded](https://cbor.io/) documents in accordance with **ISO 18013-7 (draft's date: 2023-08-02)**. +This is a Node.js library to issue and verify mDL [CBOR encoded](https://cbor.io/) documents in accordance with **ISO 18013-7 (draft's date: 2024-02-13)**. ## Installation @@ -36,8 +36,6 @@ import fs from "node:fs"; ## Getting diagnostic information - - ```javascript import { Verifier } from "@auth0/mdl"; import { inspect } from "node:util"; @@ -171,13 +169,6 @@ import { createHash } from 'node:crypto'; createHash('sha256').update(encodedReaderEngagement).digest('hex'), 'hex', ); - const sessionTranscriptBytes = cborEncode( - DataItem.fromData([ - new DataItem({ buffer: encodedDeviceEngagement }), - new DataItem({ buffer: encodedReaderPublicKey }), - engagementToApp, - ]), - ); deviceResponseMDoc = await DeviceResponse.from(issuerMDoc) .usingPresentationDefinition(presentationDefinition) From 2a0e9f844a557d225313d20dc8ba28237b03dccb Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Fri, 4 Oct 2024 15:31:59 +0200 Subject: [PATCH 04/16] docs: add a warning in the README about versions --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cfbade9..4f0743b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ [ISO 18013-5](https://www.iso.org/standard/69084.html) defines mDL (mobile Driver Licenses): an ISO standard for digital driver licenses. -This is a Node.js library to issue and verify mDL [CBOR encoded](https://cbor.io/) documents in accordance with **ISO 18013-7 (draft's date: 2024-02-13)**. +> [!WARNING] This is a Node.js library to issue and verify mDL [CBOR encoded](https://cbor.io/) documents in accordance with **ISO 18013-7 (draft's date: 2024-02-13)**. + +> [!WARNING] please use v1.*.* to use **ISO 18013-7 (draft's date: 2023-08-02)**. ## Installation From 5e1c8859782a7901a11b1163b49702e3ceac0574 Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Fri, 4 Oct 2024 17:02:36 +0200 Subject: [PATCH 05/16] docs: fix method documentation --- src/mdoc/model/DeviceResponse.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mdoc/model/DeviceResponse.ts b/src/mdoc/model/DeviceResponse.ts index 575a6ad..fbf87dd 100644 --- a/src/mdoc/model/DeviceResponse.ts +++ b/src/mdoc/model/DeviceResponse.ts @@ -89,7 +89,7 @@ export class DeviceResponse { } /** - * Set the session transcript data to use for the device response as defined in ISO/IEC 18013-7 in Annex B (OID4VP), 2023 draft. + * Set the session transcript data to use for the device response as defined in ISO/IEC 18013-7 in Annex B (OID4VP), 2024 draft. * * This should match the session transcript as it will be calculated by the verifier. * @@ -122,7 +122,7 @@ export class DeviceResponse { } /** - * Set the session transcript data to use for the device response as defined in ISO/IEC 18013-7 in Annex A (Web API), 2023 draft. + * Set the session transcript data to use for the device response as defined in ISO/IEC 18013-7 in Annex A (Web API), 2024 draft. * * This should match the session transcript as it will be calculated by the verifier. * From 75afd032479e28c54045df61ac5d35f748ff9885 Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Fri, 4 Oct 2024 18:04:15 +0200 Subject: [PATCH 06/16] feat!: use Builder Pattern for the Verifier BREAKING CHANGE: Verifier#verify options are moved to a builder pattern --- README.md | 9 +- __tests__/diagnostic.tests.ts | 10 +- .../example/disableCertVerification.tests.ts | 22 +-- __tests__/example/example1.tests.ts | 26 +-- __tests__/example/example2.tests.ts | 45 +++-- __tests__/example/example3.tests.ts | 37 ++-- __tests__/example/example4.tests.ts | 18 +- __tests__/issuing/deviceResponse.tests.ts | 42 ++-- .../issuing/deviceResponseWithMac.tests.ts | 65 ++----- __tests__/issuing/issuingMDoc.tests.ts | 7 +- __tests__/verifier.tests.ts | 29 +-- src/mdoc/Verifier.ts | 184 +++++++++++++----- 12 files changed, 284 insertions(+), 210 deletions(-) diff --git a/README.md b/README.md index 4f0743b..1a47cee 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,11 @@ import fs from "node:fs"; const trustedCerts = [fs.readFileSync('./caCert1.pem')/*, ... */]; const verifier = new Verifier(trustedCerts); - const diagnosticInfo = await verifier.getDiagnosticInformation(encodedDeviceResponse, { - ephemeralReaderKey, - encodedSessionTranscript, - }); + + const diagnosticInfo = await verifier + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .getDiagnosticInformation(encodedDeviceResponse); inspect(diagnosticInfo); })(); diff --git a/__tests__/diagnostic.tests.ts b/__tests__/diagnostic.tests.ts index 052c109..17700da 100644 --- a/__tests__/diagnostic.tests.ts +++ b/__tests__/diagnostic.tests.ts @@ -25,7 +25,10 @@ describe('diagnostic info', () => { let diagnosticInfo: DiagnosticInformation; beforeAll(async () => { - diagnosticInfo = await verifier.getDiagnosticInformation(deviceResponse, { ephemeralReaderKey, encodedSessionTranscript }); + diagnosticInfo = await verifier + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .getDiagnosticInformation(deviceResponse); }); it('should return the version', async () => { @@ -34,7 +37,10 @@ describe('diagnostic info', () => { it('should return the invalid signature reason when not providing the root certs', async () => { const verifier2 = new Verifier([]); - diagnosticInfo = await verifier2.getDiagnosticInformation(deviceResponse, { ephemeralReaderKey, encodedSessionTranscript }); + diagnosticInfo = await verifier2 + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .getDiagnosticInformation(deviceResponse); expect(diagnosticInfo.issuerSignature.reasons).toEqual([ 'No valid certificate paths found', ]); diff --git a/__tests__/example/disableCertVerification.tests.ts b/__tests__/example/disableCertVerification.tests.ts index a524948..0f6cf59 100644 --- a/__tests__/example/disableCertVerification.tests.ts +++ b/__tests__/example/disableCertVerification.tests.ts @@ -5,22 +5,22 @@ describe('example 1: valid device response with full disclosure', () => { const ephemeralReaderKey = hex`534b526561646572`; const encodedSessionTranscript = hex`d818589e83f6f68466313233343536782b437131616e506238765a55356a354330643768637362754a4c4270496177554a4944515269324562776234785c687474703a2f2f6c6f63616c686f73743a343030302f6170692f70726573656e746174696f6e5f726571756573742f64633839393964662d643665612d346338342d393938352d3337613862383161383265632f63616c6c6261636b6761626364656667`; const deviceResponse = hex`b900036776657273696f6e63312e3069646f63756d656e747381b9000367646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c6973737565725369676e6564b900026a6e616d65537061636573b90001716f72672e69736f2e31383031332e352e318cd8185867b90004686469676573744944006672616e646f6d5820fad69ca9e366c6a46ed929673544229d1156f3796dc41ffb72165d545524ce9971656c656d656e744964656e7469666965726b66616d696c795f6e616d656c656c656d656e7456616c756565536d697468d8185865b90004686469676573744944016672616e646f6d5820983235f38aa7fd6fe8d5412ae57d2a5d356a9969e7363e3e2bb82e0773efd92971656c656d656e744964656e7469666965726a676976656e5f6e616d656c656c656d656e7456616c7565644a6f686ed818586eb90004686469676573744944026672616e646f6d58201e7fcaec40dec4e9d926bf535052dfded9402925c49b3eb11142817273f0333f71656c656d656e744964656e7469666965726a62697274685f646174656c656c656d656e7456616c7565d903ec6a313938302d30362d3135d818586eb90004686469676573744944036672616e646f6d5820e51bbab7c72f05cbe7ff15678423bc871463b87fa4d403ad857cd8cbe6e4c6d371656c656d656e744964656e7469666965726a69737375655f646174656c656c656d656e7456616c7565d903ec6a323032332d30332d3031d818586fb90004686469676573744944046672616e646f6d5820e4a404254912d132825ca342eded2244eda47dd17d57f6896a106a3ff047891971656c656d656e744964656e7469666965726b6578706972795f646174656c656c656d656e7456616c7565d903ec6a323032382d30332d3331d8185868b90004686469676573744944056672616e646f6d58204b330bd5f9d3aa04e02cf3d4c1145121540a24782fc9cea7bb151691da8aaf7571656c656d656e744964656e7469666965726f69737375696e675f636f756e7472796c656c656d656e7456616c7565625553d818586eb90004686469676573744944066672616e646f6d5820c01e8d0f5e092a2aaa30b8253df565a30007dad4bc6102e1e1773e40b184619b71656c656d656e744964656e7469666965727169737375696e675f617574686f726974796c656c656d656e7456616c7565664e5920444d56d8185873b90004686469676573744944076672616e646f6d58204ffc6ec7a24204bcd27227c0910f9e9a2e8074cee956755e7a7c67c6d490a8d971656c656d656e744964656e7469666965727469737375696e675f6a7572697364696374696f6e6c656c656d656e7456616c7565684e657720596f726bd8185871b90004686469676573744944086672616e646f6d5820265e44bd5303560af9565f4c69e871062d1e8c599800cc0d795e81f8c59b5fbc71656c656d656e744964656e7469666965726f646f63756d656e745f6e756d6265726c656c656d656e7456616c75656b30312d3333332d37303730d8185863b90004686469676573744944096672616e646f6d5820cfe31db70e2dc9275f4d8314a895358899e9adc0d0e51920e028285536e833bd71656c656d656e744964656e74696669657268706f7274726169746c656c656d656e7456616c75656462737472d81858b1b900046864696765737449440a6672616e646f6d5820c4916d6ea767ee1fe70fea90a46f9c6349cba3be36781271c1ef00dbdf55ce0f71656c656d656e744964656e7469666965727264726976696e675f70726976696c656765736c656c656d656e7456616c756581b900037576656869636c655f63617465676f72795f636f646561436a69737375655f646174656a323032332d30332d30316b6578706972795f646174656a323032382d30332d3331d818587ab900046864696765737449440b6672616e646f6d58204be3c184ead50cd540b79a3e854eb007371de1c8c84af62cbf049bf180e56af371656c656d656e744964656e74696669657276756e5f64697374696e6775697368696e675f7369676e6c656c656d656e7456616c75656d7462642d75732e6e792e646d766a697373756572417574688443a10126a20442313118218159022e3082022a308201d0a003020102021457c6ccd308bde43eca3744f2a87138dabbb884e8300a06082a8648ce3d0403023053310b30090603550406130255533111300f06035504080c084e657720596f726b310f300d06035504070c06416c62616e79310f300d060355040a0c064e5920444d56310f300d060355040b0c064e5920444d56301e170d3233303931343134353531385a170d3333303931313134353531385a3053310b30090603550406130255533111300f06035504080c084e657720596f726b310f300d06035504070c06416c62616e79310f300d060355040a0c064e5920444d56310f300d060355040b0c064e5920444d563059301306072a8648ce3d020106082a8648ce3d03010703420004893c2d8347906dc6cd69b7f636af4bfd533f96184f0aadacd10830da4471dbdb60ac170d1cfc534fae2d9dcd488f7747fdf978d925ea31e9e9083c382ba9ed53a38181307f301d0603551d0e04160414ab6d2e03b91d492240338fbccadefd9333eaf6c7301f0603551d23041830168014ab6d2e03b91d492240338fbccadefd9333eaf6c7300f0603551d130101ff040530030101ff302c06096086480186f842010d041f161d4f70656e53534c2047656e657261746564204365727469666963617465300a06082a8648ce3d0403020348003045022009fd0cab97b03e78f64e74d7dcee88668c476a0afc5aa2cebffe07d3be772ea9022100da38abc98a080f49f24ffece1fffc8a6cdd5b2c0b5da8fc7b767ac3a95dcb83e590319d818590314b900066776657273696f6e63312e306f646967657374416c676f726974686d675348412d3235366c76616c756544696765737473b900026f6f72672e637573746f6d2e74657374a10058203c5116109c068fa7fbf3186baec13ee6cbe48cc6c4a00fd349b9faf0a04f2ffd716f72672e69736f2e31383031332e352e31ac00582083c653c2e7c7d2e0f072f4269a20f4eb35d438f97a1c93026aa124fb707e1479015820a70c14943a002ba0675efa295e985e356197764f4591656cb6b3241b6cbb5f1e025820f0c5dcf0eac39eda62f4f52e52692ffdc09c133f4406008696843501e5364ef903582094f3cc14be210ace4f9d4545948074faf3a0138a2d1041adac45ccae1f1208d2045820c07ad9922535003e47887654eaae055667565c7bd19a8975f0336865f82bf0f505582063cb0912892fbcd761e85b8048caa954bc0db60e3678a9759020445b09a4623e065820052c825478b90e5018e857a3ae90c8dde34f5fe5056a143db77d7cd6107df7c9075820a00f3e3094c3d2ea5d4360f260d46f48ddadabcb49e2c2b2850f2913fdc4e8aa085820aea1b320b97e7922476c68008d275ecf4904ea09d72b950de7a6054bda6e78b7095820f764a3f5d0d998b73505ef1dc18eb15d77acd12b2e2100e59001ab9c0685da540a5820a1dd0ffbd7cff2fe1ee44523d35dbbbdd5c9b59c6ae00ad6dd785554978898c90b5820fd2c88594d6594872b87ac5d5c43d97211a73ffd348a0aaea71e54ef1dc2fbc96d6465766963654b6579496e666fb90001696465766963654b6579a40102215820881879ca7a238b19bf0f4c1f8c00e9a2e19ba7a6f73eae92b851d4de1b508559225820a314b538039127b5cd50735f54519e33c134450545c5603ad9f263facc56d377200167646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c76616c6964697479496e666fb90003667369676e6564c074323032332d30392d32395431353a34313a35395a6976616c696446726f6dc074323032332d30392d32395431353a34313a35395a6a76616c6964556e74696cc074323037332d30392d32395431353a34313a35395a5840d82c982b492614bc04241484206da61b1cff64bfa0d31b550c09096bc62f26c13b4ef2cad4b87d9e73717620001db2917e78da85896d046e253595af9f42a5946c6465766963655369676e6564b900026a6e616d65537061636573d81841a06a64657669636541757468b900016f6465766963655369676e61747572658443a10126a10442313158d2d81858ce847444657669636541757468656e7469636174696f6e83f6f68466313233343536782b437131616e506238765a55356a354330643768637362754a4c4270496177554a4944515269324562776234785c687474703a2f2f6c6f63616c686f73743a343030302f6170692f70726573656e746174696f6e5f726571756573742f64633839393964662d643665612d346338342d393938352d3337613862383161383265632f63616c6c6261636b6761626364656667756f72672e69736f2e31383031332e352e312e6d444cd81841a05840a97b5f9b59d193eda662542e71cb7daf6fa8952ac8b0cce82f22e7834d1fa8f0d2cab25a708658226c03d96950d00c3516fce62a220748d4f0a696072243b5026673746174757300`; - const verifier = new Verifier([]); it('should verify properly', async () => { - await verifier.verify(deviceResponse, { - ephemeralReaderKey, - encodedSessionTranscript, - disableCertificateChainValidation: true, - }); + await new Verifier([]) + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .disableCertificateChainValidation() + .verify(deviceResponse); }); it('should return blabla ', async () => { - const diagnostic = await verifier.getDiagnosticInformation(deviceResponse, { - ephemeralReaderKey, - encodedSessionTranscript, - disableCertificateChainValidation: true, - }); + const diagnostic = + await new Verifier([]) + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .disableCertificateChainValidation() + .getDiagnosticInformation(deviceResponse); expect(diagnostic.issuerSignature.isValid).toBe(true); }); }); diff --git a/__tests__/example/example1.tests.ts b/__tests__/example/example1.tests.ts index 9738cd6..ecc0696 100644 --- a/__tests__/example/example1.tests.ts +++ b/__tests__/example/example1.tests.ts @@ -11,23 +11,23 @@ describe('example 1: valid device response with full disclosure', () => { const ephemeralReaderKey = hex`534b526561646572`; const encodedSessionTranscript = hex`d818589e83f6f68466313233343536782b437131616e506238765a55356a354330643768637362754a4c4270496177554a4944515269324562776234785c687474703a2f2f6c6f63616c686f73743a343030302f6170692f70726573656e746174696f6e5f726571756573742f64633839393964662d643665612d346338342d393938352d3337613862383161383265632f63616c6c6261636b6761626364656667`; const deviceResponse = hex`b900036776657273696f6e63312e3069646f63756d656e747381b9000367646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c6973737565725369676e6564b900026a6e616d65537061636573b90001716f72672e69736f2e31383031332e352e318cd8185867b90004686469676573744944006672616e646f6d5820fad69ca9e366c6a46ed929673544229d1156f3796dc41ffb72165d545524ce9971656c656d656e744964656e7469666965726b66616d696c795f6e616d656c656c656d656e7456616c756565536d697468d8185865b90004686469676573744944016672616e646f6d5820983235f38aa7fd6fe8d5412ae57d2a5d356a9969e7363e3e2bb82e0773efd92971656c656d656e744964656e7469666965726a676976656e5f6e616d656c656c656d656e7456616c7565644a6f686ed818586eb90004686469676573744944026672616e646f6d58201e7fcaec40dec4e9d926bf535052dfded9402925c49b3eb11142817273f0333f71656c656d656e744964656e7469666965726a62697274685f646174656c656c656d656e7456616c7565d903ec6a313938302d30362d3135d818586eb90004686469676573744944036672616e646f6d5820e51bbab7c72f05cbe7ff15678423bc871463b87fa4d403ad857cd8cbe6e4c6d371656c656d656e744964656e7469666965726a69737375655f646174656c656c656d656e7456616c7565d903ec6a323032332d30332d3031d818586fb90004686469676573744944046672616e646f6d5820e4a404254912d132825ca342eded2244eda47dd17d57f6896a106a3ff047891971656c656d656e744964656e7469666965726b6578706972795f646174656c656c656d656e7456616c7565d903ec6a323032382d30332d3331d8185868b90004686469676573744944056672616e646f6d58204b330bd5f9d3aa04e02cf3d4c1145121540a24782fc9cea7bb151691da8aaf7571656c656d656e744964656e7469666965726f69737375696e675f636f756e7472796c656c656d656e7456616c7565625553d818586eb90004686469676573744944066672616e646f6d5820c01e8d0f5e092a2aaa30b8253df565a30007dad4bc6102e1e1773e40b184619b71656c656d656e744964656e7469666965727169737375696e675f617574686f726974796c656c656d656e7456616c7565664e5920444d56d8185873b90004686469676573744944076672616e646f6d58204ffc6ec7a24204bcd27227c0910f9e9a2e8074cee956755e7a7c67c6d490a8d971656c656d656e744964656e7469666965727469737375696e675f6a7572697364696374696f6e6c656c656d656e7456616c7565684e657720596f726bd8185871b90004686469676573744944086672616e646f6d5820265e44bd5303560af9565f4c69e871062d1e8c599800cc0d795e81f8c59b5fbc71656c656d656e744964656e7469666965726f646f63756d656e745f6e756d6265726c656c656d656e7456616c75656b30312d3333332d37303730d8185863b90004686469676573744944096672616e646f6d5820cfe31db70e2dc9275f4d8314a895358899e9adc0d0e51920e028285536e833bd71656c656d656e744964656e74696669657268706f7274726169746c656c656d656e7456616c75656462737472d81858b1b900046864696765737449440a6672616e646f6d5820c4916d6ea767ee1fe70fea90a46f9c6349cba3be36781271c1ef00dbdf55ce0f71656c656d656e744964656e7469666965727264726976696e675f70726976696c656765736c656c656d656e7456616c756581b900037576656869636c655f63617465676f72795f636f646561436a69737375655f646174656a323032332d30332d30316b6578706972795f646174656a323032382d30332d3331d818587ab900046864696765737449440b6672616e646f6d58204be3c184ead50cd540b79a3e854eb007371de1c8c84af62cbf049bf180e56af371656c656d656e744964656e74696669657276756e5f64697374696e6775697368696e675f7369676e6c656c656d656e7456616c75656d7462642d75732e6e792e646d766a697373756572417574688443a10126a20442313118218159022e3082022a308201d0a003020102021457c6ccd308bde43eca3744f2a87138dabbb884e8300a06082a8648ce3d0403023053310b30090603550406130255533111300f06035504080c084e657720596f726b310f300d06035504070c06416c62616e79310f300d060355040a0c064e5920444d56310f300d060355040b0c064e5920444d56301e170d3233303931343134353531385a170d3333303931313134353531385a3053310b30090603550406130255533111300f06035504080c084e657720596f726b310f300d06035504070c06416c62616e79310f300d060355040a0c064e5920444d56310f300d060355040b0c064e5920444d563059301306072a8648ce3d020106082a8648ce3d03010703420004893c2d8347906dc6cd69b7f636af4bfd533f96184f0aadacd10830da4471dbdb60ac170d1cfc534fae2d9dcd488f7747fdf978d925ea31e9e9083c382ba9ed53a38181307f301d0603551d0e04160414ab6d2e03b91d492240338fbccadefd9333eaf6c7301f0603551d23041830168014ab6d2e03b91d492240338fbccadefd9333eaf6c7300f0603551d130101ff040530030101ff302c06096086480186f842010d041f161d4f70656e53534c2047656e657261746564204365727469666963617465300a06082a8648ce3d0403020348003045022009fd0cab97b03e78f64e74d7dcee88668c476a0afc5aa2cebffe07d3be772ea9022100da38abc98a080f49f24ffece1fffc8a6cdd5b2c0b5da8fc7b767ac3a95dcb83e590319d818590314b900066776657273696f6e63312e306f646967657374416c676f726974686d675348412d3235366c76616c756544696765737473b900026f6f72672e637573746f6d2e74657374a10058203c5116109c068fa7fbf3186baec13ee6cbe48cc6c4a00fd349b9faf0a04f2ffd716f72672e69736f2e31383031332e352e31ac00582083c653c2e7c7d2e0f072f4269a20f4eb35d438f97a1c93026aa124fb707e1479015820a70c14943a002ba0675efa295e985e356197764f4591656cb6b3241b6cbb5f1e025820f0c5dcf0eac39eda62f4f52e52692ffdc09c133f4406008696843501e5364ef903582094f3cc14be210ace4f9d4545948074faf3a0138a2d1041adac45ccae1f1208d2045820c07ad9922535003e47887654eaae055667565c7bd19a8975f0336865f82bf0f505582063cb0912892fbcd761e85b8048caa954bc0db60e3678a9759020445b09a4623e065820052c825478b90e5018e857a3ae90c8dde34f5fe5056a143db77d7cd6107df7c9075820a00f3e3094c3d2ea5d4360f260d46f48ddadabcb49e2c2b2850f2913fdc4e8aa085820aea1b320b97e7922476c68008d275ecf4904ea09d72b950de7a6054bda6e78b7095820f764a3f5d0d998b73505ef1dc18eb15d77acd12b2e2100e59001ab9c0685da540a5820a1dd0ffbd7cff2fe1ee44523d35dbbbdd5c9b59c6ae00ad6dd785554978898c90b5820fd2c88594d6594872b87ac5d5c43d97211a73ffd348a0aaea71e54ef1dc2fbc96d6465766963654b6579496e666fb90001696465766963654b6579a40102215820881879ca7a238b19bf0f4c1f8c00e9a2e19ba7a6f73eae92b851d4de1b508559225820a314b538039127b5cd50735f54519e33c134450545c5603ad9f263facc56d377200167646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c76616c6964697479496e666fb90003667369676e6564c074323032332d30392d32395431353a34313a35395a6976616c696446726f6dc074323032332d30392d32395431353a34313a35395a6a76616c6964556e74696cc074323037332d30392d32395431353a34313a35395a5840d82c982b492614bc04241484206da61b1cff64bfa0d31b550c09096bc62f26c13b4ef2cad4b87d9e73717620001db2917e78da85896d046e253595af9f42a5946c6465766963655369676e6564b900026a6e616d65537061636573d81841a06a64657669636541757468b900016f6465766963655369676e61747572658443a10126a10442313158d2d81858ce847444657669636541757468656e7469636174696f6e83f6f68466313233343536782b437131616e506238765a55356a354330643768637362754a4c4270496177554a4944515269324562776234785c687474703a2f2f6c6f63616c686f73743a343030302f6170692f70726573656e746174696f6e5f726571756573742f64633839393964662d643665612d346338342d393938352d3337613862383161383265632f63616c6c6261636b6761626364656667756f72672e69736f2e31383031332e352e312e6d444cd81841a05840a97b5f9b59d193eda662542e71cb7daf6fa8952ac8b0cce82f22e7834d1fa8f0d2cab25a708658226c03d96950d00c3516fce62a220748d4f0a696072243b5026673746174757300`; - const verifier = new Verifier([ISSUER_CERTIFICATE]); it('should verify properly', async () => { - await verifier.verify(deviceResponse, { - ephemeralReaderKey, - encodedSessionTranscript, - }); + await new Verifier([ISSUER_CERTIFICATE]) + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .getDiagnosticInformation(deviceResponse); }); it('should be able to verify without ephemeralReaderKey and encodedSessionTrasncript', async () => { - await verifier.verify(deviceResponse, { - onCheck: (verification, original) => { - if (verification.category === 'DEVICE_AUTH') { - return; - } - original(verification); - }, - }); + await new Verifier([ISSUER_CERTIFICATE]) + .verify(deviceResponse, + (verification, original) => { + if (verification.category === 'DEVICE_AUTH') { + return; + } + original(verification); + }, + ); }); }); diff --git a/__tests__/example/example2.tests.ts b/__tests__/example/example2.tests.ts index f09d7ae..48a304b 100644 --- a/__tests__/example/example2.tests.ts +++ b/__tests__/example/example2.tests.ts @@ -12,31 +12,34 @@ describe('example 2: valid device response with partial disclosure', () => { const ephemeralReaderKey = hex`534b526561646572`; const encodedSessionTranscript = hex`d818589e83f6f68466313233343536782b437131616e506238765a55356a354330643768637362754a4c4270496177554a4944515269324562776234785c687474703a2f2f6c6f63616c686f73743a343030302f6170692f70726573656e746174696f6e5f726571756573742f64633839393964662d643665612d346338342d393938352d3337613862383161383265632f63616c6c6261636b6761626364656667`; const deviceResponse = hex`b900036776657273696f6e63312e3069646f63756d656e747381b9000367646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c6973737565725369676e6564b900026a6e616d65537061636573b90001716f72672e69736f2e31383031332e352e3186d8185867b90004686469676573744944006672616e646f6d58205daf6a1077dad6aace3a4c6774a8b3b8e7cbad501732c51a9fcc5e2670e3e7bc71656c656d656e744964656e7469666965726b66616d696c795f6e616d656c656c656d656e7456616c756565536d697468d8185865b90004686469676573744944016672616e646f6d58200ef437f6d7e3368ca5368dec12d920763c702d1f4032606cccc705cd87d0f2e171656c656d656e744964656e7469666965726a676976656e5f6e616d656c656c656d656e7456616c7565644a6f686ed818586eb90004686469676573744944026672616e646f6d5820e7d36046d7bf923104d42e3f8363bbaba7cb75bda603205fd652c2c0200aa7ba71656c656d656e744964656e7469666965726a62697274685f646174656c656c656d656e7456616c7565d903ec6a313938302d30362d3135d8185868b90004686469676573744944056672616e646f6d5820d41c8606e81b5a866bb508b83db289668b6171c9d06c5ea6907795cce33e17c671656c656d656e744964656e7469666965726f69737375696e675f636f756e7472796c656c656d656e7456616c7565625553d818586eb90004686469676573744944066672616e646f6d5820c9b41ca04d3e0faedffeeddf665ce0fc7f9cec72e4d2598231aecbafc0fb844271656c656d656e744964656e7469666965727169737375696e675f617574686f726974796c656c656d656e7456616c7565664e5920444d56d8185873b90004686469676573744944076672616e646f6d5820f6a12bf7e919e5cf830b64719bde0d726d2e012879e166c3499d7b93904efe6471656c656d656e744964656e7469666965727469737375696e675f6a7572697364696374696f6e6c656c656d656e7456616c7565684e657720596f726b6a697373756572417574688443a10126a20442313118218159022e3082022a308201d0a003020102021457c6ccd308bde43eca3744f2a87138dabbb884e8300a06082a8648ce3d0403023053310b30090603550406130255533111300f06035504080c084e657720596f726b310f300d06035504070c06416c62616e79310f300d060355040a0c064e5920444d56310f300d060355040b0c064e5920444d56301e170d3233303931343134353531385a170d3333303931313134353531385a3053310b30090603550406130255533111300f06035504080c084e657720596f726b310f300d06035504070c06416c62616e79310f300d060355040a0c064e5920444d56310f300d060355040b0c064e5920444d563059301306072a8648ce3d020106082a8648ce3d03010703420004893c2d8347906dc6cd69b7f636af4bfd533f96184f0aadacd10830da4471dbdb60ac170d1cfc534fae2d9dcd488f7747fdf978d925ea31e9e9083c382ba9ed53a38181307f301d0603551d0e04160414ab6d2e03b91d492240338fbccadefd9333eaf6c7301f0603551d23041830168014ab6d2e03b91d492240338fbccadefd9333eaf6c7300f0603551d130101ff040530030101ff302c06096086480186f842010d041f161d4f70656e53534c2047656e657261746564204365727469666963617465300a06082a8648ce3d0403020348003045022009fd0cab97b03e78f64e74d7dcee88668c476a0afc5aa2cebffe07d3be772ea9022100da38abc98a080f49f24ffece1fffc8a6cdd5b2c0b5da8fc7b767ac3a95dcb83e590319d818590314b900066776657273696f6e63312e306f646967657374416c676f726974686d675348412d3235366c76616c756544696765737473b900026f6f72672e637573746f6d2e74657374a100582085b5266e8b9cfcc6ee9e987fb0c39ae82290122ccc76ab39b9ef4d19187aeca6716f72672e69736f2e31383031332e352e31ac00582038a588bb9f6834a065bf76bfa6eb3245dd150401d5dcb9338c1dc3c133cf937a0158207778d99e4baba506799ed3737f024183f20c3f1fb95ff7202777c2c4c4186a4c025820db49c1948e04d674807c47f7eee5fdb1c9b5460e972d62c366625e320c783fe5035820d4f7232bc241d67dbd4efea72e4078935547b0a8fb339d0f81e4bac6ef41f6160458200e2220b3aefe8b4896adec8d6a324c6f65888650040dcb52acd4e4de2b2b670b055820189b2e1e51eae1d552d2c5209376d5890e95f99f8c102ef537849cac5525f5da0658203930845222e95f7cabd4c636c78a1a6ce7576e9af9880e59431ba5e7d1bad780075820923ca4caeeda2b8ae2107b33bc8b83e5cf231c24a874b743b6191508aaa6adfe0858209a80c30bf56223b7b043727265440f1ee1e2aa3e009c17d3d57bc81bb1ae0ed4095820519a122225e61824dc0d98d1138f60a5a0d061357a163e99d2fa149957fd61f70a5820bbd148c5016c3436b48b3eb7af2c61d7ba0107ea98cd3295ec76396a003a5f700b5820111c864f7b02d5e408652e423a762e10c3a2725fada3a881f583c062cb7040d06d6465766963654b6579496e666fb90001696465766963654b6579a40102215820881879ca7a238b19bf0f4c1f8c00e9a2e19ba7a6f73eae92b851d4de1b508559225820a314b538039127b5cd50735f54519e33c134450545c5603ad9f263facc56d377200167646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c76616c6964697479496e666fb90003667369676e6564c074323032332d30392d32395431353a35303a35365a6976616c696446726f6dc074323032332d30392d32395431353a35303a35365a6a76616c6964556e74696cc074323037332d30392d32395431353a35303a35365a58407c342c9076394972b7c3f8c75466425545b3dc1be89856ef4c2b086460461bf0c5191c906f1be6264f75cd32469903040f1e4a3e136e01ca3929f60ffd0d92416c6465766963655369676e6564b900026a6e616d65537061636573d81841a06a64657669636541757468b900016f6465766963655369676e61747572658443a10126a10442313158d2d81858ce847444657669636541757468656e7469636174696f6e83f6f68466313233343536782b437131616e506238765a55356a354330643768637362754a4c4270496177554a4944515269324562776234785c687474703a2f2f6c6f63616c686f73743a343030302f6170692f70726573656e746174696f6e5f726571756573742f64633839393964662d643665612d346338342d393938352d3337613862383161383265632f63616c6c6261636b6761626364656667756f72672e69736f2e31383031332e352e312e6d444cd81841a0584059839f348b403848827dddca5fae719027545c1ef864e46a43a00dc91edfccb82798d96a9dc6c04e58938b67a46470dada69cf391ce8da3772fcf55d16d3c0db6673746174757300`; - const verifier = new Verifier([ISSUER_CERTIFICATE]); it('should verify properly', async () => { - await verifier.verify(deviceResponse, { - ephemeralReaderKey, - encodedSessionTranscript, - }); + await new Verifier([ISSUER_CERTIFICATE]) + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .verify(deviceResponse); }); it('should be able to verify without ephemeralReaderKey and encodedSessionTrasncript', async () => { - await verifier.verify(deviceResponse, { - onCheck: (verification, original) => { - if (verification.category === 'DEVICE_AUTH') { - return; - } - original(verification); - }, - }); + await new Verifier([ISSUER_CERTIFICATE]) + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .verify( + deviceResponse, + (verification, original) => { + if (verification.category === 'DEVICE_AUTH') { + return; + } + original(verification); + }, + ); }); it('should contain only the disclosed fields', async () => { - const { documents } = await verifier.verify(deviceResponse, { - ephemeralReaderKey, - encodedSessionTranscript, - }); + const { documents } = await new Verifier([ISSUER_CERTIFICATE]) + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .verify(deviceResponse); const numberOfAttributes = documents[0] .issuerSigned @@ -47,10 +50,10 @@ describe('example 2: valid device response with partial disclosure', () => { }); it('should validate the digest of all fields', async () => { - const { documents } = await verifier.verify(deviceResponse, { - ephemeralReaderKey, - encodedSessionTranscript, - }); + const { documents } = await new Verifier([ISSUER_CERTIFICATE]) + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .verify(deviceResponse); const { issuerAuth } = documents[0].issuerSigned; const ns = 'org.iso.18013.5.1'; diff --git a/__tests__/example/example3.tests.ts b/__tests__/example/example3.tests.ts index c469692..99945b9 100644 --- a/__tests__/example/example3.tests.ts +++ b/__tests__/example/example3.tests.ts @@ -12,24 +12,27 @@ describe('example 3: device response with partial and tampered disclosure', () = const ephemeralReaderKey = hex`534b526561646572`; const encodedSessionTranscript = hex`d818589e83f6f68466313233343536782b437131616e506238765a55356a354330643768637362754a4c4270496177554a4944515269324562776234785c687474703a2f2f6c6f63616c686f73743a343030302f6170692f70726573656e746174696f6e5f726571756573742f64633839393964662d643665612d346338342d393938352d3337613862383161383265632f63616c6c6261636b6761626364656667`; const deviceResponse = hex`b900036776657273696f6e63312e3069646f63756d656e747381b9000367646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c6973737565725369676e6564b900026a6e616d65537061636573b90001716f72672e69736f2e31383031332e352e3186d8185868a4686469676573744944006672616e646f6d5820b71c5cac1e15923cfad7468c061da8600dd3bc32015a54c7c2b1953fb9e8452771656c656d656e744964656e7469666965726b66616d696c795f6e616d656c656c656d656e7456616c75656857696c6c69616d73d8185865b90004686469676573744944016672616e646f6d58205e01b2280be9c0ac4658bd34983bcfbf8c95cbeac19a5e3338fb6b476093315a71656c656d656e744964656e7469666965726a676976656e5f6e616d656c656c656d656e7456616c7565644a6f686ed818586eb90004686469676573744944026672616e646f6d5820de5abc9d35863a82245f0477720c8253e37d0f8ef425956cf37cca33a83984f571656c656d656e744964656e7469666965726a62697274685f646174656c656c656d656e7456616c7565d903ec6a313938302d30362d3135d8185868b90004686469676573744944056672616e646f6d58205a9cd291a808ae1dac3d0807a3031cafe8e2b374050388bc932864a95f82e26e71656c656d656e744964656e7469666965726f69737375696e675f636f756e7472796c656c656d656e7456616c7565625553d818586eb90004686469676573744944066672616e646f6d5820f849a4010a2e109142451e56c4bf0dd4c4e40249e0f719fb6e2a45e68412a31a71656c656d656e744964656e7469666965727169737375696e675f617574686f726974796c656c656d656e7456616c7565664e5920444d56d8185873b90004686469676573744944076672616e646f6d5820171ca61cdc1de90abf7eb74f1e4b5b4a261b0cc3d5938ac23be8b361c69d249171656c656d656e744964656e7469666965727469737375696e675f6a7572697364696374696f6e6c656c656d656e7456616c7565684e657720596f726b6a697373756572417574688443a10126a20442313118218159022e3082022a308201d0a003020102021457c6ccd308bde43eca3744f2a87138dabbb884e8300a06082a8648ce3d0403023053310b30090603550406130255533111300f06035504080c084e657720596f726b310f300d06035504070c06416c62616e79310f300d060355040a0c064e5920444d56310f300d060355040b0c064e5920444d56301e170d3233303931343134353531385a170d3333303931313134353531385a3053310b30090603550406130255533111300f06035504080c084e657720596f726b310f300d06035504070c06416c62616e79310f300d060355040a0c064e5920444d56310f300d060355040b0c064e5920444d563059301306072a8648ce3d020106082a8648ce3d03010703420004893c2d8347906dc6cd69b7f636af4bfd533f96184f0aadacd10830da4471dbdb60ac170d1cfc534fae2d9dcd488f7747fdf978d925ea31e9e9083c382ba9ed53a38181307f301d0603551d0e04160414ab6d2e03b91d492240338fbccadefd9333eaf6c7301f0603551d23041830168014ab6d2e03b91d492240338fbccadefd9333eaf6c7300f0603551d130101ff040530030101ff302c06096086480186f842010d041f161d4f70656e53534c2047656e657261746564204365727469666963617465300a06082a8648ce3d0403020348003045022009fd0cab97b03e78f64e74d7dcee88668c476a0afc5aa2cebffe07d3be772ea9022100da38abc98a080f49f24ffece1fffc8a6cdd5b2c0b5da8fc7b767ac3a95dcb83e590319d818590314b900066776657273696f6e63312e306f646967657374416c676f726974686d675348412d3235366c76616c756544696765737473b900026f6f72672e637573746f6d2e74657374a10058201d8aeee64815a0fd40f751fba54baecbd34f02a16f1ec47dc991e2f791ca44a5716f72672e69736f2e31383031332e352e31ac00582056da6532090783336cdd615da2b4e2ba52098c9935ea77e9e700be27e4b65b32015820842183a9529c7aedca84822642628de8b3a715c4ba74ca7dd9a3e3c7da1b94f5025820712765f58ad88f21e0b6018711d533175af08825d6d7c74fac876a08985ecf20035820eaa71a09aaca7858437c4b7f49ab6c4aa2a6b6f186a8a71d4c4144c14111bc40045820eea2cad34743e16df9658a88eaae5a53237bb625e35f2af5f7eb0e346f034c07055820c32639110c08a2549737b26f9e50aaa698a88f36d53f5a1f33b152d7a41b1ef7065820d8f724284e7d33400c44192f00706ed0b6d60b721e6055fe18c9845a6e3eb42a07582050863328a3ed9e1141c82ee8b830c6e553a9d9e78b681975ee3e2225e203de2a08582035e369c38c314c352cf9ba23a1b2ca37fb5e7d9dea20c03de73356f77ff0685509582031ddd32867e9515d3f7b2ad78bf8bef8296a85d2f4cf9986804a6b0bd763988f0a582043d470465c7fb28e0f4af57ad11d55729cd4e11bcd488ec998dfc8ff99fc12e00b58200aa24b81a8927e94d69cc84ec7382fd5371b8970a41de561ab8b9a6eb51c4c706d6465766963654b6579496e666fb90001696465766963654b6579a40102215820881879ca7a238b19bf0f4c1f8c00e9a2e19ba7a6f73eae92b851d4de1b508559225820a314b538039127b5cd50735f54519e33c134450545c5603ad9f263facc56d377200167646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c76616c6964697479496e666fb90003667369676e6564c074323032332d30392d32395431353a35373a31325a6976616c696446726f6dc074323032332d30392d32395431353a35373a31325a6a76616c6964556e74696cc074323037332d30392d32395431353a35373a31325a5840bcc0aa350715291e4b6fbd609523eed20d6cad54ddb13e57e4e4ce409197b9585aceb7a787c5d2d8d7726aef2907509be35238d496518f96bdb15fb8615ddebd6c6465766963655369676e6564b900026a6e616d65537061636573d81841a06a64657669636541757468b900016f6465766963655369676e61747572658443a10126a10442313158d2d81858ce847444657669636541757468656e7469636174696f6e83f6f68466313233343536782b437131616e506238765a55356a354330643768637362754a4c4270496177554a4944515269324562776234785c687474703a2f2f6c6f63616c686f73743a343030302f6170692f70726573656e746174696f6e5f726571756573742f64633839393964662d643665612d346338342d393938352d3337613862383161383265632f63616c6c6261636b6761626364656667756f72672e69736f2e31383031332e352e312e6d444cd81841a05840105baf1c0e5c3704dac8f662bdf54ae76d8e51c021d6433f42b60637888f89a5b7c67f1ab56b62d347878f4de88786a92f256640f7709ff6d4bead043378a5476673746174757300`; - const verifier = new Verifier([ISSUER_CERTIFICATE]); it('should verify properly', async () => { - await expect(verifier.verify(deviceResponse, { - ephemeralReaderKey, - encodedSessionTranscript, - })).rejects.toThrow('The calculated digest for org.iso.18013.5.1/family_name attribute must match the digest in the issuerAuth element'); + await expect( + new Verifier([ISSUER_CERTIFICATE]) + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .verify(deviceResponse), + ).rejects.toThrow('The calculated digest for org.iso.18013.5.1/family_name attribute must match the digest in the issuerAuth element'); }); it('should return the decoded response when skipping the error', async () => { - const { documents } = await verifier.verify(deviceResponse, { - ephemeralReaderKey, - encodedSessionTranscript, - onCheck: (v, cb) => { - if (v.category === 'DATA_INTEGRITY') { return; } - cb(v); - }, - }); + const { documents } = await new Verifier([ISSUER_CERTIFICATE]) + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .verify( + deviceResponse, + (v, cb) => { + if (v.category === 'DATA_INTEGRITY') { return; } + cb(v); + }, + ); const { issuerAuth } = documents[0].issuerSigned; const ns = 'org.iso.18013.5.1'; expect(await documents[0] @@ -40,10 +43,10 @@ describe('example 3: device response with partial and tampered disclosure', () = }); it('should return the invalid attribute in the diagnostic info', async () => { - const di = await verifier.getDiagnosticInformation(deviceResponse, { - ephemeralReaderKey, - encodedSessionTranscript, - }); + const di = await new Verifier([ISSUER_CERTIFICATE]) + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .getDiagnosticInformation(deviceResponse); expect(di.attributes.find((a) => a.id === 'family_name')?.isValid).toBe(false); }); diff --git a/__tests__/example/example4.tests.ts b/__tests__/example/example4.tests.ts index dae2d63..bbcf376 100644 --- a/__tests__/example/example4.tests.ts +++ b/__tests__/example/example4.tests.ts @@ -12,20 +12,20 @@ describe('example 4: device response with device attributes', () => { const ephemeralReaderKey = hex`534b526561646572`; const encodedSessionTranscript = hex`d818589e83f6f68466313233343536782b437131616e506238765a55356a354330643768637362754a4c4270496177554a4944515269324562776234785c687474703a2f2f6c6f63616c686f73743a343030302f6170692f70726573656e746174696f6e5f726571756573742f64633839393964662d643665612d346338342d393938352d3337613862383161383265632f63616c6c6261636b6761626364656667`; const deviceResponse = hex`b900036776657273696f6e63312e3069646f63756d656e747381b9000367646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c6973737565725369676e6564b900026a6e616d65537061636573b90001716f72672e69736f2e31383031332e352e318cd8185867b90004686469676573744944006672616e646f6d58203d382b0035ffb919b1cd6e511deaaa845e78abfa4f74502bd2894452d70b589671656c656d656e744964656e7469666965726b66616d696c795f6e616d656c656c656d656e7456616c756565536d697468d8185865b90004686469676573744944016672616e646f6d582011da9772a5332e54ceb4140a97baa35e85c77760891a741122fbdca97f46315771656c656d656e744964656e7469666965726a676976656e5f6e616d656c656c656d656e7456616c7565644a6f686ed818586eb90004686469676573744944026672616e646f6d5820e9bb72564e897bf41e7bdb59de6f14e4e44b3d380a1df937247622bc72f36b8a71656c656d656e744964656e7469666965726a62697274685f646174656c656c656d656e7456616c7565d903ec6a313938302d30362d3135d818586eb90004686469676573744944036672616e646f6d58204a4ffbac2c1f26818b743257c3a6b21788e24ce98bb75ede59faf48756dc154471656c656d656e744964656e7469666965726a69737375655f646174656c656c656d656e7456616c7565d903ec6a323032332d30332d3031d818586fb90004686469676573744944046672616e646f6d5820ad0bc45bfdc67d347b74af45aad7849898718d270911aba22adb25772d0f512f71656c656d656e744964656e7469666965726b6578706972795f646174656c656c656d656e7456616c7565d903ec6a323032382d30332d3331d8185868b90004686469676573744944056672616e646f6d5820a3634fa5da0d398ec71cec99a4d7c5aa24d26b0fc4ee5346e9d61639e11a358771656c656d656e744964656e7469666965726f69737375696e675f636f756e7472796c656c656d656e7456616c7565625553d818586eb90004686469676573744944066672616e646f6d5820c574e015c58d63f6274a3925eab31f6b12f501f288a987ebb2e207e8a9686a9071656c656d656e744964656e7469666965727169737375696e675f617574686f726974796c656c656d656e7456616c7565664e5920444d56d8185873b90004686469676573744944076672616e646f6d58205b1163027cae36710ec73b3829bfe28193347d53419d41f5d09bff1eb93cd72871656c656d656e744964656e7469666965727469737375696e675f6a7572697364696374696f6e6c656c656d656e7456616c7565684e657720596f726bd8185871b90004686469676573744944086672616e646f6d5820937623125862b2135d8a9c2e0cada8e4c42e14591ce623109d8e39a6dc0331d571656c656d656e744964656e7469666965726f646f63756d656e745f6e756d6265726c656c656d656e7456616c75656b30312d3333332d37303730d8185863b90004686469676573744944096672616e646f6d5820941bb3b78a355a5678019fe2088e5bd7b715f6208d61d05d078290533693f01e71656c656d656e744964656e74696669657268706f7274726169746c656c656d656e7456616c75656462737472d81858b1b900046864696765737449440a6672616e646f6d5820934b34b4d8c82e44b1ddb0a937740f56f8ac1e39cc480946bd169015b9f38c9371656c656d656e744964656e7469666965727264726976696e675f70726976696c656765736c656c656d656e7456616c756581b900037576656869636c655f63617465676f72795f636f646561436a69737375655f646174656a323032332d30332d30316b6578706972795f646174656a323032382d30332d3331d818587ab900046864696765737449440b6672616e646f6d58203a76b962b891a01bd2c1351dbe0c30f91b53e3e6c55642d646de15f603bb024171656c656d656e744964656e74696669657276756e5f64697374696e6775697368696e675f7369676e6c656c656d656e7456616c75656d7462642d75732e6e792e646d766a697373756572417574688443a10126a20442313118218159022e3082022a308201d0a003020102021457c6ccd308bde43eca3744f2a87138dabbb884e8300a06082a8648ce3d0403023053310b30090603550406130255533111300f06035504080c084e657720596f726b310f300d06035504070c06416c62616e79310f300d060355040a0c064e5920444d56310f300d060355040b0c064e5920444d56301e170d3233303931343134353531385a170d3333303931313134353531385a3053310b30090603550406130255533111300f06035504080c084e657720596f726b310f300d06035504070c06416c62616e79310f300d060355040a0c064e5920444d56310f300d060355040b0c064e5920444d563059301306072a8648ce3d020106082a8648ce3d03010703420004893c2d8347906dc6cd69b7f636af4bfd533f96184f0aadacd10830da4471dbdb60ac170d1cfc534fae2d9dcd488f7747fdf978d925ea31e9e9083c382ba9ed53a38181307f301d0603551d0e04160414ab6d2e03b91d492240338fbccadefd9333eaf6c7301f0603551d23041830168014ab6d2e03b91d492240338fbccadefd9333eaf6c7300f0603551d130101ff040530030101ff302c06096086480186f842010d041f161d4f70656e53534c2047656e657261746564204365727469666963617465300a06082a8648ce3d0403020348003045022009fd0cab97b03e78f64e74d7dcee88668c476a0afc5aa2cebffe07d3be772ea9022100da38abc98a080f49f24ffece1fffc8a6cdd5b2c0b5da8fc7b767ac3a95dcb83e590319d818590314b900066776657273696f6e63312e306f646967657374416c676f726974686d675348412d3235366c76616c756544696765737473b900026f6f72672e637573746f6d2e74657374a10058204cdeb0a8bad523598952584739ce9dcebd76811847fa58836f7a3662764874b6716f72672e69736f2e31383031332e352e31ac00582050da04ab2d084569625b617e4da11cfd785a5e0957a37c2245a38c449a96404101582063c6bacdc365c332238d01e18ea0398bfbe8447114251cf5a38a7f1cea21d3fa0258203d2c74ba23ea297c52a954e2a892e170c2a0c26294a318eb7dbd105526a03c3b0358209237ad732db0020af89c80a89b8c770e73cff2afb9a0093a91ba6a6099a4f7750458206227a459e9dbceed6df9015120b61f1c38aaf6c43cea4435f4a87c0e7e343a73055820427a4ebabb78d89bf0488d4b44c8b5e945d6c6c9662e4a11b447ce7933d326680658204726344aabcf8e64b46ee905363805fe98d7bd17af3a7522a54aac5331ffd703075820e8edf231b7a3ad52939f230d830dd5c8f2791f944a054f9aeb15a28e30090b2b085820bf1c01e1b4af286a44082ea0fa092cc9f1eb6b19e78b83f18f37f85bb48f5792095820cf10644f20806c5ef0c4aa5f08496e4e6ab8752b713fa6f65664d911536092990a58204c6bff3c68bcd61e7d204f8dfd9bd378e1663162ad18cd8a5101527d2dccc30c0b5820b6902126d95e8238827bcca0aa8c1e011bdb56ce743dcd09217c3088eb2426016d6465766963654b6579496e666fb90001696465766963654b6579a40102215820881879ca7a238b19bf0f4c1f8c00e9a2e19ba7a6f73eae92b851d4de1b508559225820a314b538039127b5cd50735f54519e33c134450545c5603ad9f263facc56d377200167646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c76616c6964697479496e666fb90003667369676e6564c074323032332d30392d32395431353a30363a32305a6976616c696446726f6dc074323032332d30392d32395431353a30363a32305a6a76616c6964556e74696cc074323037332d30392d32395431353a30363a32305a5840a46fee0aecec72dd699babd2a2e4ed3c25c655d9bd0e79f1849d0ee64d03cd3a253e005acb48faa2cd05f46c59525ce6de612cc7cc4aecbd6ccd72397a7507d86c6465766963655369676e6564b900026a6e616d65537061636573d818583aa1726f72672e69736f2e77616c6c65742e6d444ca26776657273696f6e016a6465766963654e616d65706d444c2053757065722057616c6c65746a64657669636541757468b900016f6465766963655369676e61747572658443a10126a10442313159010dd818590108847444657669636541757468656e7469636174696f6e83f6f68466313233343536782b437131616e506238765a55356a354330643768637362754a4c4270496177554a4944515269324562776234785c687474703a2f2f6c6f63616c686f73743a343030302f6170692f70726573656e746174696f6e5f726571756573742f64633839393964662d643665612d346338342d393938352d3337613862383161383265632f63616c6c6261636b6761626364656667756f72672e69736f2e31383031332e352e312e6d444cd818583aa1726f72672e69736f2e77616c6c65742e6d444ca26776657273696f6e016a6465766963654e616d65706d444c2053757065722057616c6c657458404119c75ea0fbd87041b7a01dfe3c9808888d7335c6ffedbc56f5133cb3a9b04da8e29ec0326e954df0dcd53aee2ea84fa8b820a06d2cfb510ff61eed39ecb1536673746174757300`; - const verifier = new Verifier([ISSUER_CERTIFICATE]); it('should verify properly', async () => { - await verifier.verify(deviceResponse, { - ephemeralReaderKey, - encodedSessionTranscript, - }); + await new Verifier([ISSUER_CERTIFICATE]) + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .verify(deviceResponse); }); it('should get the right diagnostic info', async () => { - const diagnostic = await verifier.getDiagnosticInformation(deviceResponse, { - ephemeralReaderKey, - encodedSessionTranscript, - }); + const diagnostic = await new Verifier([ISSUER_CERTIFICATE]) + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .getDiagnosticInformation(deviceResponse); + expect(diagnostic).toMatchSnapshot(); }); }); diff --git a/__tests__/issuing/deviceResponse.tests.ts b/__tests__/issuing/deviceResponse.tests.ts index 682887d..db75576 100644 --- a/__tests__/issuing/deviceResponse.tests.ts +++ b/__tests__/issuing/deviceResponse.tests.ts @@ -106,9 +106,9 @@ describe('issuing a device response', () => { it('should be verifiable', async () => { const verifier = new Verifier([ISSUER_CERTIFICATE]); - await verifier.verify(encoded, { - encodedSessionTranscript: getSessionTranscriptBytes(clientId, responseUri, verifierGeneratedNonce, mdocGeneratedNonce), - }); + await verifier + .usingSessionTranscriptForOID4VP(mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce) + .verify(encoded); }); describe('should not be verifiable', () => { @@ -121,9 +121,9 @@ describe('issuing a device response', () => { 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), - }); + await verifier + .usingSessionTranscriptForOID4VP(values.mdocGeneratedNonce, values.clientId, values.responseUri, values.verifierGeneratedNonce) + .verify(encoded); 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'); @@ -157,18 +157,6 @@ describe('issuing a device response', () => { 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 }), - createHash('sha256').update(rdrEngtBytes).digest(), - ]), - ); - beforeAll(async () => { // Nothing more to do on the verifier side. @@ -190,24 +178,24 @@ describe('issuing a device response', () => { it('should be verifiable', async () => { const verifier = new Verifier([ISSUER_CERTIFICATE]); - await verifier.verify(encoded, { - encodedSessionTranscript: getSessionTranscriptBytes(readerEngagementBytes, deviceEngagementBytes, eReaderKeyBytes), - }); + await verifier + .usingSessionTranscriptForWebAPI(deviceEngagementBytes, readerEngagementBytes, eReaderKeyBytes) + .verify(encoded); }); 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, + ['deviceEngagementBytes', { deviceEngagementBytes: wrong, readerEngagementBytes, eReaderKeyBytes }] as const, + ['readerEngagementBytes', { deviceEngagementBytes, readerEngagementBytes: wrong, eReaderKeyBytes }] as const, + ['eReaderKeyBytes', { deviceEngagementBytes, readerEngagementBytes, 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), - }); + await verifier + .usingSessionTranscriptForWebAPI(values.deviceEngagementBytes, values.readerEngagementBytes, values.eReaderKeyBytes) + .verify(encoded); 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'); diff --git a/__tests__/issuing/deviceResponseWithMac.tests.ts b/__tests__/issuing/deviceResponseWithMac.tests.ts index f63416f..864a0fb 100644 --- a/__tests__/issuing/deviceResponseWithMac.tests.ts +++ b/__tests__/issuing/deviceResponseWithMac.tests.ts @@ -1,4 +1,4 @@ -import { createHash, randomFillSync } from 'node:crypto'; +import { randomFillSync } from 'node:crypto'; import * as jose from 'jose'; import { COSEKeyFromJWK } from 'cose-kit'; import { @@ -10,7 +10,6 @@ import { DeviceSignedDocument, } from '../../src'; import { DEVICE_JWK, ISSUER_CERTIFICATE, ISSUER_PRIVATE_KEY_JWK, PRESENTATION_DEFINITION_1 } from './config'; -import { DataItem, cborEncode } from '../../src/cbor'; import COSEKeyToRAW from '../../src/cose/coseKey'; const { d, ...publicKeyJWK } = DEVICE_JWK as jose.JWK; @@ -89,18 +88,6 @@ describe('issuing a device response with MAC authentication', () => { 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 - [ - createHash('sha256').update(cborEncode([clId, mdocNonce])).digest(), - createHash('sha256').update(cborEncode([respUri, mdocNonce])).digest(), - nonce, - ], // Handover = OID4VPHandover - ]), - ); - beforeAll(async () => { // This is the Device side const deviceResponseMDoc = await DeviceResponse.from(mdoc) @@ -117,10 +104,10 @@ describe('issuing a device response with MAC authentication', () => { it('should be verifiable', async () => { const verifier = new Verifier([ISSUER_CERTIFICATE]); - await verifier.verify(encoded, { - ephemeralReaderKey: ephemeralPrivateKey, - encodedSessionTranscript: getSessionTranscriptBytes(clientId, responseUri, verifierGeneratedNonce, mdocGeneratedNonce), - }); + await verifier + .usingEphemeralReaderKey(ephemeralPrivateKey) + .usingSessionTranscriptForOID4VP(mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce) + .verify(encoded); }); describe('should not be verifiable', () => { @@ -133,10 +120,10 @@ describe('issuing a device response with MAC authentication', () => { it(`with a different ${name}`, async () => { try { const verifier = new Verifier([ISSUER_CERTIFICATE]); - await verifier.verify(encoded, { - ephemeralReaderKey: ephemeralPrivateKey, - encodedSessionTranscript: getSessionTranscriptBytes(values.clientId, values.responseUri, values.verifierGeneratedNonce, values.mdocGeneratedNonce), - }); + await verifier + .usingEphemeralReaderKey(ephemeralPrivateKey) + .usingSessionTranscriptForOID4VP(values.mdocGeneratedNonce, values.clientId, values.responseUri, values.verifierGeneratedNonce) + .verify(encoded); throw new Error('should not validate with different transcripts'); } catch (error) { expect(error.message).toMatch('Unable to verify deviceAuth MAC: Device MAC must be valid'); @@ -170,18 +157,6 @@ describe('issuing a device response with MAC authentication', () => { 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 }), - createHash('sha256').update(rdrEngtBytes).digest(), - ]), - ); - beforeAll(async () => { // This is the verifier side before requesting the Device Response { @@ -208,26 +183,26 @@ describe('issuing a device response with MAC authentication', () => { it('should be verifiable', async () => { const verifier = new Verifier([ISSUER_CERTIFICATE]); - await verifier.verify(encoded, { - ephemeralReaderKey: ephemeralPrivateKey, - encodedSessionTranscript: getSessionTranscriptBytes(readerEngagementBytes, deviceEngagementBytes, eReaderKeyBytes), - }); + await verifier + .usingEphemeralReaderKey(ephemeralPrivateKey) + .usingSessionTranscriptForWebAPI(deviceEngagementBytes, readerEngagementBytes, eReaderKeyBytes) + .verify(encoded); }); 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, + ['deviceEngagementBytes', { deviceEngagementBytes: wrong, readerEngagementBytes, eReaderKeyBytes }] as const, + ['readerEngagementBytes', { deviceEngagementBytes, readerEngagementBytes: wrong, eReaderKeyBytes }] as const, + ['eReaderKeyBytes', { deviceEngagementBytes, readerEngagementBytes, eReaderKeyBytes: wrong }] as const, ].forEach(([name, values]) => { it(`with a different ${name}`, async () => { const verifier = new Verifier([ISSUER_CERTIFICATE]); try { - await verifier.verify(encoded, { - ephemeralReaderKey: ephemeralPrivateKey, - encodedSessionTranscript: getSessionTranscriptBytes(values.readerEngagementBytes, values.deviceEngagementBytes, values.eReaderKeyBytes), - }); + await verifier + .usingEphemeralReaderKey(ephemeralPrivateKey) + .usingSessionTranscriptForWebAPI(values.deviceEngagementBytes, values.readerEngagementBytes, values.eReaderKeyBytes) + .verify(encoded); throw new Error('should not validate with different transcripts'); } catch (error) { expect(error.message).toMatch('Unable to verify deviceAuth MAC: Device MAC must be valid'); diff --git a/__tests__/issuing/issuingMDoc.tests.ts b/__tests__/issuing/issuingMDoc.tests.ts index 9597af1..6a7d278 100644 --- a/__tests__/issuing/issuingMDoc.tests.ts +++ b/__tests__/issuing/issuingMDoc.tests.ts @@ -45,14 +45,15 @@ describe('issuing an MDOC', () => { it('should be verifiable', async () => { const verifier = new Verifier([ISSUER_CERTIFICATE]); - await verifier.verify(encoded, { - onCheck: (verification, original) => { + await verifier.verify( + encoded, + (verification, original) => { if (verification.category === 'DEVICE_AUTH') { return; } original(verification); }, - }); + ); }); it('should contain the validity info', () => { diff --git a/__tests__/verifier.tests.ts b/__tests__/verifier.tests.ts index d3785e2..149ed38 100644 --- a/__tests__/verifier.tests.ts +++ b/__tests__/verifier.tests.ts @@ -11,9 +11,10 @@ describe('verifier', () => { const trustedCerts: string[] = []; const verifier = new Verifier(trustedCerts); - await expect(verifier.verify(encodedDeviceResponse, { - ephemeralReaderKey, encodedSessionTranscript, - })).rejects.toThrow('No valid certificate paths found'); + await expect(verifier + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .verify(encodedDeviceResponse)).rejects.toThrow('No valid certificate paths found'); }); it('should allow the caller to pass a custom onCheck function', async () => { @@ -27,16 +28,18 @@ describe('verifier', () => { let called = false; try { - await verifier.verify(encodedDeviceResponse, { - ephemeralReaderKey, - encodedSessionTranscript, - onCheck: (verification) => { - if (verification.check.includes('Issuer certificate must be valid') && - verification.status === 'FAILED') { - called = true; - } - }, - }); + await verifier + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptBytes(encodedSessionTranscript) + .verify( + encodedDeviceResponse, + (verification) => { + if (verification.check.includes('Issuer certificate must be valid') && + verification.status === 'FAILED') { + called = true; + } + }, + ); } catch (err) { // ignore err } diff --git a/src/mdoc/Verifier.ts b/src/mdoc/Verifier.ts index d2baac8..9003553 100644 --- a/src/mdoc/Verifier.ts +++ b/src/mdoc/Verifier.ts @@ -1,3 +1,4 @@ +import { createHash } from 'node:crypto'; import { compareVersions } from 'compare-versions'; import { X509Certificate } from '@peculiar/x509'; import { importX509, JWK, KeyLike } from 'jose'; @@ -22,6 +23,7 @@ import IssuerAuth from './model/IssuerAuth'; import { IssuerSignedDocument } from './model/IssuerSignedDocument'; import { DeviceSignedDocument } from './model/DeviceSignedDocument'; import COSEKeyToRAW from '../cose/coseKey'; +import { cborEncode, DataItem } from '../cbor'; const MDL_NAMESPACE = 'org.iso.18013.5.1'; @@ -32,6 +34,22 @@ const DIGEST_ALGS = { } as { [key: string]: string }; export class Verifier { + /** + * The encoded session transcript to use for verification. + * @see {@link usingSessionTranscriptBytes} + * @see {@link usingSessionTranscriptForOID4VP} + * @see {@link usingSessionTranscriptForWebAPI} + */ + #sessionTranscriptBytes: Buffer; + + /** + * The raw private part of the ephemeral reader key. + * @see {@link usingEphemeralReaderKey} + */ + #ephemeralReaderKey: Uint8Array; + + #disableCertificateChainValidation: boolean = false; + /** * * @param issuersRootCertificates The IACA root certificates list of the supported issuers. @@ -96,13 +114,9 @@ export class Verifier { private async verifyDeviceSignature( document: IssuerSignedDocument | DeviceSignedDocument, - options: { - ephemeralPrivateKey?: Uint8Array; - sessionTranscriptBytes?: Uint8Array; - onCheck: UserDefinedVerificationCallback, - }, + onCheckCbk: UserDefinedVerificationCallback, ) { - const onCheck = onCatCheck(options.onCheck, 'DEVICE_AUTH'); + const onCheck = onCatCheck(onCheckCbk, 'DEVICE_AUTH'); if (!(document instanceof DeviceSignedDocument)) { onCheck({ @@ -125,16 +139,16 @@ export class Verifier { return; } - if (!options.sessionTranscriptBytes) { + if (!this.#sessionTranscriptBytes) { onCheck({ status: 'FAILED', - check: 'Session Transcript Bytes missing from options, aborting device signature check', + check: 'Set Session Transcript Bytes with .usingSessionTranscriptForWepAPI, .usingSessionTranscriptForOID4VP or .usingSessionTranscriptBytes, aborting device signature check', }); return; } const deviceAuthenticationBytes = calculateDeviceAutenticationBytes( - options.sessionTranscriptBytes, + this.#sessionTranscriptBytes, docType, nameSpaces, ); @@ -190,17 +204,17 @@ export class Verifier { if (!deviceAuth.deviceMac.hasSupportedAlg()) { return; } onCheck({ - status: options.ephemeralPrivateKey ? 'PASSED' : 'FAILED', - check: 'Ephemeral private key must be present when using MAC authentication', + status: this.#ephemeralReaderKey ? 'PASSED' : 'FAILED', + check: 'Set Ephemeral private key with .usingEphemeralReaderKey using MAC authentication', }); - if (!options.ephemeralPrivateKey) { return; } + if (!this.#ephemeralReaderKey) { return; } try { const deviceKeyRaw = COSEKeyToRAW(deviceKeyCoseKey); const ephemeralMacKey = await calculateEphemeralMacKey( - options.ephemeralPrivateKey, + this.#ephemeralReaderKey, deviceKeyRaw, - options.sessionTranscriptBytes, + this.#sessionTranscriptBytes, ); const isValid = await deviceAuth.deviceMac.verify( @@ -299,39 +313,134 @@ export class Verifier { })); } + /** + * Set the session transcript data to use for the verification. + * + * This is arbitrary and should match the session transcript as it will be calculated by the verifier. + * The transcript must be a CBOR encoded DataItem of an array, there is no further requirement. + * + * Example: `usingSessionTranscriptBytes(cborEncode(DataItem.fromData([a,b,c])))` where `a`, `b` and `c` can be anything including `null`. + * + * It is preferable to use {@link usingSessionTranscriptForOID4VP} or {@link usingSessionTranscriptForWebAPI} when possible. + * + * @param {Buffer} sessionTranscriptBytes - The sessionTranscriptBytes data to use in the session transcript. + * @returns {Verifier} + */ + public usingSessionTranscriptBytes(sessionTranscriptBytes: Buffer): Verifier { + if (this.#sessionTranscriptBytes) { + throw new Error( + 'A session transcript has already been set, either with .usingSessionTranscriptForOID4VP, .usingSessionTranscriptForWebAPI or .usingSessionTranscriptBytes', + ); + } + this.#sessionTranscriptBytes = sessionTranscriptBytes; + return this; + } + + /** + * Set the session transcript data to use for the verification as defined in ISO/IEC 18013-7 in Annex B (OID4VP), 2024 draft. + * + * This should match the session transcript as it will be calculated by the mdoc app. + * + * @param {string} mdocGeneratedNonce - A cryptographically random number with sufficient entropy. + * @param {string} clientId - The client_id Authorization Request parameter from the Authorization Request Object. + * @param {string} responseUri - The response_uri Authorization Request parameter from the Authorization Request Object. + * @param {string} verifierGeneratedNonce - The nonce Authorization Request parameter from the Authorization Request Object. + * @returns {Verifier} + */ + public usingSessionTranscriptForOID4VP( + mdocGeneratedNonce: string, + clientId: string, + responseUri: string, + verifierGeneratedNonce: string, + ): Verifier { + this.usingSessionTranscriptBytes( + cborEncode( + DataItem.fromData([ + null, // deviceEngagementBytes + null, // eReaderKeyBytes + [ + createHash('sha256').update(cborEncode([clientId, mdocGeneratedNonce])).digest(), + createHash('sha256').update(cborEncode([responseUri, mdocGeneratedNonce])).digest(), + verifierGeneratedNonce, + ], + ]), + ), + ); + return this; + } + + /** + * Set the session transcript data to use for the verification as defined in ISO/IEC 18013-7 in Annex A (Web API), 2024 draft. + * + * This should match the session transcript as it will be calculated by the mdoc app. + * + * @param {Buffer} deviceEngagementBytes - The device engagement, encoded as a Tagged 24 cbor + * @param {Buffer} readerEngagementBytes - The reader engagement, encoded as a Tagged 24 cbor + * @param {Buffer} eReaderKeyBytes - The reader ephemeral public key as a COSE Key, encoded as a Tagged 24 cbor + * @returns {Verifier} + */ + public usingSessionTranscriptForWebAPI( + deviceEngagementBytes: Buffer, + readerEngagementBytes: Buffer, + eReaderKeyBytes: Buffer, + ): Verifier { + this.usingSessionTranscriptBytes( + cborEncode( + DataItem.fromData([ + new DataItem({ buffer: deviceEngagementBytes }), + new DataItem({ buffer: eReaderKeyBytes }), + createHash('sha256').update(readerEngagementBytes).digest(), + ]), + ), + ); + return this; + } + + /** + * @param {Buffer} ephemeralReaderKey - The private part of the ephemeral key used in the session where the DeviceResponse was obtained. This is only required if the DeviceResponse is using the MAC method for device authentication. + * @returns {Verifier} + */ + public usingEphemeralReaderKey(ephemeralReaderKey: Uint8Array): Verifier { + this.#ephemeralReaderKey = ephemeralReaderKey; + return this; + } + + /** + * Disables the certificate validation + * @returns {Verifier} + */ + public disableCertificateChainValidation(): Verifier { + this.#disableCertificateChainValidation = true; + return this; + } + /** * Parse and validate a DeviceResponse as specified in ISO/IEC 18013-5 (Device Retrieval section). * * @param encodedDeviceResponse - * @param options.encodedSessionTranscript The CBOR encoded SessionTranscript. - * @param options.ephemeralReaderKey The private part of the ephemeral key used in the session where the DeviceResponse was obtained. This is only required if the DeviceResponse is using the MAC method for device authentication. + * @param options.disableCertificateChainValidation Whether to use the certificate validation. */ async verify( encodedDeviceResponse: Uint8Array, - options: { - encodedSessionTranscript?: Uint8Array, - ephemeralReaderKey?: Uint8Array, - disableCertificateChainValidation?: boolean, - onCheck?: UserDefinedVerificationCallback - } = {}, + onCheck?: UserDefinedVerificationCallback, ): Promise { - const onCheck = buildCallback(options.onCheck); + const onCheckCbk = buildCallback(onCheck); const dr = parse(encodedDeviceResponse); - onCheck({ + onCheckCbk({ status: dr.version ? 'PASSED' : 'FAILED', check: 'Device Response must include "version" element.', category: 'DOCUMENT_FORMAT', }); - onCheck({ + onCheckCbk({ status: compareVersions(dr.version, '1.0') >= 0 ? 'PASSED' : 'FAILED', check: 'Device Response version must be 1.0 or greater', category: 'DOCUMENT_FORMAT', }); - onCheck({ + onCheckCbk({ status: dr.documents && dr.documents.length > 0 ? 'PASSED' : 'FAILED', check: 'Device Response must include at least one document.', category: 'DOCUMENT_FORMAT', @@ -339,15 +448,11 @@ export class Verifier { for (const document of dr.documents) { const { issuerAuth } = document.issuerSigned; - await this.verifyIssuerSignature(issuerAuth, options.disableCertificateChainValidation, onCheck); + await this.verifyIssuerSignature(issuerAuth, this.#disableCertificateChainValidation, onCheckCbk); - await this.verifyDeviceSignature(document, { - ephemeralPrivateKey: options.ephemeralReaderKey, - sessionTranscriptBytes: options.encodedSessionTranscript, - onCheck, - }); + await this.verifyDeviceSignature(document, onCheckCbk); - await this.verifyData(document, onCheck); + await this.verifyData(document, onCheckCbk); } return dr; @@ -355,20 +460,9 @@ export class Verifier { async getDiagnosticInformation( encodedDeviceResponse: Buffer, - options: { - encodedSessionTranscript?: Buffer, - ephemeralReaderKey?: Buffer, - disableCertificateChainValidation?: boolean, - }, ): Promise { const dr: VerificationAssessment[] = []; - const decoded = await this.verify( - encodedDeviceResponse, - { - ...options, - onCheck: (check) => dr.push(check), - }, - ); + const decoded = await this.verify(encodedDeviceResponse, (check) => dr.push(check)); const document = decoded.documents[0]; const { issuerAuth } = document.issuerSigned; From 6eae5dec5c25e78bf1d8800f2bf7e9102954ded9 Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Fri, 4 Oct 2024 18:46:02 +0200 Subject: [PATCH 07/16] fix: remove dead code and fix linting --- __tests__/example/example1.tests.ts | 3 ++- __tests__/issuing/deviceResponse.tests.ts | 15 +-------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/__tests__/example/example1.tests.ts b/__tests__/example/example1.tests.ts index ecc0696..b64b428 100644 --- a/__tests__/example/example1.tests.ts +++ b/__tests__/example/example1.tests.ts @@ -21,7 +21,8 @@ describe('example 1: valid device response with full disclosure', () => { it('should be able to verify without ephemeralReaderKey and encodedSessionTrasncript', async () => { await new Verifier([ISSUER_CERTIFICATE]) - .verify(deviceResponse, + .verify( + deviceResponse, (verification, original) => { if (verification.category === 'DEVICE_AUTH') { return; diff --git a/__tests__/issuing/deviceResponse.tests.ts b/__tests__/issuing/deviceResponse.tests.ts index db75576..13aee9f 100644 --- a/__tests__/issuing/deviceResponse.tests.ts +++ b/__tests__/issuing/deviceResponse.tests.ts @@ -1,4 +1,4 @@ -import { createHash, randomFillSync } from 'node:crypto'; +import { randomFillSync } from 'node:crypto'; import * as jose from 'jose'; import { MDoc, @@ -9,7 +9,6 @@ import { DeviceSignedDocument, } from '../../src'; import { DEVICE_JWK, ISSUER_CERTIFICATE, ISSUER_PRIVATE_KEY_JWK, PRESENTATION_DEFINITION_1 } from './config'; -import { DataItem, cborEncode } from '../../src/cbor'; const { d, ...publicKeyJWK } = DEVICE_JWK as jose.JWK; @@ -77,18 +76,6 @@ describe('issuing a device response', () => { 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 - [ - createHash('sha256').update(cborEncode([clId, mdocNonce])).digest(), - createHash('sha256').update(cborEncode([respUri, mdocNonce])).digest(), - nonce, - ], // Handover = OID4VPHandover - ]), - ); - beforeAll(async () => { // This is the Device side const devicePrivateKey = DEVICE_JWK; From 655f534b37af75b53c574d73bc9e5e0ebf99e71b Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Fri, 4 Oct 2024 18:52:08 +0200 Subject: [PATCH 08/16] docs: take review into account --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1a47cee..d21ddbf 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ [ISO 18013-5](https://www.iso.org/standard/69084.html) defines mDL (mobile Driver Licenses): an ISO standard for digital driver licenses. -> [!WARNING] This is a Node.js library to issue and verify mDL [CBOR encoded](https://cbor.io/) documents in accordance with **ISO 18013-7 (draft's date: 2024-02-13)**. +This is a Node.js library to issue and verify mDL [CBOR encoded](https://cbor.io/) documents in accordance with **ISO 18013-7 (draft's date: 2024-02-13)**. -> [!WARNING] please use v1.*.* to use **ISO 18013-7 (draft's date: 2023-08-02)**. +Please use v1.*.* to conform to **ISO 18013-7 (draft's date: 2023-08-02)**. ## Installation From f0646f1ad28b2ae1dda659d5e5307f4b5c3755b2 Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Fri, 4 Oct 2024 18:54:29 +0200 Subject: [PATCH 09/16] docs: update README.md Co-authored-by: Sebastian Iacomuzzi --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d21ddbf..49f4e83 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This is a Node.js library to issue and verify mDL [CBOR encoded](https://cbor.io/) documents in accordance with **ISO 18013-7 (draft's date: 2024-02-13)**. -Please use v1.*.* to conform to **ISO 18013-7 (draft's date: 2023-08-02)**. +> If you are working with the **2023 draft** make sure to use the `@auth0/mdl@v1` version. ## Installation From 4fd262ceeca9720295d3808d59acbf7cfe7d0bd3 Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Fri, 4 Oct 2024 19:10:44 +0200 Subject: [PATCH 10/16] docs: update code snippets --- README.md | 87 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 49f4e83..96838a4 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,35 @@ import fs from "node:fs"; const trustedCerts = [fs.readFileSync('./caCert1.pem')/*, ... */]; const verifier = new Verifier(trustedCerts); - const mdoc = await verifier.verify(encodedDeviceResponse, { - ephemeralReaderKey, - encodedSessionTranscript, - }); - //at this point the issuer and device signature are valids. + let mdoc; + + /** ... using a OID4VP handover: */ + { + mdoc = await verifier + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptForOID4VP( + mdocGeneratedNonce, // + clientId, // Parameters coming from + responseUri, // the OID4VP transaction + verifierGeneratedNonce // + ) + .verify(encodedDeviceResponse); + } + + /** ... OR ALTERNATIVELY using an "Annex A" transcript: */ + { + mdoc = await verifier + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptForWebAPI( + encodedDeviceEngagement, // CBOR as received from the reader + encodedReaderEngagement, // CBOR as sent to the reader + encodedReaderPublicKey, // as found in the ReaderEngagement + ) + .verify(encodedDeviceResponse); + } + + //at this point the issuer and device signature are valid. inspect(mdoc); })(); ``` @@ -51,11 +74,32 @@ import fs from "node:fs"; const trustedCerts = [fs.readFileSync('./caCert1.pem')/*, ... */]; const verifier = new Verifier(trustedCerts); + let diagnosticInfo; + + /** ... using a OID4VP handover: */ + { + mdoc = await verifier + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptForOID4VP( + mdocGeneratedNonce, // + clientId, // Parameters coming from + responseUri, // the OID4VP transaction + verifierGeneratedNonce // + ) + .getDiagnosticInformation(encodedDeviceResponse); + } - const diagnosticInfo = await verifier - .usingEphemeralReaderKey(ephemeralReaderKey) - .usingSessionTranscriptBytes(encodedSessionTranscript) - .getDiagnosticInformation(encodedDeviceResponse); + /** ... OR ALTERNATIVELY using an "Annex A" transcript: */ + { + mdoc = await verifier + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptForWebAPI( + encodedDeviceEngagement, // CBOR as received from the reader + encodedReaderEngagement, // CBOR as sent to the reader + encodedReaderPublicKey, // as found in the ReaderEngagement + ) + .getDiagnosticInformation(encodedDeviceResponse); + } inspect(diagnosticInfo); })(); @@ -152,30 +196,27 @@ import { createHash } from 'node:crypto'; /** ... using a OID4VP handover: */ { - // Parameters coming from the OID4VP transaction - let mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce; - deviceResponseMDoc = await DeviceResponse.from(issuerMDoc) .usingPresentationDefinition(presentationDefinition) - .usingSessionTranscriptForOID4VP(mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce) + .usingSessionTranscriptForOID4VP( + mdocGeneratedNonce, // + clientId, // Parameters coming from + responseUri, // the OID4VP transaction + verifierGeneratedNonce // + ) .authenticateWithSignature(devicePrivateKey, 'ES256') .sign(); } /** ... OR ALTERNATIVELY using an "Annex A" transcript: */ { - let encodedReaderEngagement; // CBOR as received from the reader - let encodedDeviceEngagement; // CBOR as sent to the reader - let encodedReaderPublicKey; // as found in the ReaderEngagement - - const engagementToApp = Buffer.from( - createHash('sha256').update(encodedReaderEngagement).digest('hex'), - 'hex', - ); - deviceResponseMDoc = await DeviceResponse.from(issuerMDoc) .usingPresentationDefinition(presentationDefinition) - .usingSessionTranscriptForWebAPI(encodedDeviceEngagement, encodedReaderEngagement, encodedReaderPublicKey) + .usingSessionTranscriptForWebAPI( + encodedDeviceEngagement, // CBOR as received from the reader + encodedReaderEngagement, // CBOR as sent to the reader + encodedReaderPublicKey, // as found in the ReaderEngagement + ) .authenticateWithSignature(devicePrivateKey, 'ES256') .sign(); } From d4da76eb16f25c14b5c52b33f6f0764ca521f652 Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Fri, 4 Oct 2024 19:15:37 +0200 Subject: [PATCH 11/16] docs: apply suggestions from code review Co-authored-by: Sebastian Iacomuzzi --- src/mdoc/Verifier.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mdoc/Verifier.ts b/src/mdoc/Verifier.ts index 9003553..a4e8966 100644 --- a/src/mdoc/Verifier.ts +++ b/src/mdoc/Verifier.ts @@ -316,7 +316,7 @@ export class Verifier { /** * Set the session transcript data to use for the verification. * - * This is arbitrary and should match the session transcript as it will be calculated by the verifier. + * This is arbitrary and should match the session transcript as it was calculated by the mdoc app (ie, wallet). * The transcript must be a CBOR encoded DataItem of an array, there is no further requirement. * * Example: `usingSessionTranscriptBytes(cborEncode(DataItem.fromData([a,b,c])))` where `a`, `b` and `c` can be anything including `null`. @@ -339,7 +339,7 @@ export class Verifier { /** * Set the session transcript data to use for the verification as defined in ISO/IEC 18013-7 in Annex B (OID4VP), 2024 draft. * - * This should match the session transcript as it will be calculated by the mdoc app. + * This should match the session transcript as it was calculated by the mdoc app. * * @param {string} mdocGeneratedNonce - A cryptographically random number with sufficient entropy. * @param {string} clientId - The client_id Authorization Request parameter from the Authorization Request Object. From 10fe386b4b716b0d63d9e43291673657cbd2752f Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Mon, 7 Oct 2024 10:48:29 +0200 Subject: [PATCH 12/16] fix: don't use node:crypto --- src/mdoc/Verifier.ts | 62 ++++++++++++++++++-------------- src/mdoc/model/DeviceResponse.ts | 62 +++++++++++++++++++------------- src/mdoc/utils.ts | 7 +++- 3 files changed, 80 insertions(+), 51 deletions(-) diff --git a/src/mdoc/Verifier.ts b/src/mdoc/Verifier.ts index a4e8966..cc4f5c1 100644 --- a/src/mdoc/Verifier.ts +++ b/src/mdoc/Verifier.ts @@ -1,4 +1,3 @@ -import { createHash } from 'node:crypto'; import { compareVersions } from 'compare-versions'; import { X509Certificate } from '@peculiar/x509'; import { importX509, JWK, KeyLike } from 'jose'; @@ -10,6 +9,7 @@ import { MDoc } from './model/MDoc'; import { calculateEphemeralMacKey, calculateDeviceAutenticationBytes, + sha256, } from './utils'; import { @@ -40,7 +40,7 @@ export class Verifier { * @see {@link usingSessionTranscriptForOID4VP} * @see {@link usingSessionTranscriptForWebAPI} */ - #sessionTranscriptBytes: Buffer; + #sessionTranscriptBytes: Promise | Buffer; /** * The raw private part of the ephemeral reader key. @@ -148,7 +148,7 @@ export class Verifier { } const deviceAuthenticationBytes = calculateDeviceAutenticationBytes( - this.#sessionTranscriptBytes, + await this.#sessionTranscriptBytes, docType, nameSpaces, ); @@ -214,7 +214,7 @@ export class Verifier { const ephemeralMacKey = await calculateEphemeralMacKey( this.#ephemeralReaderKey, deviceKeyRaw, - this.#sessionTranscriptBytes, + await this.#sessionTranscriptBytes, ); const isValid = await deviceAuth.deviceMac.verify( @@ -323,10 +323,10 @@ export class Verifier { * * It is preferable to use {@link usingSessionTranscriptForOID4VP} or {@link usingSessionTranscriptForWebAPI} when possible. * - * @param {Buffer} sessionTranscriptBytes - The sessionTranscriptBytes data to use in the session transcript. + * @param {Buffer | Promise} sessionTranscriptBytes - The sessionTranscriptBytes data to use in the session transcript. * @returns {Verifier} */ - public usingSessionTranscriptBytes(sessionTranscriptBytes: Buffer): Verifier { + public usingSessionTranscriptBytes(sessionTranscriptBytes: Buffer | Promise): Verifier { if (this.#sessionTranscriptBytes) { throw new Error( 'A session transcript has already been set, either with .usingSessionTranscriptForOID4VP, .usingSessionTranscriptForWebAPI or .usingSessionTranscriptBytes', @@ -341,10 +341,10 @@ export class Verifier { * * This should match the session transcript as it was calculated by the mdoc app. * - * @param {string} mdocGeneratedNonce - A cryptographically random number with sufficient entropy. + * @param {string} mdocGeneratedNonce - The mdoc-generated nonce, taken from the `apu` parameter in the Authorization Response * @param {string} clientId - The client_id Authorization Request parameter from the Authorization Request Object. * @param {string} responseUri - The response_uri Authorization Request parameter from the Authorization Request Object. - * @param {string} verifierGeneratedNonce - The nonce Authorization Request parameter from the Authorization Request Object. + * @param {string} verifierGeneratedNonce - The nonce taken from the `apv` parameter in the Authorization Response (it should match the `nonce` sent in the Authorization Request parameter from the Authorization Request Object). * @returns {Verifier} */ public usingSessionTranscriptForOID4VP( @@ -354,21 +354,30 @@ export class Verifier { verifierGeneratedNonce: string, ): Verifier { this.usingSessionTranscriptBytes( - cborEncode( - DataItem.fromData([ - null, // deviceEngagementBytes - null, // eReaderKeyBytes - [ - createHash('sha256').update(cborEncode([clientId, mdocGeneratedNonce])).digest(), - createHash('sha256').update(cborEncode([responseUri, mdocGeneratedNonce])).digest(), - verifierGeneratedNonce, - ], - ]), - ), + this.#oid4vptranscript(mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce), ); return this; } + async #oid4vptranscript( + mdocGeneratedNonce: string, + clientId: string, + responseUri: string, + verifierGeneratedNonce: string, + ) { + return cborEncode( + DataItem.fromData([ + null, // deviceEngagementBytes + null, // eReaderKeyBytes + [ + await sha256(cborEncode([clientId, mdocGeneratedNonce])), + await sha256(cborEncode([responseUri, mdocGeneratedNonce])), + verifierGeneratedNonce, + ], + ]), + ); + } + /** * Set the session transcript data to use for the verification as defined in ISO/IEC 18013-7 in Annex A (Web API), 2024 draft. * @@ -385,12 +394,14 @@ export class Verifier { eReaderKeyBytes: Buffer, ): Verifier { this.usingSessionTranscriptBytes( - cborEncode( - DataItem.fromData([ - new DataItem({ buffer: deviceEngagementBytes }), - new DataItem({ buffer: eReaderKeyBytes }), - createHash('sha256').update(readerEngagementBytes).digest(), - ]), + sha256(readerEngagementBytes).then( + (readerEngagementBytesHash) => cborEncode( + DataItem.fromData([ + new DataItem({ buffer: deviceEngagementBytes }), + new DataItem({ buffer: eReaderKeyBytes }), + readerEngagementBytesHash, + ]), + ), ), ); return this; @@ -418,7 +429,6 @@ export class Verifier { * Parse and validate a DeviceResponse as specified in ISO/IEC 18013-5 (Device Retrieval section). * * @param encodedDeviceResponse - * @param options.disableCertificateChainValidation Whether to use the certificate validation. */ async verify( encodedDeviceResponse: Uint8Array, diff --git a/src/mdoc/model/DeviceResponse.ts b/src/mdoc/model/DeviceResponse.ts index fbf87dd..43ce17f 100644 --- a/src/mdoc/model/DeviceResponse.ts +++ b/src/mdoc/model/DeviceResponse.ts @@ -1,4 +1,3 @@ -import { createHash } from 'node:crypto'; import * as jose from 'jose'; import { COSEKeyFromJWK, COSEKeyToJWK, Mac0, Sign1, importCOSEKey } from 'cose-kit'; import { Buffer } from 'buffer'; @@ -9,7 +8,7 @@ import { IssuerSignedDocument } from './IssuerSignedDocument'; import { DeviceSignedDocument } from './DeviceSignedDocument'; import { IssuerSignedItem } from '../IssuerSignedItem'; import { parse } from '../parser'; -import { calculateDeviceAutenticationBytes, calculateEphemeralMacKey } from '../utils'; +import { calculateDeviceAutenticationBytes, calculateEphemeralMacKey, sha256 } from '../utils'; import { DataItem, cborEncode } from '../../cbor'; import COSEKeyToRAW from '../../cose/coseKey'; @@ -19,7 +18,7 @@ import COSEKeyToRAW from '../../cose/coseKey'; export class DeviceResponse { private mdoc: MDoc; private pd: PresentationDefinition; - private sessionTranscriptBytes: Buffer; + private sessionTranscriptBytes: Promise | Buffer; private useMac = true; private devicePrivateKey: Uint8Array; public deviceResponseCbor: Buffer; @@ -78,13 +77,17 @@ export class DeviceResponse { * @param {Buffer} sessionTranscriptBytes - The sessionTranscriptBytes data to use in the session transcript. * @returns {DeviceResponse} */ - public usingSessionTranscriptBytes(sessionTranscriptBytes: Buffer): DeviceResponse { + public usingSessionTranscriptBytes(sessionTranscriptBytes: Buffer | Promise): DeviceResponse { if (this.sessionTranscriptBytes) { throw new Error( 'A session transcript has already been set, either with .usingSessionTranscriptForOID4VP, .usingSessionTranscriptForWebAPI or .usingSessionTranscriptBytes', ); } - this.sessionTranscriptBytes = sessionTranscriptBytes; + if (sessionTranscriptBytes instanceof Buffer) { + this.sessionTranscriptBytes = Promise.resolve(sessionTranscriptBytes); + } else { + this.sessionTranscriptBytes = sessionTranscriptBytes; + } return this; } @@ -106,21 +109,30 @@ export class DeviceResponse { verifierGeneratedNonce: string, ): DeviceResponse { this.usingSessionTranscriptBytes( - cborEncode( - DataItem.fromData([ - null, // deviceEngagementBytes - null, // eReaderKeyBytes - [ - createHash('sha256').update(cborEncode([clientId, mdocGeneratedNonce])).digest(), - createHash('sha256').update(cborEncode([responseUri, mdocGeneratedNonce])).digest(), - verifierGeneratedNonce, - ], - ]), - ), + this.#oid4vptranscript(mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce), ); return this; } + async #oid4vptranscript( + mdocGeneratedNonce: string, + clientId: string, + responseUri: string, + verifierGeneratedNonce: string, + ) { + return cborEncode( + DataItem.fromData([ + null, // deviceEngagementBytes + null, // eReaderKeyBytes + [ + await sha256(cborEncode([clientId, mdocGeneratedNonce])), + await sha256(cborEncode([responseUri, mdocGeneratedNonce])), + verifierGeneratedNonce, + ], + ]), + ); + } + /** * Set the session transcript data to use for the device response as defined in ISO/IEC 18013-7 in Annex A (Web API), 2024 draft. * @@ -137,12 +149,14 @@ export class DeviceResponse { eReaderKeyBytes: Buffer, ): DeviceResponse { this.usingSessionTranscriptBytes( - cborEncode( - DataItem.fromData([ - new DataItem({ buffer: deviceEngagementBytes }), - new DataItem({ buffer: eReaderKeyBytes }), - createHash('sha256').update(readerEngagementBytes).digest(), - ]), + sha256(readerEngagementBytes).then( + (readerEngagementBytesHash) => cborEncode( + DataItem.fromData([ + new DataItem({ buffer: deviceEngagementBytes }), + new DataItem({ buffer: eReaderKeyBytes }), + readerEngagementBytesHash, + ]), + ), ), ); return this; @@ -239,7 +253,7 @@ export class DeviceResponse { private async getDeviceSigned(docType: string): Promise { const deviceAuthenticationBytes = calculateDeviceAutenticationBytes( - this.sessionTranscriptBytes, + await this.sessionTranscriptBytes, docType, this.nameSpaces, ); @@ -264,7 +278,7 @@ export class DeviceResponse { const ephemeralMacKey = await calculateEphemeralMacKey( key, this.ephemeralPublicKey, - sessionTranscriptBytes, + await sessionTranscriptBytes, ); const mac = await Mac0.create( diff --git a/src/mdoc/utils.ts b/src/mdoc/utils.ts index 8106c1b..bc9bf62 100644 --- a/src/mdoc/utils.ts +++ b/src/mdoc/utils.ts @@ -11,6 +11,11 @@ const { subtle } = webcrypto; pkijs.setEngine('webcrypto', new pkijs.CryptoEngine({ name: 'webcrypto', crypto: webcrypto, subtle })); +export async function sha256(data: Buffer): Promise { + const hash = await webcrypto.subtle.digest('sha-256', data); + return Buffer.from(hash); +} + export const hmacSHA256 = async ( key: ArrayBuffer, data: ArrayBuffer, @@ -50,7 +55,7 @@ export const calculateEphemeralMacKey = async ( Buffer.from(publicKey).toString('hex'), true, ).slice(1); - const salt = new Uint8Array(await subtle.digest('SHA-256', sessionTranscriptBytes)); + const salt = await sha256(Buffer.from(sessionTranscriptBytes)); const info = Buffer.from('EMacKey', 'utf-8'); const result = await hkdf('sha256', ikm, salt, info, 32); return result; From 2e172eb75ceaa6ff878f586ea44c99c39853e5b9 Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Tue, 8 Oct 2024 11:02:31 +0200 Subject: [PATCH 13/16] chore: move helper functions to utils --- src/mdoc/Verifier.ts | 35 ++++---------------------------- src/mdoc/model/DeviceResponse.ts | 34 +++---------------------------- src/mdoc/utils.ts | 34 +++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 62 deletions(-) diff --git a/src/mdoc/Verifier.ts b/src/mdoc/Verifier.ts index cc4f5c1..7e35cba 100644 --- a/src/mdoc/Verifier.ts +++ b/src/mdoc/Verifier.ts @@ -9,7 +9,8 @@ import { MDoc } from './model/MDoc'; import { calculateEphemeralMacKey, calculateDeviceAutenticationBytes, - sha256, + oid4vpTranscript, + webapiTranscript, } from './utils'; import { @@ -23,7 +24,6 @@ import IssuerAuth from './model/IssuerAuth'; import { IssuerSignedDocument } from './model/IssuerSignedDocument'; import { DeviceSignedDocument } from './model/DeviceSignedDocument'; import COSEKeyToRAW from '../cose/coseKey'; -import { cborEncode, DataItem } from '../cbor'; const MDL_NAMESPACE = 'org.iso.18013.5.1'; @@ -354,30 +354,11 @@ export class Verifier { verifierGeneratedNonce: string, ): Verifier { this.usingSessionTranscriptBytes( - this.#oid4vptranscript(mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce), + oid4vpTranscript(mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce), ); return this; } - async #oid4vptranscript( - mdocGeneratedNonce: string, - clientId: string, - responseUri: string, - verifierGeneratedNonce: string, - ) { - return cborEncode( - DataItem.fromData([ - null, // deviceEngagementBytes - null, // eReaderKeyBytes - [ - await sha256(cborEncode([clientId, mdocGeneratedNonce])), - await sha256(cborEncode([responseUri, mdocGeneratedNonce])), - verifierGeneratedNonce, - ], - ]), - ); - } - /** * Set the session transcript data to use for the verification as defined in ISO/IEC 18013-7 in Annex A (Web API), 2024 draft. * @@ -394,15 +375,7 @@ export class Verifier { eReaderKeyBytes: Buffer, ): Verifier { this.usingSessionTranscriptBytes( - sha256(readerEngagementBytes).then( - (readerEngagementBytesHash) => cborEncode( - DataItem.fromData([ - new DataItem({ buffer: deviceEngagementBytes }), - new DataItem({ buffer: eReaderKeyBytes }), - readerEngagementBytesHash, - ]), - ), - ), + webapiTranscript(deviceEngagementBytes, readerEngagementBytes, eReaderKeyBytes), ); return this; } diff --git a/src/mdoc/model/DeviceResponse.ts b/src/mdoc/model/DeviceResponse.ts index 43ce17f..ec9b595 100644 --- a/src/mdoc/model/DeviceResponse.ts +++ b/src/mdoc/model/DeviceResponse.ts @@ -8,8 +8,7 @@ import { IssuerSignedDocument } from './IssuerSignedDocument'; import { DeviceSignedDocument } from './DeviceSignedDocument'; import { IssuerSignedItem } from '../IssuerSignedItem'; import { parse } from '../parser'; -import { calculateDeviceAutenticationBytes, calculateEphemeralMacKey, sha256 } from '../utils'; -import { DataItem, cborEncode } from '../../cbor'; +import { calculateDeviceAutenticationBytes, calculateEphemeralMacKey, oid4vpTranscript, webapiTranscript } from '../utils'; import COSEKeyToRAW from '../../cose/coseKey'; /** @@ -109,30 +108,11 @@ export class DeviceResponse { verifierGeneratedNonce: string, ): DeviceResponse { this.usingSessionTranscriptBytes( - this.#oid4vptranscript(mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce), + oid4vpTranscript(mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce), ); return this; } - async #oid4vptranscript( - mdocGeneratedNonce: string, - clientId: string, - responseUri: string, - verifierGeneratedNonce: string, - ) { - return cborEncode( - DataItem.fromData([ - null, // deviceEngagementBytes - null, // eReaderKeyBytes - [ - await sha256(cborEncode([clientId, mdocGeneratedNonce])), - await sha256(cborEncode([responseUri, mdocGeneratedNonce])), - verifierGeneratedNonce, - ], - ]), - ); - } - /** * Set the session transcript data to use for the device response as defined in ISO/IEC 18013-7 in Annex A (Web API), 2024 draft. * @@ -149,15 +129,7 @@ export class DeviceResponse { eReaderKeyBytes: Buffer, ): DeviceResponse { this.usingSessionTranscriptBytes( - sha256(readerEngagementBytes).then( - (readerEngagementBytesHash) => cborEncode( - DataItem.fromData([ - new DataItem({ buffer: deviceEngagementBytes }), - new DataItem({ buffer: eReaderKeyBytes }), - readerEngagementBytesHash, - ]), - ), - ), + webapiTranscript(deviceEngagementBytes, readerEngagementBytes, eReaderKeyBytes), ); return this; } diff --git a/src/mdoc/utils.ts b/src/mdoc/utils.ts index bc9bf62..17cb02b 100644 --- a/src/mdoc/utils.ts +++ b/src/mdoc/utils.ts @@ -95,3 +95,37 @@ export function fromPEM(pem: string): Uint8Array { const base64 = pem.replace(/-{5}(BEGIN|END) .*-{5}/gm, '').replace(/\s/gm, ''); return Buffer.from(base64, 'base64'); } + +export async function oid4vpTranscript( + mdocGeneratedNonce: string, + clientId: string, + responseUri: string, + verifierGeneratedNonce: string, +) { + return cborEncode( + DataItem.fromData([ + null, // deviceEngagementBytes + null, // eReaderKeyBytes + [ + await sha256(cborEncode([clientId, mdocGeneratedNonce])), + await sha256(cborEncode([responseUri, mdocGeneratedNonce])), + verifierGeneratedNonce, + ], + ]), + ); +} + +export async function webapiTranscript( + deviceEngagementBytes: Buffer, + readerEngagementBytes: Buffer, + eReaderKeyBytes: Buffer, +) { + return sha256(readerEngagementBytes) + .then((readerEngagementBytesHash) => cborEncode( + DataItem.fromData([ + new DataItem({ buffer: deviceEngagementBytes }), + new DataItem({ buffer: eReaderKeyBytes }), + readerEngagementBytesHash, + ]), + )); +} From 40bc7006bd5ad09a0242f5151a678a3d43a06c91 Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Tue, 8 Oct 2024 11:05:11 +0200 Subject: [PATCH 14/16] fix: remove unnecessary runtime type checking --- src/mdoc/model/DeviceResponse.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/mdoc/model/DeviceResponse.ts b/src/mdoc/model/DeviceResponse.ts index ec9b595..2bebf18 100644 --- a/src/mdoc/model/DeviceResponse.ts +++ b/src/mdoc/model/DeviceResponse.ts @@ -82,11 +82,7 @@ export class DeviceResponse { 'A session transcript has already been set, either with .usingSessionTranscriptForOID4VP, .usingSessionTranscriptForWebAPI or .usingSessionTranscriptBytes', ); } - if (sessionTranscriptBytes instanceof Buffer) { - this.sessionTranscriptBytes = Promise.resolve(sessionTranscriptBytes); - } else { - this.sessionTranscriptBytes = sessionTranscriptBytes; - } + this.sessionTranscriptBytes = sessionTranscriptBytes; return this; } From 62e5c28cb4decc9ca3282c6e7a85de2ededc375f Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Tue, 8 Oct 2024 11:10:56 +0200 Subject: [PATCH 15/16] docs: updates to README.MD Co-authored-by: Sebastian Iacomuzzi --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 96838a4..4801541 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ import fs from "node:fs"; let mdoc; - /** ... using a OID4VP handover: */ + /** ... using OID4VP protocol (Annex B): */ { mdoc = await verifier .usingEphemeralReaderKey(ephemeralReaderKey) @@ -42,7 +42,7 @@ import fs from "node:fs"; .verify(encodedDeviceResponse); } - /** ... OR ALTERNATIVELY using an "Annex A" transcript: */ + /** ... OR ALTERNATIVELY using the Web API protocol (Annex A): */ { mdoc = await verifier .usingEphemeralReaderKey(ephemeralReaderKey) @@ -76,7 +76,7 @@ import fs from "node:fs"; let diagnosticInfo; - /** ... using a OID4VP handover: */ + /** ... using OID4VP protocol (Annex B): */ { mdoc = await verifier .usingEphemeralReaderKey(ephemeralReaderKey) @@ -89,7 +89,7 @@ import fs from "node:fs"; .getDiagnosticInformation(encodedDeviceResponse); } - /** ... OR ALTERNATIVELY using an "Annex A" transcript: */ + /** ... OR ALTERNATIVELY using the Web API protocol (Annex A): */ { mdoc = await verifier .usingEphemeralReaderKey(ephemeralReaderKey) @@ -194,7 +194,7 @@ import { createHash } from 'node:crypto'; ], }; - /** ... using a OID4VP handover: */ + /** ... using OID4VP protocol (Annex B): */ { deviceResponseMDoc = await DeviceResponse.from(issuerMDoc) .usingPresentationDefinition(presentationDefinition) @@ -208,7 +208,7 @@ import { createHash } from 'node:crypto'; .sign(); } - /** ... OR ALTERNATIVELY using an "Annex A" transcript: */ + /** ... OR ALTERNATIVELY using the Web API protocol (Annex A): */ { deviceResponseMDoc = await DeviceResponse.from(issuerMDoc) .usingPresentationDefinition(presentationDefinition) From 4fb40a2618d1d4b8a9653013c2733c4978eafde8 Mon Sep 17 00:00:00 2001 From: Juan Manuel CABRERA Date: Thu, 14 Nov 2024 10:23:47 +0100 Subject: [PATCH 16/16] feat: wip Do a proper device request --- src/mdoc/model/DeviceRequest.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/mdoc/model/DeviceRequest.ts diff --git a/src/mdoc/model/DeviceRequest.ts b/src/mdoc/model/DeviceRequest.ts new file mode 100644 index 0000000..bcc4c35 --- /dev/null +++ b/src/mdoc/model/DeviceRequest.ts @@ -0,0 +1,33 @@ +export type PresentationDefinitionField = { + path: string[]; + intent_to_retain: boolean; +} + +export type Format = { + mso_mdoc: { + alg: string[]; + }; +}; + +export type InputDescriptor = { + id: string; + format: Format; + constraints: { + limit_disclosure: string; + fields: PresentationDefinitionField[] + } +}; + +export type DeviceRequest = { + version: '1.1' + docRequests: DocRequest[]; + /** + * List of MAC Keys from the reader, one per supported curve. + * Optional field. + */ + macKeys?: Buffer[]; +}; + +export type DocRequest = { + +}