Skip to content

Commit 9c4c66c

Browse files
authored
Add support for di_vp proofs in credential requests as PoP (#257)
Signed-off-by: Sebastian Dechant <763247+S3bb1@users.noreply.github.com>
1 parent 52f1030 commit 9c4c66c

12 files changed

Lines changed: 435 additions & 3 deletions

File tree

.changeset/all-garlics-know.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@openid4vc/oauth2": patch
3+
"@openid4vc/openid4vci": patch
4+
---
5+
6+
Add support for di_vp proofs in credential requests

packages/oauth2/src/callbacks.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ export type VerifyJwtCallback = (
4343
}
4444
>
4545

46+
export type VerifyDataIntegrityProofCallback = (
47+
dataIntegrityProof: Record<string, unknown>,
48+
document: Record<string, unknown>
49+
) => OrPromise<
50+
| {
51+
verified: true
52+
signerJwk: Jwk
53+
}
54+
| {
55+
verified: false
56+
signerJwk?: Jwk
57+
}
58+
>
59+
4660
export interface DecryptJweCallbackOptions {
4761
jwk?: Jwk
4862
}
@@ -105,6 +119,12 @@ export interface CallbackContext {
105119
*/
106120
verifyJwt: VerifyJwtCallback
107121

122+
/**
123+
* Verify a Data Integrity proof (e.g. the `proof` of a `di_vp` key proof's Verifiable
124+
* Presentation). Optional because most issuers won't support the `di_vp` proof type.
125+
*/
126+
verifyDataIntegrityProof?: VerifyDataIntegrityProofCallback
127+
108128
/**
109129
* Generate random callback to generate random bytes. Used for
110130
* e.g. the 'jti' value in a dpop jwt, and 'code_verifier' in pkce.

packages/oauth2/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export type {
7272
GenerateRandomCallback,
7373
HashCallback,
7474
SignJwtCallback,
75+
VerifyDataIntegrityProofCallback,
7576
VerifyJwtCallback,
7677
} from './callbacks'
7778
export { HashAlgorithm } from './callbacks'

packages/openid4vci/src/Openid4vciIssuer.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ import {
2929
type VerifyCredentialRequestAttestationProofOptions,
3030
verifyCredentialRequestAttestationProof,
3131
} from './formats/proof-type/attestation/attestation-proof-type'
32+
import {
33+
type VerifyCredentialRequestDiVpProofOptions,
34+
verifyCredentialRequestDiVpProof,
35+
} from './formats/proof-type/di-vp/di-vp-proof-type'
3236
import {
3337
type VerifyCredentialRequestJwtProofOptions,
3438
verifyCredentialRequestJwtProof,
@@ -209,6 +213,38 @@ export class Openid4vciIssuer {
209213
}
210214
}
211215

216+
/**
217+
* @throws Oauth2ServerErrorResponseError - if verification of the di_vp proof failed. You can
218+
* extract the credential error response from this.
219+
*/
220+
public async verifyCredentialRequestDiVpProof(
221+
options: Pick<VerifyCredentialRequestDiVpProofOptions, 'vp' | 'now' | 'expectedNonce' | 'nonceExpiresAt'> & {
222+
issuerMetadata: IssuerMetadataResult
223+
}
224+
) {
225+
try {
226+
return await verifyCredentialRequestDiVpProof({
227+
callbacks: this.options.callbacks,
228+
credentialIssuer: options.issuerMetadata.credentialIssuer.credential_issuer,
229+
expectedNonce: options.expectedNonce,
230+
nonceExpiresAt: options.nonceExpiresAt,
231+
vp: options.vp,
232+
now: options.now,
233+
})
234+
} catch (error) {
235+
throw new Oauth2ServerErrorResponseError(
236+
{
237+
error: Oauth2ErrorCodes.InvalidProof,
238+
error_description: error instanceof Openid4vciError ? error.message : 'Invalid proof',
239+
},
240+
{
241+
internalMessage: 'Error verifying credential request di_vp proof',
242+
cause: error,
243+
}
244+
)
245+
}
246+
}
247+
212248
/**
213249
* @throws Oauth2ServerErrorResponseError - when validation of the credential request fails
214250
* You can extract the credential error response from this.

packages/openid4vci/src/credential-request/__tests__/parse-credential-request.test.mts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,87 @@ describe('Parse Credential Request', () => {
422422
})
423423
})
424424

425+
test('parse draft 14 credential request with known di_vp proof_type', () => {
426+
expect(
427+
parseCredentialRequest({
428+
issuerMetadata: {
429+
authorizationServers: [],
430+
credentialIssuer: issuerMetadata,
431+
originalDraftVersion: Openid4vciVersion.Draft14,
432+
knownCredentialConfigurations: {
433+
my_credential: {
434+
format: 'dc+sd-jwt',
435+
vct: 'hello',
436+
},
437+
},
438+
},
439+
credentialRequest: {
440+
credential_identifier: 'some-identifier',
441+
extra_prop: 'should-stay',
442+
proof: {
443+
proof_type: 'di_vp',
444+
di_vp: {
445+
'@context': ['https://www.w3.org/ns/credentials/v2'],
446+
type: ['VerifiablePresentation'],
447+
proof: {
448+
type: 'DataIntegrityProof',
449+
cryptosuite: 'eddsa-2022',
450+
proofPurpose: 'authentication',
451+
verificationMethod: 'did:key:z6Mk...#z6Mk...',
452+
created: '2023-03-01T14:56:29.280619Z',
453+
challenge: '82d4cb36-...',
454+
domain: 'https://issuer.com',
455+
proofValue: 'z5hrbHzZ...',
456+
},
457+
},
458+
},
459+
},
460+
})
461+
).toStrictEqual({
462+
proofs: {
463+
di_vp: [
464+
{
465+
'@context': ['https://www.w3.org/ns/credentials/v2'],
466+
type: ['VerifiablePresentation'],
467+
proof: {
468+
type: 'DataIntegrityProof',
469+
cryptosuite: 'eddsa-2022',
470+
proofPurpose: 'authentication',
471+
verificationMethod: 'did:key:z6Mk...#z6Mk...',
472+
created: '2023-03-01T14:56:29.280619Z',
473+
challenge: '82d4cb36-...',
474+
domain: 'https://issuer.com',
475+
proofValue: 'z5hrbHzZ...',
476+
},
477+
},
478+
],
479+
},
480+
credentialIdentifier: 'some-identifier',
481+
credentialRequest: {
482+
credential_identifier: 'some-identifier',
483+
extra_prop: 'should-stay',
484+
proof: {
485+
proof_type: 'di_vp',
486+
di_vp: {
487+
'@context': ['https://www.w3.org/ns/credentials/v2'],
488+
type: ['VerifiablePresentation'],
489+
proof: {
490+
type: 'DataIntegrityProof',
491+
cryptosuite: 'eddsa-2022',
492+
proofPurpose: 'authentication',
493+
verificationMethod: 'did:key:z6Mk...#z6Mk...',
494+
created: '2023-03-01T14:56:29.280619Z',
495+
challenge: '82d4cb36-...',
496+
domain: 'https://issuer.com',
497+
proofValue: 'z5hrbHzZ...',
498+
},
499+
},
500+
},
501+
},
502+
credentialResponseEncryption: undefined,
503+
})
504+
})
505+
425506
test('parse draft 14 credential request with unknown proof_type', () => {
426507
expect(
427508
parseCredentialRequest({

packages/openid4vci/src/credential-request/parse-credential-request.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { parseWithErrorHandling } from '@openid4vc/utils'
22
import z from 'zod'
33
import { attestationProofTypeIdentifier } from '../formats/proof-type/attestation/z-attestation-proof-type'
4+
import { diVpProofTypeIdentifier } from '../formats/proof-type/di-vp/z-di-vp-proof-type'
45
import { jwtProofTypeIdentifier } from '../formats/proof-type/jwt/z-jwt-proof-type'
56
import { getKnownCredentialConfigurationSupportedById } from '../metadata/credential-issuer/credential-issuer-metadata'
67
import type { CredentialConfigurationSupportedWithFormats } from '../metadata/credential-issuer/z-credential-issuer-metadata'
@@ -105,6 +106,8 @@ export function parseCredentialRequest(options: ParseCredentialRequestOptions):
105106
proofs = { [jwtProofTypeIdentifier]: [knownProof.data.jwt] }
106107
} else if (knownProof.success && knownProof.data.proof_type === attestationProofTypeIdentifier) {
107108
proofs = { [attestationProofTypeIdentifier]: [knownProof.data.attestation] }
109+
} else if (knownProof.success && knownProof.data.proof_type === diVpProofTypeIdentifier) {
110+
proofs = { [diVpProofTypeIdentifier]: [knownProof.data.di_vp] }
108111
}
109112

110113
const credentialResponseEncryption = credentialRequest.credential_response_encryption

packages/openid4vci/src/credential-request/z-credential-request-common.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import z from 'zod'
44
import {
55
zAttestationProofTypeIdentifier,
66
zCredentialRequestProofAttestation,
7+
zCredentialRequestProofDiVp,
78
zCredentialRequestProofJwt,
9+
zDiVpProofTypeIdentifier,
810
zJwtProofTypeIdentifier,
911
} from '../formats/proof-type'
1012

@@ -14,7 +16,11 @@ const zCredentialRequestProofCommon = z
1416
})
1517
.loose()
1618

17-
export const allCredentialRequestProofs = [zCredentialRequestProofJwt, zCredentialRequestProofAttestation] as const
19+
export const allCredentialRequestProofs = [
20+
zCredentialRequestProofJwt,
21+
zCredentialRequestProofAttestation,
22+
zCredentialRequestProofDiVp,
23+
] as const
1824

1925
export const zCredentialRequestProof = z.union([
2026
zCredentialRequestProofCommon,
@@ -25,6 +31,7 @@ const zCredentialRequestProofsCommon = z.record(z.string(), z.array(z.unknown())
2531
export const zCredentialRequestProofs = z.object({
2632
[zJwtProofTypeIdentifier.value]: z.optional(z.array(zCredentialRequestProofJwt.shape.jwt)),
2733
[zAttestationProofTypeIdentifier.value]: z.optional(z.array(zCredentialRequestProofAttestation.shape.attestation)),
34+
[zDiVpProofTypeIdentifier.value]: z.optional(z.array(zCredentialRequestProofDiVp.shape.di_vp)),
2835
})
2936

3037
type CredentialRequestProofCommon = z.infer<typeof zCredentialRequestProofCommon>

0 commit comments

Comments
 (0)