Skip to content

Commit

Permalink
feat: PEX and OID4VP multi-vp support (#2039)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <timo@animo.id>
  • Loading branch information
TimoGlastra authored Oct 8, 2024
1 parent 1d83159 commit 8fdf41d
Show file tree
Hide file tree
Showing 11 changed files with 482 additions and 276 deletions.
2 changes: 1 addition & 1 deletion packages/anoncreds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dependencies": {
"@astronautlabs/jsonpath": "^1.1.2",
"@credo-ts/core": "workspace:*",
"@sphereon/pex-models": "^2.2.4",
"@sphereon/pex-models": "^2.3.1",
"big-integer": "^1.6.51",
"bn.js": "^5.2.1",
"class-transformer": "0.5.1",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
"@sd-jwt/sd-jwt-vc": "^0.7.0",
"@sd-jwt/types": "^0.7.0",
"@sd-jwt/utils": "^0.7.0",
"@sphereon/pex": "^5.0.0-unstable.8",
"@sphereon/pex-models": "^2.2.4",
"@sphereon/pex": "5.0.0-unstable.2",
"@sphereon/pex-models": "^2.3.1",
"@sphereon/ssi-types": "0.29.1-unstable.121",
"@stablelib/ed25519": "^1.0.2",
"@types/ws": "^8.5.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
} from '@sphereon/ssi-types'

import { PEVersion, PEX, Status } from '@sphereon/pex'
import { PartialSdJwtDecodedVerifiableCredential } from '@sphereon/pex/dist/main/lib'
import { injectable } from 'tsyringe'

import { Hasher, getJwkFromKey } from '../../crypto'
Expand Down Expand Up @@ -242,9 +243,10 @@ export class DifPresentationExchangeService {
})

