diff --git a/packages/siop-oid4vp/lib/helpers/jwtUtils.ts b/packages/siop-oid4vp/lib/helpers/jwtUtils.ts index eb8776de..bb5d87e6 100644 --- a/packages/siop-oid4vp/lib/helpers/jwtUtils.ts +++ b/packages/siop-oid4vp/lib/helpers/jwtUtils.ts @@ -4,7 +4,7 @@ import { JwtHeader, JwtPayload, SIOPErrors } from '../types' export type JwtType = 'id-token' | 'request-object' | 'verifier-attestation' -export type JwtProtectionMethod = 'did' | 'x5c' | 'jwk' | 'custom' +export type JwtProtectionMethod = 'did' | 'x5c' | 'jwk' | 'openid-federation' | 'custom' export function parseJWT(jwt: string) { const header = jwtDecode(jwt, { header: true }) diff --git a/packages/siop-oid4vp/lib/types/Errors.ts b/packages/siop-oid4vp/lib/types/Errors.ts index 5950b30e..f8f1a242 100644 --- a/packages/siop-oid4vp/lib/types/Errors.ts +++ b/packages/siop-oid4vp/lib/types/Errors.ts @@ -1,5 +1,6 @@ enum SIOPErrors { // todo: INVALID_REQUEST mapping onto response conforming to spec + INVALID_CLIENT_ID_MUST_MATCH_REDIRECT_URI = `Invalid request object payload. The redirect_uri must match the client_id with client_id_scheme 'redirect_uri'.`, INVALID_REQUEST = 'The request contained invalid or conflicting parameters', AUTH_REQUEST_EXPECTS_VP = 'authentication request expects a verifiable presentation in the response', AUTH_REQUEST_DOESNT_EXPECT_VP = "authentication request doesn't expect a verifiable presentation in the response", @@ -13,6 +14,7 @@ enum SIOPErrors { NO_RESPONSE = 'No response (payload) provided.', NO_PRESENTATION_SUBMISSION = 'The VP did not contain a presentation submission. Did you forget to call PresentationExchange.checkSubmissionFrom?', BAD_VERIFIER_ATTESTATION = 'Invalid verifier attestation. Bad JWT structure.', + BAD_VERIFIER_ATTESTATION_REDIRECT_URIS = `Invalid verifier attestation. redirect_uri cannot be found in the the attestation jwts's redirect_uris.`, CREDENTIAL_FORMATS_NOT_SUPPORTED = 'CREDENTIAL_FORMATS_NOT_SUPPORTED', CREDENTIALS_FORMATS_NOT_PROVIDED = 'Credentials format not provided by RP/OP', COULD_NOT_FIND_VCS_MATCHING_PD = 'Could not find VerifiableCredentials matching presentationDefinition object in the provided VC list', @@ -20,11 +22,12 @@ enum SIOPErrors { DID_METHODS_NOT_SUPORTED = 'DID_METHODS_NOT_SUPPORTED', ERROR_VERIFYING_SIGNATURE = 'Error verifying the DID Auth Token signature.', INVALID_JWT = 'Received an invalid JWT.', - INVALID_REQUEST_OBJECT_X509_SCHEME_JWT = `Request Object uses client_id_scheme 'x509_san_dns' | 'x509_san_uri', but now x5c header is present.`, - INVALID_REQUEST_OBJECT_DID_SCHEME_JWT = `Request Object uses client_id_scheme 'did', but now kid header is present.`, - MISSING_ATTESTATION_JWT = `Request Object uses client_id_scheme 'verifier_attestation', but now jwt header is present.`, - MISSING_ATTESTATION_JWT_TYP = `Request Object uses client_id_scheme 'verifier_attestation', but the jwt is not 'verifier-attestation+jwt'.`, + MISSING_X5C_HEADER_WITH_CLIENT_ID_SCHEME_X509 = `Missing x5c header with client_id_scheme 'x509_san_dns' | 'x509_san_uri'.`, + MISSING_KID_HEADER_WITH_CLIENT_ID_SCHEME_DID = `Missing kid header with client_id_scheme 'did'.`, + MISSING_ATTESTATION_JWT_WITH_CLIENT_ID_SCHEME_ATTESTATION = `Missing jwt header jwt with client_id_scheme 'verifier_attestation'.`, + MISSING_ATTESTATION_JWT_TYP = `Attestation JWT missing typ 'verifier-attestation+jwt'.`, INVALID_CLIENT_ID_SCHEME = 'Invalid client_id_scheme.', + INVALID_REQUEST_OBJECT_ENTITY_ID_SCHEME_CLIENT_ID = `Request Object uses client_id_scheme 'entity_id', but the client_id is not a string.`, EXPIRED = 'The token has expired', INVALID_AUDIENCE = 'Audience is invalid. Should be a string value.', NO_AUDIENCE = 'No audience found in JWT payload or not configured', diff --git a/packages/siop-oid4vp/lib/types/JwtVerifier.ts b/packages/siop-oid4vp/lib/types/JwtVerifier.ts index 9be3ddf4..d1ca0c53 100644 --- a/packages/siop-oid4vp/lib/types/JwtVerifier.ts +++ b/packages/siop-oid4vp/lib/types/JwtVerifier.ts @@ -32,6 +32,15 @@ interface X5cJwtVerifier extends JwtVerifierBase { issuer: string } +interface OpenIdFederationjwtVerifier extends JwtVerifierBase { + method: 'openid-federation' + + /** + * The OpenId federation Entity + */ + entityId: string +} + type JwkJwtVerifier = | (JwtVerifierBase & { method: 'jwk' @@ -52,83 +61,99 @@ interface CustomJwtVerifier extends JwtVerifierBase { method: 'custom' } -export type JwtVerifier = DidJwtVerifier | X5cJwtVerifier | CustomJwtVerifier | JwkJwtVerifier +export type JwtVerifier = DidJwtVerifier | X5cJwtVerifier | CustomJwtVerifier | JwkJwtVerifier | OpenIdFederationjwtVerifier -export const getJwtVerifierWithContext = async ( - jwt: { header: JwtHeader; payload: JwtPayload }, - options: { type: JwtType }, -): Promise => { - const type = options.type +export const getDidJwtVerifier = (jwt: { header: JwtHeader; payload: JwtPayload }, options: { type: JwtType }): DidJwtVerifier => { + const { type } = options + if (!jwt.header.kid) throw new Error(`${SIOPErrors.INVALID_JWT} Missing kid header.`) - if (jwt.header.kid?.startsWith('did:')) { - if (!jwt.header.kid.includes('#')) { - throw new Error(`${SIOPErrors.INVALID_JWT}. '${type}' contains an invalid kid header.`) - } - return { method: 'did', didUrl: jwt.header.kid, type } - } else if (jwt.header.x5c) { - if (!Array.isArray(jwt.header.x5c) || jwt.header.x5c.length === 0 || !jwt.header.x5c.every((cert) => typeof cert === 'string')) { - throw new Error(`${SIOPErrors.INVALID_JWT}. '${type}' contains an invalid x5c header.`) - } - return { method: 'x5c', x5c: jwt.header.x5c, issuer: jwt.payload.iss, type } - } else if (jwt.header.jwk) { - if (typeof jwt.header.jwk !== 'object') { - throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' contains an invalid jwk header.`) - } - if (type !== 'id-token') { - // Users need to check if the iss claim matches an entity they trust - // for type === 'verifier-attestation' - return { method: 'jwk', type, jwk: jwt.header.jwk } - } + if (!jwt.header.kid.includes('#')) { + throw new Error(`${SIOPErrors.INVALID_JWT}. '${type}' contains an invalid kid header.`) + } + return { method: 'did', didUrl: jwt.header.kid, type: type } +} - if (typeof jwt.payload.sub_jwk !== 'string') { - throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' is missing the sub_jwk claim.`) - } +export const getX5cVerifier = (jwt: { header: JwtHeader; payload: JwtPayload }, options: { type: JwtType }): X5cJwtVerifier => { + const { type } = options + if (!jwt.header.x5c) throw new Error(`${SIOPErrors.INVALID_JWT} Missing x5c header.`) - const jwkThumbPrintUri = jwt.payload.sub_jwk - const digestAlgorithm = await getDigestAlgorithmFromJwkThumbprintUri(jwkThumbPrintUri) - const selfComputedJwkThumbPrintUri = await calculateJwkThumbprintUri(jwt.header.jwk as JWK, digestAlgorithm) + if (!Array.isArray(jwt.header.x5c) || jwt.header.x5c.length === 0 || !jwt.header.x5c.every((cert) => typeof cert === 'string')) { + throw new Error(`${SIOPErrors.INVALID_JWT}. '${type}' contains an invalid x5c header.`) + } + return { method: 'x5c', x5c: jwt.header.x5c, issuer: jwt.payload.iss, type: type } +} - if (selfComputedJwkThumbPrintUri !== jwkThumbPrintUri) { - throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' contains an invalid sub_jwk claim.`) - } +export const getJwkVerifier = async (jwt: { header: JwtHeader; payload: JwtPayload }, options: { type: JwtType }): Promise => { + const { type } = options + if (!jwt.header.jwk) throw new Error(`${SIOPErrors.INVALID_JWT} Missing jwk header.`) + + if (typeof jwt.header.jwk !== 'object') { + throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' contains an invalid jwk header.`) + } + if (type !== 'id-token') { + // Users need to check if the iss claim matches an entity they trust + // for type === 'verifier-attestation' + return { method: 'jwk', type, jwk: jwt.header.jwk } + } - return { method: 'jwk', type, jwk: jwt.header.jwk, jwkThumbprint: jwt.payload.sub_jwk } + if (typeof jwt.payload.sub_jwk !== 'string') { + throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' missing sub_jwk claim.`) } - return { method: 'custom', type } + const jwkThumbPrintUri = jwt.payload.sub_jwk + const digestAlgorithm = await getDigestAlgorithmFromJwkThumbprintUri(jwkThumbPrintUri) + const selfComputedJwkThumbPrintUri = await calculateJwkThumbprintUri(jwt.header.jwk as JWK, digestAlgorithm) + + if (selfComputedJwkThumbPrintUri !== jwkThumbPrintUri) { + throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' contains an invalid sub_jwk claim.`) + } + + return { method: 'jwk', type, jwk: jwt.header.jwk, jwkThumbprint: jwt.payload.sub_jwk } +} + +export const getJwtVerifierWithContext = async ( + jwt: { header: JwtHeader; payload: JwtPayload }, + options: { type: JwtType }, +): Promise => { + const { header, payload } = jwt + + if (header.kid?.startsWith('did:')) return getDidJwtVerifier({ header, payload }, options) + else if (jwt.header.x5c) return getX5cVerifier({ header, payload }, options) + else if (jwt.header.jwk) return getJwkVerifier({ header, payload }, options) + + return { method: 'custom', type: options.type } } export type VerifyJwtCallback = (jwtVerifier: JwtVerifier, jwt: { header: JwtHeader; payload: JwtPayload; raw: string }) => Promise export const getRequestObjectJwtVerifier = async ( jwt: { header: JwtHeader; payload: RequestObjectPayload }, - options: { type: 'request-object'; raw: string }, + options: { raw: string }, ): Promise => { - const type = options.type + const type = 'request-object' const clientIdScheme = jwt.payload.client_id_scheme const clientId = jwt.payload.client_id - if (clientIdScheme === 'did') { - if (!jwt.header.kid) { - throw new Error(SIOPErrors.INVALID_REQUEST_OBJECT_DID_SCHEME_JWT) - } + if (!clientIdScheme) { return getJwtVerifierWithContext(jwt, { type }) + } + + if (clientIdScheme === 'did') { + return getDidJwtVerifier(jwt, { type }) } else if (clientIdScheme === 'pre-registered') { // All validations must be done manually // The Verifier metadata is obtained using [RFC7591] or through out-of-band mechanisms. return getJwtVerifierWithContext(jwt, { type }) } else if (clientIdScheme === 'x509_san_dns' || clientIdScheme === 'x509_san_uri') { - // Make sure that the jwt is x509 protected - if (!jwt.header.x5c) { - throw new Error(SIOPErrors.INVALID_REQUEST_OBJECT_X509_SCHEME_JWT) - } - return getJwtVerifierWithContext(jwt, { type }) + return getX5cVerifier(jwt, { type }) } else if (clientIdScheme === 'redirect_uri') { if (jwt.payload.redirect_uri && jwt.payload.redirect_uri !== clientId) { - throw new Error(`Invalid request object payload. The redirect_uri must match the client_id with client_id_scheme 'redirect_uri'.`) + throw new Error(SIOPErrors.INVALID_CLIENT_ID_MUST_MATCH_REDIRECT_URI) + } + if (options.raw.split('.').length > 2) { + throw new Error(`${SIOPErrors.INVALID_JWT} '${type}' JWT must not not be signed.`) } - if (options.raw.split('.').length > 2) throw new Error(`${SIOPErrors.INVALID_JWT} The '${type}' Jwt must not not be signed.`) return getJwtVerifierWithContext(jwt, { type }) } else if (clientIdScheme === 'verifier_attestation') { const verifierAttestationSubtype = 'verifier-attestation+jwt' @@ -163,7 +188,7 @@ export const getRequestObjectJwtVerifier = async ( !jwt.payload.redirect_uri || !attestationPayload.redirect_uris.includes(jwt.payload.redirect_uri) ) { - throw new Error(`${SIOPErrors.BAD_VERIFIER_ATTESTATION} request object redirect_uri in not included in the verifier attestation jwt.`) + throw new Error(SIOPErrors.BAD_VERIFIER_ATTESTATION_REDIRECT_URIS) } } @@ -171,11 +196,12 @@ export const getRequestObjectJwtVerifier = async ( // If the Wallet cannot establish trust, it MUST refuse the request. return { method: 'jwk', type, jwk: attestationPayload.cnf['jwk'] as JWK } } else if (clientIdScheme === 'entity_id') { - // TODO! - throw new Error('Not implemented yet') - } else if (clientIdScheme) { - throw new Error(SIOPErrors.INVALID_CLIENT_ID_SCHEME) + if (!clientId.startsWith('http')) { + throw new Error(SIOPErrors.INVALID_REQUEST_OBJECT_ENTITY_ID_SCHEME_CLIENT_ID) + } + + return { method: 'openid-federation', type, entityId: clientId } } - return getJwtVerifierWithContext(jwt, { type }) + throw new Error(SIOPErrors.INVALID_CLIENT_ID_SCHEME) }