From c97c16fb77be22bd9a210b65b19629f7ef94586a Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Thu, 22 Feb 2024 12:12:41 -0600 Subject: [PATCH] add DPoP keygen and thumbprint (#186) --- src/dpop.js | 39 +++++++++++++++++++++++++++++++++++++++ src/dpop.test.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/dpop.js b/src/dpop.js index 2bd38c57..f2925b90 100644 --- a/src/dpop.js +++ b/src/dpop.js @@ -1,6 +1,39 @@ /* @flow */ /* eslint-disable promise/no-native, no-restricted-globals */ +type KeyPair = {| + privateKey: mixed, + publicKey: mixed, +|}; + +type GenerateKeyPair = () => Promise; + +// https://datatracker.ietf.org/doc/html/rfc7518#section-3.1 +const KEY_OPTIONS = { + create: { + name: "ECDSA", + namedCurve: "P-256", + }, + extractable: false, + usages: ["sign", "verify"], +}; + +let keyPair; +export const generateKeyPair: GenerateKeyPair = async () => { + if (!keyPair) { + const { create, extractable, usages } = KEY_OPTIONS; + const { publicKey, privateKey } = await window.crypto.subtle.generateKey( + create, + extractable, + usages + ); + + keyPair = keyPair || { publicKey, privateKey }; + } + + return keyPair; +}; + export const stringToBytes = (string: string): Uint8Array => { return new Uint8Array([...string].map((c) => c.charCodeAt(0))); }; @@ -28,4 +61,10 @@ export const sha256 = async (string: string): Promise => { return base64encodeUrlSafe(binaryString); }; +export const jsonWebKeyThumbprint = async (jwk: Object): Promise => { + // https://datatracker.ietf.org/doc/html/rfc7638#section-3.2 + const { crv, e, kty, n, x, y } = jwk; + return await sha256(JSON.stringify({ crv, e, kty, n, x, y })); +}; + /* eslint-enable promise/no-native, no-restricted-globals */ diff --git a/src/dpop.test.js b/src/dpop.test.js index e5833ae0..5325eaca 100644 --- a/src/dpop.test.js +++ b/src/dpop.test.js @@ -6,6 +6,8 @@ import { base64decodeUrlSafe, base64encodeUrlSafe, bytesToString, + generateKeyPair, + jsonWebKeyThumbprint, sha256, stringToBytes, } from "./dpop"; @@ -44,4 +46,30 @@ describe("DPoP", () => { expect.assertions(1); }); }); + describe("key pair generation", () => { + it("memoizes the key pair", async () => { + const { publicKey: publicKey1 } = await generateKeyPair(); + const jwk1 = await window.crypto.subtle.exportKey("jwk", publicKey1); + const { publicKey: publicKey2 } = await generateKeyPair(); + const jwk2 = await window.crypto.subtle.exportKey("jwk", publicKey2); + expect(jwk1.x).toBeTruthy(); + expect(jwk1).toStrictEqual(jwk2); + }); + }); + describe("JSON Web Key Thumbprint", () => { + it("generates a correct thumbprint", async () => { + // testing a known JSON Web Key and its thumbprint from: + // https://datatracker.ietf.org/doc/html/rfc7638#section-3.1 + const key = { + kty: "RSA", + n: "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", + e: "AQAB", + alg: "RS256", + kid: "2011-04-29", + }; + expect(await jsonWebKeyThumbprint(key)).toBe( + "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs" + ); + }); + }); });