Skip to content

Commit

Permalink
feat: jarm encryption/decrytion working
Browse files Browse the repository at this point in the history
  • Loading branch information
auer-martin committed Sep 27, 2024
1 parent 0bed31c commit 6ddbee0
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 232 deletions.
2 changes: 1 addition & 1 deletion packages/openid4vc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
},
"dependencies": {
"@credo-ts/core": "workspace:*",
"@sphereon/did-auth-siop": "0.16.1-unstable.68",
"@sphereon/did-auth-siop": "link:../../../Code/OID4VC/packages/siop-oid4vp",
"@sphereon/oid4vc-common": "0.16.1-unstable.68",
"@sphereon/oid4vci-client": "0.16.1-unstable.68",
"@sphereon/oid4vci-common": "0.16.1-unstable.68",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import type {
OpenId4VcSiopResolvedAuthorizationRequest,
} from './OpenId4vcSiopHolderServiceOptions'
import type { OpenId4VcJwtIssuer } from '../shared'
import type { AgentContext, VerifiablePresentation } from '@credo-ts/core'
import type { JoseJweEncryptJwt } from '@protokoll/jose'
import type { AgentContext, JwkJson, VerifiablePresentation } from '@credo-ts/core'
import type {
AuthorizationResponsePayload,
PresentationExchangeResponseOpts,
Expand All @@ -13,40 +12,23 @@ import type {
} from '@sphereon/did-auth-siop'

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

import { getSphereonVerifiablePresentation } from '../shared/transform'
import { getCreateJwtCallback, getVerifyJwtCallback, openIdTokenIssuerToJwtIssuer } from '../shared/utils'

const encryptJwt: JoseJweEncryptJwt = async (input) => {
const { payload, protectedHeader, jwk, alg, keyManagementParameters } = input
const encode = TextEncoder.prototype.encode.bind(new TextEncoder())
const recipientPublicKey = await jose.importJWK(jwk, alg)

const joseEncryptJwt = new jose.EncryptJWT(payload).setProtectedHeader(protectedHeader)

if (keyManagementParameters) {
joseEncryptJwt.setKeyManagementParameters({
apu: encode(keyManagementParameters.apu),
apv: encode(keyManagementParameters.apv),
})
}

const jwe = await joseEncryptJwt.encrypt(recipientPublicKey)
return { jwe }
}

@injectable()
export class OpenId4VcSiopHolderService {
public constructor(private presentationExchangeService: DifPresentationExchangeService) {}
Expand Down Expand Up @@ -175,19 +157,12 @@ export class OpenId4VcSiopHolderService {
const { authorizationResponsePayload, requestObjectPayload } = opts

const jwk = await OP.extractEncJwksFromClientMetadata(requestObjectPayload.client_metadata)
if (!jwk.alg) {
throw new CredoError(
'Missing alg in jwk. Cannot determine encryption algorithm, for creating the JARM response.'
)
if (!jwk.kty) {
throw new CredoError('Missing kty in jwk.')
}
const { jwe } = await encryptJwt({
jwk,
const jwe = await this.encryptJarmResponse(agentContext, {
jwkJson: { ...jwk, kty: jwk.kty },
payload: authorizationResponsePayload,
protectedHeader: {
alg: jwk.alg,
enc: 'A256GCM', // TODO:
kid: jwk.kid,
},
})

return { response: jwe }
Expand Down Expand Up @@ -330,4 +305,33 @@ export class OpenId4VcSiopHolderService {
)
}
}

private async encryptJarmResponse(
agentContext: AgentContext,
options: { jwkJson: JwkJson; payload: Record<string, unknown> }
) {
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.'
)
}

const data = Buffer.from(JSON.stringify(payload))
const jwe = await agentContext.wallet.directEncryptCompactJweEcdhEs({
data,
recipientKey: key,
header: {
alg: jwkJson.alg,
kid: jwkJson.kid,
enc: 'A256GCM',
},
encryptionAlgorithm: 'A256GCM',
})

return jwe
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { OpenId4VcSiopAuthorizationResponsePayload } from '../shared'
import type {
AgentContext,
DifPresentationExchangeDefinition,
JwkJson,
Query,
QueryOptions,
RecordSavedEvent,
Expand Down Expand Up @@ -38,6 +39,7 @@ import {
X509Service,
getDomainFromUrl,
KeyType,
getJwkFromKey,
} from '@credo-ts/core'
import {
AuthorizationRequest,
Expand Down Expand Up @@ -74,16 +76,6 @@ import {
import { OpenId4VcRelyingPartyEventHandler } from './repository/OpenId4VcRelyingPartyEventEmitter'
import { OpenId4VcRelyingPartySessionManager } from './repository/OpenId4VcRelyingPartySessionManager'

export const ISO_MDL_7_EPHEMERAL_READER_PUBLIC_KEY_JWK = {
kty: 'EC',
use: 'enc',
crv: 'P-256',
x: 'xVLtZaPPK-xvruh1fEClNVTR6RCZBsQai2-DrnyKkxg',
y: '-5-QtFqJqGwOjEL3Ut89nrE0MeaUp5RozksKHpBiyw0',
alg: 'ECDH-ES',
kid: 'P8p0virRlh6fAkh5-YSeHt4EIv-hFGneYk14d8DF51w',
}

/**
* @internal
*/
Expand Down Expand Up @@ -498,10 +490,12 @@ export class OpenId4VcSiopVerifierService {
? SphereonResponseMode.DIRECT_POST
: SphereonResponseMode.DIRECT_POST_JWT

const jarmKey =
mode === SphereonResponseMode.DIRECT_POST_JWT
? await agentContext.wallet.createKey({ keyType: KeyType.P256 })
: undefined
let jarmEncryptionJwk: (JwkJson & { kid: string; use: 'enc' }) | undefined

if (mode === SphereonResponseMode.DIRECT_POST_JWT) {
const key = await agentContext.wallet.createKey({ keyType: KeyType.P256 })
jarmEncryptionJwk = { ...getJwkFromKey(key).toJson(), kid: key.fingerprint, use: 'enc' }
}

builder
.withResponseUri(authorizationResponseUrl)
Expand All @@ -526,7 +520,7 @@ export class OpenId4VcSiopVerifierService {

// TODO: we should probably allow some dynamic values here
.withClientMetadata({
jwks: jarmKey ? { keys: [ISO_MDL_7_EPHEMERAL_READER_PUBLIC_KEY_JWK] } : undefined,
jwks: jarmEncryptionJwk ? { keys: [jarmEncryptionJwk] } : undefined,
client_id: clientId,
client_id_scheme: clientIdScheme,
passBy: PassBy.VALUE,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,50 +1,15 @@
import type { OpenId4VcVerificationRequest } from './requestContext'
import type { OpenId4VcVerificationSessionRecord } from '../repository'
import type { JwkJson } from '@credo-ts/core'
import type { AgentContext } from '@credo-ts/core/src/agent/context/AgentContext'
import type { AuthorizationResponsePayload } from '@sphereon/did-auth-siop'
import type { AuthorizationResponsePayload, DecryptCompact } from '@sphereon/did-auth-siop'
import type { Response, Router } from 'express'

import { CredoError } from '@credo-ts/core'
import { CredoError, Key, TypedArrayEncoder } from '@credo-ts/core'
import { AuthorizationRequest, RP } from '@sphereon/did-auth-siop'
import * as jose from 'jose'

import { getRequestContext, sendErrorResponse } from '../../shared/router'
import { OpenId4VcSiopVerifierService } from '../OpenId4VcSiopVerifierService'

export const ISO_MDL_7_EPHEMERAL_READER_PRIVATE_KEY_JWK = {
kty: 'EC',
d: '_Hc7lRd1Zt8sDAb1-pCgI9qS3oobKNa-mjRDhaKjH90',
use: 'enc',
crv: 'P-256',
x: 'xVLtZaPPK-xvruh1fEClNVTR6RCZBsQai2-DrnyKkxg',
y: '-5-QtFqJqGwOjEL3Ut89nrE0MeaUp5RozksKHpBiyw0',
alg: 'ECDH-ES',
kid: 'P8p0virRlh6fAkh5-YSeHt4EIv-hFGneYk14d8DF51w',
}

const decryptCompact = async (input: { jwk: { kid: string }; jwe: string }) => {
const { jwe, jwk } = input

let jwkToUse: JwkJson
if (jwk.kid === ISO_MDL_7_EPHEMERAL_READER_PRIVATE_KEY_JWK.kid) {
jwkToUse = ISO_MDL_7_EPHEMERAL_READER_PRIVATE_KEY_JWK
} else {
throw new CredoError('Invalid JWK provided for decryption')
}

const privateKey = await jose.importJWK(jwkToUse)
const decode = TextDecoder.prototype.decode.bind(new TextDecoder())

const { plaintext, protectedHeader } = await jose.compactDecrypt(jwe, privateKey)

return {
plaintext: decode(plaintext),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protectedHeader: protectedHeader as any,
}
}

export interface OpenId4VcSiopAuthorizationEndpointConfig {
/**
* The path at which the authorization endpoint should be made available. Note that it will be
Expand Down Expand Up @@ -81,6 +46,24 @@ async function getVerificationSession(
return session
}

const decryptJarmResponse = (agentContext: AgentContext): DecryptCompact => {
return async (input) => {
const { jwe: compactJwe, jwk: jwkJson } = input
const key = Key.fromFingerprint(jwkJson.kid)
if (!agentContext.wallet.directDecryptCompactJweEcdhEs) {
throw new CredoError('Cannot decrypt Jarm Response, wallet does not support directDecryptCompactJweEcdhEs')
}

const { data, header } = await agentContext.wallet.directDecryptCompactJweEcdhEs({ compactJwe, recipientKey: key })
const decryptedPayload = TypedArrayEncoder.toUtf8String(data)

return {
plaintext: decryptedPayload,
protectedHeader: header as Record<string, unknown> & { alg: string; enc: string },
}
}
}

export function configureAuthorizationEndpoint(router: Router, config: OpenId4VcSiopAuthorizationEndpointConfig) {
router.post(config.endpointPath, async (request: OpenId4VcVerificationRequest, response: Response, next) => {
const { agentContext, verifier } = getRequestContext(request)
Expand All @@ -107,7 +90,7 @@ export function configureAuthorizationEndpoint(router: Router, config: OpenId4Vc
}
return { authRequestParams: requestObjectPayload }
},
decryptCompact,
decryptCompact: decryptJarmResponse(agentContext),
})

authorizationResponsePayload = res.authResponseParams as AuthorizationResponsePayload
Expand Down
Loading

0 comments on commit 6ddbee0

Please sign in to comment.