Skip to content

Commit

Permalink
feat: jarm alpha
Browse files Browse the repository at this point in the history
  • Loading branch information
auer-martin committed Sep 10, 2024
1 parent 2f1fcee commit 703e09e
Show file tree
Hide file tree
Showing 20 changed files with 10,931 additions and 7,575 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { assertValidVerifyAuthorizationRequestOpts } from '../authorization-requ
import { IDToken } from '../id-token'
import { AuthorizationResponsePayload, ResponseType, SIOPErrors, VerifiedAuthorizationRequest, VerifiedAuthorizationResponse } from '../types'

import { assertValidVerifiablePresentations, extractPresentationsFromAuthorizationResponse, verifyPresentations } from './OpenID4VP'
import {
assertValidVerifiablePresentations,
extractPresentationsFromAuthorizationResponse,
MdocVerifiablePresentation,
verifyPresentations,
} from './OpenID4VP'
import { assertValidResponseOpts } from './Opts'
import { createResponsePayload } from './Payload'
import { AuthorizationResponseOpts, PresentationDefinitionWithLocation, VerifyAuthorizationResponseOpts } from './types'
Expand Down Expand Up @@ -206,8 +211,13 @@ export class AuthorizationResponse {
if (this._payload?.vp_token) {
const presentations = await extractPresentationsFromAuthorizationResponse(this, opts)
// We do not verify them, as that is done elsewhere. So we simply can take the first nonce

if (!nonce) {
nonce = presentations[0].decoded.nonce
if (presentations[0] instanceof MdocVerifiablePresentation) {
nonce = presentations[0].nonce
} else {
nonce = presentations[0].decoded.nonce
}
}
}
const idTokenPayload = await this.idToken?.payload()
Expand Down
61 changes: 52 additions & 9 deletions packages/siop-oid4vp/lib/authorization-response/OpenID4VP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,30 @@ import {
VPTokenLocation,
} from './types'

function extractNonceFromWrappedVerifiablePresentation(wrappedVp: WrappedVerifiablePresentation): string | undefined {
// SD-JWT uses kb-jwt for the nonce
if (CredentialMapper.isWrappedSdJwtVerifiablePresentation(wrappedVp)) {
export class MdocVerifiablePresentation {
constructor(private _deviceSignedBase64Url: string) {}

public deviceSignedBase64Url = this._deviceSignedBase64Url

public format = 'mso_mdoc'
public claimFormat = 'mso_mdoc'

get nonce(): string {
return JSON.parse(this.deviceSignedBase64Url).nonce as string
}

set nonce(nonce: string) {
const obj = JSON.parse(this.deviceSignedBase64Url)
obj.nonce = nonce
this.deviceSignedBase64Url = JSON.stringify(obj)
}
}

function extractNonceFromWrappedVerifiablePresentation(wrappedVp: WrappedVerifiablePresentation | MdocVerifiablePresentation): string | undefined {
if (wrappedVp instanceof MdocVerifiablePresentation) {
return wrappedVp.nonce
} else if (CredentialMapper.isWrappedSdJwtVerifiablePresentation(wrappedVp)) {
// SD-JWT uses kb-jwt for the nonce
// TODO: replace this once `kbJwt.payload` is available on the decoded sd-jwt (pr in ssi-sdk)
// If it doesn't end with ~, it contains a kbJwt
if (!wrappedVp.presentation.compactSdJwtVc.endsWith('~')) {
Expand Down Expand Up @@ -127,12 +148,17 @@ export const verifyPresentations = async (
export const extractPresentationsFromAuthorizationResponse = async (
response: AuthorizationResponse,
opts?: { hasher?: Hasher },
): Promise<WrappedVerifiablePresentation[]> => {
const wrappedVerifiablePresentations: WrappedVerifiablePresentation[] = []
): Promise<(WrappedVerifiablePresentation | MdocVerifiablePresentation)[]> => {
const wrappedVerifiablePresentations: (WrappedVerifiablePresentation | MdocVerifiablePresentation)[] = []
if (response.payload.vp_token) {
const presentations = Array.isArray(response.payload.vp_token) ? response.payload.vp_token : [response.payload.vp_token]
for (const presentation of presentations) {
wrappedVerifiablePresentations.push(CredentialMapper.toWrappedVerifiablePresentation(presentation, { hasher: opts?.hasher }))
if (typeof presentation === 'string' && !presentation.includes('~')) {
const mdocVerifiablePresentation = new MdocVerifiablePresentation(presentation)
wrappedVerifiablePresentations.push(mdocVerifiablePresentation)
} else {
wrappedVerifiablePresentations.push(CredentialMapper.toWrappedVerifiablePresentation(presentation, { hasher: opts?.hasher }))
}
}
}
return wrappedVerifiablePresentations
Expand Down Expand Up @@ -267,7 +293,7 @@ export const putPresentationSubmissionInLocation = async (

export const assertValidVerifiablePresentations = async (args: {
presentationDefinitions: PresentationDefinitionWithLocation[]
presentations: WrappedVerifiablePresentation[]
presentations: (WrappedVerifiablePresentation | MdocVerifiablePresentation)[]
verificationCallback: PresentationVerificationCallback
opts?: {
limitDisclosureSignatureSuites?: string[]
Expand All @@ -277,14 +303,31 @@ export const assertValidVerifiablePresentations = async (args: {
hasher?: Hasher
}
}) => {
const mdocVerifiablePresentations = args.presentations.filter(
(p) => p instanceof MdocVerifiablePresentation === true,
) as MdocVerifiablePresentation[]
if (mdocVerifiablePresentations.length > 0) {
if (args.verificationCallback) {
for (const mdocVerifiablePresentation of mdocVerifiablePresentations) {
await args.verificationCallback(mdocVerifiablePresentation, args.opts?.presentationSubmission)
}
}

// TODO: PEX DOES NOT YET SUPPORT THIS with mdoc
return
}

const presentations = args.presentations as WrappedVerifiablePresentation[]

if (
(!args.presentationDefinitions || args.presentationDefinitions.filter((a) => a.definition).length === 0) &&
(!args.presentations || (Array.isArray(args.presentations) && args.presentations.filter((vp) => vp.presentation).length === 0))
(!args.presentations || (Array.isArray(args.presentations) && presentations.filter((vp) => vp.presentation).length === 0))
) {
return
}

PresentationExchange.assertValidPresentationDefinitionWithLocations(args.presentationDefinitions)
const presentationsWithFormat = args.presentations
const presentationsWithFormat = presentations

if (args.presentationDefinitions && args.presentationDefinitions.length && (!presentationsWithFormat || presentationsWithFormat.length === 0)) {
throw new Error(SIOPErrors.AUTH_REQUEST_EXPECTS_VP)
Expand Down
1 change: 1 addition & 0 deletions packages/siop-oid4vp/lib/authorization-response/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './AuthorizationResponse'
export * from './types'
export * from './Payload'
export * from './ResponseRegistration'
export * from './OpenID4VP'
16 changes: 14 additions & 2 deletions packages/siop-oid4vp/lib/authorization-response/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@ import { IPresentationDefinition, PresentationSignCallBackParams } from '@sphere
import { Format } from '@sphereon/pex-models'
import { CompactSdJwtVc, Hasher, PresentationSubmission, W3CVerifiablePresentation } from '@sphereon/ssi-types'

import { ResponseMode, ResponseRegistrationOpts, ResponseURIType, SupportedVersion, VerifiablePresentationWithFormat, Verification } from '../types'
import { EncryptJwtCallback } from '../helpers/Jwe'
import {
ResponseMode,
ResponseRegistrationOpts,
ResponseType,
ResponseURIType,
SupportedVersion,
VerifiablePresentationWithFormat,
Verification,
} from '../types'
import { CreateJwtCallback } from '../types/VpJwtIssuer'
import { VerifyJwtCallback } from '../types/VpJwtVerifier'

import { AuthorizationResponse } from './AuthorizationResponse'
import { MdocVerifiablePresentation as MdocVerifiablePresentation } from './OpenID4VP'

export interface AuthorizationResponseOpts {
// redirectUri?: string; // It's typically comes from the request opts as a measure to prevent hijacking.
Expand All @@ -17,8 +27,10 @@ export interface AuthorizationResponseOpts {
version?: SupportedVersion
audience?: string
createJwtCallback: CreateJwtCallback
encryptJwtCallback?: EncryptJwtCallback
jwtIssuer?: JwtIssuer
responseMode?: ResponseMode
responseType?: [ResponseType]
// did: string;
expiresIn?: number
accessToken?: string
Expand Down Expand Up @@ -78,7 +90,7 @@ export enum VPTokenLocation {
export type PresentationVerificationResult = { verified: boolean; reason?: string }

export type PresentationVerificationCallback = (
args: W3CVerifiablePresentation | CompactSdJwtVc,
args: W3CVerifiablePresentation | CompactSdJwtVc | MdocVerifiablePresentation,
presentationSubmission: PresentationSubmission,
) => Promise<PresentationVerificationResult>

Expand Down
4 changes: 4 additions & 0 deletions packages/siop-oid4vp/lib/helpers/Jwe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { JoseJweDecrypt, JoseJweEncrypt } from '@protokoll/jarm'

export type EncryptJwtCallback = JoseJweEncrypt
export type DecryptJwtCallback = JoseJweDecrypt
7 changes: 6 additions & 1 deletion packages/siop-oid4vp/lib/helpers/Revocation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { CredentialMapper, W3CVerifiableCredential, WrappedVerifiableCredential, WrappedVerifiablePresentation } from '@sphereon/ssi-types'

import { MdocVerifiablePresentation } from '../authorization-response/OpenID4VP'
import { RevocationStatus, RevocationVerification, RevocationVerificationCallback, VerifiableCredentialTypeFormat } from '../types'

export const verifyRevocation = async (
vpToken: WrappedVerifiablePresentation,
vpToken: WrappedVerifiablePresentation | MdocVerifiablePresentation,
revocationVerificationCallback: RevocationVerificationCallback,
revocationVerification: RevocationVerification,
): Promise<void> => {
Expand All @@ -14,6 +15,10 @@ export const verifyRevocation = async (
throw new Error(`Revocation callback not provided`)
}

if (vpToken instanceof MdocVerifiablePresentation) {
return
}

const vcs = CredentialMapper.isWrappedSdJwtVerifiablePresentation(vpToken) ? [vpToken.vcs[0]] : vpToken.presentation.verifiableCredential
for (const vc of vcs) {
if (
Expand Down
1 change: 1 addition & 0 deletions packages/siop-oid4vp/lib/helpers/SIOPSpecVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const authorizationRequestVersionDiscovery = (authorizationRequest: Autho
const versions = []
const authorizationRequestCopy: AuthorizationRequestPayload = JSON.parse(JSON.stringify(authorizationRequest))
const vd13Validation = AuthorizationRequestPayloadVD12OID4VPD20Schema(authorizationRequestCopy)

if (vd13Validation) {
if (
!authorizationRequestCopy.registration_uri &&
Expand Down
67 changes: 63 additions & 4 deletions packages/siop-oid4vp/lib/op/OP.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EventEmitter } from 'events'

import { JarmClientMetadataParams, sendJarmAuthResponse } from '@protokoll/jarm'
import { JwtIssuer, uuidv4 } from '@sphereon/oid4vc-common'
import { IIssuerId } from '@sphereon/ssi-types'

Expand All @@ -17,6 +18,7 @@ import {
AuthorizationEvent,
AuthorizationEvents,
ContentType,
JWK,
ParsedAuthorizationRequestURI,
RegisterEventListener,
ResponseIss,
Expand Down Expand Up @@ -155,18 +157,32 @@ export class OP {
}

// TODO SK Can you please put some documentation on it?
public async submitAuthorizationResponse(authorizationResponse: AuthorizationResponseWithCorrelationId): Promise<Response> {
public async submitAuthorizationResponse(
authorizationResponse: AuthorizationResponseWithCorrelationId,
clientMetadata?: {
jwks?: { keys: JWK[] }
jwks_uri?: string
} & JarmClientMetadataParams,
): Promise<Response> {
const { correlationId, response } = authorizationResponse
if (!correlationId) {
throw Error('No correlation Id provided')
}

const isJarmResponseMode = (responseMode: string): responseMode is 'direct_post.jwt' | 'query.jwt' | 'fragment.jwt' => {
return responseMode === ResponseMode.DIRECT_POST_JWT || responseMode === ResponseMode.QUERY_JWT || responseMode === ResponseMode.FRAGMENT_JWT
}

const responseMode = response.options.responseMode

if (
!response ||
(response.options?.responseMode &&
!(
response.options.responseMode === ResponseMode.POST ||
response.options.responseMode === ResponseMode.FORM_POST ||
response.options.responseMode === ResponseMode.DIRECT_POST
responseMode === ResponseMode.POST ||
responseMode === ResponseMode.FORM_POST ||
responseMode === ResponseMode.DIRECT_POST ||
isJarmResponseMode(responseMode)
))
) {
throw new Error(SIOPErrors.BAD_PARAMS)
Expand All @@ -178,6 +194,49 @@ export class OP {
if (!responseUri) {
throw Error('No response URI present')
}

if (isJarmResponseMode(responseMode)) {
if (!clientMetadata) {
throw new Error(`Sending an authorization response with response_mode '${responseMode}' requires providing client_metadata`)
}

if (!this._createResponseOptions.encryptJwtCallback) {
throw new Error(`Sending an authorization response with response_mode '${responseMode}' requires providing an encryptJwtCallback`)
}

if (!clientMetadata.jwks) {
throw new Error('Currently the jarm response decryption key can only be extracted from the jwks client_metadata parameter')
}

const decJwk = clientMetadata.jwks.keys.find((key) => key.use === 'enc')
if (!decJwk) {
throw new Error('No decyption key found in the jwks client_metadata parameter')
}

const { jwe } = await this.createResponseOptions.encryptJwtCallback({
jwk: decJwk,
plaintext: JSON.stringify(response.payload),
})

let responseType: 'id_token' | 'id_token vp_token' | 'vp_token'
if (idToken && payload.vp_token) {
responseType = 'id_token vp_token'
} else if (idToken) {
responseType = 'id_token'
} else if (payload.vp_token) {
responseType = 'vp_token'
}

return sendJarmAuthResponse({
authRequestParams: {
response_uri: responseUri,
response_mode: responseMode,
response_type: responseType,
},
authResponseParams: { response: jwe },
})
}

const authResponseAsURI = encodeJsonAsURI(payload, { arraysWithIndex: ['presentation_submission', 'vp_token'] })
return post(responseUri, authResponseAsURI, { contentType: ContentType.FORM_URL_ENCODED, exceptionOnHttpErrorStatus: true })
.then((result: SIOPResonse<unknown>) => {
Expand Down
7 changes: 7 additions & 0 deletions packages/siop-oid4vp/lib/op/OPBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Hasher, IIssuerId } from '@sphereon/ssi-types'

import { PropertyTargets } from '../authorization-request'
import { PresentationSignCallback } from '../authorization-response'
import { EncryptJwtCallback } from '../helpers/Jwe'
import { ResponseIss, ResponseMode, ResponseRegistrationOpts, SupportedVersion, VerifyJwtCallback } from '../types'
import { CreateJwtCallback } from '../types/VpJwtIssuer'

Expand All @@ -15,6 +16,7 @@ export class OPBuilder {
responseMode?: ResponseMode = ResponseMode.DIRECT_POST
responseRegistration?: Partial<ResponseRegistrationOpts> = {}
createJwtCallback?: CreateJwtCallback
encryptJwtCallback?: EncryptJwtCallback
verifyJwtCallback?: VerifyJwtCallback
presentationSignCallback?: PresentationSignCallback
supportedVersions?: SupportedVersion[]
Expand Down Expand Up @@ -64,6 +66,11 @@ export class OPBuilder {
return this
}

withEncryptJwtCallback(encryptJwtCallback: EncryptJwtCallback): OPBuilder {
this.encryptJwtCallback = encryptJwtCallback
return this
}

withVerifyJwtCallback(verifyJwtCallback: VerifyJwtCallback): OPBuilder {
this.verifyJwtCallback = verifyJwtCallback
return this
Expand Down
1 change: 1 addition & 0 deletions packages/siop-oid4vp/lib/op/Opts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const createResponseOptsFromBuilderOrExistingOpts = (opts: {
expiresIn: opts.builder.expiresIn,
jwtIssuer: responseOpts?.jwtIssuer,
createJwtCallback: opts.builder.createJwtCallback,
encryptJwtCallback: opts.builder.encryptJwtCallback,
responseMode: opts.builder.responseMode,
...(responseOpts?.version
? { version: responseOpts.version }
Expand Down
9 changes: 8 additions & 1 deletion packages/siop-oid4vp/lib/rp/RPBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Hasher } from '@sphereon/ssi-types'

import { PropertyTarget, PropertyTargets } from '../authorization-request'
import { PresentationVerificationCallback } from '../authorization-response'
import { DecryptJwtCallback } from '../helpers/Jwe'
import { ClientIdScheme, CreateJwtCallback, RequestAud, VerifyJwtCallback } from '../types'
import {
AuthorizationRequestPayload,
Expand All @@ -27,6 +28,7 @@ import { IRPSessionManager } from './types'
export class RPBuilder {
requestObjectBy: ObjectBy
createJwtCallback?: CreateJwtCallback
decryptJwtCallback: DecryptJwtCallback
verifyJwtCallback?: VerifyJwtCallback
revocationVerification?: RevocationVerification
revocationVerificationCallback?: RevocationVerificationCallback
Expand Down Expand Up @@ -133,7 +135,7 @@ export class RPBuilder {
return this
}

withResponsetUri(redirectUri: string, targets?: PropertyTargets): RPBuilder {
withResponseUri(redirectUri: string, targets?: PropertyTargets): RPBuilder {
this._authorizationRequestPayload.response_uri = assignIfAuth({ propertyValue: redirectUri, targets }, false)
this._requestObjectPayload.response_uri = assignIfRequestObject({ propertyValue: redirectUri, targets }, true)
return this
Expand Down Expand Up @@ -212,6 +214,11 @@ export class RPBuilder {
return this
}

withDecryptJwtCallback(decryptJwtCallback: DecryptJwtCallback): RPBuilder {
this.decryptJwtCallback = decryptJwtCallback
return this
}

withPresentationDefinition(definitionOpts: { definition: IPresentationDefinition; definitionUri?: string }, targets?: PropertyTargets): RPBuilder {
const { definition, definitionUri } = definitionOpts

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,10 @@ export const AuthorizationRequestPayloadVD11SchemaObj = {
"form_post",
"post",
"direct_post",
"query"
"query",
"direct_post.jwt",
"query.jwt",
"fragment.jwt"
]
},
"ClaimPayloadCommon": {
Expand Down
Loading

0 comments on commit 703e09e

Please sign in to comment.