From 56a291c2a59c2966fdf428d7cf7e2e69389fd38b Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Wed, 31 Jul 2024 13:04:51 +0200 Subject: [PATCH] fix: jwk thumprint using crypto.subtle Signed-off-by: Timo Glastra --- packages/client/lib/AuthorizationCodeClient.ts | 3 +-- packages/common/lib/dpop/DPoP.ts | 7 ++++--- packages/common/lib/hasher.ts | 17 +++++++++++++++++ packages/common/lib/index.ts | 1 + packages/common/lib/jwt/JwkThumbprint.ts | 9 ++------- .../oid4vci-common/lib/functions/RandomUtils.ts | 4 ++-- packages/oid4vci-common/package.json | 2 -- packages/siop-oid4vp/lib/helpers/State.ts | 5 ++--- packages/siop-oid4vp/lib/op/Opts.ts | 4 +++- packages/siop-oid4vp/lib/rp/Opts.ts | 4 +++- packages/siop-oid4vp/package.json | 2 -- pnpm-lock.yaml | 12 ------------ 12 files changed, 35 insertions(+), 35 deletions(-) create mode 100644 packages/common/lib/hasher.ts diff --git a/packages/client/lib/AuthorizationCodeClient.ts b/packages/client/lib/AuthorizationCodeClient.ts index f3b9d129..80817660 100644 --- a/packages/client/lib/AuthorizationCodeClient.ts +++ b/packages/client/lib/AuthorizationCodeClient.ts @@ -114,7 +114,7 @@ export const createAuthorizationRequestUrl = async ({ const client_id = clientId ?? authorizationRequest.clientId; // Authorization server metadata takes precedence - const authorizationMetadata = endpointMetadata.authorizationServerMetadata ?? endpointMetadata.credentialIssuerMetadata + const authorizationMetadata = endpointMetadata.authorizationServerMetadata ?? endpointMetadata.credentialIssuerMetadata; let { authorizationDetails } = authorizationRequest; const parMode = authorizationMetadata?.require_pushed_authorization_requests @@ -182,7 +182,6 @@ export const createAuthorizationRequestUrl = async ({ } const parEndpoint = authorizationMetadata?.pushed_authorization_request_endpoint; - let queryObj: Record | PushedAuthorizationResponse = { response_type: ResponseType.AUTH_CODE, ...(!pkce.disabled && { diff --git a/packages/common/lib/dpop/DPoP.ts b/packages/common/lib/dpop/DPoP.ts index 91668824..a27fbc5c 100644 --- a/packages/common/lib/dpop/DPoP.ts +++ b/packages/common/lib/dpop/DPoP.ts @@ -1,8 +1,9 @@ import { jwtDecode } from 'jwt-decode'; -import SHA from 'sha.js'; import * as u8a from 'uint8arrays'; import { v4 as uuidv4 } from 'uuid'; +import { defaultHasher } from '../hasher'; + import { calculateJwkThumbprint, CreateJwtCallback, @@ -68,7 +69,7 @@ export async function createDPoP(options: CreateDPoPOpts): Promise { throw new Error('expected access token without scheme'); } - const ath = jwtPayloadProps.accessToken ? u8a.toString(SHA('sha256').update(jwtPayloadProps.accessToken).digest(), 'base64url') : undefined; + const ath = jwtPayloadProps.accessToken ? u8a.toString(defaultHasher(jwtPayloadProps.accessToken, 'sha256'), 'base64url') : undefined; return createJwtCallback( { method: 'jwk', type: 'dpop', alg: jwtIssuer.alg, jwk: jwtIssuer.jwk, dPoPSigningAlgValuesSupported }, { @@ -195,7 +196,7 @@ export async function verifyDPoP( } const accessToken = authorizationHeader.replace('DPoP ', ''); - const expectedAth = u8a.toString(SHA('sha256').update(accessToken).digest(), 'base64url'); + const expectedAth = u8a.toString(defaultHasher(accessToken, 'sha256'), 'base64url'); if (dPoPPayload.ath !== expectedAth) { throw new Error('invalid_dpop_proof. Invalid ath claim'); } diff --git a/packages/common/lib/hasher.ts b/packages/common/lib/hasher.ts new file mode 100644 index 00000000..4a7e3d61 --- /dev/null +++ b/packages/common/lib/hasher.ts @@ -0,0 +1,17 @@ +import { Hasher } from '@sphereon/ssi-types'; +import sha from 'sha.js'; + +const supportedAlgorithms = ['sha256', 'sha384', 'sha512'] as const; +type SupportedAlgorithms = (typeof supportedAlgorithms)[number]; + +export const defaultHasher: Hasher = (data, algorithm) => { + if (!supportedAlgorithms.includes(algorithm as SupportedAlgorithms)) { + throw new Error(`Unsupported hashing algorithm ${algorithm}`); + } + + return new Uint8Array( + sha(algorithm as SupportedAlgorithms) + .update(data) + .digest(), + ); +}; diff --git a/packages/common/lib/index.ts b/packages/common/lib/index.ts index 48b9a82f..a5a2147c 100644 --- a/packages/common/lib/index.ts +++ b/packages/common/lib/index.ts @@ -7,3 +7,4 @@ export * from './jwt'; export * from './dpop'; export { v4 as uuidv4 } from 'uuid'; +export { defaultHasher } from './hasher'; diff --git a/packages/common/lib/jwt/JwkThumbprint.ts b/packages/common/lib/jwt/JwkThumbprint.ts index c1bbc9ef..639a2a2e 100644 --- a/packages/common/lib/jwt/JwkThumbprint.ts +++ b/packages/common/lib/jwt/JwkThumbprint.ts @@ -1,5 +1,6 @@ import * as u8a from 'uint8arrays'; +import { defaultHasher } from '../hasher'; import { DigestAlgorithm } from '../types'; import { JWK } from '.'; @@ -10,11 +11,6 @@ const check = (value: unknown, description: string) => { } }; -const digest = async (algorithm: DigestAlgorithm, data: Uint8Array) => { - const subtleDigest = `SHA-${algorithm.slice(-3)}`; - return new Uint8Array(await crypto.subtle.digest(subtleDigest, data)); -}; - export async function calculateJwkThumbprint(jwk: JWK, digestAlgorithm?: DigestAlgorithm): Promise { if (!jwk || typeof jwk !== 'object') { throw new TypeError('JWK must be an object'); @@ -48,8 +44,7 @@ export async function calculateJwkThumbprint(jwk: JWK, digestAlgorithm?: DigestA default: throw Error('"kty" (Key Type) Parameter missing or unsupported'); } - const data = u8a.fromString(JSON.stringify(components), 'utf-8'); - return u8a.toString(await digest(algorithm, data), 'base64url'); + return u8a.toString(defaultHasher(algorithm, JSON.stringify(components)), 'base64url'); } export async function getDigestAlgorithmFromJwkThumbprintUri(uri: string): Promise { diff --git a/packages/oid4vci-common/lib/functions/RandomUtils.ts b/packages/oid4vci-common/lib/functions/RandomUtils.ts index 20214445..d41d29f6 100644 --- a/packages/oid4vci-common/lib/functions/RandomUtils.ts +++ b/packages/oid4vci-common/lib/functions/RandomUtils.ts @@ -1,4 +1,4 @@ -import SHA from 'sha.js'; +import { defaultHasher } from '@sphereon/oid4vc-common'; import * as u8a from 'uint8arrays'; import { SupportedEncodings } from 'uint8arrays/to-string'; @@ -26,7 +26,7 @@ export const createCodeChallenge = (codeVerifier: string, codeChallengeMethod?: if (codeChallengeMethod === CodeChallengeMethod.plain) { return codeVerifier; } else if (!codeChallengeMethod || codeChallengeMethod === CodeChallengeMethod.S256) { - return u8a.toString(SHA('sha256').update(codeVerifier).digest(), 'base64url'); + return u8a.toString(defaultHasher(codeVerifier, 'sha256'), 'base64url'); } else { // Just a precaution if a new method would be introduced throw Error(`code challenge method ${codeChallengeMethod} not implemented`); diff --git a/packages/oid4vci-common/package.json b/packages/oid4vci-common/package.json index 3cb1bc57..8b7a3564 100644 --- a/packages/oid4vci-common/package.json +++ b/packages/oid4vci-common/package.json @@ -14,13 +14,11 @@ "@sphereon/ssi-types": "0.28.0", "cross-fetch": "^3.1.8", "jwt-decode": "^4.0.0", - "sha.js": "^2.4.11", "uint8arrays": "3.1.1", "uuid": "^9.0.0" }, "devDependencies": { "@types/jest": "^29.5.12", - "@types/sha.js": "^2.4.4", "@types/uuid": "^9.0.1", "typescript": "5.4.5" }, diff --git a/packages/siop-oid4vp/lib/helpers/State.ts b/packages/siop-oid4vp/lib/helpers/State.ts index eca202b7..5ed17d1f 100644 --- a/packages/siop-oid4vp/lib/helpers/State.ts +++ b/packages/siop-oid4vp/lib/helpers/State.ts @@ -1,5 +1,4 @@ -import { uuidv4 } from '@sphereon/oid4vc-common' -import SHA from 'sha.js' +import { defaultHasher, uuidv4 } from '@sphereon/oid4vc-common' import { base64urlEncodeBuffer } from './Encodings' @@ -8,7 +7,7 @@ export function getNonce(state: string, nonce?: string) { } export function toNonce(input: string): string { - const buff = SHA('sha256').update(input).digest() + const buff = defaultHasher(input, 'sha256') return base64urlEncodeBuffer(buff) } diff --git a/packages/siop-oid4vp/lib/op/Opts.ts b/packages/siop-oid4vp/lib/op/Opts.ts index 7ea19624..3536168e 100644 --- a/packages/siop-oid4vp/lib/op/Opts.ts +++ b/packages/siop-oid4vp/lib/op/Opts.ts @@ -1,3 +1,5 @@ +import { defaultHasher } from '@sphereon/oid4vc-common' + import { VerifyAuthorizationRequestOpts } from '../authorization-request' import { AuthorizationResponseOpts } from '../authorization-response' import { LanguageTagUtils } from '../helpers' @@ -63,7 +65,7 @@ export const createVerifyRequestOptsFromBuilderOrExistingOpts = (opts: { return opts.builder ? { verifyJwtCallback: opts.builder.verifyJwtCallback, - hasher: opts.builder.hasher, + hasher: opts.builder.hasher ?? defaultHasher, verification: {}, supportedVersions: opts.builder.supportedVersions, correlationId: undefined, diff --git a/packages/siop-oid4vp/lib/rp/Opts.ts b/packages/siop-oid4vp/lib/rp/Opts.ts index 0808fc77..952bcddd 100644 --- a/packages/siop-oid4vp/lib/rp/Opts.ts +++ b/packages/siop-oid4vp/lib/rp/Opts.ts @@ -1,3 +1,5 @@ +import { defaultHasher } from '@sphereon/oid4vc-common' + import { CreateAuthorizationRequestOpts, PropertyTarget, PropertyTargets, RequestPropertyWithTargets } from '../authorization-request' import { VerifyAuthorizationResponseOpts } from '../authorization-response' // import { CreateAuthorizationRequestOptsSchema } from '../schemas'; @@ -49,7 +51,7 @@ export const createRequestOptsFromBuilderOrExistingOpts = (opts: { builder?: RPB export const createVerifyResponseOptsFromBuilderOrExistingOpts = (opts: { builder?: RPBuilder; verifyOpts?: VerifyAuthorizationResponseOpts }) => { return opts.builder ? { - hasher: opts.builder.hasher, + hasher: opts.builder.hasher ?? defaultHasher, verifyJwtCallback: opts.builder.verifyJwtCallback, verification: { presentationVerificationCallback: opts.builder.presentationVerificationCallback, diff --git a/packages/siop-oid4vp/package.json b/packages/siop-oid4vp/package.json index 5a9fd207..61662b35 100644 --- a/packages/siop-oid4vp/package.json +++ b/packages/siop-oid4vp/package.json @@ -26,7 +26,6 @@ "language-tags": "^1.0.9", "multiformats": "^12.1.3", "qs": "^6.11.2", - "sha.js": "^2.4.11", "uint8arrays": "^3.1.1" }, "devDependencies": { @@ -50,7 +49,6 @@ "@types/jwt-decode": "^3.1.0", "@types/language-tags": "^1.0.4", "@types/qs": "^6.9.11", - "@types/sha.js": "^2.4.4", "@typescript-eslint/eslint-plugin": "^5.52.0", "@typescript-eslint/parser": "^5.52.0", "ajv": "^8.12.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f244f07..9761825e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -407,9 +407,6 @@ importers: jwt-decode: specifier: ^4.0.0 version: 4.0.0 - sha.js: - specifier: ^2.4.11 - version: 2.4.11 uint8arrays: specifier: 3.1.1 version: 3.1.1 @@ -420,9 +417,6 @@ importers: '@types/jest': specifier: ^29.5.12 version: 29.5.12 - '@types/sha.js': - specifier: ^2.4.4 - version: 2.4.4 '@types/uuid': specifier: ^9.0.1 version: 9.0.8 @@ -471,9 +465,6 @@ importers: qs: specifier: ^6.11.2 version: 6.12.3 - sha.js: - specifier: ^2.4.11 - version: 2.4.11 uint8arrays: specifier: ^3.1.1 version: 3.1.1 @@ -538,9 +529,6 @@ importers: '@types/qs': specifier: ^6.9.11 version: 6.9.15 - '@types/sha.js': - specifier: ^2.4.4 - version: 2.4.4 '@typescript-eslint/eslint-plugin': specifier: ^5.52.0 version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)