Skip to content

Commit

Permalink
feat: allow an arbitrary session transcript (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmcabrera authored Oct 1, 2024
1 parent 2572d27 commit ebd27c6
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 129 deletions.
79 changes: 69 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,20 @@ import fs from "node:fs";
## Generating a device response

```js
import { DeviceResponse } from "@auth0/mdl";
(() => {
import { DeviceResponse, DataItem, MDoc, DataItem, cborEncode} from '@auth0/mdl';
import { createHash } from 'node:crypto';

(async () => {
let issuerMDoc;
let deviceResponseMDoc;

// This is what the MDL issuer does to generate a credential:
/**
* This is what the MDL issuer does to generate a credential:
*/
{
let issuerPrivateKey;
let issuerCertificate;
let devicePublicKey; // the public key for the device, as a JWK
const document = await new Document('org.iso.18013.5.1.mDL')
.addIssuerNameSpace('org.iso.18013.5.1', {
family_name: 'Jones',
Expand All @@ -110,7 +117,7 @@ import { DeviceResponse } from "@auth0/mdl";
.addValidityInfo({
signed: new Date(),
})
.addDeviceKeyInfo({ deviceKey: publicKeyJWK })
.addDeviceKeyInfo({ deviceKey: devicePublicKey })
.sign({
issuerPrivateKey,
issuerCertificate,
Expand All @@ -119,13 +126,65 @@ import { DeviceResponse } from "@auth0/mdl";
issuerMDoc = new MDoc([document]).encode();
}

// This is what the DEVICE does to generate a response:
/**
* This is what the DEVICE does to generate a response...
*/
{
deviceResponseMDoc = await DeviceResponse.from(issuerMDoc)
.usingPresentationDefinition(PRESENTATION_DEFINITION_1)
.usingHandover([mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce])
.authenticateWithSignature(devicePrivateKey, 'ES256')
.sign();
let devicePrivateKey; // the private key for the device, as a JWK
let presentationDefinition = {
// the presentation definition we create a response for
id: 'family_name_only',
input_descriptors: [
{
id: 'org.iso.18013.5.1.mDL',
format: { mso_mdoc: { alg: ['EdDSA', 'ES256'] } },
constraints: {
limit_disclosure: 'required',
fields: [{
path: ["$['org.iso.18013.5.1']['family_name']"],
intent_to_retain: false,
}],
},
},
],
};

/** ... using a OID4VP handover: */
{
// Parameters coming from the OID4VP transaction
let mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce;

deviceResponseMDoc = await DeviceResponse.from(issuerMDoc)
.usingPresentationDefinition(presentationDefinition)
.usingHandover([mdocGeneratedNonce, clientId, responseUri, 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',
);
const sessionTranscriptBytes = cborEncode(
DataItem.fromData([
new DataItem({ buffer: encodedDeviceEngagement }),
new DataItem({ buffer: encodedReaderPublicKey }),
engagementToApp,
]),
);

deviceResponseMDoc = await DeviceResponse.from(issuerMDoc)
.usingPresentationDefinition(presentationDefinition)
.usingSessionTranscriptBytes(sessionTranscriptBytes)
.authenticateWithSignature(devicePrivateKey, 'ES256')
.sign();
}
}
})();
```
Expand Down
2 changes: 1 addition & 1 deletion __tests__/diagnostic.tests.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { hex } from 'buffer-tag';
import { Verifier } from '../src/index';
import { DiagnosticInformation } from '../src/deviceResponse/types';
import { DiagnosticInformation } from '../src/mdoc/model/types';

export const ISSUER_CERTIFICATE = `-----BEGIN CERTIFICATE-----
MIICKjCCAdCgAwIBAgIUV8bM0wi95D7KN0TyqHE42ru4hOgwCgYIKoZIzj0EAwIw
Expand Down
138 changes: 97 additions & 41 deletions __tests__/issuing/deviceResponse.tests.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { randomFillSync } from 'crypto';
import * as jose from 'jose';
import {
MDoc,
Expand Down Expand Up @@ -77,55 +78,110 @@ describe('issuing a device response', () => {

mdoc = new MDoc([document]);
}
});

describe('using OID4VP handover', () => {
beforeAll(async () => {
// This is the Device side
{
const verifierGeneratedNonce = 'abcdefg';
const mdocGeneratedNonce = '123456';
const clientId = 'Cq1anPb8vZU5j5C0d7hcsbuJLBpIawUJIDQRi2Ebwb4';
const responseUri = 'http://localhost:4000/api/presentation_request/dc8999df-d6ea-4c84-9985-37a8b81a82ec/callback';
const devicePrivateKey = DEVICE_JWK;

const deviceResponseMDoc = await DeviceResponse.from(mdoc)
.usingPresentationDefinition(PRESENTATION_DEFINITION_1)
.usingHandover([mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce])
.authenticateWithSignature(devicePrivateKey, 'ES256')
.addDeviceNameSpace('com.foobar-device', { test: 1234 })
.sign();

encodedSessionTranscript = getSessionTranscriptBytes(
{ client_id: clientId, response_uri: responseUri, nonce: verifierGeneratedNonce },
mdocGeneratedNonce,
);

encoded = deviceResponseMDoc.encode();
}
{
const verifierGeneratedNonce = 'abcdefg';
const mdocGeneratedNonce = '123456';
const clientId = 'Cq1anPb8vZU5j5C0d7hcsbuJLBpIawUJIDQRi2Ebwb4';
const responseUri = 'http://localhost:4000/api/presentation_request/dc8999df-d6ea-4c84-9985-37a8b81a82ec/callback';
const devicePrivateKey = DEVICE_JWK;

const deviceResponseMDoc = await DeviceResponse.from(mdoc)
.usingPresentationDefinition(PRESENTATION_DEFINITION_1)
.usingHandover([mdocGeneratedNonce, clientId, responseUri, verifierGeneratedNonce])
.authenticateWithSignature(devicePrivateKey, 'ES256')
.addDeviceNameSpace('com.foobar-device', { test: 1234 })
.sign();

encodedSessionTranscript = getSessionTranscriptBytes(
{ client_id: clientId, response_uri: responseUri, nonce: verifierGeneratedNonce },
mdocGeneratedNonce,
);

encoded = deviceResponseMDoc.encode();
}

const parsedMDOC = parse(encoded);
[parsedDocument] = parsedMDOC.documents as DeviceSignedDocument[];
});

const parsedMDOC = parse(encoded);
[parsedDocument] = parsedMDOC.documents as DeviceSignedDocument[];
});
it('should be verifiable', async () => {
const verifier = new Verifier([ISSUER_CERTIFICATE]);
await verifier.verify(encoded, {
encodedSessionTranscript,
});
});

it('should be verifiable', async () => {
const verifier = new Verifier([ISSUER_CERTIFICATE]);
await verifier.verify(encoded, {
encodedSessionTranscript,
it('should contain the validity info', () => {
const { validityInfo } = parsedDocument.issuerSigned.issuerAuth.decodedPayload;
expect(validityInfo).toBeDefined();
expect(validityInfo.signed).toEqual(new Date('2023-10-24'));
expect(validityInfo.validFrom).toEqual(new Date('2023-10-24'));
expect(validityInfo.validUntil).toEqual(new Date('2050-10-24'));
});
});

it('should contain the validity info', () => {
const { validityInfo } = parsedDocument.issuerSigned.issuerAuth.decodedPayload;
expect(validityInfo).toBeDefined();
expect(validityInfo.signed).toEqual(new Date('2023-10-24'));
expect(validityInfo.validFrom).toEqual(new Date('2023-10-24'));
expect(validityInfo.validUntil).toEqual(new Date('2050-10-24'));
});
it('should contain the device namespaces', () => {
expect(parsedDocument.getDeviceNameSpace('com.foobar-device'))
.toEqual({ test: 1234 });
});

it('should contain the device namespaces', () => {
expect(parsedDocument.getDeviceNameSpace('com.foobar-device'))
.toEqual({ test: 1234 });
it('should generate the signature without payload', () => {
expect(parsedDocument.deviceSigned.deviceAuth.deviceSignature?.payload).toBeNull();
});
});

it('should generate the signature without payload', () => {
expect(parsedDocument.deviceSigned.deviceAuth.deviceSignature?.payload).toBeNull();
describe('using an arbitrary session transcript', () => {
beforeAll(async () => {
// This is the Device side
{
const devicePrivateKey = DEVICE_JWK;

// The session transcript can be anything, as long as the wallet and the verifier agree on what it is exactly.
const sessionTranscript = Buffer.alloc(32);
randomFillSync(sessionTranscript);
encodedSessionTranscript = cborEncode(DataItem.fromData(sessionTranscript));
console.log(encodedSessionTranscript.toString('hex'));

const deviceResponseMDoc = await DeviceResponse.from(mdoc)
.usingPresentationDefinition(PRESENTATION_DEFINITION_1)
.usingSessionTranscriptBytes(encodedSessionTranscript)
.authenticateWithSignature(devicePrivateKey, 'ES256')
.addDeviceNameSpace('com.foobar-device', { test: 1234 })
.sign();

encoded = deviceResponseMDoc.encode();
}

const parsedMDOC = parse(encoded);
[parsedDocument] = parsedMDOC.documents as DeviceSignedDocument[];
});

it('should be verifiable', async () => {
const verifier = new Verifier([ISSUER_CERTIFICATE]);
await verifier.verify(encoded, {
encodedSessionTranscript,
});
});

it('should contain the validity info', () => {
const { validityInfo } = parsedDocument.issuerSigned.issuerAuth.decodedPayload;
expect(validityInfo).toBeDefined();
expect(validityInfo.signed).toEqual(new Date('2023-10-24'));
expect(validityInfo.validFrom).toEqual(new Date('2023-10-24'));
expect(validityInfo.validUntil).toEqual(new Date('2050-10-24'));
});

it('should contain the device namespaces', () => {
expect(parsedDocument.getDeviceNameSpace('com.foobar-device'))
.toEqual({ test: 1234 });
});

it('should generate the signature without payload', () => {
expect(parsedDocument.deviceSigned.deviceAuth.deviceSignature?.payload).toBeNull();
});
});
});
Loading

0 comments on commit ebd27c6

Please sign in to comment.