Skip to content

Commit

Permalink
feat: add support for openid4vp response encryption (JARM) (#2046)
Browse files Browse the repository at this point in the history
Signed-off-by: Martin Auer <martin.auer97@gmail.com>
  • Loading branch information
auer-martin authored Oct 8, 2024
1 parent ea03cb9 commit 1d83159
Show file tree
Hide file tree
Showing 19 changed files with 2,220 additions and 399 deletions.
5 changes: 5 additions & 0 deletions .changeset/popular-camels-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@credo-ts/openid4vc': patch
---

feat: add jarm-support
4 changes: 2 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@
"@sd-jwt/sd-jwt-vc": "^0.7.0",
"@sd-jwt/types": "^0.7.0",
"@sd-jwt/utils": "^0.7.0",
"@sphereon/pex": "^3.3.2",
"@sphereon/pex": "^5.0.0-unstable.8",
"@sphereon/pex-models": "^2.2.4",
"@sphereon/ssi-types": "^0.28.0",
"@sphereon/ssi-types": "0.29.1-unstable.121",
"@stablelib/ed25519": "^1.0.2",
"@types/ws": "^8.5.4",
"abort-controller": "^3.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,10 @@ import type { VerificationMethod } from '../dids'
import type { SdJwtVcRecord } from '../sd-jwt-vc'
import type { W3cCredentialRecord } from '../vc'
import type { IAnonCredsDataIntegrityService } from '../vc/data-integrity/models/IAnonCredsDataIntegrityService'
import type {
PresentationSignCallBackParams,
SdJwtDecodedVerifiableCredentialWithKbJwtInput,
Validated,
VerifiablePresentationResult,
} from '@sphereon/pex'
import type { PresentationSignCallBackParams, Validated, VerifiablePresentationResult } from '@sphereon/pex'
import type { InputDescriptorV2 } from '@sphereon/pex-models'
import type {
SdJwtDecodedVerifiableCredential,
W3CVerifiablePresentation as SphereonW3cVerifiablePresentation,
W3CVerifiablePresentation,
} from '@sphereon/ssi-types'
Expand Down Expand Up @@ -246,10 +242,9 @@ export class DifPresentationExchangeService {
})

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

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

if (!domain) {
throw new CredoError("Missing 'domain' property, unable to set required 'aud' property in SD-JWT KB-JWT")
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/utils/domain.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export function getDomainFromUrl(url: string): string {
if (!url.startsWith('https://')) {
if (!url.startsWith('http://') && !url.startsWith('https://')) {
throw new Error('URL must start with "https://"')
}

const regex = /[#/?]/
const domain = url.substring('https://'.length).split(regex)[0]
const domain = url.split('://')[1].split(regex)[0]
return domain
}
12 changes: 6 additions & 6 deletions packages/openid4vc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@
},
"dependencies": {
"@credo-ts/core": "workspace:*",
"@sphereon/did-auth-siop": "0.16.1-next.3",
"@sphereon/oid4vc-common": "0.16.1-next.3",
"@sphereon/oid4vci-client": "0.16.1-next.3",
"@sphereon/oid4vci-common": "0.16.1-next.3",
"@sphereon/oid4vci-issuer": "0.16.1-next.3",
"@sphereon/ssi-types": "0.28.0",
"@sphereon/did-auth-siop": "0.16.1-next.66",
"@sphereon/oid4vc-common": "0.16.1-next.66",
"@sphereon/oid4vci-client": "0.16.1-next.66",
"@sphereon/oid4vci-common": "0.16.1-next.66",
"@sphereon/oid4vci-issuer": "0.16.1-next.66",
"@sphereon/ssi-types": "0.29.1-unstable.121",
"class-transformer": "^0.5.1",
"rxjs": "^7.8.0"
},
Expand Down
117 changes: 108 additions & 9 deletions packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,28 @@ import type {
OpenId4VcSiopResolvedAuthorizationRequest,
} from './OpenId4vcSiopHolderServiceOptions'
import type { OpenId4VcJwtIssuer } from '../shared'
import type { AgentContext, VerifiablePresentation } from '@credo-ts/core'
import type { VerifiedAuthorizationRequest, PresentationExchangeResponseOpts } from '@sphereon/did-auth-siop'
import type { AgentContext, JwkJson, VerifiablePresentation } from '@credo-ts/core'
import type {
AuthorizationResponsePayload,
PresentationExchangeResponseOpts,
RequestObjectPayload,
VerifiedAuthorizationRequest,
} from '@sphereon/did-auth-siop'

import {
Hasher,
W3cJwtVerifiablePresentation,
parseDid,
Buffer,
CredoError,
injectable,
W3cJsonLdVerifiablePresentation,
asArray,
DifPresentationExchangeService,
DifPresentationExchangeSubmissionLocation,
Hasher,
KeyType,
TypedArrayEncoder,
W3cJsonLdVerifiablePresentation,
W3cJwtVerifiablePresentation,
asArray,
getJwkFromJson,
injectable,
parseDid,
} from '@credo-ts/core'
import { OP, ResponseIss, ResponseMode, ResponseType, SupportedVersion, VPTokenLocation } from '@sphereon/did-auth-siop'

Expand Down Expand Up @@ -143,7 +152,50 @@ export class OpenId4VcSiopHolderService {
}
)

const response = await openidProvider.submitAuthorizationResponse(authorizationResponseWithCorrelationId)
const createJarmResponse = async (opts: {
authorizationResponsePayload: AuthorizationResponsePayload
requestObjectPayload: RequestObjectPayload
}) => {
const { authorizationResponsePayload, requestObjectPayload } = opts

const jwk = await OP.extractEncJwksFromClientMetadata(requestObjectPayload.client_metadata)
if (!jwk.kty) {
throw new CredoError('Missing kty in jwk.')
}

const validatedMetadata = OP.validateJarmMetadata({
client_metadata: requestObjectPayload.client_metadata,
server_metadata: {
authorization_encryption_alg_values_supported: ['ECDH-ES'],
authorization_encryption_enc_values_supported: ['A256GCM'],
},
})

if (validatedMetadata.type !== 'encrypted') {
throw new CredoError('Only encrypted JARM responses are supported.')
}

// Extract nonce from the request, we use this as the `apv`
const nonce = authorizationRequest.payload?.nonce
if (!nonce || typeof nonce !== 'string') {
throw new CredoError('Missing nonce in authorization request payload')
}

const jwe = await this.encryptJarmResponse(agentContext, {
jwkJson: jwk as JwkJson,
payload: authorizationResponsePayload,
authorizationRequestNonce: nonce,
alg: validatedMetadata.client_metadata.authorization_encrypted_response_alg,
enc: validatedMetadata.client_metadata.authorization_encrypted_response_enc,
})

return { response: jwe }
}

const response = await openidProvider.submitAuthorizationResponse(
authorizationResponseWithCorrelationId,
createJarmResponse
)
let responseDetails: string | Record<string, unknown> | undefined = undefined
try {
responseDetails = await response.text()
Expand Down Expand Up @@ -277,4 +329,51 @@ export class OpenId4VcSiopHolderService {
)
}
}

