diff --git a/README.md b/README.md index 8d2ec62e6c..eb86f51c65 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ The following specifications are implemented by `jose` - JSON Web Algorithms (JWA) - [RFC7518][spec-jwa] - JSON Web Token (JWT) - [RFC7519][spec-jwt] - JSON Web Key Thumbprint - [RFC7638][spec-thumbprint] +- JSON Web Key Thumbprint URI - [RFC9278][spec-thumbprint-uri] - JWS Unencoded Payload Option - [RFC7797][spec-b64] - CFRG Elliptic Curve ECDH and Signatures - [RFC8037][spec-okp] - secp256k1 EC Key curve support - [JOSE Registrations for WebAuthn Algorithms][spec-secp256k1] @@ -56,8 +57,9 @@ import * as jose from 'https://deno.land/x/jose/index.ts' - Signing - [Compact](docs/classes/jws_compact_sign.CompactSign.md#readme), [Flattened](docs/classes/jws_flattened_sign.FlattenedSign.md#readme), [General](docs/classes/jws_general_sign.GeneralSign.md#readme) - Verification - [Compact](docs/functions/jws_compact_verify.compactVerify.md#readme), [Flattened](docs/functions/jws_flattened_verify.flattenedVerify.md#readme), [General](docs/functions/jws_general_verify.generalVerify.md#readme) - JSON Web Key (JWK) - - [Thumbprints](docs/functions/jwk_thumbprint.calculateJwkThumbprint.md#readme) - - [EmbeddedJWK](docs/functions/jwk_embedded.EmbeddedJWK.md#readme) + - [Calculating JWK Thumbprint](docs/functions/jwk_thumbprint.calculateJwkThumbprint.md#readme) + - [Calculating JWK Thumbprint URI](docs/functions/jwk_thumbprint.calculateJwkThumbprintUri.md#readme) + - [Verification using a JWK Embedded in a JWS Header](docs/functions/jwk_embedded.EmbeddedJWK.md#readme) - JSON Web Key Set (JWKS) - [Verify using a local JWKSet](docs/functions/jwks_local.createLocalJWKSet.md#readme) - [Verify using a remote JWKSet](docs/functions/jwks_remote.createRemoteJWKSet.md#readme) @@ -142,6 +144,7 @@ install size should not be a cause for concern. [spec-okp]: https://www.rfc-editor.org/rfc/rfc8037 [spec-secp256k1]: https://www.rfc-editor.org/rfc/rfc8812 [spec-thumbprint]: https://www.rfc-editor.org/rfc/rfc7638 +[spec-thumbprint-uri]: https://www.rfc-editor.org/rfc/rfc9278 [support-sponsor]: https://github.com/sponsors/panva [conditional-exports]: https://nodejs.org/api/packages.html#packages_conditional_exports [webcrypto]: https://www.w3.org/TR/WebCryptoAPI/ diff --git a/docs/README.md b/docs/README.md index 9b17fa981f..6268a31d3f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,8 +39,9 @@ import * as jose from 'https://deno.land/x/jose/index.ts' - Signing - [Compact](classes/jws_compact_sign.CompactSign.md#readme), [Flattened](classes/jws_flattened_sign.FlattenedSign.md#readme), [General](classes/jws_general_sign.GeneralSign.md#readme) - Verification - [Compact](functions/jws_compact_verify.compactVerify.md#readme), [Flattened](functions/jws_flattened_verify.flattenedVerify.md#readme), [General](functions/jws_general_verify.generalVerify.md#readme) - JSON Web Key (JWK) - - [Thumbprints](functions/jwk_thumbprint.calculateJwkThumbprint.md#readme) - - [EmbeddedJWK](functions/jwk_embedded.EmbeddedJWK.md#readme) + - [Calculating JWK Thumbprint](functions/jwk_thumbprint.calculateJwkThumbprint.md#readme) + - [Calculating JWK Thumbprint URI](functions/jwk_thumbprint.calculateJwkThumbprintUri.md#readme) + - [Verification using a JWK Embedded in a JWS Header](functions/jwk_embedded.EmbeddedJWK.md#readme) - JSON Web Key Set (JWKS) - [Verify using a local JWKSet](functions/jwks_local.createLocalJWKSet.md#readme) - [Verify using a remote JWKSet](functions/jwks_remote.createRemoteJWKSet.md#readme) diff --git a/docs/functions/jwk_thumbprint.calculateJwkThumbprint.md b/docs/functions/jwk_thumbprint.calculateJwkThumbprint.md index 050a7aac08..05458af882 100644 --- a/docs/functions/jwk_thumbprint.calculateJwkThumbprint.md +++ b/docs/functions/jwk_thumbprint.calculateJwkThumbprint.md @@ -11,20 +11,22 @@ Calculates a base64url-encoded JSON Web Key (JWK) Thumbprint as per ```js const thumbprint = await jose.calculateJwkThumbprint({ - kty: 'RSA', - e: 'AQAB', - n: '12oBZRhCiZFJLcPg59LkZZ9mdhSMTKAQZYq32k_ti5SBB6jerkh-WzOMAO664r_qyLkqHUSp3u5SbXtseZEpN3XPWGKSxjsy-1JyEFTdLSYe6f9gfrmxkUF_7DTpq0gn6rntP05g2-wFW50YO7mosfdslfrTJYWHFhJALabAeYirYD7-9kqq9ebfFMF4sRRELbv9oi36As6Q9B3Qb5_C1rAzqfao_PCsf9EPsTZsVVVkA5qoIAr47lo1ipfiBPxUCCNSdvkmDTYgvvRm6ZoMjFbvOtgyts55fXKdMWv7I9HMD5HwE9uW839PWA514qhbcIsXEYSFMPMV6fnlsiZvQQ', + kty: 'EC', + crv: 'P-256', + x: 'jJ6Flys3zK9jUhnOHf6G49Dyp5hah6CNP84-gY-n9eo', + y: 'nhI6iD5eFXgBTLt_1p3aip-5VbZeMhxeFSpjfEAf7Ww', }) console.log(thumbprint) +// 'w9eYdC6_s_tLQ8lH6PUpc0mddazaqtPgeC2IgWDiqY8' ``` #### Parameters -| Name | Type | Default value | Description | -| :------ | :------ | :------ | :------ | -| `jwk` | [`JWK`](../interfaces/types.JWK.md) | `undefined` | JSON Web Key. | -| `digestAlgorithm` | ``"sha256"`` \| ``"sha384"`` \| ``"sha512"`` | `'sha256'` | Digest Algorithm to use for calculating the thumbprint. Default is sha256. Accepted is "sha256", "sha384", "sha512". | +| Name | Type | Description | +| :------ | :------ | :------ | +| `jwk` | [`JWK`](../interfaces/types.JWK.md) | JSON Web Key. | +| `digestAlgorithm?` | ``"sha256"`` \| ``"sha384"`` \| ``"sha512"`` | Digest Algorithm to use for calculating the thumbprint. Default is "sha256". | #### Returns diff --git a/docs/functions/jwk_thumbprint.calculateJwkThumbprintUri.md b/docs/functions/jwk_thumbprint.calculateJwkThumbprintUri.md new file mode 100644 index 0000000000..27d4f3c662 --- /dev/null +++ b/docs/functions/jwk_thumbprint.calculateJwkThumbprintUri.md @@ -0,0 +1,32 @@ +# Function: calculateJwkThumbprintUri + +[💗 Help the project](https://github.com/sponsors/panva) + +▸ **calculateJwkThumbprintUri**(`jwk`, `digestAlgorithm?`): `Promise`<`string`\> + +Calculates a JSON Web Key (JWK) Thumbprint URI as per [RFC9278](https://www.rfc-editor.org/rfc/rfc9278). + +**`example`** Usage + +```js +const thumbprintUri = await jose.calculateJwkThumbprintUri({ + kty: 'EC', + crv: 'P-256', + x: 'jJ6Flys3zK9jUhnOHf6G49Dyp5hah6CNP84-gY-n9eo', + y: 'nhI6iD5eFXgBTLt_1p3aip-5VbZeMhxeFSpjfEAf7Ww', +}) + +console.log(thumbprint) +// 'urn:ietf:params:oauth:jwk-thumbprint:sha-256:w9eYdC6_s_tLQ8lH6PUpc0mddazaqtPgeC2IgWDiqY8' +``` + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `jwk` | [`JWK`](../interfaces/types.JWK.md) | JSON Web Key. | +| `digestAlgorithm?` | ``"sha256"`` \| ``"sha384"`` \| ``"sha512"`` | Digest Algorithm to use for calculating the thumbprint. Default is "sha256". | + +#### Returns + +`Promise`<`string`\> diff --git a/docs/modules/jwk_thumbprint.md b/docs/modules/jwk_thumbprint.md index d10144cb54..5472add1ed 100644 --- a/docs/modules/jwk_thumbprint.md +++ b/docs/modules/jwk_thumbprint.md @@ -7,3 +7,4 @@ ### Functions - [calculateJwkThumbprint](../functions/jwk_thumbprint.calculateJwkThumbprint.md) +- [calculateJwkThumbprintUri](../functions/jwk_thumbprint.calculateJwkThumbprintUri.md) diff --git a/src/index.ts b/src/index.ts index 089d03b9f5..cc72bcfbba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,7 +31,7 @@ export type { Signature } from './jws/general/sign.js' export { SignJWT } from './jwt/sign.js' export { EncryptJWT } from './jwt/encrypt.js' -export { calculateJwkThumbprint } from './jwk/thumbprint.js' +export { calculateJwkThumbprint, calculateJwkThumbprintUri } from './jwk/thumbprint.js' export { EmbeddedJWK } from './jwk/embedded.js' export { createLocalJWKSet } from './jwks/local.js' diff --git a/src/jwk/thumbprint.ts b/src/jwk/thumbprint.ts index 2d8c692f1a..f3e6e3d7dd 100644 --- a/src/jwk/thumbprint.ts +++ b/src/jwk/thumbprint.ts @@ -20,26 +20,29 @@ const check = (value: unknown, description: string) => { * * ```js * const thumbprint = await jose.calculateJwkThumbprint({ - * kty: 'RSA', - * e: 'AQAB', - * n: '12oBZRhCiZFJLcPg59LkZZ9mdhSMTKAQZYq32k_ti5SBB6jerkh-WzOMAO664r_qyLkqHUSp3u5SbXtseZEpN3XPWGKSxjsy-1JyEFTdLSYe6f9gfrmxkUF_7DTpq0gn6rntP05g2-wFW50YO7mosfdslfrTJYWHFhJALabAeYirYD7-9kqq9ebfFMF4sRRELbv9oi36As6Q9B3Qb5_C1rAzqfao_PCsf9EPsTZsVVVkA5qoIAr47lo1ipfiBPxUCCNSdvkmDTYgvvRm6ZoMjFbvOtgyts55fXKdMWv7I9HMD5HwE9uW839PWA514qhbcIsXEYSFMPMV6fnlsiZvQQ', + * kty: 'EC', + * crv: 'P-256', + * x: 'jJ6Flys3zK9jUhnOHf6G49Dyp5hah6CNP84-gY-n9eo', + * y: 'nhI6iD5eFXgBTLt_1p3aip-5VbZeMhxeFSpjfEAf7Ww', * }) * * console.log(thumbprint) + * // 'w9eYdC6_s_tLQ8lH6PUpc0mddazaqtPgeC2IgWDiqY8' * ``` * * @param jwk JSON Web Key. - * @param digestAlgorithm Digest Algorithm to use for calculating the thumbprint. Default is sha256. - * Accepted is "sha256", "sha384", "sha512". + * @param digestAlgorithm Digest Algorithm to use for calculating the thumbprint. Default is "sha256". */ export async function calculateJwkThumbprint( jwk: JWK, - digestAlgorithm: 'sha256' | 'sha384' | 'sha512' = 'sha256', + digestAlgorithm?: 'sha256' | 'sha384' | 'sha512', ): Promise { if (!isObject(jwk)) { throw new TypeError('JWK must be an object') } + digestAlgorithm ??= 'sha256' + if ( digestAlgorithm !== 'sha256' && digestAlgorithm !== 'sha384' && @@ -77,3 +80,32 @@ export async function calculateJwkThumbprint( const data = encoder.encode(JSON.stringify(components)) return base64url(await digest(digestAlgorithm, data)) } + +/** + * Calculates a JSON Web Key (JWK) Thumbprint URI as per [RFC9278](https://www.rfc-editor.org/rfc/rfc9278). + * + * @example Usage + * + * ```js + * const thumbprintUri = await jose.calculateJwkThumbprintUri({ + * kty: 'EC', + * crv: 'P-256', + * x: 'jJ6Flys3zK9jUhnOHf6G49Dyp5hah6CNP84-gY-n9eo', + * y: 'nhI6iD5eFXgBTLt_1p3aip-5VbZeMhxeFSpjfEAf7Ww', + * }) + * + * console.log(thumbprint) + * // 'urn:ietf:params:oauth:jwk-thumbprint:sha-256:w9eYdC6_s_tLQ8lH6PUpc0mddazaqtPgeC2IgWDiqY8' + * ``` + * + * @param jwk JSON Web Key. + * @param digestAlgorithm Digest Algorithm to use for calculating the thumbprint. Default is "sha256". + */ +export async function calculateJwkThumbprintUri( + jwk: JWK, + digestAlgorithm?: 'sha256' | 'sha384' | 'sha512', +): Promise { + digestAlgorithm ??= 'sha256' + const thumbprint = await calculateJwkThumbprint(jwk, digestAlgorithm) + return `urn:ietf:params:oauth:jwk-thumbprint:sha-${digestAlgorithm.slice(-3)}:${thumbprint}` +} diff --git a/test/jwk/thumbprint.test.mjs b/test/jwk/thumbprint.test.mjs index 458b045e9a..d7a003d1a6 100644 --- a/test/jwk/thumbprint.test.mjs +++ b/test/jwk/thumbprint.test.mjs @@ -1,17 +1,44 @@ import test from 'ava' import { keyRoot } from '../dist.mjs' -const { calculateJwkThumbprint } = await import(keyRoot) +const { calculateJwkThumbprint, calculateJwkThumbprintUri } = await import(keyRoot) + +const jwk = { + kty: 'RSA', + n: '0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw', + e: 'AQAB', + alg: 'RS256', +} test('https://www.rfc-editor.org/rfc/rfc7638#section-3.1', async (t) => { + t.is(await calculateJwkThumbprint(jwk), 'NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs') + t.is(await calculateJwkThumbprint(jwk, 'sha256'), 'NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs') + t.is( + await calculateJwkThumbprint(jwk, 'sha384'), + 'R9_OfJjSjaw8Fuum86UzK5ixTdN9bo9BaqPSiseq89DWfmqCdpSgUHus-cxDUNc8', + ) + t.is( + await calculateJwkThumbprint(jwk, 'sha512'), + 'DpvEwocfn3FjeWWQjcJHzWrpKTIymKwgoL1xVgQcud48-qZDSRCr1zfWZQdHAJn_ciqXqPTSARyg-L-NyNGpVA', + ) +}) + +test('https://www.rfc-editor.org/rfc/rfc9278', async (t) => { + t.is( + await calculateJwkThumbprintUri(jwk), + 'urn:ietf:params:oauth:jwk-thumbprint:sha-256:NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs', + ) + t.is( + await calculateJwkThumbprintUri(jwk, 'sha256'), + 'urn:ietf:params:oauth:jwk-thumbprint:sha-256:NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs', + ) + t.is( + await calculateJwkThumbprintUri(jwk, 'sha384'), + 'urn:ietf:params:oauth:jwk-thumbprint:sha-384:R9_OfJjSjaw8Fuum86UzK5ixTdN9bo9BaqPSiseq89DWfmqCdpSgUHus-cxDUNc8', + ) t.is( - await calculateJwkThumbprint({ - kty: 'RSA', - n: '0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw', - e: 'AQAB', - alg: 'RS256', - }), - 'NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs', + await calculateJwkThumbprintUri(jwk, 'sha512'), + 'urn:ietf:params:oauth:jwk-thumbprint:sha-512:DpvEwocfn3FjeWWQjcJHzWrpKTIymKwgoL1xVgQcud48-qZDSRCr1zfWZQdHAJn_ciqXqPTSARyg-L-NyNGpVA', ) })