From f8cf5ff430cee74d5c620063e1d733be497cecaa Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 27 May 2024 16:42:28 +0200 Subject: [PATCH] chore(release): 1.4.1 --- CHANGELOG.md | 7 ++ build/index.d.ts | 92 +++++++++++++++++++++ build/index.js | 199 ++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 4 +- package.json | 2 +- 5 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 build/index.d.ts create mode 100644 build/index.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 383bc12..c8673d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.4.1](https://github.com/panva/dpop/compare/v1.4.0...v1.4.1) (2024-05-27) + + +### Fixes + +* `additional` may not be null ([#10](https://github.com/panva/dpop/issues/10)) ([be6b00b](https://github.com/panva/dpop/commit/be6b00bba8bcbbf53445eb92aa459fd96387d9b7)) + ## [1.4.0](https://github.com/panva/dpop/compare/v1.2.0...v1.4.0) (2023-09-08) diff --git a/build/index.d.ts b/build/index.d.ts new file mode 100644 index 0000000..7bd5716 --- /dev/null +++ b/build/index.d.ts @@ -0,0 +1,92 @@ +export declare type JsonObject = { + [Key in string]?: JsonValue; +}; +export declare type JsonArray = JsonValue[]; +export declare type JsonPrimitive = string | number | boolean | null; +export declare type JsonValue = JsonPrimitive | JsonObject | JsonArray; +/** + * Supported JWS `alg` Algorithm identifiers. + * + * @example PS256 CryptoKey algorithm + * ```ts + * interface Ps256Algorithm extends RsaHashedKeyAlgorithm { + * name: 'RSA-PSS' + * hash: { name: 'SHA-256' } + * } + * ``` + * + * @example CryptoKey algorithm for the `ES256` JWS Algorithm Identifier + * ```ts + * interface Es256Algorithm extends EcKeyAlgorithm { + * name: 'ECDSA' + * namedCurve: 'P-256' + * } + * ``` + * + * @example CryptoKey algorithm for the `RS256` JWS Algorithm Identifier + * ```ts + * interface Rs256Algorithm extends RsaHashedKeyAlgorithm { + * name: 'RSASSA-PKCS1-v1_5' + * hash: { name: 'SHA-256' } + * } + * ``` + * + * @example CryptoKey algorithm for the `EdDSA` JWS Algorithm Identifier (Experimental) + * + * Runtime support for this algorithm is very limited, it depends on the [Secure Curves in the Web + * Cryptography API](https://wicg.github.io/webcrypto-secure-curves/) proposal which is yet to be + * widely adopted. If the proposal changes this implementation will follow up with a minor release. + * + * ```ts + * interface EdDSAAlgorithm extends KeyAlgorithm { + * name: 'Ed25519' + * } + * ``` + */ +export declare type JWSAlgorithm = 'PS256' | 'ES256' | 'RS256' | 'EdDSA'; +export interface KeyPair extends CryptoKeyPair { + /** + * Private + * {@link https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey CryptoKey} + * instance to sign the DPoP Proof JWT with. + * + * Its algorithm must be compatible with a supported + * {@link JWSAlgorithm JWS `alg` Algorithm}. + */ + privateKey: CryptoKey; + /** + * The public key corresponding to {@link DPoPOptions.privateKey} + */ + publicKey: CryptoKey; +} +/** + * Generates a unique DPoP Proof JWT. + * + * @param keypair + * @param htu The HTTP URI (without query and fragment parts) of the request + * @param htm The HTTP method of the request + * @param nonce Server-provided nonce. + * @param accessToken Associated access token's value. + * @param additional Any additional claims. + */ +export default function DPoP(keypair: KeyPair, htu: string, htm: string, nonce?: string, accessToken?: string, additional?: Record): Promise; +export interface GenerateKeyPairOptions { + /** + * Indicates whether or not the private key may be exported. + * Default is `false`. + */ + extractable?: boolean; + /** + * (RSA algorithms only) The length, in bits, of the RSA modulus. + * Default is `2048`. + */ + modulusLength?: number; +} +/** + * Generates a + * {@link https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair CryptoKeyPair} + * for a given JWS `alg` Algorithm identifier. + * + * @param alg Supported JWS `alg` Algorithm identifier. + */ +export declare function generateKeyPair(alg: JWSAlgorithm, options?: GenerateKeyPairOptions): Promise; diff --git a/build/index.js b/build/index.js new file mode 100644 index 0000000..add4836 --- /dev/null +++ b/build/index.js @@ -0,0 +1,199 @@ +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +function buf(input) { + if (typeof input === 'string') { + return encoder.encode(input); + } + return decoder.decode(input); +} +function checkRsaKeyAlgorithm(algorithm) { + if (typeof algorithm.modulusLength !== 'number' || algorithm.modulusLength < 2048) { + throw new OperationProcessingError(`${algorithm.name} modulusLength must be at least 2048 bits`); + } +} +function subtleAlgorithm(key) { + switch (key.algorithm.name) { + case 'ECDSA': + return { name: key.algorithm.name, hash: 'SHA-256' }; + case 'RSA-PSS': + checkRsaKeyAlgorithm(key.algorithm); + return { + name: key.algorithm.name, + saltLength: 256 >> 3, + }; + case 'RSASSA-PKCS1-v1_5': + checkRsaKeyAlgorithm(key.algorithm); + return { name: key.algorithm.name }; + case 'Ed25519': + return { name: key.algorithm.name }; + } + throw new UnsupportedOperationError(); +} +async function jwt(header, claimsSet, key) { + if (key.usages.includes('sign') === false) { + throw new TypeError('private CryptoKey instances used for signing assertions must include "sign" in their "usages"'); + } + const input = `${b64u(buf(JSON.stringify(header)))}.${b64u(buf(JSON.stringify(claimsSet)))}`; + const signature = b64u(await crypto.subtle.sign(subtleAlgorithm(key), key, buf(input))); + return `${input}.${signature}`; +} +const CHUNK_SIZE = 0x8000; +function encodeBase64Url(input) { + if (input instanceof ArrayBuffer) { + input = new Uint8Array(input); + } + const arr = []; + for (let i = 0; i < input.byteLength; i += CHUNK_SIZE) { + arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); + } + return btoa(arr.join('')).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); +} +function b64u(input) { + return encodeBase64Url(input); +} +function randomBytes() { + return b64u(crypto.getRandomValues(new Uint8Array(32))); +} +class UnsupportedOperationError extends Error { + constructor(message) { + super(message ?? 'operation not supported'); + this.name = this.constructor.name; + Error.captureStackTrace?.(this, this.constructor); + } +} +class OperationProcessingError extends Error { + constructor(message) { + super(message); + this.name = this.constructor.name; + Error.captureStackTrace?.(this, this.constructor); + } +} +function psAlg(key) { + switch (key.algorithm.hash.name) { + case 'SHA-256': + return 'PS256'; + default: + throw new UnsupportedOperationError('unsupported RsaHashedKeyAlgorithm hash name'); + } +} +function rsAlg(key) { + switch (key.algorithm.hash.name) { + case 'SHA-256': + return 'RS256'; + default: + throw new UnsupportedOperationError('unsupported RsaHashedKeyAlgorithm hash name'); + } +} +function esAlg(key) { + switch (key.algorithm.namedCurve) { + case 'P-256': + return 'ES256'; + default: + throw new UnsupportedOperationError('unsupported EcKeyAlgorithm namedCurve'); + } +} +function determineJWSAlgorithm(key) { + switch (key.algorithm.name) { + case 'RSA-PSS': + return psAlg(key); + case 'RSASSA-PKCS1-v1_5': + return rsAlg(key); + case 'ECDSA': + return esAlg(key); + case 'Ed25519': + return 'EdDSA'; + default: + throw new UnsupportedOperationError('unsupported CryptoKey algorithm name'); + } +} +function isCryptoKey(key) { + return key instanceof CryptoKey; +} +function isPrivateKey(key) { + return isCryptoKey(key) && key.type === 'private'; +} +function isPublicKey(key) { + return isCryptoKey(key) && key.type === 'public'; +} +function epochTime() { + return Math.floor(Date.now() / 1000); +} +export default async function DPoP(keypair, htu, htm, nonce, accessToken, additional) { + const privateKey = keypair?.privateKey; + const publicKey = keypair?.publicKey; + if (!isPrivateKey(privateKey)) { + throw new TypeError('"keypair.privateKey" must be a private CryptoKey'); + } + if (!isPublicKey(publicKey)) { + throw new TypeError('"keypair.publicKey" must be a public CryptoKey'); + } + if (publicKey.extractable !== true) { + throw new TypeError('"keypair.publicKey.extractable" must be true'); + } + if (typeof htu !== 'string') { + throw new TypeError('"htu" must be a string'); + } + if (typeof htm !== 'string') { + throw new TypeError('"htm" must be a string'); + } + if (nonce !== undefined && typeof nonce !== 'string') { + throw new TypeError('"nonce" must be a string or undefined'); + } + if (accessToken !== undefined && typeof accessToken !== 'string') { + throw new TypeError('"accessToken" must be a string or undefined'); + } + if (additional !== undefined && + (typeof additional !== 'object' || additional === null || Array.isArray(additional))) { + throw new TypeError('"additional" must be an object'); + } + return jwt({ + alg: determineJWSAlgorithm(privateKey), + typ: 'dpop+jwt', + jwk: await publicJwk(publicKey), + }, { + ...additional, + iat: epochTime(), + jti: randomBytes(), + htm, + nonce, + htu, + ath: accessToken ? b64u(await crypto.subtle.digest('SHA-256', buf(accessToken))) : undefined, + }, privateKey); +} +async function publicJwk(key) { + const { kty, e, n, x, y, crv } = await crypto.subtle.exportKey('jwk', key); + return { kty, crv, e, n, x, y }; +} +export async function generateKeyPair(alg, options) { + let algorithm; + if (typeof alg !== 'string' || alg.length === 0) { + throw new TypeError('"alg" must be a non-empty string'); + } + switch (alg) { + case 'PS256': + algorithm = { + name: 'RSA-PSS', + hash: 'SHA-256', + modulusLength: options?.modulusLength ?? 2048, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + }; + break; + case 'RS256': + algorithm = { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256', + modulusLength: options?.modulusLength ?? 2048, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + }; + break; + case 'ES256': + algorithm = { name: 'ECDSA', namedCurve: 'P-256' }; + break; + case 'EdDSA': + algorithm = { name: 'Ed25519' }; + break; + default: + throw new UnsupportedOperationError(); + } + return (crypto.subtle.generateKey(algorithm, options?.extractable ?? false, ['sign', 'verify'])); +} diff --git a/package-lock.json b/package-lock.json index 50b4b01..e7b82b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dpop", - "version": "1.4.0", + "version": "1.4.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "dpop", - "version": "1.4.0", + "version": "1.4.1", "license": "MIT", "devDependencies": { "@types/node": "^17.0.34", diff --git a/package.json b/package.json index f8f2853..43a1edd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dpop", - "version": "1.4.0", + "version": "1.4.1", "description": "DPoP (RFC9449) for Web Platform API JavaScript runtimes", "keywords": [ "dpop",