Skip to content

Commit

Permalink
feat: add OID4VP DC API session transcript calculation (#58)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra authored Feb 12, 2025
1 parent f0c45f7 commit 4187667
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/small-apricots-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@animo-id/mdoc": patch
---

feat: add OID4VP DC API session transcript calculation
95 changes: 95 additions & 0 deletions src/__tests__/issuing/device-response.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,101 @@ describe('issuing a device response', () => {
})
})

describe('using OID4VPDCAPI handover', () => {
const verifierGeneratedNonce = 'abcdefg'
const origin = 'http://localhost:4000'
const clientId = 'Cq1anPb8vZU5j5C0d7hcsbuJLBpIawUJIDQRi2Ebwb4'

beforeAll(async () => {
// This is the Device side
const devicePrivateKey = DEVICE_JWK
const deviceResponseMDoc = await DeviceResponse.from(mdoc)
.usingPresentationDefinition(PRESENTATION_DEFINITION_1)
.usingSessionTranscriptForForOID4VPDCApi({
clientId,
origin,
verifierGeneratedNonce,
})
.authenticateWithSignature(devicePrivateKey, 'ES256')
.addDeviceNameSpace('com.foobar-device', { test: 1234 })
.sign(mdocContext)

encodedDeviceResponse = deviceResponseMDoc.encode()
const parsedMDOC = parseDeviceResponse(encodedDeviceResponse)
;[parsedDocument] = parsedMDOC.documents as [DeviceSignedDocument, ...DeviceSignedDocument[]]
})

it('should be verifiable', async () => {
const verifier = new Verifier()
await verifier.verifyDeviceResponse(
{
trustedCertificates: [new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData)],
encodedDeviceResponse,
encodedSessionTranscript: await DeviceResponse.calculateSessionTranscriptForOID4VPDCApi({
context: mdocContext,
clientId,
origin,
verifierGeneratedNonce,
}),
},
mdocContext
)
})

describe('should not be verifiable', () => {
const testCases = ['clientId', 'origin', 'verifierGeneratedNonce']

testCases.forEach((name) => {
const values = {
clientId,
origin,
verifierGeneratedNonce,
[name]: 'wrong',
}
it(`with a different ${name}`, async () => {
try {
const verifier = new Verifier()
await verifier.verifyDeviceResponse(
{
trustedCertificates: [new Uint8Array(new X509Certificate(ISSUER_CERTIFICATE).rawData)],
encodedDeviceResponse,
encodedSessionTranscript: await DeviceResponse.calculateSessionTranscriptForOID4VPDCApi({
context: mdocContext,
clientId: values.clientId,
origin: values.origin,
verifierGeneratedNonce: values.verifierGeneratedNonce,
}),
},
mdocContext
)
throw new Error('should not validate with different transcripts')
} catch (error) {
expect((error as Error).message).toMatch(
'Unable to verify deviceAuth signature (ECDSA/EdDSA): Device signature must be valid'
)
}
})
})
})

it('should contain the validity info', () => {
const { validityInfo } = parsedDocument.issuerSigned.issuerAuth.decodedPayload
expect(validityInfo).toBeDefined()
expect(validityInfo.signed).toEqual(signed)
expect(validityInfo.validFrom).toEqual(signed)
expect(validityInfo.validUntil).toEqual(validUntil)
expect(validityInfo.expectedUpdate).toBeUndefined()
})

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

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

describe('using WebAPI handover', () => {
// The actual value for the engagements & the key do not matter,
// as long as the device and the reader agree on what value to use.
Expand Down
46 changes: 46 additions & 0 deletions src/mdoc/model/device-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,52 @@ export class DeviceResponse {
)
}

public static async calculateSessionTranscriptForOID4VPDCApi(input: {
context: {
crypto: MdocContext['crypto']
}
clientId: string
origin: string
verifierGeneratedNonce: string
}) {
const { clientId, verifierGeneratedNonce, context, origin } = input

return cborEncode(
DataItem.fromData([
null, // deviceEngagementBytes
null, // eReaderKeyBytes
[
'OpenID4VPDCAPIHandover', // A fixed identifier for this handover type
await context.crypto.digest({
digestAlgorithm: 'SHA-256',
bytes: cborEncode([origin, clientId, verifierGeneratedNonce]),
}),
],
])
)
}

/**
* Set the session transcript data to use for the device response as defined in [OID4VP B.3.4.1, Draft 24](https://openid.net/specs/openid-4-verifiable-presentations-1_0-24.html#appendix-B.3.4.1)
*
* This should match the session transcript as it will be calculated by the verifier.
*
* @param {string} clientId - The client_id Authorization Request parameter from the Authorization Request Object.
* @param {string} origin - The origin of the Authorization Request, as defined in Appendix A.2. of OID4VP
* @param {string} verifierGeneratedNonce - The nonce Authorization Request parameter from the Authorization Request Object.
* @returns {DeviceResponse}
*/
public usingSessionTranscriptForForOID4VPDCApi(input: {
origin: string
clientId: string
verifierGeneratedNonce: string
}): DeviceResponse {
this.usingSessionTranscriptCallback((context) =>
DeviceResponse.calculateSessionTranscriptForOID4VPDCApi({ ...input, context })
)
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), 2024 draft.
*
Expand Down

0 comments on commit 4187667

Please sign in to comment.