UNPKG

@bedrock/vc-delivery

Version:
288 lines (252 loc) 9.81 kB
/*! * Copyright (c) 2022-2026 Digital Bazaar, Inc. All rights reserved. */ import * as bedrock from '@bedrock/core'; import {AsymmetricKey, KmsClient} from '@digitalbazaar/webkms-client'; import {exportJWK, generateKeyPair} from 'jose'; import {oid4vp, signJWT} from '@digitalbazaar/oid4-client'; import {getClientBaseUrl} from './clientProfiles.js'; import {getZcapClient} from '../helpers.js'; import {httpsAgent} from '@bedrock/https-agent'; import {randomUUID} from 'node:crypto'; const {util: {BedrockError}} = bedrock; const ENCRYPTED_RESPONSE_MODES = new Set([ 'direct_post.jwt', 'dc_api.jwt', 'dc_api' ]); const OID4VP_JWT_TYP = 'oauth-authz-req+jwt'; const SUPPORTED_CLIENT_ID_SCHEMES = new Set([ 'redirect_uri', 'x509_san_dns', 'x509_hash', 'decentralized_identifier' ]); const TEXT_ENCODER = new TextEncoder(); export async function create({ workflow, exchange, clientProfile, clientProfileId, verifiablePresentationRequest }) { const authorizationRequest = verifiablePresentationRequest ? oid4vp.fromVpr({verifiablePresentationRequest}) : // default authz request {response_type: 'vp_token'}; // get params from step OID4VP client profile to apply to the AR const { client_id, client_id_scheme, dcql_query, expected_origins, nonce, presentation_definition, response_mode, response_uri } = clientProfile; const clientBaseUrl = getClientBaseUrl({workflow, exchange, clientProfileId}); // client_id_scheme (draft versions of OID4VP use this param) authorizationRequest.client_id_scheme = client_id_scheme ?? 'redirect_uri'; // dcql_query authorizationRequest.dcql_query = dcql_query ?? authorizationRequest.dcql_query; // presentation_definition authorizationRequest.presentation_definition = presentation_definition ?? authorizationRequest.presentation_definition; // response_mode authorizationRequest.response_mode = response_mode ?? 'direct_post'; // response_uri authorizationRequest.response_uri = response_uri ?? `${clientBaseUrl}/authorization/response`; // client_id (defaults to `response_uri`) if(client_id) { authorizationRequest.client_id = client_id; } else if(authorizationRequest.client_id_scheme === 'redirect_uri') { // use prefix; this is compatible with both draft 18 and 1.0+ authorizationRequest.client_id = `redirect_uri:${authorizationRequest.response_uri}`; } // `x509_san_dns` requires the `direct_post.jwt` response mode when using // `direct_post` if(authorizationRequest.response_mode === 'direct_post' && oid4vp.authzRequest.usesClientIdScheme({ authorizationRequest, scheme: 'x509_san_dns' })) { authorizationRequest.response_mode += '.jwt'; } // expected origins; safe to always include authorizationRequest.expected_origins = expected_origins ?? [new URL(authorizationRequest.response_uri).origin]; // nonce if(nonce) { authorizationRequest.nonce = nonce; } else if(authorizationRequest.nonce === undefined) { // if no nonce has been set for the authorization request, use the // exchange ID authorizationRequest.nonce = exchange.id; } // client_metadata; create from the `clientProfile` the rest of the AR and // generate any necessary secrets for it const {client_metadata, secrets} = await _createClientMetaData({ authorizationRequest, clientProfile }); authorizationRequest.client_metadata = client_metadata; // only set default `aud` for signed OID4VP authz requests if(client_metadata.require_signed_request_object) { authorizationRequest.aud = 'https://self-issued.me/v2'; } return {authorizationRequest, secrets}; } export async function encode({ workflow, clientProfile, authorizationRequest } = {}) { // if required, construct authz request as signed JWT if(authorizationRequest.client_metadata.require_signed_request_object) { return _createJwt({workflow, clientProfile, authorizationRequest}); } // construct authz request as unsecured JWT with `typ` header set // to `OID4VP_JWT_TYP` value; note that the `UnsecuredJWT` API from `jose` // cannot be used because it does not allow customization of the JWT header // which is required to comply with OID4VP 1.0 const header = Buffer .from(JSON.stringify({alg: 'none', typ: OID4VP_JWT_TYP}), 'utf8') .toString('base64url'); const payload = Buffer .from(TEXT_ENCODER.encode(JSON.stringify(authorizationRequest))) .toString('base64url'); const jwt = `${header}.${payload}.`; return jwt; } export function removeClientIdPrefix({clientId} = {}) { for(const idScheme of SUPPORTED_CLIENT_ID_SCHEMES) { const prefix = `${idScheme}:`; if(clientId.startsWith(prefix)) { return clientId.slice(prefix.length); } } return clientId; } async function _createClientMetaData({ authorizationRequest, clientProfile } = {}) { // for storing client profile secrets const secrets = {}; // create base `client_metadata` from client profile if given, falling // back to the value in the authz request which might be auto-generated const client_metadata = structuredClone( clientProfile.client_metadata ?? authorizationRequest.client_metadata ?? {}); // ensure `vp_formats` and `vp_formats_supported` exists and track whether it // was present or not const hasVpFormats = !!client_metadata.vp_formats; if(!hasVpFormats) { client_metadata.vp_formats = {}; } // add `mso_mdoc` format if requested and not already present if(!client_metadata.vp_formats.mso_mdoc && oid4vp.authzRequest.requestsFormat({ authorizationRequest, format: 'mso_mdoc' })) { client_metadata.vp_formats.mso_mdoc = { alg: ['EdDSA', 'ES256'] }; } // add `jwt_vp` format if requested and not already present if(!client_metadata.vp_formats.jwt_vp && oid4vp.authzRequest.requestsFormat({ authorizationRequest, format: 'jwt_vp' })) { // support various aliases for different versions of OID4VP client_metadata.vp_formats.jwt_vp = client_metadata.vp_formats.jwt_vp_json = { alg: ['EdDSA', 'Ed25519', 'ES256', 'ES384'] }; } // add `ldp_vp` format if requested and not already present or if no other // formats are present if(!hasVpFormats || (!client_metadata.vp_formats.ldp_vp && oid4vp.authzRequest.requestsFormat({ authorizationRequest, format: 'ldp_vp' }))) { // support various aliases for different versions of OID4VP client_metadata.vp_formats.di_vp = client_metadata.vp_formats.ldp_vp = { proof_type: [ 'ecdsa-rdfc-2019', 'eddsa-rdfc-2022', 'Ed25519Signature2020' ] }; } // `x509_san_dns` client ID scheme requires authz request signing; // any client ID scheme that requires this if(oid4vp.authzRequest.usesClientIdScheme({ authorizationRequest, scheme: 'x509_san_dns' })) { client_metadata.require_signed_request_object = true; } // offer encryption options for encrypted response modes if(ENCRYPTED_RESPONSE_MODES.has(authorizationRequest.response_mode)) { // generate ECDH-ES P-256 key const kp = await generateKeyPair('ECDH-ES', { crv: 'P-256', extractable: true }); const [privateKeyJwk, publicKeyJwk] = await Promise.all([ exportJWK(kp.privateKey), exportJWK(kp.publicKey) ]); publicKeyJwk.use = 'enc'; publicKeyJwk.alg = 'ECDH-ES'; privateKeyJwk.kid = publicKeyJwk.kid = `urn:uuid:${randomUUID()}`; secrets.keyAgreementKeyPairs = [{privateKeyJwk, publicKeyJwk}]; // create / prepend to public JWK key set client_metadata.jwks = { ...client_metadata.jwks, keys: [publicKeyJwk].concat(client_metadata.jwks?.keys ?? []) }; } return {client_metadata, secrets}; } async function _createJwt({workflow, clientProfile, authorizationRequest}) { try { // create zcap client const {zcapClient, zcaps} = await getZcapClient({workflow}); // get any `x5c` and the zcap to use to sign the authz request via // the client profile const { authorizationRequestSigningParameters: {x5c} = {}, zcapReferenceIds: {signAuthorizationRequest: refId} = {} } = clientProfile; if(refId === undefined) { throw new BedrockError( 'The OID4VP client profile does not specify which capability in the ' + 'workflow configuration to use to sign authorization requests.', { name: 'DataError', details: {httpStatusCode: 500, public: true} }); } const capability = zcaps[refId]; if(capability === undefined) { throw new BedrockError( 'The capability specified by the OID4VP client profile for signing ' + 'authorization requests was not found in the workflow configuration.', { name: 'DataError', details: {httpStatusCode: 500, public: true} }); } // create a WebKMS `signer` interface const {invocationSigner} = zcapClient; const kmsClient = new KmsClient({httpsAgent}); const signer = await AsymmetricKey.fromCapability({ capability, invocationSigner, kmsClient }); const keyDescription = await signer.getKeyDescription(); const kid = keyDescription.id; // create the JWT payload and header to be signed const payload = {...authorizationRequest}; const protectedHeader = {typ: OID4VP_JWT_TYP, alg: 'ES256', kid, x5c}; // create the JWT return signJWT({payload, protectedHeader, signer}); } catch(cause) { throw new BedrockError( `Could not sign authorization request: ${cause.message}`, { name: cause instanceof BedrockError ? cause.name : 'OperationError', cause, details: {httpStatusCode: 500, public: true} }); } }