diff --git a/README.md b/README.md index 6de2b02..4801541 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: 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)**. + +> If you are working with the **2023 draft** make sure to use the `@auth0/mdl@v1` version. ## Installation @@ -24,20 +26,41 @@ 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 OID4VP protocol (Annex B): */ + { + mdoc = await verifier + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptForOID4VP( + mdocGeneratedNonce, // + clientId, // Parameters coming from + responseUri, // the OID4VP transaction + verifierGeneratedNonce // + ) + .verify(encodedDeviceResponse); + } + + /** ... OR ALTERNATIVELY using the Web API protocol (Annex A): */ + { + 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); })(); ``` ## Getting diagnostic information - - ```javascript import { Verifier } from "@auth0/mdl"; import { inspect } from "node:util"; @@ -51,10 +74,32 @@ import fs from "node:fs"; const trustedCerts = [fs.readFileSync('./caCert1.pem')/*, ... */]; const verifier = new Verifier(trustedCerts); - const diagnosticInfo = await verifier.getDiagnosticInformation(encodedDeviceResponse, { - ephemeralReaderKey, - encodedSessionTranscript, - }); + let diagnosticInfo; + + /** ... using OID4VP protocol (Annex B): */ + { + mdoc = await verifier + .usingEphemeralReaderKey(ephemeralReaderKey) + .usingSessionTranscriptForOID4VP( + mdocGeneratedNonce, // + clientId, // Parameters coming from + responseUri, // the OID4VP transaction + verifierGeneratedNonce // + ) + .getDiagnosticInformation(encodedDeviceResponse); + } + + /** ... OR ALTERNATIVELY using the Web API protocol (Annex A): */ + { + 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); })(); @@ -149,39 +194,29 @@ import { createHash } from 'node:crypto'; ], }; - /** ... using a OID4VP handover: */ + /** ... using OID4VP protocol (Annex B): */ { - // 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: */ + /** ... OR ALTERNATIVELY using the Web API protocol (Annex A): */ { - 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', - ); - const sessionTranscriptBytes = cborEncode( - DataItem.fromData([ - new DataItem({ buffer: encodedDeviceEngagement }), - new DataItem({ buffer: encodedReaderPublicKey }), - engagementToApp, - ]), - ); - 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(); } 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..b64b428 100644 --- a/__tests__/example/example1.tests.ts +++ b/__tests__/example/example1.tests.ts @@ -11,23 +11,24 @@ 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 68924c4..13aee9f 100644 --- a/__tests__/issuing/deviceResponse.tests.ts +++ b/__tests__/issuing/deviceResponse.tests.ts @@ -1,6 +1,5 @@ -import { createHash, randomFillSync } from 'node:crypto'; +import { randomFillSync } from 'node:crypto'; import * as jose from 'jose'; -import { COSEKeyFromJWK } from 'cose-kit'; import { MDoc, Document, @@ -10,8 +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'; -import COSEKeyToRAW from '../../src/cose/coseKey'; const { d, ...publicKeyJWK } = DEVICE_JWK as jose.JWK; @@ -79,14 +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 - [mdocNonce, clId, respUri, nonce], // Handover = OID4VPHandover - ]), - ); - beforeAll(async () => { // This is the Device side const devicePrivateKey = DEVICE_JWK; @@ -104,9 +93,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', () => { @@ -119,9 +108,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'); @@ -155,18 +144,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 }), - rdrEngtBytes, - ]), - ); - beforeAll(async () => { // Nothing more to do on the verifier side. @@ -188,24 +165,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 f2a5a01..864a0fb 100644 --- a/__tests__/issuing/deviceResponseWithMac.tests.ts +++ b/__tests__/issuing/deviceResponseWithMac.tests.ts @@ -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,14 +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 - [mdocNonce, clId, respUri, nonce], // Handover = OID4VPHandover - ]), - ); - beforeAll(async () => { // This is the Device side const deviceResponseMDoc = await DeviceResponse.from(mdoc) @@ -113,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', () => { @@ -129,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'); @@ -166,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 }), - rdrEngtBytes, - ]), - ); - beforeAll(async () => { // This is the verifier side before requesting the Device Response { @@ -204,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..7e35cba 100644 --- a/src/mdoc/Verifier.ts +++ b/src/mdoc/Verifier.ts @@ -9,6 +9,8 @@ import { MDoc } from './model/MDoc'; import { calculateEphemeralMacKey, calculateDeviceAutenticationBytes, + oid4vpTranscript, + webapiTranscript, } from './utils'; import { @@ -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: Promise | 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, + await 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, + await this.#sessionTranscriptBytes, ); const isValid = await deviceAuth.deviceMac.verify( @@ -299,39 +313,117 @@ export class Verifier { })); } + /** + * Set the session transcript data to use for the verification. + * + * 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`. + * + * It is preferable to use {@link usingSessionTranscriptForOID4VP} or {@link usingSessionTranscriptForWebAPI} when possible. + * + * @param {Buffer | Promise} sessionTranscriptBytes - The sessionTranscriptBytes data to use in the session transcript. + * @returns {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', + ); + } + 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 was calculated by the mdoc app. + * + * @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 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( + mdocGeneratedNonce: string, + clientId: string, + responseUri: string, + verifierGeneratedNonce: string, + ): Verifier { + this.usingSessionTranscriptBytes( + oid4vpTranscript(mdocGeneratedNonce, clientId, responseUri, 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( + webapiTranscript(deviceEngagementBytes, readerEngagementBytes, eReaderKeyBytes), + ); + 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. */ 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 +431,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 +443,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; 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 = { + +} diff --git a/src/mdoc/model/DeviceResponse.ts b/src/mdoc/model/DeviceResponse.ts index dd28284..2bebf18 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 } from '../utils'; -import { DataItem, cborEncode } from '../../cbor'; +import { calculateDeviceAutenticationBytes, calculateEphemeralMacKey, oid4vpTranscript, webapiTranscript } from '../utils'; import COSEKeyToRAW from '../../cose/coseKey'; /** @@ -18,7 +17,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; @@ -64,23 +63,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. * @@ -94,7 +76,7 @@ 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', @@ -105,7 +87,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,19 +104,13 @@ export class DeviceResponse { verifierGeneratedNonce: string, ): DeviceResponse { this.usingSessionTranscriptBytes( - cborEncode( - DataItem.fromData([ - null, // deviceEngagementBytes - null, // eReaderKeyBytes - [mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce], - ]), - ), + oid4vpTranscript(mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce), ); return this; } /** - * 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. * @@ -149,13 +125,7 @@ export class DeviceResponse { eReaderKeyBytes: Buffer, ): DeviceResponse { this.usingSessionTranscriptBytes( - cborEncode( - DataItem.fromData([ - new DataItem({ buffer: deviceEngagementBytes }), - new DataItem({ buffer: eReaderKeyBytes }), - readerEngagementBytes, - ]), - ), + webapiTranscript(deviceEngagementBytes, readerEngagementBytes, eReaderKeyBytes), ); return this; } @@ -251,7 +221,7 @@ export class DeviceResponse { private async getDeviceSigned(docType: string): Promise { const deviceAuthenticationBytes = calculateDeviceAutenticationBytes( - this.sessionTranscriptBytes, + await this.sessionTranscriptBytes, docType, this.nameSpaces, ); @@ -276,7 +246,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..17cb02b 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; @@ -90,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, + ]), + )); +}