private async encryptJarmResponse(
agentContext: AgentContext,
options: {
jwkJson: JwkJson
payload: Record<string, unknown>
alg: string
enc: string
authorizationRequestNonce: string
}
) {
const { payload, jwkJson } = options
const jwk = getJwkFromJson(jwkJson)
const key = jwk.key

if (!agentContext.wallet.directEncryptCompactJweEcdhEs) {
throw new CredoError(
'Cannot decrypt Jarm Response, wallet does not support directEncryptCompactJweEcdhEs. You need to upgrade your wallet implementation.'
)
}

if (options.alg !== 'ECDH-ES') {
throw new CredoError("Only 'ECDH-ES' is supported as 'alg' value for JARM response encryption")
}

if (options.enc !== 'A256GCM') {
throw new CredoError("Only 'A256GCM' is supported as 'enc' value for JARM response encryption")
}

if (key.keyType !== KeyType.P256) {
throw new CredoError(`Only '${KeyType.P256}' key type is supported for JARM response encryption`)
}

const data = Buffer.from(JSON.stringify(payload))
const jwe = await agentContext.wallet.directEncryptCompactJweEcdhEs({
data,
recipientKey: key,
header: {
kid: jwkJson.kid,
},
encryptionAlgorithm: options.enc,
apu: TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromString(await agentContext.wallet.generateNonce())),
apv: TypedArrayEncoder.toBase64URL(TypedArrayEncoder.fromString(options.authorizationRequestNonce)),
})

return jwe
}
}
6 changes: 4 additions & 2 deletions packages/openid4vc/src/openid4vc-holder/__tests__/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export const matrrLaunchpadDraft11JwtVcJson = {

export const waltIdDraft11JwtVcJson = {
credentialOffer:
'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',
'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',
getMetadataResponse: {
issuer: 'https://issuer.portal.walt.id',
authorization_endpoint: 'https://issuer.portal.walt.id/authorize',
Expand Down Expand Up @@ -235,7 +235,9 @@ export const waltIdDraft11JwtVcJson = {
'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJjMDQyMmUxMy1kNTU0LTQwMmUtOTQ0OS0yZjA0ZjAyNjMzNTMiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IkFDQ0VTUyJ9.pkNF05uUy72QAoZwdf1Uz1XRc4aGs1hhnim-x1qIeMe17TMUYV2D6BOATQtDItxnnhQz2MBfqUSQKYi7CFirDA',
token_type: 'bearer',
c_nonce: 'd4364dac-f026-4380-a4c3-2bfe2d2df52a',
c_nonce_expires_in: 27,
c_nonce_expires_in: 300000,
expires_in: 180000,
authorization_pending: false,
},

authorizationCode:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ describe('OpenId4VcHolder | OpenID4VP', () => {
})

expect(submittedResponse).toMatchObject({
expires_in: 6000,
id_token: expect.any(String),
state: expect.any(String),
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,7 @@ export class OpenId4VcIssuerService {
signCallback: this.getSdJwtVcCredentialSigningCallback(agentContext, signOptions),
}
} else {
throw new CredoError(`Unsupported credential format`)
throw new CredoError(`Unsupported credential format ${signOptions.format}`)
}
}
}
Expand Down
Loading

0 comments on commit 1d83159

Please sign in to comment.