From b09da533bc0b3faaff63be788431ffb429903e30 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Sun, 29 Sep 2024 17:41:59 +0200 Subject: [PATCH] feat: validate jarm metadata --- packages/jarm/lib/metadata/index.ts | 5 +- .../lib/metadata/jarm-validate-metadata.ts | 90 +++++++++++++++++++ .../metadata/v-jarm-client-metadata-params.ts | 43 --------- .../lib/metadata/v-jarm-client-metadata.ts | 42 +++++++++ ...ta-params.ts => v-jarm-server-metadata.ts} | 18 ++-- packages/jarm/lib/utils.ts | 15 ++++ packages/jarm/lib/v-response-type-registry.ts | 18 +--- packages/siop-oid4vp/lib/op/OP.ts | 6 +- packages/siop-oid4vp/lib/rp/RP.ts | 6 +- packages/siop-oid4vp/lib/types/JWT.types.ts | 5 ++ packages/siop-oid4vp/lib/types/SIOP.types.ts | 6 +- 11 files changed, 178 insertions(+), 76 deletions(-) create mode 100644 packages/jarm/lib/metadata/jarm-validate-metadata.ts delete mode 100644 packages/jarm/lib/metadata/v-jarm-client-metadata-params.ts create mode 100644 packages/jarm/lib/metadata/v-jarm-client-metadata.ts rename packages/jarm/lib/metadata/{v-jarm-server-metadata-params.ts => v-jarm-server-metadata.ts} (76%) diff --git a/packages/jarm/lib/metadata/index.ts b/packages/jarm/lib/metadata/index.ts index 02d732b1..aaf86a43 100644 --- a/packages/jarm/lib/metadata/index.ts +++ b/packages/jarm/lib/metadata/index.ts @@ -1,2 +1,3 @@ -export * from './v-jarm-client-metadata-params.js'; -export * from './v-jarm-server-metadata-params.js'; +export * from './v-jarm-client-metadata.js'; +export * from './v-jarm-server-metadata.js'; +export * from './jarm-validate-metadata.js'; diff --git a/packages/jarm/lib/metadata/jarm-validate-metadata.ts b/packages/jarm/lib/metadata/jarm-validate-metadata.ts new file mode 100644 index 00000000..a1231cab --- /dev/null +++ b/packages/jarm/lib/metadata/jarm-validate-metadata.ts @@ -0,0 +1,90 @@ +import * as v from 'valibot'; + +import { + vJarmClientMetadata, + vJarmClientMetadataEncrypt, + vJarmClientMetadataSign, + vJarmClientMetadataSignEncrypt, +} from '../metadata/v-jarm-client-metadata.js'; +import { vJarmServerMetadata } from '../metadata/v-jarm-server-metadata.js'; +import { assertValueSupported } from '../utils.js'; + +export const vJarmAuthResponseValidateMetadataInput = v.object({ + client_metadata: vJarmClientMetadata, + server_metadata: v.partial(vJarmServerMetadata), +}); +export type JarmMetadataValidate = v.InferInput; + +export const vJarmMetadataValidateOut = v.variant('type', [ + v.object({ + type: v.literal('signed'), + client_metadata: vJarmClientMetadataSign, + }), + v.object({ + type: v.literal('encrypted'), + client_metadata: vJarmClientMetadataEncrypt, + }), + v.object({ + type: v.literal('signed encrypted'), + client_metadata: vJarmClientMetadataSignEncrypt, + }), +]); + +export const jarmMetadataValidate = (vJarmMetadataValidate: JarmMetadataValidate): v.InferOutput => { + const { client_metadata, server_metadata } = vJarmMetadataValidate; + + assertValueSupported({ + supported: server_metadata.authorization_signing_alg_values_supported ?? [], + actual: client_metadata.authorization_signed_response_alg, + required: !!client_metadata.authorization_signed_response_alg, + error: new Error('Invalid authorization_signed_response_alg'), + }); + + assertValueSupported({ + supported: server_metadata.authorization_encryption_alg_values_supported ?? [], + actual: client_metadata.authorization_encrypted_response_alg, + required: !!client_metadata.authorization_encrypted_response_alg, + error: new Error('Invalid authorization_encrypted_response_alg'), + }); + + assertValueSupported({ + supported: server_metadata.authorization_encryption_enc_values_supported ?? [], + actual: client_metadata.authorization_encrypted_response_enc, + required: !!client_metadata.authorization_encrypted_response_enc, + error: new Error('Invalid authorization_encrypted_response_enc'), + }); + + if ( + client_metadata.authorization_signed_response_alg && + client_metadata.authorization_encrypted_response_alg && + client_metadata.authorization_encrypted_response_enc + ) { + return { + type: 'signed encrypted', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + client_metadata: client_metadata as any, + }; + } else if ( + client_metadata.authorization_signed_response_alg && + !client_metadata.authorization_encrypted_response_alg && + !client_metadata.authorization_encrypted_response_enc + ) { + return { + type: 'signed', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + client_metadata: client_metadata as any, + }; + } else if ( + !client_metadata.authorization_signed_response_alg && + client_metadata.authorization_encrypted_response_alg && + client_metadata.authorization_encrypted_response_enc + ) { + return { + type: 'encrypted', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + client_metadata: client_metadata as any, + }; + } else { + throw new Error(`Invalid jarm client_metadata combination`); + } +}; diff --git a/packages/jarm/lib/metadata/v-jarm-client-metadata-params.ts b/packages/jarm/lib/metadata/v-jarm-client-metadata-params.ts deleted file mode 100644 index 0407e675..00000000 --- a/packages/jarm/lib/metadata/v-jarm-client-metadata-params.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as v from 'valibot'; - -const vJarmClientMetadataParamsBase = v.object({ - authorization_signed_response_alg: v.pipe( - v.optional(v.string(), 'RS256'), - v.description( - 'JWA. If this is specified, the response will be signed using JWS and the configured algorithm. The algorithm none is not allowed.' - ) - ), - - authorization_encrypted_response_alg: v.optional(v.never()), - authorization_encrypted_response_enc: v.optional(v.never()), -}); - -/** - * Clients may register their public encryption keys using the jwks_uri or jwks metadata parameters. - */ -export const vJarmClientMetadataParams = v.union([ - v.object({ - ...vJarmClientMetadataParamsBase.entries, - }), - v.object({ - ...vJarmClientMetadataParamsBase.entries, - - authorization_encrypted_response_alg: v.pipe( - v.string(), - v.description( - 'JWE alg algorithm JWA. If both signing and encryption are requested, the response will be signed then encrypted with the provided algorithm.' - ) - ), - - authorization_encrypted_response_enc: v.pipe( - v.optional(v.string(), 'A128CBC-HS256'), - v.description( - 'JWE enc algorithm JWA. If both signing and encryption are requested, the response will be signed then encrypted with the provided algorithm.' - ) - ), - }), -]); - -export type JarmClientMetadataParams = v.InferInput< - typeof vJarmClientMetadataParams ->; diff --git a/packages/jarm/lib/metadata/v-jarm-client-metadata.ts b/packages/jarm/lib/metadata/v-jarm-client-metadata.ts new file mode 100644 index 00000000..5addf710 --- /dev/null +++ b/packages/jarm/lib/metadata/v-jarm-client-metadata.ts @@ -0,0 +1,42 @@ +import * as v from 'valibot'; + +export const vJarmClientMetadataSign = v.object({ + authorization_signed_response_alg: v.pipe( + v.optional(v.string()), // @default 'RS256' This makes no sense with openid4vp if just encrypted can be specified + v.description( + 'JWA. If this is specified, the response will be signed using JWS and the configured algorithm. The algorithm none is not allowed.', + ), + ), + + authorization_encrypted_response_alg: v.optional(v.never()), + authorization_encrypted_response_enc: v.optional(v.never()), +}); + +export const vJarmClientMetadataEncrypt = v.object({ + authorization_signed_response_alg: v.optional(v.never()), + authorization_encrypted_response_alg: v.pipe( + v.string(), + v.description( + 'JWE alg algorithm JWA. If both signing and encryption are requested, the response will be signed then encrypted with the provided algorithm.', + ), + ), + + authorization_encrypted_response_enc: v.pipe( + v.optional(v.string(), 'A128CBC-HS256'), + v.description( + 'JWE enc algorithm JWA. If both signing and encryption are requested, the response will be signed then encrypted with the provided algorithm.', + ), + ), +}); + +export const vJarmClientMetadataSignEncrypt = v.object({ + ...v.pick(vJarmClientMetadataSign, ['authorization_signed_response_alg']).entries, + ...v.pick(vJarmClientMetadataEncrypt, ['authorization_encrypted_response_alg', 'authorization_encrypted_response_enc']).entries, +}); + +/** + * Clients may register their public encryption keys using the jwks_uri or jwks metadata parameters. + */ +export const vJarmClientMetadata = v.union([vJarmClientMetadataSign, vJarmClientMetadataEncrypt, vJarmClientMetadataSignEncrypt]); + +export type JarmClientMetadata = v.InferInput; diff --git a/packages/jarm/lib/metadata/v-jarm-server-metadata-params.ts b/packages/jarm/lib/metadata/v-jarm-server-metadata.ts similarity index 76% rename from packages/jarm/lib/metadata/v-jarm-server-metadata-params.ts rename to packages/jarm/lib/metadata/v-jarm-server-metadata.ts index 465c0e3e..deca18bb 100644 --- a/packages/jarm/lib/metadata/v-jarm-server-metadata-params.ts +++ b/packages/jarm/lib/metadata/v-jarm-server-metadata.ts @@ -3,29 +3,27 @@ import * as v from 'valibot'; /** * Authorization servers SHOULD publish the supported algorithms for signing and encrypting the JWT of an authorization response by utilizing OAuth 2.0 Authorization Server Metadata [RFC8414] parameters. */ -export const vJarmServerMetadataParams = v.object({ +export const vJarmServerMetadata = v.object({ authorization_signing_alg_values_supported: v.pipe( v.array(v.string()), v.description( - 'JSON array containing a list of the JWS [RFC7515] signing algorithms (alg values) JWA [RFC7518] supported by the authorization endpoint to sign the response.' - ) + 'JSON array containing a list of the JWS [RFC7515] signing algorithms (alg values) JWA [RFC7518] supported by the authorization endpoint to sign the response.', + ), ), authorization_encryption_alg_values_supported: v.pipe( v.array(v.string()), v.description( - 'JSON array containing a list of the JWE [RFC7516] encryption algorithms (alg values) JWA [RFC7518] supported by the authorization endpoint to encrypt the response.' - ) + 'JSON array containing a list of the JWE [RFC7516] encryption algorithms (alg values) JWA [RFC7518] supported by the authorization endpoint to encrypt the response.', + ), ), authorization_encryption_enc_values_supported: v.pipe( v.array(v.string()), v.description( - 'JSON array containing a list of the JWE [RFC7516] encryption algorithms (enc values) JWA [RFC7518] supported by the authorization endpoint to encrypt the response.' - ) + 'JSON array containing a list of the JWE [RFC7516] encryption algorithms (enc values) JWA [RFC7518] supported by the authorization endpoint to encrypt the response.', + ), ), }); -export type JarmServerMetadataParams = v.InferInput< - typeof vJarmServerMetadataParams ->; +export type JarmServerMetadata = v.InferInput; diff --git a/packages/jarm/lib/utils.ts b/packages/jarm/lib/utils.ts index 0d6c80b7..067b0f19 100644 --- a/packages/jarm/lib/utils.ts +++ b/packages/jarm/lib/utils.ts @@ -25,3 +25,18 @@ export function appendFragmentParams(input: { url: URL; fragments: Record { + supported: T[]; + actual: T; + error: Error; + required: boolean; +} + +export function assertValueSupported(input: AssertValueSupported): T | undefined { + const { required, error, supported, actual } = input; + const intersection = supported.find((value) => value === actual); + + if (required && !intersection) throw error; + return intersection; +} diff --git a/packages/jarm/lib/v-response-type-registry.ts b/packages/jarm/lib/v-response-type-registry.ts index fee542b6..1f792379 100644 --- a/packages/jarm/lib/v-response-type-registry.ts +++ b/packages/jarm/lib/v-response-type-registry.ts @@ -3,19 +3,9 @@ import * as v from 'valibot'; export const oAuthResponseTypes = v.picklist(['code', 'token']); // NOTE: MAKE SURE THAT THE RESPONSE TYPES ARE SORTED CORRECTLY -export const oAuthMRTEPResponseTypes = v.picklist([ - 'none', - 'id_token', - 'code token', - 'code id_token', - 'id_token token', - 'code id_token token', -]); +export const oAuthMRTEPResponseTypes = v.picklist(['none', 'id_token', 'code token', 'code id_token', 'id_token token', 'code id_token token']); -export const openid4vpResponseTypes = v.picklist([ - 'vp_token', - 'id_token vp_token', -]); +export const openid4vpResponseTypes = v.picklist(['vp_token', 'id_token vp_token']); export const vTransformedResponseTypes = v.picklist([ ...openid4vpResponseTypes.options, @@ -25,8 +15,8 @@ export const vTransformedResponseTypes = v.picklist([ export const vResponseType = v.pipe( v.string(), - v.transform(val => val.split(' ').sort().join(' ')), - vTransformedResponseTypes + v.transform((val) => val.split(' ').sort().join(' ')), + vTransformedResponseTypes, ); export type ResponseType = v.InferInput; diff --git a/packages/siop-oid4vp/lib/op/OP.ts b/packages/siop-oid4vp/lib/op/OP.ts index 1244b411..ca73ec2e 100644 --- a/packages/siop-oid4vp/lib/op/OP.ts +++ b/packages/siop-oid4vp/lib/op/OP.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'events' -import { jarmAuthResponseSend } from '@sphereon/jarm' +import { jarmAuthResponseSend, JarmClientMetadata, jarmMetadataValidate, JarmServerMetadata } from '@sphereon/jarm' import { JwtIssuer, uuidv4 } from '@sphereon/oid4vc-common' import { IIssuerId } from '@sphereon/ssi-types' @@ -361,4 +361,8 @@ export class OP { get verifyRequestOptions(): Partial { return this._verifyRequestOptions } + + public static validateJarmMetadata(input: { client_metadata: JarmClientMetadata; server_metadata: Partial }) { + return jarmMetadataValidate(input) + } } diff --git a/packages/siop-oid4vp/lib/rp/RP.ts b/packages/siop-oid4vp/lib/rp/RP.ts index 911c1f75..dea48ec1 100644 --- a/packages/siop-oid4vp/lib/rp/RP.ts +++ b/packages/siop-oid4vp/lib/rp/RP.ts @@ -25,6 +25,7 @@ import { AuthorizationEvent, AuthorizationEvents, AuthorizationResponsePayload, + DecryptCompact, PassBy, RegisterEventListener, RequestObjectPayload, @@ -143,10 +144,7 @@ export class RP { static async processJarmAuthorizationResponse( response: string, opts: { - decryptCompact: (input: { - jwk: { kid: string } - jwe: string - }) => Promise<{ plaintext: string; protectedHeader: Record & { alg: string; enc: string } }> + decryptCompact: DecryptCompact getAuthRequestPayload: (input: JarmDirectPostJwtResponseParams | JarmAuthResponseParams) => Promise<{ authRequestParams: RequestObjectPayload }> }, ) { diff --git a/packages/siop-oid4vp/lib/types/JWT.types.ts b/packages/siop-oid4vp/lib/types/JWT.types.ts index eb92f1e4..d562aff2 100644 --- a/packages/siop-oid4vp/lib/types/JWT.types.ts +++ b/packages/siop-oid4vp/lib/types/JWT.types.ts @@ -72,3 +72,8 @@ export interface JWK { } // export declare type ECCurve = 'P-256' | 'secp256k1' | 'P-384' | 'P-521'; + +export type DecryptCompact = (input: { + jwk: { kid: string } + jwe: string +}) => Promise<{ plaintext: string; protectedHeader: Record & { alg: string; enc: string } }> diff --git a/packages/siop-oid4vp/lib/types/SIOP.types.ts b/packages/siop-oid4vp/lib/types/SIOP.types.ts index 45363623..b44484cf 100644 --- a/packages/siop-oid4vp/lib/types/SIOP.types.ts +++ b/packages/siop-oid4vp/lib/types/SIOP.types.ts @@ -1,5 +1,5 @@ // noinspection JSUnusedGlobalSymbols -import { JarmClientMetadataParams } from '@sphereon/jarm' +import { JarmClientMetadata } from '@sphereon/jarm' import { SigningAlgo } from '@sphereon/oid4vc-common' import { Format, PresentationDefinitionV1, PresentationDefinitionV2 } from '@sphereon/pex-models' import { @@ -376,7 +376,7 @@ export type DiscoveryMetadataPayload = DiscoveryMetadataPayloadVID1 | JWT_VCDisc export type DiscoveryMetadataOpts = (JWT_VCDiscoveryMetadataOpts | DiscoveryMetadataOptsVID1 | DiscoveryMetadataOptsVD11) & DiscoveryMetadataCommonOpts -export type ClientMetadataOpts = RPRegistrationMetadataOpts & ClientMetadataProperties & JarmClientMetadataParams & JwksMetadataParams +export type ClientMetadataOpts = RPRegistrationMetadataOpts & ClientMetadataProperties & JarmClientMetadata & JwksMetadataParams export type ResponseRegistrationOpts = DiscoveryMetadataOpts & ClientMetadataProperties @@ -712,3 +712,5 @@ export enum ContentType { FORM_URL_ENCODED = 'application/x-www-form-urlencoded', UTF_8 = 'UTF-8', } + +export { JarmClientMetadata }