return {
verifiablePresentations: verifiablePresentationResultsWithFormat.flatMap((resultWithFormat) =>
resultWithFormat.verifiablePresentationResult.verifiablePresentations.map((encoded) =>
getVerifiablePresentationFromEncoded(agentContext, encoded)
verifiablePresentations: verifiablePresentationResultsWithFormat.map((resultWithFormat) =>
getVerifiablePresentationFromEncoded(
agentContext,
resultWithFormat.verifiablePresentationResult.verifiablePresentation
)
),
presentationSubmission,
Expand Down Expand Up @@ -502,7 +504,9 @@ export class DifPresentationExchangeService {

return signedPresentation.encoded as W3CVerifiablePresentation
} else if (presentationToCreate.claimFormat === ClaimFormat.SdJwtVc) {
const sdJwtInput = presentationInput as SdJwtDecodedVerifiableCredential
const sdJwtInput = presentationInput as
| SdJwtDecodedVerifiableCredential
| PartialSdJwtDecodedVerifiableCredential

if (!domain) {
throw new CredoError("Missing 'domain' property, unable to set required 'aud' property in SD-JWT KB-JWT")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,10 @@ export const matrrLaunchpadDraft11JwtVcJson = {
}

export const waltIdDraft11JwtVcJson = {
credentialOffer:
credentialOfferPreAuth:
'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%22UniversityDegree%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJlZmMyZjVkZC0wZjQ0LTRmMzgtYTkwMi0zMjA0ZTczMmMzOTEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.OHzYTP_u6I95hHBmjF3RchydGidq3nsT0QHdgJ1AXyR5AFkrTfJwsW4FQIdOdda93uS7FOh_vSVGY0Qngzm7Ag%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D',
credentialOfferAuth:
'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%22UniversityDegree%22%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22efc2f5dd-0f44-4f38-a902-3204e732c391%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJlZmMyZjVkZC0wZjQ0LTRmMzgtYTkwMi0zMjA0ZTczMmMzOTEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.OHzYTP_u6I95hHBmjF3RchydGidq3nsT0QHdgJ1AXyR5AFkrTfJwsW4FQIdOdda93uS7FOh_vSVGY0Qngzm7Ag%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D',
getMetadataResponse: {
issuer: 'https://issuer.portal.walt.id',
authorization_endpoint: 'https://issuer.portal.walt.id/authorize',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ describe('OpenId4VcHolder', () => {
.post('/credential')
.reply(200, fixture.credentialResponse)

const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer)
const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOfferPreAuth)

await expect(() =>
holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode(resolved, {
Expand Down Expand Up @@ -286,7 +286,7 @@ describe('OpenId4VcHolder', () => {
.reply(404)

const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer(
fixture.credentialOffer
fixture.credentialOfferAuth
)

const resolvedAuthorizationRequest = await holder.modules.openId4VcHolder.resolveIssuanceAuthorizationRequest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@ export class OpenId4VcSiopVerifierService {

const relyingParty = await this.getRelyingParty(agentContext, options.verificationSession.verifierId, {
presentationDefinition: presentationDefinitionsWithLocation?.[0]?.definition,
clientId: requestClientId,
authorizationResponseUrl,
clientId: requestClientId,
})

// This is very unfortunate, but storing state in sphereon's SiOP-OID4VP library
Expand Down Expand Up @@ -331,10 +331,12 @@ export class OpenId4VcSiopVerifierService {
throw new CredoError('Unable to extract submission from the response.')
}

const vps = Array.isArray(presentations) ? presentations : [presentations]
// FIXME: should return type be an array? As now it doesn't always match the submission
const presentationsArray = Array.isArray(presentations) ? presentations : [presentations]

presentationExchange = {
definition: presentationDefinitions[0].definition,
presentations: vps.map(getVerifiablePresentationFromSphereonWrapped),
presentations: presentationsArray.map(getVerifiablePresentationFromSphereonWrapped),
submission,
}
}
Expand Down Expand Up @@ -459,10 +461,10 @@ export class OpenId4VcSiopVerifierService {
responseMode,
}: {
responseMode?: ResponseMode
authorizationResponseUrl: string
idToken?: boolean
presentationDefinition?: DifPresentationExchangeDefinition
clientId: string
authorizationResponseUrl: string
clientIdScheme?: ClientIdScheme
}
) {
Expand Down Expand Up @@ -519,6 +521,7 @@ export class OpenId4VcSiopVerifierService {
: undefined

builder
.withClientId(clientId)
.withResponseUri(authorizationResponseUrl)
.withIssuer(ResponseIss.SELF_ISSUED_V2)
.withAudience(RequestAud.SELF_ISSUED_V2)
Expand All @@ -541,9 +544,11 @@ export class OpenId4VcSiopVerifierService {

// TODO: we should probably allow some dynamic values here
.withClientMetadata({
client_id: clientId,
...jarmClientMetadata,
client_id_scheme: clientIdScheme,
// FIXME: not passing client_id here means it will not be added
// to the authorization request url (not the signed payload). Need
// to fix that in Sphereon lib
client_id: clientId,
passBy: PassBy.VALUE,
responseTypesSupported: [ResponseType.VP_TOKEN],
subject_syntax_types_supported: supportedDidMethods.map((m) => `did:${m}`),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ describe('OpenId4VcVerifier', () => {
expect(jwt.header.kid).toEqual(verifier.kid)
expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA)
expect(jwt.header.typ).toEqual('JWT')
expect(jwt.payload.additionalClaims.scope).toEqual(undefined)
expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.did)
expect(jwt.payload.additionalClaims.response_uri).toEqual(
`http://redirect-uri/${openIdVerifier.verifierId}/authorize`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,22 @@ export function configureAuthorizationEndpoint(router: Router, config: OpenId4Vc
nonce: authorizationResponsePayload.nonce,
})
}

if (typeof authorizationResponsePayload.presentation_submission === 'string') {
authorizationResponsePayload.presentation_submission = JSON.parse(
authorizationResponsePayload.presentation_submission
)
authorizationResponsePayload.presentation_submission = JSON.parse(request.body.presentation_submission)
}

// This feels hacky, and should probably be moved to OID4VP lib. However the OID4VP spec allows either object, string, or array...
if (
typeof authorizationResponsePayload.vp_token === 'string' &&
(authorizationResponsePayload.vp_token.startsWith('{') || authorizationResponsePayload.vp_token.startsWith('['))
) {
authorizationResponsePayload.vp_token = JSON.parse(authorizationResponsePayload.vp_token)
}

if (!verificationSession) {
throw new CredoError('Missing verification session, cannot verify authorization response.')
}

await openId4VcVerifierService.verifyAuthorizationResponse(agentContext, {
authorizationResponse: authorizationResponsePayload,
verificationSession,
Expand Down
14 changes: 14 additions & 0 deletions packages/openid4vc/src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
JwtPayload,
SignatureSuiteRegistry,
X509Service,
getDomainFromUrl,
getJwkClassFromKeyType,
getJwkFromJson,
getJwkFromKey,
Expand Down Expand Up @@ -135,6 +136,19 @@ export async function openIdTokenIssuerToJwtIssuer(
throw new CredoError(`No supported signature algorithms found key type: '${jwk.keyType}'`)
}

if (!openId4VcTokenIssuer.issuer.startsWith('https://')) {
throw new CredoError('The X509 certificate issuer must be a HTTPS URI.')
}

if (
!leafCertificate.sanUriNames?.includes(openId4VcTokenIssuer.issuer) &&
!leafCertificate.sanDnsNames?.includes(getDomainFromUrl(openId4VcTokenIssuer.issuer))
) {
throw new Error(
`The 'iss' claim in the payload does not match a 'SAN-URI' or 'SAN-DNS' name in the x5c certificate.`
)
}

return {
...openId4VcTokenIssuer,
alg,
Expand Down
Loading

0 comments on commit 8fdf41d

Please sign in to comment.