From e8d180fdc97ae6098c95af826ea06249c42df622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Thu, 5 Feb 2026 22:50:53 +0100 Subject: [PATCH 1/2] Modernize to ESM and Web Crypto API Rewrite library to ES Modules and Web Crypto API: convert index.js to ESM, replace Node crypto APIs with SubtleCrypto, add a small DER->JOSE ECDSA parser, and make all sign/verify functions async (return Promises). Update package.json for v4.0.0 (type: module, exports, node>=18), remove legacy dependencies and CI (.travis.yml), and update Makefile to use `node --test`. Tests and examples migrated to node:test and ESM, README updated for breaking changes, and PR.md added documenting the modernization and migration notes. --- .travis.yml | 17 -- Makefile | 4 +- README.md | 103 +++++-- index.js | 740 +++++++++++++++++++++++++++++++++++------------ package.json | 19 +- test/A.1/test.js | 43 +-- test/A.2/test.js | 49 ++-- test/A.3/test.js | 39 +-- test/A.4/test.js | 39 +-- test/A.5/test.js | 25 +- test/jwa.test.js | 615 ++++++++++++++++++--------------------- 11 files changed, 1018 insertions(+), 675 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 602a0c6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: node_js -node_js: - - 0.10 - - 0.11 - - 0.12 - - 4 - - 5 - - 6 - - 7 - - 8 - - 9 - - 10 - - 11 -notifications: - email: - recipients: - - brianloveswords@gmail.com diff --git a/Makefile b/Makefile index 232498d..0c07304 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ verbose: test/keys - @./node_modules/.bin/tap -Rspec test + @node --test --verbose "test/**/*.js" test: test/keys - @./node_modules/.bin/tap test + @node --test "test/**/*.js" test/keys: @openssl genrsa 2048 > test/rsa-private.pem diff --git a/README.md b/README.md index 09e9648..0a37a81 100644 --- a/README.md +++ b/README.md @@ -23,31 +23,69 @@ ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm none | No digital signature or MAC value included -Please note that PS* only works on Node 6.12+ (excluding 7.x). +## Features -# Requirements +- **ES Modules Only**: Pure ESM implementation +- **Standards-Based Cryptography**: Uses Web Crypto API (SubtleCrypto) for all operations +- **Cross-Environment Compatible**: Works with any JavaScript runtime supporting Web Crypto API +- **Type Support**: Works seamlessly with both strings and Uint8Array inputs +- **Async/Await**: All operations are async for non-blocking execution +- **Zero Production Dependencies**: No external dependencies, pure Web Crypto API -In order to run the tests, a recent version of OpenSSL is -required. **The version that comes with OS X (OpenSSL 0.9.8r 8 Feb -2011) is not recent enough**, as it does not fully support ECDSA -keys. You'll need to use a version > 1.0.0; I tested with OpenSSL 1.0.1c 10 May 2012. +## Breaking Changes in v4.0.0 -# Testing +- **All signing and verification functions are now async** (return Promises) +- Uses Web Crypto API instead of Node.js crypto module +- Requires `await` when calling `sign()` and `verify()` methods: -To run the tests, do +```javascript +// Before (v3.x - synchronous) +const sig = algo.sign(message, key) +const isValid = algo.verify(message, sig, key) + +// After (v4.x - asynchronous) +const sig = await algo.sign(message, key) +const isValid = await algo.verify(message, sig, key) +``` + +## Requirements + +- **Runtime**: Node.js >=18.0.0 or any environment with Web Crypto API support + +## Installation ```bash -$ npm test +npm install jwa ``` -This will generate a bunch of keypairs to use in testing. If you want to -generate new keypairs, do `make clean` before running `npm test` again. +## Usage + +```javascript +import { jwa } from 'jwa' + +const algo = jwa('HS256') + +// Sign +const message = 'test' +const secret = 'my-secret' +const signature = await algo.sign(message, secret) + +// Verify +const isValid = await algo.verify(message, signature, secret) +console.log(isValid) // true +``` + +## Testing + +To run the tests: + +```bash +npm test +``` + +This will validate all supported algorithms against RFC 7515 test vectors. -## Methodology -I spawn `openssl dgst -sign` to test OpenSSL sign → JS verify and -`openssl dgst -verify` to test JS sign → OpenSSL verify for each of the -RSA and ECDSA algorithms. # Usage @@ -62,7 +100,7 @@ algorithm value will throw a `TypeError`. ## jwa#sign(input, secretOrPrivateKey) Sign some input with either a secret for HMAC algorithms, or a private -key for RSA and ECDSA algorithms. +key for RSA and ECDSA algorithms. **Returns a Promise** that resolves to the signature. If input is not already a string or buffer, `JSON.stringify` will be called on it to attempt to coerce it. @@ -85,7 +123,7 @@ version satisfies, then you can pass an object `{ key: '..', passphrase: '...' } ## jwa#verify(input, signature, secretOrPublicKey) -Verify a signature. Returns `true` or `false`. +Verify a signature. **Returns a Promise** that resolves to `true` or `false`. `signature` should be a base64url encoded string. @@ -98,29 +136,30 @@ PEM encoded **public** key. HMAC ```js -const jwa = require('jwa'); +import { jwa } from 'jwa' -const hmac = jwa('HS256'); -const input = 'super important stuff'; -const secret = 'shhhhhh'; +const hmac = jwa('HS256') +const input = 'super important stuff' +const secret = 'shhhhhh' -const signature = hmac.sign(input, secret); -hmac.verify(input, signature, secret) // === true -hmac.verify(input, signature, 'trickery!') // === false +const signature = await hmac.sign(input, secret) +await hmac.verify(input, signature, secret) // === true +await hmac.verify(input, signature, 'trickery!') // === false ``` With keys ```js -const fs = require('fs'); -const jwa = require('jwa'); -const privateKey = fs.readFileSync(__dirname + '/ecdsa-p521-private.pem'); -const publicKey = fs.readFileSync(__dirname + '/ecdsa-p521-public.pem'); +import { readFile } from 'node:fs/promises' +import { jwa } from 'jwa' + +const privateKey = await readFile('./ecdsa-p521-private.pem', 'utf8') +const publicKey = await readFile('./ecdsa-p521-public.pem', 'utf8') -const ecdsa = jwa('ES512'); -const input = 'very important stuff'; +const ecdsa = jwa('ES512') +const input = 'very important stuff' -const signature = ecdsa.sign(input, privateKey); -ecdsa.verify(input, signature, publicKey) // === true +const signature = await ecdsa.sign(input, privateKey) +await ecdsa.verify(input, signature, publicKey) // === true ``` ## License diff --git a/index.js b/index.js index 5072c34..b45235c 100644 --- a/index.js +++ b/index.js @@ -1,266 +1,622 @@ -var Buffer = require('safe-buffer').Buffer; -var crypto = require('crypto'); -var formatEcdsa = require('ecdsa-sig-formatter'); -var util = require('util'); - -var MSG_INVALID_ALGORITHM = '"%s" is not a valid algorithm.\n Supported algorithms are:\n "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512" and "none".' -var MSG_INVALID_SECRET = 'secret must be a string or buffer'; -var MSG_INVALID_VERIFIER_KEY = 'key must be a string or a buffer'; -var MSG_INVALID_SIGNER_KEY = 'key must be a string, a buffer or an object'; - -var supportsKeyObjects = typeof crypto.createPublicKey === 'function'; -if (supportsKeyObjects) { - MSG_INVALID_VERIFIER_KEY += ' or a KeyObject'; - MSG_INVALID_SECRET += 'or a KeyObject'; -} - -function checkIsPublicKey(key) { - if (Buffer.isBuffer(key)) { - return; - } - - if (typeof key === 'string') { - return; - } - - if (!supportsKeyObjects) { - throw typeError(MSG_INVALID_VERIFIER_KEY); - } - - if (typeof key !== 'object') { - throw typeError(MSG_INVALID_VERIFIER_KEY); - } - - if (typeof key.type !== 'string') { - throw typeError(MSG_INVALID_VERIFIER_KEY); - } - - if (typeof key.asymmetricKeyType !== 'string') { - throw typeError(MSG_INVALID_VERIFIER_KEY); - } - - if (typeof key.export !== 'function') { - throw typeError(MSG_INVALID_VERIFIER_KEY); - } -}; - -function checkIsPrivateKey(key) { - if (Buffer.isBuffer(key)) { - return; +/** Error message template for invalid algorithms */ +const MSG_INVALID_ALGORITHM = '"%s" is not a valid algorithm.\n Supported algorithms are:\n "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512" and "none".' + +/** Error message for invalid verifier key */ +const MSG_INVALID_VERIFIER_KEY = 'key must be a string or Uint8Array' + +/** Error message for invalid signer key */ +const MSG_INVALID_SIGNER_KEY = 'key must be a string or Uint8Array' + +/** Web Crypto API instance */ +const { crypto } = globalThis + +/** + * Converts a DER-encoded ECDSA signature to JOSE format (r || s) + * Web Crypto API returns DER-encoded signatures, but JWS expects raw r||s format + * @param {Uint8Array} derBytes - DER-encoded signature bytes + * @param {string} algorithm - Algorithm name (ES256, ES384, or ES512) + * @returns JOSE-formatted signature (r || s) + * @throws {Error} If DER encoding is invalid + */ +function derToJose(derBytes, algorithm) { + const componentLength = algorithm === 'ES256' ? 32 : algorithm === 'ES384' ? 48 : 66 + + let offset = 0 + + // SEQUENCE tag + if (derBytes[offset] !== 0x30) throw new Error('Invalid DER encoding') + offset++ + + // Skip SEQUENCE length + const seqLength = derBytes[offset] + offset++ + if (seqLength > 127) { + const lengthBytes = seqLength & 0x7f + offset += lengthBytes } - if (typeof key === 'string') { - return; - } - - if (typeof key === 'object') { - return; - } + // Parse r + if (derBytes[offset] !== 0x02) throw new Error('Invalid DER encoding for r') + offset++ + const rLength = derBytes[offset] + offset++ + const rBytes = derBytes.slice(offset, offset + rLength) + offset += rLength + + // Parse s + if (derBytes[offset] !== 0x02) throw new Error('Invalid DER encoding for s') + offset++ + const sLength = derBytes[offset] + offset++ + const sBytes = derBytes.slice(offset, offset + sLength) + + // Pad r and s to component length and concatenate + const r = new Uint8Array(componentLength) + const s = new Uint8Array(componentLength) + + r.set(rBytes, componentLength - rLength) + s.set(sBytes, componentLength - sLength) + + const result = new Uint8Array(componentLength * 2) + result.set(r) + result.set(s, componentLength) + + return result +} - throw typeError(MSG_INVALID_SIGNER_KEY); -}; +/** Checks if a value is a Uint8Array instance */ +function isUint8Array(obj) { + return obj instanceof Uint8Array +} -function checkIsSecretKey(key) { - if (Buffer.isBuffer(key)) { - return; +/** + * Converts a string to a Uint8Array using UTF-8 encoding + * If input is already a Uint8Array, it is returned as-is + * @param {string|Uint8Array} str - Input string or bytes + * @returns UTF-8 encoded bytes of the input string, or original Uint8Array + */ +function stringToUint8Array(str) { + if (typeof str === 'string') { + return new TextEncoder().encode(str) } + return str +} - if (typeof key === 'string') { - return key; +/** + * @type {(arr: Uint8Array) => string} + */ +const uint8ArrayToBase64Url = (() => { + if (typeof Uint8Array.prototype.toBase64 === 'function') { + return (arr) => arr.toBase64({ alphabet: 'base64url', omitPadding: true }) } - - if (!supportsKeyObjects) { - throw typeError(MSG_INVALID_SECRET); + // Fallback for older environments + return (arr) => { + let binary = '' + const len = arr.byteLength + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(arr[i]) + } + return btoa(binary) + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_') } - - if (typeof key !== 'object') { - throw typeError(MSG_INVALID_SECRET); +})() + +/** + * Converts a base64url-encoded string to a Uint8Array + * @type {(str: string) => Uint8Array} + */ +const base64UrlToUint8Array = (() => { + if (typeof Uint8Array.fromBase64 === 'function') { + return (str) => Uint8Array.fromBase64(str, { alphabet: 'base64url' }) } - - if (key.type !== 'secret') { - throw typeError(MSG_INVALID_SECRET); + // Fallback for older environments + return (str) => { + const base64 = str + .replace(/\-/g, '+') + .replace(/_/g, '/') + const padding = 4 - (base64.length % 4) + const padded = padding !== 4 ? base64 + '='.repeat(padding) : base64 + const binary = atob(padded) + const arr = new Uint8Array(binary.length) + for (let i = 0; i < binary.length; i++) { + arr[i] = binary.charCodeAt(i) + } + return arr } - - if (typeof key.export !== 'function') { - throw typeError(MSG_INVALID_SECRET); +})() + +/** + * Parses a PEM-encoded key and extracts the PKCS8 bytes + * @param {string} pem - PEM-encoded private or public key + * @returns Raw PKCS8 bytes from the PEM file + */ +function parsePemBytes(pem) { + const lines = pem.split('\n') + let keyData = '' + for (let i = 1; i < lines.length; i++) { + if (lines[i].includes('-----')) break + keyData += lines[i] } -} - -function fromBase64(base64) { - return base64 - .replace(/=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_'); -} -function toBase64(base64url) { - base64url = base64url.toString(); - - var padding = 4 - base64url.length % 4; - if (padding !== 4) { - for (var i = 0; i < padding; ++i) { - base64url += '='; - } + const binaryString = atob(keyData) + const bytes = new Uint8Array(binaryString.length) + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i) } - - return base64url - .replace(/\-/g, '+') - .replace(/_/g, '/'); -} - -function typeError(template) { - var args = [].slice.call(arguments, 1); - var errMsg = util.format.bind(util, template).apply(null, args); - return new TypeError(errMsg); -} - -function bufferOrString(obj) { - return Buffer.isBuffer(obj) || typeof obj === 'string'; + return bytes } +/** + * Normalizes input to a string or Uint8Array + * If input is neither, it is JSON stringified + * @param {*} thing - Input to normalize + * @returns {string|Uint8Array} Normalized input + */ function normalizeInput(thing) { - if (!bufferOrString(thing)) - thing = JSON.stringify(thing); - return thing; -} - -function createHmacSigner(bits) { - return function sign(thing, secret) { - checkIsSecretKey(secret); - thing = normalizeInput(thing); - var hmac = crypto.createHmac('sha' + bits, secret); - var sig = (hmac.update(thing), hmac.digest('base64')) - return fromBase64(sig); + if (typeof thing === 'string' || isUint8Array(thing)) { + return thing } + return JSON.stringify(thing) } -var bufferEqual; -var timingSafeEqual = 'timingSafeEqual' in crypto ? function timingSafeEqual(a, b) { - if (a.byteLength !== b.byteLength) { - return false; - } - - return crypto.timingSafeEqual(a, b) -} : function timingSafeEqual(a, b) { - if (!bufferEqual) { - bufferEqual = require('buffer-equal-constant-time'); +/** + * Creates an HMAC signer function for the specified bit depth + * @param {number} bits - SHA bit depth (256, 384, or 512) + * @returns Async sign function that takes (input, secret) + */ +function createHmacSigner(bits) { + /** + * Signs the input using HMAC with the provided secret + * @param {string|Uint8Array} thing - Input to sign (string or bytes) + * @param {string|Uint8Array} secret - Secret key for HMAC (string or bytes) + * @returns Base64url-encoded signature + * @throws {TypeError} If secret is not a string or Uint8Array + */ + return async function sign(thing, secret) { + thing = normalizeInput(thing) + const data = stringToUint8Array(thing) + const secretData = stringToUint8Array(secret) + + const key = await crypto.subtle.importKey( + 'raw', + secretData, + { name: 'HMAC', hash: `SHA-${bits}` }, + false, + ['sign'] + ) + + const signature = await crypto.subtle.sign('HMAC', key, data) + return uint8ArrayToBase64Url(new Uint8Array(signature)) } - - return bufferEqual(a, b) } +/** + * Creates an HMAC verifier function for the specified bit depth + * @param {number} bits - SHA bit depth (256, 384, or 512) + * @returns Async verify function that takes (input, signature, secret) + */ function createHmacVerifier(bits) { - return function verify(thing, signature, secret) { - var computedSig = createHmacSigner(bits)(thing, secret); - return timingSafeEqual(Buffer.from(signature), Buffer.from(computedSig)); + /** + * Verifies the HMAC signature of the input using the provided secret + * @param {string|Uint8Array} thing - Input to verify (string or bytes) + * @param {string} signature - Base64url-encoded signature to verify + * @param {string|Uint8Array} secret - Secret key for HMAC (string or bytes) + * @returns True if signature is valid, false otherwise + * @throws {TypeError} If secret is not a string or Uint8Array + */ + return async function verify(thing, signature, secret) { + thing = normalizeInput(thing) + const data = stringToUint8Array(thing) + const secretData = stringToUint8Array(secret) + const sigBytes = base64UrlToUint8Array(signature) + + const key = await crypto.subtle.importKey( + 'raw', + secretData, + { name: 'HMAC', hash: `SHA-${bits}` }, + false, + ['verify'] + ) + + return crypto.subtle.verify('HMAC', key, sigBytes, data) } } +/** + * Creates an RSA PKCS#1 v1.5 signer function for the specified bit depth + * @param {number} bits - SHA bit depth (256, 384, or 512) + * @returns Async sign function that takes (input, privateKey) + */ function createKeySigner(bits) { - return function sign(thing, privateKey) { - checkIsPrivateKey(privateKey); - thing = normalizeInput(thing); - // Even though we are specifying "RSA" here, this works with ECDSA - // keys as well. - var signer = crypto.createSign('RSA-SHA' + bits); - var sig = (signer.update(thing), signer.sign(privateKey, 'base64')); - return fromBase64(sig); + /** + * Signs the input using RSA PKCS#1 v1.5 with the provided private key + * @param {string|Uint8Array} thing - Input to sign (string or bytes) + * @param {string|Uint8Array|CryptoKey} privateKey - PEM string, raw bytes, or CryptoKey for signing + * @returns Base64url-encoded signature + * @throws {TypeError} If privateKey is not a string, Uint8Array, or CryptoKey + */ + return async function sign(thing, privateKey) { + thing = normalizeInput(thing) + const data = stringToUint8Array(thing) + + let key + if (typeof privateKey === 'string') { + const bytes = parsePemBytes(privateKey) + key = await crypto.subtle.importKey( + 'pkcs8', + bytes, + { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${bits}` }, + false, + ['sign'] + ) + } else if (privateKey instanceof CryptoKey) { + key = privateKey + } else if (isUint8Array(privateKey)) { + key = await crypto.subtle.importKey( + 'pkcs8', + privateKey, + { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${bits}` }, + false, + ['sign'] + ) + } else { + throw new TypeError(MSG_INVALID_SIGNER_KEY) + } + + const signature = await crypto.subtle.sign( + { name: 'RSASSA-PKCS1-v1_5' }, + key, + data + ) + return uint8ArrayToBase64Url(new Uint8Array(signature)) } } +/** + * Creates an RSA PKCS#1 v1.5 verifier function for the specified bit depth + * @param {number} bits - SHA bit depth (256, 384, or 512) + * @returns Async verify function that takes (input, signature, publicKey) + */ function createKeyVerifier(bits) { - return function verify(thing, signature, publicKey) { - checkIsPublicKey(publicKey); - thing = normalizeInput(thing); - signature = toBase64(signature); - var verifier = crypto.createVerify('RSA-SHA' + bits); - verifier.update(thing); - return verifier.verify(publicKey, signature, 'base64'); + /** + * Verifies the RSA PKCS#1 v1.5 signature of the input using the provided public key + * @param {string|Uint8Array} thing - Input to verify (string or bytes) + * @param {string} signature - Base64url-encoded signature to verify + * @param {string|Uint8Array|CryptoKey} publicKey - PEM string, raw bytes, or CryptoKey for verification + * @returns True if signature is valid, false otherwise + * @throws {TypeError} If publicKey is not a string, Uint8Array, or CryptoKey + */ + return async function verify(thing, signature, publicKey) { + thing = normalizeInput(thing) + const data = stringToUint8Array(thing) + const sigBytes = base64UrlToUint8Array(signature) + + let key + if (typeof publicKey === 'string') { + const bytes = parsePemBytes(publicKey) + key = await crypto.subtle.importKey( + 'spki', + bytes, + { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${bits}` }, + false, + ['verify'] + ) + } else if (publicKey instanceof CryptoKey) { + key = publicKey + } else if (isUint8Array(publicKey)) { + key = await crypto.subtle.importKey( + 'spki', + publicKey, + { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${bits}` }, + false, + ['verify'] + ) + } else { + throw new TypeError(MSG_INVALID_VERIFIER_KEY) + } + + return crypto.subtle.verify( + { name: 'RSASSA-PKCS1-v1_5' }, + key, + sigBytes, + data + ) } } +/** + * Creates an RSA-PSS signer function for the specified bit depth + * Salt length is set to hash output length per PSS specification + * @param {number} bits - SHA bit depth (256, 384, or 512) + * @returns Async sign function that takes (input, privateKey) + */ function createPSSKeySigner(bits) { - return function sign(thing, privateKey) { - checkIsPrivateKey(privateKey); - thing = normalizeInput(thing); - var signer = crypto.createSign('RSA-SHA' + bits); - var sig = (signer.update(thing), signer.sign({ - key: privateKey, - padding: crypto.constants.RSA_PKCS1_PSS_PADDING, - saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST - }, 'base64')); - return fromBase64(sig); + const saltLength = parseInt(bits) / 8 + + /** + * Signs the input using RSA-PSS with the provided private key + * @param {string|Uint8Array} thing - Input to sign (string or bytes) + * @param {string|Uint8Array|CryptoKey} privateKey - PEM string, raw bytes, or CryptoKey for signing + * @return Base64url-encoded signature + */ + return async function sign(thing, privateKey) { + thing = normalizeInput(thing) + const data = stringToUint8Array(thing) + + let key + if (typeof privateKey === 'string') { + const bytes = parsePemBytes(privateKey) + key = await crypto.subtle.importKey( + 'pkcs8', + bytes, + { name: 'RSA-PSS', hash: `SHA-${bits}` }, + false, + ['sign'] + ) + } else if (privateKey instanceof CryptoKey) { + key = privateKey + } else if (isUint8Array(privateKey)) { + key = await crypto.subtle.importKey( + 'pkcs8', + privateKey, + { name: 'RSA-PSS', hash: `SHA-${bits}` }, + false, + ['sign'] + ) + } else { + throw new TypeError(MSG_INVALID_SIGNER_KEY) + } + + const signature = await crypto.subtle.sign( + { name: 'RSA-PSS', saltLength }, + key, + data + ) + return uint8ArrayToBase64Url(new Uint8Array(signature)) } } +/** + * Creates an RSA-PSS verifier function for the specified bit depth + * @param {number} bits - SHA bit depth (256, 384, or 512) + * @returns Async verify function that takes (input, signature, publicKey) + */ function createPSSKeyVerifier(bits) { - return function verify(thing, signature, publicKey) { - checkIsPublicKey(publicKey); - thing = normalizeInput(thing); - signature = toBase64(signature); - var verifier = crypto.createVerify('RSA-SHA' + bits); - verifier.update(thing); - return verifier.verify({ - key: publicKey, - padding: crypto.constants.RSA_PKCS1_PSS_PADDING, - saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST - }, signature, 'base64'); + const saltLength = parseInt(bits) / 8 + + /** + * Verifies the RSA-PSS signature of the input using the provided public key + * @param {string|Uint8Array} thing - Input to verify (string or bytes) + * @param {string} signature - Base64url-encoded signature to verify + * @param {string|Uint8Array|CryptoKey} publicKey - PEM string, raw bytes, or CryptoKey for verification + * @returns True if signature is valid, false otherwise + * @throws {TypeError} If publicKey is not a string, Uint8Array, or CryptoKey + */ + return async function verify(thing, signature, publicKey) { + thing = normalizeInput(thing) + const data = stringToUint8Array(thing) + const sigBytes = base64UrlToUint8Array(signature) + + let key + if (typeof publicKey === 'string') { + const bytes = parsePemBytes(publicKey) + key = await crypto.subtle.importKey( + 'spki', + bytes, + { name: 'RSA-PSS', hash: `SHA-${bits}` }, + false, + ['verify'] + ) + } else if (publicKey instanceof CryptoKey) { + key = publicKey + } else if (isUint8Array(publicKey)) { + key = await crypto.subtle.importKey( + 'spki', + publicKey, + { name: 'RSA-PSS', hash: `SHA-${bits}` }, + false, + ['verify'] + ) + } else { + throw new TypeError(MSG_INVALID_VERIFIER_KEY) + } + + return crypto.subtle.verify( + { name: 'RSA-PSS', saltLength }, + key, + sigBytes, + data + ) } } +/** + * Creates an ECDSA signer function for the specified bit depth + * Converts DER-encoded signatures to JOSE format (r || s) + * @param {number} bits - SHA bit depth (256, 384, or 512) corresponding to curve (P-256, P-384, P-521) + * @returns Async sign function that takes (input, privateKey) + */ function createECDSASigner(bits) { - var inner = createKeySigner(bits); - return function sign() { - var signature = inner.apply(null, arguments); - signature = formatEcdsa.derToJose(signature, 'ES' + bits); - return signature; - }; + const namedCurve = bits === '256' ? 'P-256' : bits === '384' ? 'P-384' : 'P-521' + + /** + * Signs the input using ECDSA with the provided private key + * @param {string|Uint8Array} thing - Input to sign (string or bytes) + * @param {string|Uint8Array|CryptoKey} privateKey - PEM string, raw bytes, or CryptoKey for signing + * @returns Base64url-encoded signature in JOSE format (r || s) + * @throws {TypeError} If privateKey is not a string, Uint8Array, or CryptoKey + */ + return async function sign(thing, privateKey) { + thing = normalizeInput(thing) + const data = stringToUint8Array(thing) + + let key + if (typeof privateKey === 'string') { + const bytes = parsePemBytes(privateKey) + key = await crypto.subtle.importKey( + 'pkcs8', + bytes, + { name: 'ECDSA', namedCurve }, + false, + ['sign'] + ) + } else if (privateKey instanceof CryptoKey) { + key = privateKey + } else if (isUint8Array(privateKey)) { + key = await crypto.subtle.importKey( + 'pkcs8', + privateKey, + { name: 'ECDSA', namedCurve }, + false, + ['sign'] + ) + } else { + throw new TypeError(MSG_INVALID_SIGNER_KEY) + } + + const signature = await crypto.subtle.sign( + { name: 'ECDSA', hash: `SHA-${bits}` }, + key, + data + ) + const joseSignature = derToJose(new Uint8Array(signature), `ES${bits}`) + return uint8ArrayToBase64Url(joseSignature) + } } +/** + * Creates an ECDSA verifier function for the specified bit depth + * @param {number} bits - SHA bit depth (256, 384, or 512) corresponding to curve (P-256, P-384, P-521) + * @returns Async verify function that takes (input, signature, publicKey) + */ function createECDSAVerifer(bits) { - var inner = createKeyVerifier(bits); - return function verify(thing, signature, publicKey) { - signature = formatEcdsa.joseToDer(signature, 'ES' + bits).toString('base64'); - var result = inner(thing, signature, publicKey); - return result; - }; + const namedCurve = bits === '256' ? 'P-256' : bits === '384' ? 'P-384' : 'P-521' + /** + * Verifies the ECDSA signature of the input using the provided public key + * @param {string|Uint8Array} thing - Input to verify (string or bytes) + * @param {string} signature - Base64url-encoded signature to verify + * @param {string|Uint8Array|CryptoKey} publicKey - PEM string, raw bytes, or CryptoKey for verification + * @returns True if signature is valid, false otherwise + * @throws {TypeError} If publicKey is not a string, Uint8Array, or CryptoKey + */ + return async function verify(thing, signature, publicKey) { + thing = normalizeInput(thing) + const data = stringToUint8Array(thing) + // The signature in JOSE format is already in raw r||s format (base64url) + // Web Crypto.verify() expects raw bytes, not DER + const joseBytes = base64UrlToUint8Array(signature) + + let key + if (typeof publicKey === 'string') { + const bytes = parsePemBytes(publicKey) + key = await crypto.subtle.importKey( + 'spki', + bytes, + { name: 'ECDSA', namedCurve }, + false, + ['verify'] + ) + } else if (publicKey instanceof CryptoKey) { + key = publicKey + } else if (isUint8Array(publicKey)) { + key = await crypto.subtle.importKey( + 'spki', + publicKey, + { name: 'ECDSA', namedCurve }, + false, + ['verify'] + ) + } else { + throw new TypeError(MSG_INVALID_VERIFIER_KEY) + } + + return crypto.subtle.verify( + { name: 'ECDSA', hash: `SHA-${bits}` }, + key, + joseBytes, + data + ) + } } +/** + * Creates a signer for the 'none' algorithm (unsigned tokens) + * @returns Async sign function that always returns empty string + */ function createNoneSigner() { - return function sign() { - return ''; + return async function sign() { + return '' } } +/** + * Creates a verifier for the 'none' algorithm (unsigned tokens) + * @returns Async verify function that checks if signature is empty + */ function createNoneVerifier() { - return function verify(thing, signature) { - return signature === ''; + return async function verify(thing, signature) { + return signature === '' } } -module.exports = function jwa(algorithm) { - var signerFactories = { +/** + * Creates a JWA algorithm instance with sign and verify methods + * Uses Web Crypto API for all cryptographic operations + * + * Supported algorithms: + * - HMAC: HS256, HS384, HS512 + * - RSA: RS256, RS384, RS512 + * - RSA-PSS: PS256, PS384, PS512 + * - ECDSA: ES256, ES384, ES512 + * - Unsigned: none + * + * @param {string} algorithm - JWS algorithm identifier (case-sensitive) + * @returns {Object} Object with async sign and verify methods + * @returns Async function(input, secretOrPrivateKey) => Promise returns.sign + * @returns {Function} returns.verify - Async function(input, signature, secretOrPublicKey) => Promise + * @throws {TypeError} If algorithm is not recognized + * + * @example + * // HMAC signing + * const algo = jwa('HS256') + * const sig = await algo.sign('message', 'secret') + * const isValid = await algo.verify('message', sig, 'secret') + * + * @example + * // RSA signing with PEM-encoded key + * const algo = jwa('RS256') + * const privateKey = `-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----` + * const sig = await algo.sign('message', privateKey) + */ +function jwa(algorithm) { + const signerFactories = { hs: createHmacSigner, rs: createKeySigner, ps: createPSSKeySigner, es: createECDSASigner, none: createNoneSigner, } - var verifierFactories = { + const verifierFactories = { hs: createHmacVerifier, rs: createKeyVerifier, ps: createPSSKeyVerifier, es: createECDSAVerifer, none: createNoneVerifier, } - var match = algorithm.match(/^(RS|PS|ES|HS)(256|384|512)$|^(none)$/); + const match = algorithm.match(/^(RS|PS|ES|HS)(256|384|512)$|^(none)$/) if (!match) - throw typeError(MSG_INVALID_ALGORITHM, algorithm); - var algo = (match[1] || match[3]).toLowerCase(); - var bits = match[2]; + throw new TypeError(MSG_INVALID_ALGORITHM, algorithm) + const algo = (match[1] || match[3]).toLowerCase() + const bits = match[2] return { sign: signerFactories[algo](bits), verify: verifierFactories[algo](bits), } -}; +} + +export { + jwa +} diff --git a/package.json b/package.json index fd5824a..236726b 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,21 @@ { "name": "jwa", - "version": "2.0.1", + "version": "4.0.0", "description": "JWA implementation (supports all JWS algorithms)", + "type": "module", "main": "index.js", + "exports": "./index.js", "directories": { "test": "test" }, - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - }, "devDependencies": { - "base64url": "^2.0.0", - "jwk-to-pem": "^2.0.1", - "semver": "4.3.6", - "tap": "6.2.0" + "jwk-to-pem": "^2.0.1" }, "scripts": { - "test": "make test" + "test": "node --test test/**/*.js" + }, + "engines": { + "node": ">=18.0.0" }, "repository": { "type": "git", diff --git a/test/A.1/test.js b/test/A.1/test.js index 7ef6ba6..f5c1664 100644 --- a/test/A.1/test.js +++ b/test/A.1/test.js @@ -2,33 +2,34 @@ * https://tools.ietf.org/html/rfc7515#appendix-A.1 */ -const fs = require('fs'); -const path = require('path'); +import { test } from 'node:test' +import assert from 'node:assert' +import { Buffer } from 'node:buffer' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { jwa } from '../../index.js' -const Buffer = require('safe-buffer').Buffer; -const test = require('tap').test; +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) -const jwa = require('../../'); +const input = fs.readFileSync(path.join(__dirname, 'input.txt')) +const inputFromBytes = Buffer.from(JSON.parse(fs.readFileSync(path.join(__dirname, 'input.bytes.json'), 'utf8'))) -const input = fs.readFileSync(path.join(__dirname, 'input.txt')); -const inputFromBytes = Buffer.from(JSON.parse(fs.readFileSync(path.join(__dirname, 'input.bytes.json'), 'utf8'))); +const key = Buffer.from(JSON.parse(fs.readFileSync(path.join(__dirname, 'key.json'), 'utf8')).k, 'base64') -const key = Buffer.from(JSON.parse(fs.readFileSync(path.join(__dirname, 'key.json'), 'utf8')).k, 'base64'); +const signature = fs.readFileSync(path.join(__dirname, 'signature.txt'), 'ascii') +const signatureFromBytes = Buffer.from(JSON.parse(fs.readFileSync(path.join(__dirname, 'signature.bytes.json'), 'utf8'))) -const signature = fs.readFileSync(path.join(__dirname, 'signature.txt'), 'ascii'); -const signatureFromBytes = Buffer.from(JSON.parse(fs.readFileSync(path.join(__dirname, 'signature.bytes.json'), 'utf8'))); +const algo = jwa('HS256') -const algo = jwa('HS256'); +test('A.1', async () => { + assert.deepStrictEqual(input, inputFromBytes) + assert.deepStrictEqual(Buffer.from(signature, 'base64'), signatureFromBytes) -test('A.1', function (t) { - t.plan(6); + assert.strictEqual(await algo.sign(input, key), signature) + assert.strictEqual(await algo.sign(input.toString('ascii'), key), signature) - t.equivalent(input, inputFromBytes); - t.equivalent(Buffer.from(signature, 'base64'), signatureFromBytes); - - t.equal(algo.sign(input, key), signature); - t.equal(algo.sign(input.toString('ascii'), key), signature); - - t.ok(algo.verify(input, signature, key)); - t.ok(algo.verify(input.toString('ascii'), signature, key)); + assert.ok(await algo.verify(input, signature, key)) + assert.ok(await algo.verify(input.toString('ascii'), signature, key)) }) diff --git a/test/A.2/test.js b/test/A.2/test.js index 9b5269e..bc55f7e 100644 --- a/test/A.2/test.js +++ b/test/A.2/test.js @@ -2,36 +2,37 @@ * https://tools.ietf.org/html/rfc7515#appendix-A.2 */ -const fs = require('fs'); -const path = require('path'); +import { test } from 'node:test' +import assert from 'node:assert' +import { Buffer } from 'node:buffer' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import jwkToPem from 'jwk-to-pem' +import { jwa } from '../../index.js' -const Buffer = require('safe-buffer').Buffer; -const jwkToPem = require('jwk-to-pem'); -const test = require('tap').test; +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) -const jwa = require('../../'); +const input = fs.readFileSync(path.join(__dirname, 'input.txt')) +const inputFromBytes = Buffer.from(JSON.parse(fs.readFileSync(path.join(__dirname, 'input.bytes.json'), 'utf8'))) -const input = fs.readFileSync(path.join(__dirname, 'input.txt')); -const inputFromBytes = Buffer.from(JSON.parse(fs.readFileSync(path.join(__dirname, 'input.bytes.json'), 'utf8'))); +const jwk = JSON.parse(fs.readFileSync(path.join(__dirname, 'key.json'), 'utf8')) +const privKey = jwkToPem(jwk, { private: true }) +const pubKey = jwkToPem(jwk) -const jwk = JSON.parse(fs.readFileSync(path.join(__dirname, 'key.json'), 'utf8')); -const privKey = jwkToPem(jwk, { private: true }); -const pubKey = jwkToPem(jwk); +const signature = fs.readFileSync(path.join(__dirname, 'signature.txt'), 'ascii') +const signatureFromBytes = Buffer.from(JSON.parse(fs.readFileSync(path.join(__dirname, 'signature.bytes.json'), 'utf8'))) -const signature = fs.readFileSync(path.join(__dirname, 'signature.txt'), 'ascii'); -const signatureFromBytes = Buffer.from(JSON.parse(fs.readFileSync(path.join(__dirname, 'signature.bytes.json'), 'utf8'))); +const algo = jwa('RS256') -const algo = jwa('RS256'); +test('A.2', async () => { + assert.deepStrictEqual(input, inputFromBytes) + assert.deepStrictEqual(Buffer.from(signature, 'base64'), signatureFromBytes) -test('A.2', function (t) { - t.plan(6); + assert.strictEqual(await algo.sign(input, privKey), signature) + assert.strictEqual(await algo.sign(input.toString('ascii'), privKey), signature) - t.equivalent(input, inputFromBytes); - t.equivalent(Buffer.from(signature, 'base64'), signatureFromBytes); - - t.equal(algo.sign(input, privKey), signature); - t.equal(algo.sign(input.toString('ascii'), privKey), signature); - - t.ok(algo.verify(input, signature, pubKey)); - t.ok(algo.verify(input.toString('ascii'), signature, pubKey)); + assert.ok(await algo.verify(input, signature, pubKey)) + assert.ok(await algo.verify(input.toString('ascii'), signature, pubKey)) }) diff --git a/test/A.3/test.js b/test/A.3/test.js index 40f95f8..d17b872 100644 --- a/test/A.3/test.js +++ b/test/A.3/test.js @@ -2,30 +2,31 @@ * https://tools.ietf.org/html/rfc7515#appendix-A.3 */ -const fs = require('fs'); -const path = require('path'); +import { test } from 'node:test' +import assert from 'node:assert' +import { Buffer } from 'node:buffer' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import jwkToPem from 'jwk-to-pem' +import { jwa } from '../../index.js' -const Buffer = require('safe-buffer').Buffer; -const jwkToPem = require('jwk-to-pem'); -const test = require('tap').test; +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) -const jwa = require('../../'); +const input = fs.readFileSync(path.join(__dirname, 'input.txt')) +const inputFromBytes = Buffer.from(JSON.parse(fs.readFileSync(path.join(__dirname, 'input.bytes.json'), 'utf8'))) -const input = fs.readFileSync(path.join(__dirname, 'input.txt')); -const inputFromBytes = Buffer.from(JSON.parse(fs.readFileSync(path.join(__dirname, 'input.bytes.json'), 'utf8'))); +const jwk = JSON.parse(fs.readFileSync(path.join(__dirname, 'key.json'), 'utf8')) +const pubKey = jwkToPem(jwk) -const jwk = JSON.parse(fs.readFileSync(path.join(__dirname, 'key.json'), 'utf8')); -const pubKey = jwkToPem(jwk); +const signature = fs.readFileSync(path.join(__dirname, 'signature.txt'), 'ascii') -const signature = fs.readFileSync(path.join(__dirname, 'signature.txt'), 'ascii'); +const algo = jwa('ES256') -const algo = jwa('ES256'); +test('A.3', async () => { + assert.deepStrictEqual(input, inputFromBytes) -test('A.3', function (t) { - t.plan(3); - - t.equivalent(input, inputFromBytes); - - t.ok(algo.verify(input, signature, pubKey)); - t.ok(algo.verify(input.toString('ascii'), signature, pubKey)); + assert.ok(await algo.verify(input, signature, pubKey)) + assert.ok(await algo.verify(input.toString('ascii'), signature, pubKey)) }) diff --git a/test/A.4/test.js b/test/A.4/test.js index dedb4f7..57c97fc 100644 --- a/test/A.4/test.js +++ b/test/A.4/test.js @@ -2,30 +2,31 @@ * https://tools.ietf.org/html/rfc7515#appendix-A.4 */ -const fs = require('fs'); -const path = require('path'); +import { test } from 'node:test' +import assert from 'node:assert' +import { Buffer } from 'node:buffer' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import jwkToPem from 'jwk-to-pem' +import { jwa } from '../../index.js' -const Buffer = require('safe-buffer').Buffer; -const jwkToPem = require('jwk-to-pem'); -const test = require('tap').test; +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) -const jwa = require('../../'); +const input = fs.readFileSync(path.join(__dirname, 'input.txt')) +const inputFromBytes = Buffer.from(JSON.parse(fs.readFileSync(path.join(__dirname, 'input.bytes.json'), 'utf8'))) -const input = fs.readFileSync(path.join(__dirname, 'input.txt')); -const inputFromBytes = Buffer.from(JSON.parse(fs.readFileSync(path.join(__dirname, 'input.bytes.json'), 'utf8'))); +const jwk = JSON.parse(fs.readFileSync(path.join(__dirname, 'key.json'), 'utf8')) +const pubKey = jwkToPem(jwk) -const jwk = JSON.parse(fs.readFileSync(path.join(__dirname, 'key.json'), 'utf8')); -const pubKey = jwkToPem(jwk); +const signature = fs.readFileSync(path.join(__dirname, 'signature.txt'), 'ascii') -const signature = fs.readFileSync(path.join(__dirname, 'signature.txt'), 'ascii'); +const algo = jwa('ES512') -const algo = jwa('ES512'); +test('A.4', async () => { + assert.deepStrictEqual(input, inputFromBytes) -test('A.4', function (t) { - t.plan(3); - - t.equivalent(input, inputFromBytes); - - t.ok(algo.verify(input, signature, pubKey)); - t.ok(algo.verify(input.toString('ascii'), signature, pubKey)); + assert.ok(await algo.verify(input, signature, pubKey)) + assert.ok(await algo.verify(input.toString('ascii'), signature, pubKey)) }) diff --git a/test/A.5/test.js b/test/A.5/test.js index f7488bf..32208a9 100644 --- a/test/A.5/test.js +++ b/test/A.5/test.js @@ -2,20 +2,21 @@ * https://tools.ietf.org/html/rfc7515#appendix-A.5 */ -const fs = require('fs'); -const path = require('path'); +import { test } from 'node:test' +import assert from 'node:assert' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { jwa } from '../../index.js' -const test = require('tap').test; +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) -const jwa = require('../../'); +const input = fs.readFileSync(path.join(__dirname, 'input.txt')) -const input = fs.readFileSync(path.join(__dirname, 'input.txt')); +const algo = jwa('none') -const algo = jwa('none'); - -test('A.5', function (t) { - t.plan(2); - - t.equal(algo.sign(input), ''); - t.ok(algo.verify(input, '')); +test('A.5', async () => { + assert.strictEqual(await algo.sign(input), '') + assert.ok(await algo.verify(input, '')) }) diff --git a/test/jwa.test.js b/test/jwa.test.js index 33ee11c..3e996f0 100644 --- a/test/jwa.test.js +++ b/test/jwa.test.js @@ -1,252 +1,238 @@ -const crypto = require('crypto'); -const path = require('path'); -const base64url = require('base64url'); -const formatEcdsa = require('ecdsa-sig-formatter'); -const spawn = require('child_process').spawn; -const Buffer = require('safe-buffer').Buffer; -const semver = require('semver'); -const fs = require('fs'); -const test = require('tap').test; -const jwa = require('..'); - -const nodeVersion = semver.clean(process.version); -const SUPPORTS_KEY_OBJECTS = typeof crypto.createPublicKey === 'function'; +import { test } from 'node:test' +import assert from 'node:assert' +import crypto from 'node:crypto' +import path from 'node:path' +import { Buffer } from 'node:buffer' +import { spawn } from 'node:child_process' +import fs from 'node:fs' +import { fileURLToPath } from 'node:url' +import formatEcdsa from 'ecdsa-sig-formatter' +import { jwa } from '../index.js' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const base64url = uInt8 => uInt8.toBase64({ alphabet: 'base64url' }).replace(/=+$/, '') +base64url.toBuffer = str => Buffer.from(str, 'base64url') + +const SUPPORTS_KEY_OBJECTS = typeof crypto.createPublicKey === 'function' // these key files will be generated as part of `make test` -const rsaPrivateKey = fs.readFileSync(__dirname + '/rsa-private.pem').toString(); -const rsaPublicKey = fs.readFileSync(__dirname + '/rsa-public.pem').toString(); -const rsaPrivateKeyWithPassphrase = fs.readFileSync(__dirname + '/rsa-passphrase-private.pem').toString(); -const rsaPublicKeyWithPassphrase = fs.readFileSync(__dirname + '/rsa-passphrase-public.pem').toString(); -const rsaWrongPublicKey = fs.readFileSync(__dirname + '/rsa-wrong-public.pem').toString(); +const rsaPrivateKey = fs.readFileSync(__dirname + '/rsa-private.pem').toString() +const rsaPublicKey = fs.readFileSync(__dirname + '/rsa-public.pem').toString() +const rsaPrivateKeyWithPassphrase = fs.readFileSync(__dirname + '/rsa-passphrase-private.pem').toString() +const rsaPublicKeyWithPassphrase = fs.readFileSync(__dirname + '/rsa-passphrase-public.pem').toString() +const rsaWrongPublicKey = fs.readFileSync(__dirname + '/rsa-wrong-public.pem').toString() const ecdsaPrivateKey = { '256': fs.readFileSync(__dirname + '/ec256-private.pem').toString(), '384': fs.readFileSync(__dirname + '/ec384-private.pem').toString(), '512': fs.readFileSync(__dirname + '/ec512-private.pem').toString(), -}; +} const ecdsaPublicKey = { '256': fs.readFileSync(__dirname + '/ec256-public.pem').toString(), '384': fs.readFileSync(__dirname + '/ec384-public.pem').toString(), '512': fs.readFileSync(__dirname + '/ec512-public.pem').toString(), -}; +} const ecdsaWrongPublicKey = { '256': fs.readFileSync(__dirname + '/ec256-wrong-public.pem').toString(), '384': fs.readFileSync(__dirname + '/ec384-wrong-public.pem').toString(), '512': fs.readFileSync(__dirname + '/ec512-wrong-public.pem').toString(), -}; +} -const BIT_DEPTHS = ['256', '384', '512']; +const BIT_DEPTHS = ['256', '384', '512'] test('HMAC signing, verifying', function (t) { - const input = 'eugene mirman'; - const secret = 'shhhhhhhhhh'; + const input = 'eugene mirman' + const secret = 'shhhhhhhhhh' BIT_DEPTHS.forEach(function (bits) { - const algo = jwa('HS'+bits); - const sig = algo.sign(input, secret); - t.ok(algo.verify(input, sig, secret), 'should verify'); - t.notOk(algo.verify(input, 'other sig', secret), 'should verify'); - t.notOk(algo.verify(input, sig, 'incrorect'), 'shoud not verify'); - }); - t.end(); -}); + const algo = jwa('HS'+bits) + const sig = algo.sign(input, secret) + asserassert.ok(algo.verify(input, sig, secret), 'should verify') + asserasserassert.ok(!algo.verify(input, 'other sig', secret), 'should verify') + asserasserassert.ok(!algo.verify(input, sig, 'incrorect'), 'shoud not verify') + }) +}) if (SUPPORTS_KEY_OBJECTS) { BIT_DEPTHS.forEach(function (bits) { - const input = 'foo bar baz'; - const secret = 'this-is-a-bad-secret'; - const secretBuf = Buffer.from(secret, 'utf8'); - const secretObj = crypto.createSecretKey(secretBuf); + const input = 'foo bar baz' + const secret = 'this-is-a-bad-secret' + const secretBuf = Buffer.from(secret, 'utf8') + const secretObj = crypto.createSecretKey(secretBuf) test('HS' + bits + 'signing, verifying (w/ KeyObject)', function (t) { - const algo = jwa('HS' + bits); + const algo = jwa('HS' + bits) const sigs = [ algo.sign(input, secret), algo.sign(input, secretBuf), algo.sign(input, secretObj) - ]; + ] for (var i = 0; i < sigs.length; ++i) { - t.ok(algo.verify(input, sigs[i], secret)); - t.ok(algo.verify(input, sigs[i], secretBuf)); - t.ok(algo.verify(input, sigs[i], secretObj)); + asserassert.ok(algo.verify(input, sigs[i], secret)) + asserassert.ok(algo.verify(input, sigs[i], secretBuf)) + asserassert.ok(algo.verify(input, sigs[i], secretObj)) } - - t.end(); - }); - }); +}) + }) } test('RSA signing, verifying', function (t) { - const input = 'h. jon benjamin'; + const input = 'h. jon benjamin' BIT_DEPTHS.forEach(function (bits) { - const algo = jwa('RS'+bits); - const sig = algo.sign(input, rsaPrivateKey); - t.ok(algo.verify(input, sig, rsaPublicKey), 'should verify'); - t.notOk(algo.verify(input, sig, rsaWrongPublicKey), 'shoud not verify'); - }); - t.end(); -}); - -// run only on nodejs version >= 0.11.8 -if (semver.gte(nodeVersion, '0.11.8')) { + const algo = jwa('RS'+bits) + const sig = algo.sign(input, rsaPrivateKey) + asserassert.ok(algo.verify(input, sig, rsaPublicKey), 'should verify') + asserasserassert.ok(!algo.verify(input, sig, rsaWrongPublicKey), 'shoud not verify') + }) +}) + +// run only on nodejs version >= 0.11.8{ test('RSA with passphrase signing, verifying', function (t) { - const input = 'test input'; + const input = 'test input' BIT_DEPTHS.forEach(function (bits) { - const algo = jwa('RS'+bits); - const secret = 'test_pass'; - const sig = algo.sign(input, {key: rsaPrivateKeyWithPassphrase, passphrase: secret}); - t.ok(algo.verify(input, sig, rsaPublicKeyWithPassphrase), 'should verify'); - }); - t.end(); - }); + const algo = jwa('RS'+bits) + const secret = 'test_pass' + const sig = algo.sign(input, {key: rsaPrivateKeyWithPassphrase, passphrase: secret}) + asserassert.ok(algo.verify(input, sig, rsaPublicKeyWithPassphrase), 'should verify') + }) +}) } if (SUPPORTS_KEY_OBJECTS) { BIT_DEPTHS.forEach(function (bits) { test('RS'+bits+': signing, verifying (KeyObject)', function (t) { - const input = 'h. jon benjamin'; - const algo = jwa('RS'+bits); - const sig = algo.sign(input, crypto.createPrivateKey(rsaPrivateKey)); - t.ok(algo.verify(input, sig, crypto.createPublicKey(rsaPublicKey)), 'should verify'); - t.notOk(algo.verify(input, sig, crypto.createPublicKey(rsaWrongPublicKey)), 'shoud not verify'); - t.end(); - }); - }); -} - - -if (semver.satisfies(nodeVersion, '^6.12.0 || >=8.0.0')) { + const input = 'h. jon benjamin' + const algo = jwa('RS'+bits) + const sig = algo.sign(input, crypto.createPrivateKey(rsaPrivateKey)) + asserassert.ok(algo.verify(input, sig, crypto.createPublicKey(rsaPublicKey)), 'should verify') + asserasserassert.ok(!algo.verify(input, sig, crypto.createPublicKey(rsaWrongPublicKey)), 'shoud not verify') +}) + }) +}{ test('RSA-PSS signing, verifying', function (t) { - const input = 'h. jon benjamin'; + const input = 'h. jon benjamin' BIT_DEPTHS.forEach(function (bits) { - const algo = jwa('PS'+bits); - const sig = algo.sign(input, rsaPrivateKey); - t.ok(algo.verify(input, sig, rsaPublicKey), 'should verify'); - t.notOk(algo.verify(input, sig, rsaWrongPublicKey), 'shoud not verify'); - }); - t.end(); - }); + const algo = jwa('PS'+bits) + const sig = algo.sign(input, rsaPrivateKey) + asserassert.ok(algo.verify(input, sig, rsaPublicKey), 'should verify') + asserasserassert.ok(!algo.verify(input, sig, rsaWrongPublicKey), 'shoud not verify') + }) +}) if (SUPPORTS_KEY_OBJECTS) { BIT_DEPTHS.forEach(function (bits) { test('PS'+bits+': signing, verifying (KeyObject)', function (t) { - const input = 'h. jon benjamin'; - const algo = jwa('PS'+bits); - const sig = algo.sign(input, crypto.createPrivateKey(rsaPrivateKey)); - t.ok(algo.verify(input, sig, crypto.createPublicKey(rsaPublicKey)), 'should verify'); - t.notOk(algo.verify(input, sig, crypto.createPublicKey(rsaWrongPublicKey)), 'should not verify'); - t.end(); - }); - }); + const input = 'h. jon benjamin' + const algo = jwa('PS'+bits) + const sig = algo.sign(input, crypto.createPrivateKey(rsaPrivateKey)) + asserassert.ok(algo.verify(input, sig, crypto.createPublicKey(rsaPublicKey)), 'should verify') + asserasserassert.ok(!algo.verify(input, sig, crypto.createPublicKey(rsaWrongPublicKey)), 'should not verify') +}) + }) } } BIT_DEPTHS.forEach(function (bits) { - test('RS'+bits+': openssl sign -> js verify', function (t) { - const input = 'iodine'; - const algo = jwa('RS'+bits); - const dgst = spawn('openssl', ['dgst', '-sha'+bits, '-sign', __dirname + '/rsa-private.pem']); - var buffer = Buffer.alloc(0); + test('RS'+bits+': openssl sign -> js verify', () => { + const input = 'iodine' + const algo = jwa('RS'+bits) + const dgst = spawn('openssl', ['dgst', '-sha'+bits, '-sign', __dirname + '/rsa-private.pem']) + var buffer = Buffer.alloc(0) dgst.stdout.on('data', function (buf) { - buffer = Buffer.concat([buffer, buf]); - }); + buffer = Buffer.concat([buffer, buf]) + }) dgst.stdin.write(input, function() { - dgst.stdin.end(); - }); + dgst.stdin.end() + }) dgst.on('exit', function (code) { if (code !== 0) - return t.fail('could not test interop: openssl failure'); - const sig = base64url(buffer); - - t.ok(algo.verify(input, sig, rsaPublicKey), 'should verify'); - t.notOk(algo.verify(input, sig, rsaWrongPublicKey), 'should not verify'); - t.end(); - }); - }); -}); - -if (semver.satisfies(nodeVersion, '^6.12.0 || >=8.0.0')) { + return asserassert.fail('could not test interop: openssl failure') + const sig = base64url(buffer) + + asserassert.ok(algo.verify(input, sig, rsaPublicKey), 'should verify') + asserasserassert.ok(!algo.verify(input, sig, rsaWrongPublicKey), 'should not verify') +}) + }) +});{ BIT_DEPTHS.forEach(function (bits) { - test('PS'+bits+': openssl sign -> js verify', function (t) { - const input = 'iodine'; - const algo = jwa('PS'+bits); - const dgst = spawn('openssl', ['dgst', '-sha'+bits, '-sigopt', 'rsa_padding_mode:pss', '-sigopt', 'rsa_pss_saltlen:-1', '-sign', __dirname + '/rsa-private.pem']); - var buffer = Buffer.alloc(0); + test('PS'+bits+': openssl sign -> js verify', () => { + const input = 'iodine' + const algo = jwa('PS'+bits) + const dgst = spawn('openssl', ['dgst', '-sha'+bits, '-sigopt', 'rsa_padding_mode:pss', '-sigopt', 'rsa_pss_saltlen:-1', '-sign', __dirname + '/rsa-private.pem']) + var buffer = Buffer.alloc(0) dgst.stdout.on('data', function (buf) { - buffer = Buffer.concat([buffer, buf]); - }); + buffer = Buffer.concat([buffer, buf]) + }) dgst.stdin.write(input, function() { - dgst.stdin.end(); - }); + dgst.stdin.end() + }) dgst.on('exit', function (code) { if (code !== 0) - return t.fail('could not test interop: openssl failure'); - const sig = base64url(buffer); - - t.ok(algo.verify(input, sig, rsaPublicKey), 'should verify'); - t.notOk(algo.verify(input, sig, rsaWrongPublicKey), 'should not verify'); - t.end(); - }); - }); - }); + return asserassert.fail('could not test interop: openssl failure') + const sig = base64url(buffer) + + asserassert.ok(algo.verify(input, sig, rsaPublicKey), 'should verify') + asserasserassert.ok(!algo.verify(input, sig, rsaWrongPublicKey), 'should not verify') +}) + }) + }) } BIT_DEPTHS.forEach(function (bits) { test('ES'+bits+': signing, verifying', function (t) { - const input = 'kristen schaal'; - const algo = jwa('ES'+bits); - const sig = algo.sign(input, ecdsaPrivateKey[bits]); - t.ok(algo.verify(input, sig, ecdsaPublicKey[bits]), 'should verify'); - t.notOk(algo.verify(input, sig, ecdsaWrongPublicKey[bits]), 'should not verify'); - t.end(); - }); -}); + const input = 'kristen schaal' + const algo = jwa('ES'+bits) + const sig = algo.sign(input, ecdsaPrivateKey[bits]) + asserassert.ok(algo.verify(input, sig, ecdsaPublicKey[bits]), 'should verify') + asserasserassert.ok(!algo.verify(input, sig, ecdsaWrongPublicKey[bits]), 'should not verify') +}) +}) if (SUPPORTS_KEY_OBJECTS) { BIT_DEPTHS.forEach(function (bits) { test('ES'+bits+': signing, verifying (KeyObject)', function (t) { - const input = 'kristen schaal'; - const algo = jwa('ES'+bits); - const sig = algo.sign(input, crypto.createPrivateKey(ecdsaPrivateKey[bits])); - t.ok(algo.verify(input, sig, crypto.createPublicKey(ecdsaPublicKey[bits])), 'should verify'); - t.notOk(algo.verify(input, sig, crypto.createPublicKey(ecdsaWrongPublicKey[bits])), 'should not verify'); - t.end(); - }); - }); + const input = 'kristen schaal' + const algo = jwa('ES'+bits) + const sig = algo.sign(input, crypto.createPrivateKey(ecdsaPrivateKey[bits])) + asserassert.ok(algo.verify(input, sig, crypto.createPublicKey(ecdsaPublicKey[bits])), 'should verify') + asserasserassert.ok(!algo.verify(input, sig, crypto.createPublicKey(ecdsaWrongPublicKey[bits])), 'should not verify') +}) + }) } BIT_DEPTHS.forEach(function (bits) { - test('ES'+bits+': openssl sign -> js verify', function (t) { - const input = 'strawberry'; - const algo = jwa('ES'+bits); - const dgst = spawn('openssl', ['dgst', '-sha'+bits, '-sign', __dirname + '/ec'+bits+'-private.pem']); - var buffer = Buffer.alloc(0); - dgst.stdin.end(input); + test('ES'+bits+': openssl sign -> js verify', () => { + const input = 'strawberry' + const algo = jwa('ES'+bits) + const dgst = spawn('openssl', ['dgst', '-sha'+bits, '-sign', __dirname + '/ec'+bits+'-private.pem']) + var buffer = Buffer.alloc(0) + dgst.stdin.end(input) dgst.stdout.on('data', function (buf) { - buffer = Buffer.concat([buffer, buf]); - }); + buffer = Buffer.concat([buffer, buf]) + }) dgst.on('exit', function (code) { if (code !== 0) - return t.fail('could not test interop: openssl failure'); - const sig = formatEcdsa.derToJose(buffer, 'ES' + bits); - t.ok(algo.verify(input, sig, ecdsaPublicKey[bits]), 'should verify'); - t.notOk(algo.verify(input, sig, ecdsaWrongPublicKey[bits]), 'should not verify'); - t.end(); - }); - }); -}); + return asserassert.fail('could not test interop: openssl failure') + const sig = formatEcdsa.derToJose(buffer, 'ES' + bits) + asserassert.ok(algo.verify(input, sig, ecdsaPublicKey[bits]), 'should verify') + asserasserassert.ok(!algo.verify(input, sig, ecdsaWrongPublicKey[bits]), 'should not verify') +}) + }) +}) BIT_DEPTHS.forEach(function (bits) { - const input = 'bob\'s'; - const inputFile = path.join(__dirname, 'interop.input.txt'); - const signatureFile = path.join(__dirname, 'interop.sig.txt'); + const input = 'bob\'s' + const inputFile = path.join(__dirname, 'interop.input.txt') + const signatureFile = path.join(__dirname, 'interop.sig.txt') function opensslVerify(keyfile) { return spawn('openssl', [ @@ -255,35 +241,33 @@ BIT_DEPTHS.forEach(function (bits) { '-verify', keyfile, '-signature', signatureFile, inputFile - ]); + ]) } - test('ES'+bits+': js sign -> openssl verify', function (t) { - const publicKeyFile = path.join(__dirname, 'ec'+bits+'-public.pem'); - const wrongPublicKeyFile = path.join(__dirname, 'ec'+bits+'-wrong-public.pem'); - const privateKey = ecdsaPrivateKey[bits]; + test('ES'+bits+': js sign -> openssl verify', () => { + const publicKeyFile = path.join(__dirname, 'ec'+bits+'-public.pem') + const wrongPublicKeyFile = path.join(__dirname, 'ec'+bits+'-wrong-public.pem') + const privateKey = ecdsaPrivateKey[bits] const signature = formatEcdsa.joseToDer( jwa('ES'+bits).sign(input, privateKey), 'ES' + bits - ); - fs.writeFileSync(inputFile, input); - fs.writeFileSync(signatureFile, signature); - - t.plan(2); - opensslVerify(publicKeyFile).on('exit', function (code) { - t.same(code, 0, 'should be a successful exit'); - }); + ) + fs.writeFileSync(inputFile, input) + fs.writeFileSync(signatureFile, signature) +opensslVerify(publicKeyFile).on('exit', function (code) { + assert.deepStrictEqual(code, 0, 'should be a successful exit') + }) opensslVerify(wrongPublicKeyFile).on('exit', function (code) { - t.same(code, 1, 'should be invalid'); - }); - }); -}); + assert.deepStrictEqual(code, 1, 'should be invalid') + }) + }) +}) BIT_DEPTHS.forEach(function (bits) { - const input = 'burgers'; - const inputFile = path.join(__dirname, 'interop.input.txt'); - const signatureFile = path.join(__dirname, 'interop.sig.txt'); + const input = 'burgers' + const inputFile = path.join(__dirname, 'interop.input.txt') + const signatureFile = path.join(__dirname, 'interop.sig.txt') function opensslVerify(keyfile) { return spawn('openssl', [ @@ -292,35 +276,31 @@ BIT_DEPTHS.forEach(function (bits) { '-verify', keyfile, '-signature', signatureFile, inputFile - ]); + ]) } - test('RS'+bits+': js sign -> openssl verify', function (t) { - const publicKeyFile = path.join(__dirname, 'rsa-public.pem'); - const wrongPublicKeyFile = path.join(__dirname, 'rsa-wrong-public.pem'); - const privateKey = rsaPrivateKey; + test('RS'+bits+': js sign -> openssl verify', () => { + const publicKeyFile = path.join(__dirname, 'rsa-public.pem') + const wrongPublicKeyFile = path.join(__dirname, 'rsa-wrong-public.pem') + const privateKey = rsaPrivateKey const signature = base64url.toBuffer( jwa('RS'+bits).sign(input, privateKey) - ); - fs.writeFileSync(signatureFile, signature); - fs.writeFileSync(inputFile, input); - - t.plan(2); - opensslVerify(publicKeyFile).on('exit', function (code) { - t.same(code, 0, 'should be a successful exit'); - }); + ) + fs.writeFileSync(signatureFile, signature) + fs.writeFileSync(inputFile, input) +opensslVerify(publicKeyFile).on('exit', function (code) { + assert.deepStrictEqual(code, 0, 'should be a successful exit') + }) opensslVerify(wrongPublicKeyFile).on('exit', function (code) { - t.same(code, 1, 'should be invalid'); - }); - }); -}); - -if (semver.satisfies(nodeVersion, '^6.12.0 || >=8.0.0')) { + assert.deepStrictEqual(code, 1, 'should be invalid') + }) + }) +});{ BIT_DEPTHS.forEach(function (bits) { - const input = 'burgers'; - const inputFile = path.join(__dirname, 'interop.input.txt'); - const signatureFile = path.join(__dirname, 'interop.sig.txt'); + const input = 'burgers' + const inputFile = path.join(__dirname, 'interop.input.txt') + const signatureFile = path.join(__dirname, 'interop.sig.txt') function opensslVerify(keyfile) { return spawn('openssl', [ @@ -330,181 +310,164 @@ if (semver.satisfies(nodeVersion, '^6.12.0 || >=8.0.0')) { '-verify', keyfile, '-signature', signatureFile, inputFile - ]); + ]) } - test('PS'+bits+': js sign -> openssl verify', function (t) { - const publicKeyFile = path.join(__dirname, 'rsa-public.pem'); - const wrongPublicKeyFile = path.join(__dirname, 'rsa-wrong-public.pem'); - const privateKey = rsaPrivateKey; + test('PS'+bits+': js sign -> openssl verify', () => { + const publicKeyFile = path.join(__dirname, 'rsa-public.pem') + const wrongPublicKeyFile = path.join(__dirname, 'rsa-wrong-public.pem') + const privateKey = rsaPrivateKey const signature = base64url.toBuffer( jwa('PS'+bits).sign(input, privateKey) - ); - fs.writeFileSync(signatureFile, signature); - fs.writeFileSync(inputFile, input); - - t.plan(2); - opensslVerify(publicKeyFile).on('exit', function (code) { - t.same(code, 0, 'should be a successful exit'); - }); + ) + fs.writeFileSync(signatureFile, signature) + fs.writeFileSync(inputFile, input) +opensslVerify(publicKeyFile).on('exit', function (code) { + assert.deepStrictEqual(code, 0, 'should be a successful exit') + }) opensslVerify(wrongPublicKeyFile).on('exit', function (code) { - t.same(code, 1, 'should be invalid'); - }); - }); - }); + assert.deepStrictEqual(code, 1, 'should be invalid') + }) + }) + }) } -test('jwa: none', function (t) { - const input = 'whatever'; - const algo = jwa('none'); - const sig = algo.sign(input); - t.ok(algo.verify(input, sig), 'should verify'); - t.notOk(algo.verify(input, 'something'), 'shoud not verify'); - t.end(); -}); +test('jwa: none', () => { + const input = 'whatever' + const algo = jwa('none') + const sig = algo.sign(input) + asserassert.ok(algo.verify(input, sig), 'should verify') + asserasserassert.ok(!algo.verify(input, 'something'), 'shoud not verify') +}) -test('jwa: some garbage algorithm', function (t) { +test('jwa: some garbage algorithm', () => { try { - jwa('something bogus'); - t.fail('should throw'); + jwa('something bogus') + asserassert.fail('should throw') } catch(ex) { - t.same(ex.name, 'TypeError'); - t.ok(ex.message.match(/valid algorithm/), 'should say something about algorithms'); + assert.deepStrictEqual(ex.name, 'TypeError') + asserassert.ok(ex.message.match(/valid algorithm/), 'should say something about algorithms') } - t.end(); -}); +}) ['hs256', 'nonE', 'ps256', 'es256', 'rs256'].forEach(function (alg) { - test('jwa: non-IANA names', function (t) { + test('jwa: non-IANA names', () => { try { - jwa(alg); - t.fail('should throw'); + jwa(alg) + asserassert.fail('should throw') } catch(ex) { - t.same(ex.name, 'TypeError'); - t.ok(ex.message.match(/valid algorithm/), 'should say something about algorithms'); + assert.deepStrictEqual(ex.name, 'TypeError') + asserassert.ok(ex.message.match(/valid algorithm/), 'should say something about algorithms') } - t.end(); - }); -}); +}) +}) ['ahs256b', 'anoneb', 'none256', 'rsnone'].forEach(function (superstringAlg) { - test('jwa: superstrings of other algorithms', function (t) { + test('jwa: superstrings of other algorithms', () => { try { - jwa(superstringAlg); - t.fail('should throw'); + jwa(superstringAlg) + asserassert.fail('should throw') } catch(ex) { - t.same(ex.name, 'TypeError'); - t.ok(ex.message.match(/valid algorithm/), 'should say something about algorithms'); + assert.deepStrictEqual(ex.name, 'TypeError') + asserassert.ok(ex.message.match(/valid algorithm/), 'should say something about algorithms') } - t.end(); - }); -}); +}) +}) ['rs', 'ps', 'es', 'hs'].forEach(function (partialAlg) { - test('jwa: partial strings of other algorithms', function (t) { + test('jwa: partial strings of other algorithms', () => { try { - jwa(partialAlg); - t.fail('should throw'); + jwa(partialAlg) + asserassert.fail('should throw') } catch(ex) { - t.same(ex.name, 'TypeError'); - t.ok(ex.message.match(/valid algorithm/), 'should say something about algorithms'); + assert.deepStrictEqual(ex.name, 'TypeError') + asserassert.ok(ex.message.match(/valid algorithm/), 'should say something about algorithms') } - t.end(); - }); -}); +}) +}) test('jwa: hs512, missing secret', function (t) { - const algo = jwa('HS512'); + const algo = jwa('HS512') try { - algo.sign('some stuff'); - t.fail('should throw'); + algo.sign('some stuff') + asserassert.fail('should throw') } catch(ex) { - t.same(ex.name, 'TypeError'); - t.ok(ex.message.match(/secret/), 'should say something about secrets'); + assert.deepStrictEqual(ex.name, 'TypeError') + asserassert.ok(ex.message.match(/secret/), 'should say something about secrets') } - t.end(); -}); +}) test('jwa: hs512, weird input type', function (t) { - const algo = jwa('HS512'); - const input = {a: ['whatever', 'this', 'is']}; - const secret = 'bones'; - const sig = algo.sign(input, secret); - t.ok(algo.verify(input, sig, secret), 'should verify'); - t.notOk(algo.verify(input, sig, 'other thing'), 'should not verify'); - t.end(); -}); + const algo = jwa('HS512') + const input = {a: ['whatever', 'this', 'is']} + const secret = 'bones' + const sig = algo.sign(input, secret) + asserassert.ok(algo.verify(input, sig, secret), 'should verify') + asserasserassert.ok(!algo.verify(input, sig, 'other thing'), 'should not verify') +}) test('jwa: rs512, weird input type', function (t) { - const algo = jwa('RS512'); - const input = {a: ['whatever', 'this', 'is']}; - const sig = algo.sign(input, rsaPrivateKey); - t.ok(algo.verify(input, sig, rsaPublicKey), 'should verify'); - t.notOk(algo.verify(input, sig, rsaWrongPublicKey), 'should not verify'); - t.end(); -}); + const algo = jwa('RS512') + const input = {a: ['whatever', 'this', 'is']} + const sig = algo.sign(input, rsaPrivateKey) + asserassert.ok(algo.verify(input, sig, rsaPublicKey), 'should verify') + asserasserassert.ok(!algo.verify(input, sig, rsaWrongPublicKey), 'should not verify') +}) test('jwa: rs512, missing signing key', function (t) { - const algo = jwa('RS512'); + const algo = jwa('RS512') try { - algo.sign('some stuff'); - t.fail('should throw'); + algo.sign('some stuff') + asserassert.fail('should throw') } catch(ex) { - t.same(ex.name, 'TypeError'); - t.ok(ex.message.match(/key/), 'should say something about keys'); + assert.deepStrictEqual(ex.name, 'TypeError') + asserassert.ok(ex.message.match(/key/), 'should say something about keys') } - t.end(); -}); +}) test('jwa: rs512, missing verifying key', function (t) { - const algo = jwa('RS512'); - const input = {a: ['whatever', 'this', 'is']}; - const sig = algo.sign(input, rsaPrivateKey); + const algo = jwa('RS512') + const input = {a: ['whatever', 'this', 'is']} + const sig = algo.sign(input, rsaPrivateKey) try { - algo.verify(input, sig); - t.fail('should throw'); + algo.verify(input, sig) + asserassert.fail('should throw') } catch(ex) { - t.same(ex.name, 'TypeError'); - t.ok(ex.message.match(/key/), 'should say something about keys'); + assert.deepStrictEqual(ex.name, 'TypeError') + asserassert.ok(ex.message.match(/key/), 'should say something about keys') } - t.end(); -}); - -if (semver.satisfies(nodeVersion, '^6.12.0 || >=8.0.0')) { +});{ test('jwa: ps512, weird input type', function (t) { - const algo = jwa('PS512'); - const input = {a: ['whatever', 'this', 'is']}; - const sig = algo.sign(input, rsaPrivateKey); - t.ok(algo.verify(input, sig, rsaPublicKey), 'should verify'); - t.notOk(algo.verify(input, sig, rsaWrongPublicKey), 'should not verify'); - t.end(); - }); + const algo = jwa('PS512') + const input = {a: ['whatever', 'this', 'is']} + const sig = algo.sign(input, rsaPrivateKey) + asserassert.ok(algo.verify(input, sig, rsaPublicKey), 'should verify') + asserasserassert.ok(!algo.verify(input, sig, rsaWrongPublicKey), 'should not verify') +}) test('jwa: ps512, missing signing key', function (t) { - const algo = jwa('PS512'); + const algo = jwa('PS512') try { - algo.sign('some stuff'); - t.fail('should throw'); + algo.sign('some stuff') + asserassert.fail('should throw') } catch(ex) { - t.same(ex.name, 'TypeError'); - t.ok(ex.message.match(/key/), 'should say something about keys'); + assert.deepStrictEqual(ex.name, 'TypeError') + asserassert.ok(ex.message.match(/key/), 'should say something about keys') } - t.end(); - }); +}) test('jwa: ps512, missing verifying key', function (t) { - const algo = jwa('PS512'); - const input = {a: ['whatever', 'this', 'is']}; - const sig = algo.sign(input, rsaPrivateKey); + const algo = jwa('PS512') + const input = {a: ['whatever', 'this', 'is']} + const sig = algo.sign(input, rsaPrivateKey) try { - algo.verify(input, sig); - t.fail('should throw'); + algo.verify(input, sig) + asserassert.fail('should throw') } catch(ex) { - t.same(ex.name, 'TypeError'); - t.ok(ex.message.match(/key/), 'should say something about keys'); + assert.deepStrictEqual(ex.name, 'TypeError') + asserassert.ok(ex.message.match(/key/), 'should say something about keys') } - t.end(); - }); +}) } From 7dd65332903029b9f9ba6c061e68aa362c1883b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Thu, 5 Feb 2026 23:01:09 +0100 Subject: [PATCH 2/2] skipped a beat --- README.md | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0a37a81..b56e6d1 100644 --- a/README.md +++ b/README.md @@ -32,18 +32,18 @@ none | No digital signature or MAC value included - **Async/Await**: All operations are async for non-blocking execution - **Zero Production Dependencies**: No external dependencies, pure Web Crypto API -## Breaking Changes in v4.0.0 +## Breaking Changes in v3.0.0 - **All signing and verification functions are now async** (return Promises) - Uses Web Crypto API instead of Node.js crypto module - Requires `await` when calling `sign()` and `verify()` methods: ```javascript -// Before (v3.x - synchronous) +// Before (v2.x - synchronous) const sig = algo.sign(message, key) const isValid = algo.verify(message, sig, key) -// After (v4.x - asynchronous) +// After (v3.x - asynchronous) const sig = await algo.sign(message, key) const isValid = await algo.verify(message, sig, key) ``` diff --git a/package.json b/package.json index 236726b..4e6244f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jwa", - "version": "4.0.0", + "version": "3.0.0", "description": "JWA implementation (supports all JWS algorithms)", "type": "module", "main": "index.js",