Skip to content

Commit

Permalink
feat: add support for RFC 9278 - JWK Thumbprint URI
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Aug 17, 2022
1 parent fe5d093 commit d06ce65
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 26 deletions.
7 changes: 5 additions & 2 deletions README.md
Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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/
Expand Down
5 changes: 3 additions & 2 deletions docs/README.md
Expand Up @@ -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)
Expand Down
16 changes: 9 additions & 7 deletions docs/functions/jwk_thumbprint.calculateJwkThumbprint.md
Expand Up @@ -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

Expand Down
32 changes: 32 additions & 0 deletions 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`\>
1 change: 1 addition & 0 deletions docs/modules/jwk_thumbprint.md
Expand Up @@ -7,3 +7,4 @@
### Functions

- [calculateJwkThumbprint](../functions/jwk_thumbprint.calculateJwkThumbprint.md)
- [calculateJwkThumbprintUri](../functions/jwk_thumbprint.calculateJwkThumbprintUri.md)
2 changes: 1 addition & 1 deletion src/index.ts
Expand Up @@ -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'
Expand Down
44 changes: 38 additions & 6 deletions src/jwk/thumbprint.ts
Expand Up @@ -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<string> {
if (!isObject(jwk)) {
throw new TypeError('JWK must be an object')
}

digestAlgorithm ??= 'sha256'

if (
digestAlgorithm !== 'sha256' &&
digestAlgorithm !== 'sha384' &&
Expand Down Expand Up @@ -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<string> {
digestAlgorithm ??= 'sha256'
const thumbprint = await calculateJwkThumbprint(jwk, digestAlgorithm)
return `urn:ietf:params:oauth:jwk-thumbprint:sha-${digestAlgorithm.slice(-3)}:${thumbprint}`
}
43 changes: 35 additions & 8 deletions 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',
)
})

Expand Down

0 comments on commit d06ce65

Please sign in to comment.