Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for openid4vp response encryption (JARM) #2046

Merged
merged 15 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading