UNPKG

@sphereon/ssi-sdk.oid4vci-issuer

Version:

433 lines (396 loc) • 16.2 kB
import { AuthorizationServerMetadata, CredentialRequest, IssuerMetadata, Jwt, JWTHeader, JWTPayload, JwtVerifyResult, type OID4VCICredentialFormat, StatusListOpts, } from '@sphereon/oid4vci-common' import { CredentialDataSupplier, CredentialIssuanceInput, CredentialSignerCallback, VcIssuer, VcIssuerBuilder } from '@sphereon/oid4vci-issuer' import { getAgentResolver, IDIDOptions } from '@sphereon/ssi-sdk-ext.did-utils' import { legacyKeyRefsToIdentifierOpts, ManagedIdentifierOptsOrResult } from '@sphereon/ssi-sdk-ext.identifier-resolution' import { contextHasPlugin } from '@sphereon/ssi-sdk.agent-config' import { SdJwtVcPayload } from '@sphereon/ssi-sdk.sd-jwt' import { IStatusListPlugin } from '@sphereon/ssi-sdk.vc-status-list' import { CompactSdJwtVc, CredentialMapper, ICredential, W3CVerifiableCredential } from '@sphereon/ssi-types' import { CredentialPayload, ProofFormat } from '@veramo/core' import { bytesToBase64 } from '@veramo/utils' import { createJWT, decodeJWT, JWTVerifyOptions, verifyJWT } from 'did-jwt' import { Resolvable } from 'did-resolver' import { jwtDecode } from 'jwt-decode' import { IIssuerOptions, IRequiredContext } from './types/IOID4VCIIssuer' import fetch from 'cross-fetch' import { AuthorizationResponseStateStatus } from '@sphereon/did-auth-siop' export function getJwtVerifyCallback({ verifyOpts }: { verifyOpts?: JWTVerifyOptions }, _context: IRequiredContext) { return async (args: { jwt: string; kid?: string }): Promise<JwtVerifyResult> => { const resolver = getAgentResolver(_context, { resolverResolution: true, uniresolverResolution: true, localResolution: true, }) verifyOpts = { ...verifyOpts, resolver: verifyOpts?.resolver } // Resolver separately as that is a function if (!verifyOpts?.resolver || typeof verifyOpts?.resolver?.resolve !== 'function') { verifyOpts.resolver = resolver } const result = await _context.agent.jwtVerifyJwsSignature({ jws: args.jwt }) if (!result.error) { const identifier = result.jws.signatures[0].identifier if (!identifier) { return Promise.reject(Error('the jws did not contain a signature with an identifier')) } const jwkInfo = identifier.jwks[0] if (!jwkInfo) { return Promise.reject(Error(`the identifier of type ${identifier.method} is missing jwks (ExternalJwkInfo)`)) } const { alg } = jwkInfo.jwk const header = jwtDecode<JWTHeader>(args.jwt, { header: true }) const payload = jwtDecode<JWTPayload>(args.jwt, { header: false }) const kid = args.kid ?? header.kid //const jwk = !kid ? jwkInfo.jwk : undefined // TODO double-check if this is correct const jwk = jwkInfo.jwk // FIXME workaround IATAB2B-57 return { alg, ...identifier, jwt: { header, payload }, ...(kid && { kid }), ...(jwk && { jwk }), } as JwtVerifyResult } const decodedJwt = (await decodeJWT(args.jwt)) as Jwt const kid = args.kid ?? decodedJwt.header.kid if (!kid || !kid.startsWith('did:')) { // No DID method present in header. We already performed the validation above. So return that return { alg: decodedJwt.header.alg, jwt: decodedJwt, } as JwtVerifyResult } const did = kid.split('#')[0] const didResult = await verifyJWT(args.jwt, verifyOpts) if (!didResult.verified) { console.log(`JWT invalid: ${args.jwt}`) throw Error('JWT did not verify successfully') } const didResolution = await resolver.resolve(did) if (!didResolution || !didResolution.didDocument) { throw Error(`Could not resolve did: ${did}, metadata: ${didResolution?.didResolutionMetadata}`) } const alg = decodedJwt.header.alg return { alg, kid, did, didDocument: didResolution.didDocument, jwt: decodedJwt, } } } export async function getAccessTokenKeyRef( opts: { /** * Uniform identifier options */ idOpts?: ManagedIdentifierOptsOrResult /** * @deprecated */ iss?: string /** * @deprecated */ keyRef?: string /** * @deprecated */ didOpts?: IDIDOptions }, context: IRequiredContext, ) { let identifier = legacyKeyRefsToIdentifierOpts(opts) return await context.agent.identifierManagedGet(identifier) } export async function getAccessTokenSignerCallback( opts: { /** * Uniform identifier options */ idOpts?: ManagedIdentifierOptsOrResult /** * @deprecated */ iss?: string /** * @deprecated */ keyRef?: string /** * @deprecated */ didOpts?: IDIDOptions }, context: IRequiredContext, ) { const signer = async (data: string | Uint8Array) => { let dataString, encoding: 'base64' | undefined const resolution = await legacyKeyRefsToIdentifierOpts(opts) const keyRef = resolution.kmsKeyRef if (!keyRef) { throw Error('Cannot sign access tokens without a key ref') } if (typeof data === 'string') { dataString = data encoding = undefined } else { dataString = bytesToBase64(data) encoding = 'base64' } return context.agent.keyManagerSign({ keyRef, data: dataString, encoding }) } async function accessTokenSignerCallback(jwt: Jwt, kid?: string): Promise<string> { const issuer = opts.idOpts?.issuer ?? (typeof opts.idOpts?.identifier === 'string' ? opts.idOpts.identifier : (opts.didOpts?.idOpts?.identifier?.toString() ?? opts?.iss)) if (!issuer) { throw Error('No issuer configured for access tokens') } let kidHeader: string | undefined = jwt?.header?.kid ?? kid if (!kidHeader) { if ( opts.idOpts?.method === 'did' || opts.idOpts?.method === 'kid' || (typeof opts.didOpts?.idOpts.identifier === 'string' && opts.didOpts?.idOpts?.identifier?.startsWith('did:')) ) { // @ts-ignore kidHeader = opts.idOpts?.kid ?? opts.didOpts?.idOpts?.kid ?? opts?.didOpts?.identifierOpts?.kid } } return await createJWT(jwt.payload, { signer, issuer }, { ...jwt.header, ...(kidHeader && { kid: kidHeader }), typ: 'JWT' }) } return accessTokenSignerCallback } export async function getCredentialSignerCallback( idOpts: ManagedIdentifierOptsOrResult & { crypto?: Crypto }, context: IRequiredContext, ): Promise<CredentialSignerCallback> { async function issueVCCallback(args: { credentialRequest: CredentialRequest credential: CredentialIssuanceInput jwtVerifyResult: JwtVerifyResult format?: OID4VCICredentialFormat statusLists?: Array<StatusListOpts> }): Promise<W3CVerifiableCredential | CompactSdJwtVc> { const { jwtVerifyResult, format, statusLists } = args const credential = args.credential as ICredential // TODO: SDJWT let proofFormat: ProofFormat const resolution = await context.agent.identifierManagedGet(idOpts) proofFormat = format?.includes('ld') ? 'lds' : 'jwt' const issuer = resolution.issuer ?? resolution.kmsKeyRef if (CredentialMapper.isW3cCredential(credential)) { if (!credential.issuer) { credential.issuer = { id: issuer } } else if (typeof credential.issuer === 'object' && !credential.issuer.id) { credential.issuer.id = issuer } const subjectIsArray = Array.isArray(credential.credentialSubject) let credentialSubjects = Array.isArray(credential.credentialSubject) ? credential.credentialSubject : [credential.credentialSubject] credentialSubjects = credentialSubjects.map((subject) => { if (!subject.id) { subject.id = jwtVerifyResult.did } return subject }) credential.credentialSubject = subjectIsArray ? credentialSubjects : credentialSubjects[0] // TODO: We should extend the plugin capabilities of issuance so we do not have to tuck this into the sign callback if (contextHasPlugin<IStatusListPlugin>(context, 'slAddStatusToCredential')) { // Add status list if enabled (and when the input has a credentialStatus object (can be empty)) const credentialStatusVC = await context.agent.slAddStatusToCredential({ credential, statusLists }) if (credential.credentialStatus && !credential.credentialStatus.statusListCredential) { credential.credentialStatus = credentialStatusVC.credentialStatus // TODO update statusLists somehow? } } const result = await context.agent.createVerifiableCredential({ credential: credential as CredentialPayload, proofFormat, removeOriginalFields: false, fetchRemoteContexts: true, domain: typeof credential.issuer === 'object' ? credential.issuer.id : credential.issuer, ...(resolution.kid && { header: { kid: resolution.kid } }), }) return (proofFormat === 'jwt' && 'jwt' in result.proof ? result.proof.jwt : result) as W3CVerifiableCredential } else if (CredentialMapper.isSdJwtDecodedCredentialPayload(credential)) { const sdJwtPayload = credential as SdJwtVcPayload if (sdJwtPayload.iss === undefined) { sdJwtPayload.iss = issuer } if (sdJwtPayload.iat === undefined) { sdJwtPayload.iat = Math.floor(new Date().getTime() / 1000) } let disclosureFrame if ('disclosureFrame' in credential) { disclosureFrame = credential['disclosureFrame'] delete credential['disclosureFrame'] } else { disclosureFrame = { _sd: credential['_sd'], } } if (contextHasPlugin<IStatusListPlugin>(context, 'slAddStatusToSdJwtCredential')) { if ((sdJwtPayload.status && sdJwtPayload.status.status_list) || (statusLists && statusLists.length > 0)) { // Add status list if enabled (and when the input has a credentialStatus object (can be empty)) const sdJwtPayloadWithStatus = await context.agent.slAddStatusToSdJwtCredential({ credential: sdJwtPayload, statusLists }) if (sdJwtPayload.status?.status_list?.idx) { if (!sdJwtPayloadWithStatus.status || !sdJwtPayloadWithStatus.status.status_list) { // sdJwtPayload and sdJwtPayloadWithStatus is the same for now, but we should use the result anyway as this could be subject to change return Promise.reject(Error('slAddStatusToSdJwtCredential did not return a status_list')) } // Update statusListId & statusListIndex back to the credential session TODO SSISDK-4 This is not a clean way to do this. if (statusLists && statusLists.length > 0) { const statusList = statusLists[0] statusList.statusListId = sdJwtPayloadWithStatus.status.status_list.uri statusList.statusListIndex = sdJwtPayloadWithStatus.status.status_list.idx } sdJwtPayload.status.status_list.idx = sdJwtPayloadWithStatus.status.status_list.idx } } } const result = await context.agent.createSdJwtVc({ credentialPayload: sdJwtPayload, disclosureFrame: disclosureFrame, resolution, }) return result.credential } /*else if (CredentialMapper.isMsoMdocDecodedCredential(credential)) { TODO }*/ return Promise.reject('VC issuance failed, an incorrect or unsupported credential was supplied') } return issueVCCallback } export async function createVciIssuerBuilder( args: { issuerOpts: IIssuerOptions issuerMetadata: IssuerMetadata authorizationServerMetadata: AuthorizationServerMetadata resolver?: Resolvable credentialDataSupplier?: CredentialDataSupplier }, context: IRequiredContext, ): Promise<VcIssuerBuilder> { const { issuerOpts, issuerMetadata, authorizationServerMetadata } = args const builder = new VcIssuerBuilder() // @ts-ignore const resolver = args.resolver ?? args?.issuerOpts?.didOpts?.resolveOpts?.resolver ?? args.issuerOpts?.didOpts?.resolveOpts?.jwtVerifyOpts?.resolver ?? getAgentResolver(context) if (!resolver) { throw Error('A Resolver is necessary to verify DID JWTs') } const idOpts = legacyKeyRefsToIdentifierOpts({ didOpts: issuerOpts.didOpts, idOpts: issuerOpts.idOpts }) const jwtVerifyOpts: JWTVerifyOptions = { ...issuerOpts?.didOpts?.resolveOpts?.jwtVerifyOpts, ...args?.issuerOpts?.resolveOpts?.jwtVerifyOpts, resolver, audience: issuerMetadata.credential_issuer as string, // FIXME legacy version had {display: NameAndLocale | NameAndLocale[]} as credential_issuer } builder.withIssuerMetadata(issuerMetadata) builder.withAuthorizationMetadata(authorizationServerMetadata) // builder.withUserPinRequired(issuerOpts.userPinRequired ?? false) was removed from implementers draft v1 builder.withCredentialSignerCallback(await getCredentialSignerCallback(idOpts, context)) if (issuerOpts.asClientOpts) { builder.withASClientMetadata(issuerOpts.asClientOpts) // @ts-ignore // const authorizationServer = issuerMetadata.authorization_servers[0] as string // Set the OIDC verifier // builder.withJWTVerifyCallback(oidcAccessTokenVerifyCallback({clientMetadata: issuerOpts.asClientOpts, credentialIssuer: issuerMetadata.credential_issuer as string, authorizationServer})) } // Do not use it when asClient is used builder.withJWTVerifyCallback(getJwtVerifyCallback({ verifyOpts: jwtVerifyOpts }, context)) if (args.credentialDataSupplier) { builder.withCredentialDataSupplier(args.credentialDataSupplier) } builder.withInMemoryCNonceState() builder.withInMemoryCredentialOfferState() builder.withInMemoryCredentialOfferURIState() return builder } export async function createVciIssuer( { issuerOpts, issuerMetadata, authorizationServerMetadata, credentialDataSupplier, }: { issuerOpts: IIssuerOptions issuerMetadata: IssuerMetadata authorizationServerMetadata: AuthorizationServerMetadata credentialDataSupplier?: CredentialDataSupplier }, context: IRequiredContext, ): Promise<VcIssuer> { return ( await createVciIssuerBuilder( { issuerOpts, issuerMetadata, authorizationServerMetadata, credentialDataSupplier, }, context, ) ).build() } export async function createAuthRequestUriCallback(opts: { path: string; presentationDefinitionId: string }): Promise<() => Promise<string>> { async function authRequestUriCallback(): Promise<string> { const path = opts.path.replace(':definitionId', opts.presentationDefinitionId) return fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json', }, }).then(async (response): Promise<string> => { if (response.status >= 400) { return Promise.reject(Error(await response.text())) } else { const responseData = await response.json() if (!responseData.authRequestURI) { return Promise.reject(Error('Missing auth request uri in response body')) } return responseData.authRequestURI } }) } return authRequestUriCallback } export async function createVerifyAuthResponseCallback(opts: { path: string presentationDefinitionId: string }): Promise<(correlationId: string) => Promise<boolean>> { async function verifyAuthResponseCallback(correlationId: string): Promise<boolean> { return fetch(opts.path, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ definitionId: opts.presentationDefinitionId, correlationId }), }).then(async (response): Promise<boolean> => { if (response.status >= 400) { return Promise.reject(Error(await response.text())) } else { const responseData = await response.json() if (!responseData.status) { return Promise.reject(Error('Missing status in response body')) } return responseData.status === AuthorizationResponseStateStatus.VERIFIED } }) } return verifyAuthResponseCallback }