Skip to content

Commit e6208f5

Browse files
authored
Update OID4VP support to Draft 28 (#22)
1 parent cae1f95 commit e6208f5

23 files changed

Lines changed: 270 additions & 220 deletions

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"imports": {
2424
"@deno/dnt": "jsr:@deno/dnt@^0.41.3",
2525
"@std/assert": "jsr:@std/assert@^1.0.9",
26+
"@std/encoding": "jsr:@std/encoding@^1.0.10",
2627
"@std/testing": "jsr:@std/testing@^1.0.6",
2728
"hono": "npm:hono@^4.7.2"
2829
}

deno.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ app.get("/options", async (ctx) => {
9595
*/
9696
const request = await generatePresentationRequest({
9797
credentialOptions: sdjwtvcRequestComplex,
98-
// encryptResponse: true, // Optional, defaults to `true`
98+
// encryptResponse: false, // Optional, defaults to `true`
9999
serverAESKeySecret,
100100
});
101101

packages/server/src/dcapi/types.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,21 @@ export type DigitalCredentialRequestOptions = {
1919
};
2020

2121
export type DigitalCredentialRequest = {
22-
protocol: string;
22+
/** https://openid.net/specs/openid-4-verifiable-presentations-1_0-28.html#appendix-A.1-3 */
23+
protocol: 'openid4vp-v1-unsigned';
2324
data: DCAPIRequestOID4VP;
2425
};
2526
/**
2627
* Credential-agnostic OID4VP-specific request parameters
2728
*
28-
* https://openid.net/specs/openid-4-verifiable-presentations-1_0-24.html#appendix-A.2
29+
* https://openid.net/specs/openid-4-verifiable-presentations-1_0-28.html#appendix-A.2
2930
*/
3031
export type DCAPIRequestOID4VP = {
3132
/** The value `"vp_token"` */
3233
response_type: 'vp_token';
3334
/** The value `"dc_api"` (when unsigned and unencrypted) or `"dc_api.jwt"` (when signed or encrypted) */
3435
response_mode: 'dc_api' | 'dc_api.jwt';
35-
/** Ex: `"web-origin:https://example.com"` */
36+
/** Only used for signed requests (not currently supported) */
3637
client_id?: string;
3738
/** Base64URL-encoded random bytes to ensure uniqueness of the presentation */
3839
nonce: string;

packages/server/src/formats/mdoc/generateSessionTranscript.test.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,51 @@
11
import { assertEquals } from '@std/assert';
2+
import { decodeHex, encodeHex } from '@std/encoding';
3+
import { encodeCBOR } from '@levischuck/tiny-cbor';
24

35
import { generateSessionTranscript } from './generateSessionTranscript.ts';
4-
import { base64url } from '../../helpers/index.ts';
6+
import type { Uint8Array_ } from '../../helpers/types.ts';
57

6-
Deno.test('should generate OID4VP-specific session transcript', async () => {
8+
Deno.test('matches OID4VP Draft 28 SessionTranscript test example', async () => {
9+
/**
10+
* Yes, I know it's weird that this test uses hex encoding when comparing outputs. These hex
11+
* strings were pulled from OID4VP Draft 28, so I'm keeping them as-is for sake of finding them
12+
* later within the spec with a simple copy-paste.
13+
*
14+
* Example pulled from here:
15+
* https://openid.net/specs/openid-4-verifiable-presentations-1_0-28.html#appendix-B.2.6.1
16+
*/
717
const sessionTranscript = await generateSessionTranscript(
8-
'http://localhost:8000',
9-
'web-origin:http://localhost:8000',
10-
'Y_Sl5cgcgTiw7XnikC24SCDvPEFb81gcOp3lrsdSwZ8',
18+
'https://example.com',
19+
'exc7gBkxjx1rdc9udRrveKvSsJIq80avlXeLHhGwqtA',
20+
{
21+
kty: 'EC',
22+
crv: 'P-256',
23+
x: 'DxiH5Q4Yx3UrukE2lWCErq8N8bqC9CHLLrAwLz5BmE0',
24+
y: 'XtLM4-3h5o3HUH0MHVJV0kyq0iBlrBwlh8qEDMZ4-Pc',
25+
use: 'enc',
26+
alg: 'ECDH-ES',
27+
// @ts-ignore: It's in the example
28+
kid: '1',
29+
},
1130
);
1231

32+
/** https://openid.net/specs/openid-4-verifiable-presentations-1_0-28.html#appendix-B.2.6.1-9 */
33+
assertEquals(
34+
encodeHex(encodeCBOR(sessionTranscript) as Uint8Array_),
35+
'83f6f682764f70656e4944345650444341504948616e646f7665725820fbece366f4212f9762c74cfdbf83b8c69e371d5d68cea09cb4c48ca6daab761a',
36+
);
37+
38+
/** https://openid.net/specs/openid-4-verifiable-presentations-1_0-28.html#appendix-B.2.6.1-13 */
1339
assertEquals(
1440
sessionTranscript,
1541
[
1642
null,
1743
null,
1844
[
1945
'OpenID4VPDCAPIHandover',
20-
base64url.base64URLToBuffer('_PyOmRZ-t89FGXttog73cBXC1EGe9dQUICV3odM9ihk'),
46+
decodeHex(
47+
'fbece366f4212f9762c74cfdbf83b8c69e371d5d68cea09cb4c48ca6daab761a',
48+
),
2149
],
2250
],
2351
);

packages/server/src/formats/mdoc/generateSessionTranscript.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
11
import { encodeCBOR } from '@levischuck/tiny-cbor';
2+
import * as jose from 'jose';
23

34
import type { DCAPIOID4VPSessionTranscript } from './types.ts';
5+
import type { Uint8Array_ } from '../../helpers/types.ts';
6+
import { base64url } from '../../helpers/index.ts';
47

58
/**
69
* See OID4VP for SessionTranscript composition:
7-
* https://openid.net/specs/openid-4-verifiable-presentations-1_0-24.html#appendix-B.3.4
10+
* https://openid.net/specs/openid-4-verifiable-presentations-1_0-28.html#appendix-B.2.6
811
*/
912
export async function generateSessionTranscript(
1013
requestOrigin: string,
11-
clientID: string,
1214
nonce: string,
15+
verifierPublicKeyJWK?: JsonWebKey,
1316
): Promise<DCAPIOID4VPSessionTranscript> {
1417
type OpenID4VPDCAPIHandoverInfo = [
1518
origin: string,
16-
client_id: string,
1719
nonce: string,
20+
jwk_thumbprint: Uint8Array_ | null,
1821
];
1922

20-
const handoverInfo: OpenID4VPDCAPIHandoverInfo = [requestOrigin, clientID, nonce];
23+
let jwkThumbprint: Uint8Array_ | null = null;
24+
if (verifierPublicKeyJWK) {
25+
const thumbprintBase64URL = await jose.calculateJwkThumbprint(verifierPublicKeyJWK);
26+
jwkThumbprint = base64url.base64URLToBuffer(thumbprintBase64URL);
27+
}
28+
29+
const handoverInfo: OpenID4VPDCAPIHandoverInfo = [requestOrigin, nonce, jwkThumbprint];
2130

2231
const handoverInfoCBOR = encodeCBOR(handoverInfo);
2332
const handoverInfoHash = await crypto.subtle.digest('SHA-256', handoverInfoCBOR);

packages/server/src/formats/mdoc/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ export type MdocCOSESign1SigStructure = [
210210
];
211211

212212
/**
213-
* https://openid.net/specs/openid-4-verifiable-presentations-1_0-24.html#appendix-B.3.4.1
213+
* https://openid.net/specs/openid-4-verifiable-presentations-1_0-28.html#appendix-B.2.6
214214
*/
215215
export type DCAPIOID4VPSessionTranscript = [
216216
deviceEngagementBytes: null,

packages/server/src/formats/mdoc/verifyDeviceSigned.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,16 @@ import type {
1212
import { SimpleDigiCredsError } from '../../helpers/simpleDigiCredsError.ts';
1313
import type { Uint8Array_ } from '../../helpers/types.ts';
1414

15-
export async function verifyDeviceSigned({ document, nonce, possibleOrigins }: {
15+
export async function verifyDeviceSigned({
16+
document,
17+
nonce,
18+
possibleOrigins,
19+
verifierPublicKeyJWK,
20+
}: {
1621
document: DecodedDocument;
1722
nonce: string;
1823
possibleOrigins: string[];
24+
verifierPublicKeyJWK?: JsonWebKey;
1925
}): Promise<VerifiedDeviceSigned> {
2026
const issuerSigned = document.get('issuerSigned');
2127
const deviceSigned = document.get('deviceSigned');
@@ -35,9 +41,16 @@ export async function verifyDeviceSigned({ document, nonce, possibleOrigins }: {
3541
const dateValidUntil = new Date(Date.parse(validUntil));
3642
const now = new Date(Date.now());
3743

38-
if (dateValidFrom > now || dateValidUntil < now) {
44+
if (dateValidFrom > now) {
3945
throw new SimpleDigiCredsError({
40-
message: `Credential is not yet valid or is expired`,
46+
message: 'Credential is not yet valid',
47+
code: 'MdocVerificationError',
48+
});
49+
}
50+
51+
if (dateValidUntil < now) {
52+
throw new SimpleDigiCredsError({
53+
message: `Credential is expired`,
4154
code: 'MdocVerificationError',
4255
});
4356
}
@@ -54,9 +67,11 @@ export async function verifyDeviceSigned({ document, nonce, possibleOrigins }: {
5467
let verified: boolean = false;
5568
let verifiedOrigin: string = '';
5669
for (const origin of possibleOrigins) {
57-
const clientID = `web-origin:${origin}`;
58-
59-
const sessionTranscript = await generateSessionTranscript(origin, clientID, nonce);
70+
const sessionTranscript = await generateSessionTranscript(
71+
origin,
72+
nonce,
73+
verifierPublicKeyJWK,
74+
);
6075

6176
const deviceSignedNameSpaces = deviceSigned.get('nameSpaces');
6277

packages/server/src/formats/mdoc/verifyMdocPresentation.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ export async function verifyMDocPresentation({
1515
presentation,
1616
nonce,
1717
possibleOrigins,
18+
verifierPublicKeyJWK,
1819
}: {
1920
presentation: string;
2021
nonce: string;
2122
possibleOrigins: string[];
23+
verifierPublicKeyJWK?: JsonWebKey;
2224
}): Promise<VerifiedCredential> {
2325
if (!base64url.isBase64URLString(presentation)) {
2426
throw new SimpleDigiCredsError({
@@ -42,7 +44,7 @@ export async function verifyMDocPresentation({
4244
return {
4345
claims: {},
4446
issuerMeta: {},
45-
credentialMeta: { verifiedOrigin: '' },
47+
presentationMeta: { verifiedOrigin: '' },
4648
};
4749
}
4850

@@ -53,13 +55,13 @@ export async function verifyMDocPresentation({
5355
expiresOn,
5456
issuedAt,
5557
validFrom,
56-
} = await verifyDeviceSigned({ document, nonce, possibleOrigins });
58+
} = await verifyDeviceSigned({ document, nonce, possibleOrigins, verifierPublicKeyJWK });
5759
if (!deviceSignedVerified) {
5860
console.error('could not verify DeviceSigned (mdoc)');
5961
return {
6062
claims: {},
6163
issuerMeta: {},
62-
credentialMeta: { verifiedOrigin: '' },
64+
presentationMeta: { verifiedOrigin: '' },
6365
};
6466
}
6567

@@ -91,7 +93,7 @@ export async function verifyMDocPresentation({
9193
expiresOn,
9294
validFrom,
9395
},
94-
credentialMeta: {
96+
presentationMeta: {
9597
verifiedOrigin,
9698
},
9799
};

packages/server/src/formats/sd-jwt-vc/assertKeyBindingJWTClaims.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ export function assertKeyBindingJWTClaims({
1919
// Verify `iat`
2020
const issuedAtDate = new Date(payload.iat * 1000);
2121
const currentDate = new Date();
22-
// Add 1 second to the current date to account for clock skew
23-
const currentDatePlus1Second = new Date(currentDate.getTime() + 1000);
22+
// Add a bit of time to the current date to account for clock skew
23+
const currentDatePlusSkew = new Date(currentDate.getTime() + 1500);
2424

25-
if (issuedAtDate > currentDatePlus1Second) {
25+
if (issuedAtDate > currentDatePlusSkew) {
2626
const iatISO = issuedAtDate.toISOString();
27-
const currentISO = currentDatePlus1Second.toISOString();
27+
const currentISO = currentDatePlusSkew.toISOString();
2828
throw new SimpleDigiCredsError({
2929
message: `Key Binding JWT was issued at (${iatISO}), after the current date (${currentISO})`,
3030
code: 'SDJWTVerificationError',
@@ -35,7 +35,8 @@ export function assertKeyBindingJWTClaims({
3535
let verifiedAUD = false;
3636
let verifiedOrigin = '';
3737
for (const origin of possibleOrigins) {
38-
if (payload.aud === `web-origin:${origin}`) {
38+
/** https://openid.net/specs/openid-4-verifiable-presentations-1_0-28.html#appendix-A.4-6 */
39+
if (payload.aud === `origin:${origin}`) {
3940
verifiedAUD = true;
4041
verifiedOrigin = origin;
4142
break;

0 commit comments

Comments
 (0)