Skip to content

Commit

Permalink
fix: jwk thumprint using crypto.subtle
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra committed Jul 31, 2024
1 parent 86d0423 commit 56a291c
Show file tree
Hide file tree
Showing 12 changed files with 35 additions and 35 deletions.
3 changes: 1 addition & 2 deletions packages/client/lib/AuthorizationCodeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -182,7 +182,6 @@ export const createAuthorizationRequestUrl = async ({
}
const parEndpoint = authorizationMetadata?.pushed_authorization_request_endpoint;


let queryObj: Record<string, any> | PushedAuthorizationResponse = {
response_type: ResponseType.AUTH_CODE,
...(!pkce.disabled && {
Expand Down
7 changes: 4 additions & 3 deletions packages/common/lib/dpop/DPoP.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -68,7 +69,7 @@ export async function createDPoP(options: CreateDPoPOpts): Promise<string> {
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 },
{
Expand Down Expand Up @@ -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');
}
Expand Down
17 changes: 17 additions & 0 deletions packages/common/lib/hasher.ts
Original file line number Diff line number Diff line change
@@ -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(),
);
};
1 change: 1 addition & 0 deletions packages/common/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './jwt';
export * from './dpop';

export { v4 as uuidv4 } from 'uuid';
export { defaultHasher } from './hasher';
9 changes: 2 additions & 7 deletions packages/common/lib/jwt/JwkThumbprint.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as u8a from 'uint8arrays';

import { defaultHasher } from '../hasher';
import { DigestAlgorithm } from '../types';

import { JWK } from '.';
Expand All @@ -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<string> {
if (!jwk || typeof jwk !== 'object') {
throw new TypeError('JWK must be an object');
Expand Down Expand Up @@ -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<DigestAlgorithm> {
Expand Down
4 changes: 2 additions & 2 deletions packages/oid4vci-common/lib/functions/RandomUtils.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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`);
Expand Down
2 changes: 0 additions & 2 deletions packages/oid4vci-common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
5 changes: 2 additions & 3 deletions packages/siop-oid4vp/lib/helpers/State.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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)
}

Expand Down
4 changes: 3 additions & 1 deletion packages/siop-oid4vp/lib/op/Opts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { defaultHasher } from '@sphereon/oid4vc-common'

import { VerifyAuthorizationRequestOpts } from '../authorization-request'
import { AuthorizationResponseOpts } from '../authorization-response'
import { LanguageTagUtils } from '../helpers'
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion packages/siop-oid4vp/lib/rp/Opts.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 0 additions & 2 deletions packages/siop-oid4vp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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",
Expand Down
12 changes: 0 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 56a291c

Please sign in to comment.