Skip to content

Commit

Permalink
feat: add jarm support node-only
Browse files Browse the repository at this point in the history
  • Loading branch information
auer-martin committed Sep 27, 2024
1 parent ac8b0de commit 4e25bc8
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,49 @@ import type {
} 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 { JoseJweEncryptJwt } from '@protokoll/jose'
import type {
AuthorizationResponsePayload,
PresentationExchangeResponseOpts,
RequestObjectPayload,
VerifiedAuthorizationRequest,
} from '@sphereon/did-auth-siop'

import {
Hasher,
W3cJwtVerifiablePresentation,
parseDid,
CredoError,
injectable,
W3cJsonLdVerifiablePresentation,
asArray,
DifPresentationExchangeService,
DifPresentationExchangeSubmissionLocation,
Hasher,
W3cJsonLdVerifiablePresentation,
W3cJwtVerifiablePresentation,
asArray,
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 @@ -143,7 +168,35 @@ 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.alg) {
throw new CredoError(
'Missing alg in jwk. Cannot determine encryption algorithm, for creating the JARM response.'
)
}
const { jwe } = await encryptJwt({
jwk,
payload: authorizationResponsePayload,
protectedHeader: {
alg: jwk.alg,
enc: 'A256GCM', // TODO:
kid: jwk.kid,
},
})

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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
OpenId4VcSiopCreateVerifierOptions,
OpenId4VcSiopVerifiedAuthorizationResponse,
OpenId4VcSiopVerifyAuthorizationResponseOptions,
ResponseMode,
} from './OpenId4VcSiopVerifierServiceOptions'
import type { OpenId4VcVerificationSessionRecord } from './repository'
import type { OpenId4VcSiopAuthorizationResponsePayload } from '../shared'
Expand Down Expand Up @@ -36,6 +37,7 @@ import {
DidsApi,
X509Service,
getDomainFromUrl,
KeyType,
} from '@credo-ts/core'
import {
AuthorizationRequest,
Expand All @@ -44,7 +46,7 @@ import {
PropertyTarget,
RequestAud,
ResponseIss,
ResponseMode,
ResponseMode as SphereonResponseMode,
ResponseType,
RevocationVerification,
RP,
Expand Down Expand Up @@ -72,6 +74,16 @@ 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 @@ -146,6 +158,7 @@ export class OpenId4VcSiopVerifierService {
authorizationResponseUrl,
clientId,
clientIdScheme,
responseMode: options.responseMode,
})

// We always use shortened URIs currently
Expand Down Expand Up @@ -352,26 +365,45 @@ export class OpenId4VcSiopVerifierService {
agentContext: AgentContext,
{
authorizationResponse,
authorizationResponseParams,
verifierId,
}: {
authorizationResponse: OpenId4VcSiopAuthorizationResponsePayload
verifierId?: string
}
}:
| {
authorizationResponse?: never
authorizationResponseParams: {
state?: string
nonce?: string
}
verifierId?: string
}
| {
authorizationResponse: OpenId4VcSiopAuthorizationResponsePayload
authorizationResponseParams?: never
verifierId?: string
}
) {
const authorizationResponseInstance = await AuthorizationResponse.fromPayload(authorizationResponse).catch(() => {
throw new CredoError(`Unable to parse authorization response payload. ${JSON.stringify(authorizationResponse)}`)
})
let nonce: string | undefined
let state: string | undefined

const responseNonce = await authorizationResponseInstance.getMergedProperty<string>('nonce', {
hasher: Hasher.hash,
})
const responseState = await authorizationResponseInstance.getMergedProperty<string>('state', {
hasher: Hasher.hash,
})
if (authorizationResponse) {
const authorizationResponseInstance = await AuthorizationResponse.fromPayload(authorizationResponse).catch(() => {
throw new CredoError(`Unable to parse authorization response payload. ${JSON.stringify(authorizationResponse)}`)
})

nonce = await authorizationResponseInstance.getMergedProperty<string>('nonce', {
hasher: Hasher.hash,
})
state = await authorizationResponseInstance.getMergedProperty<string>('state', {
hasher: Hasher.hash,
})
} else {
nonce = authorizationResponseParams.nonce
state = authorizationResponse
}

const verificationSession = await this.openId4VcVerificationSessionRepository.findSingleByQuery(agentContext, {
nonce: responseNonce,
payloadState: responseState,
nonce,
payloadState: state,
verifierId,
})

Expand Down Expand Up @@ -421,7 +453,9 @@ export class OpenId4VcSiopVerifierService {
clientId,
clientIdScheme,
authorizationResponseUrl,
responseMode,
}: {
responseMode?: ResponseMode
authorizationResponseUrl: string
idToken?: boolean
presentationDefinition?: DifPresentationExchangeDefinition
Expand Down Expand Up @@ -459,6 +493,16 @@ export class OpenId4VcSiopVerifierService {
.resolve(OpenId4VcRelyingPartyEventHandler)
.getEventEmitterForVerifier(agentContext.contextCorrelationId, verifierId)

const mode =
!responseMode || responseMode === 'direct_post'
? SphereonResponseMode.DIRECT_POST
: SphereonResponseMode.DIRECT_POST_JWT

const jarmKey =
mode === SphereonResponseMode.DIRECT_POST_JWT
? await agentContext.wallet.createKey({ keyType: KeyType.P256 })
: undefined

builder
.withResponseUri(authorizationResponseUrl)
.withIssuer(ResponseIss.SELF_ISSUED_V2)
Expand All @@ -469,7 +513,7 @@ export class OpenId4VcSiopVerifierService {
SupportedVersion.SIOPv2_D12_OID4VP_D18,
SupportedVersion.SIOPv2_D12_OID4VP_D20,
])
.withResponseMode(ResponseMode.DIRECT_POST)
.withResponseMode(mode)
.withHasher(Hasher.hash)
// FIXME: should allow verification of revocation
// .withRevocationVerificationCallback()
Expand All @@ -482,6 +526,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,
client_id: clientId,
client_id_scheme: clientIdScheme,
passBy: PassBy.VALUE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import type {
VerifiablePresentation,
} from '@credo-ts/core'

export type ResponseMode = 'direct_post' | 'direct_post.jwt'

export interface OpenId4VcSiopCreateAuthorizationRequestOptions {
/**
* Signing information for the request JWT. This will be used to sign the request JWT
Expand All @@ -35,6 +37,15 @@ export interface OpenId4VcSiopCreateAuthorizationRequestOptions {
presentationExchange?: {
definition: DifPresentationExchangeDefinitionV2
}

/**
* The response mode to use for the authorization request.
* @default to `direct_post`.
*
* With response_mode `direct_post` the response will be posted directly to the `response_uri` provided in the request.
* With response_mode `direct_post.jwt` the response will be `signed` `encrypted` or `signed and encrypted` and then posted to the `response_uri` provided in the request.
*/
responseMode?: ResponseMode
}

export interface OpenId4VcSiopVerifyAuthorizationResponseOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ 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('openid')
expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.did)
expect(jwt.payload.additionalClaims.response_uri).toEqual(
`http://redirect-uri/${openIdVerifier.verifierId}/authorize`
Expand Down
Loading

0 comments on commit 4e25bc8

Please sign in to comment.