UNPKG

micro-rsa-dsa-dh

Version:

Minimal implementation of older cryptography algorithms: RSA, DSA, DH, ElGamal

315 lines 14.1 kB
import { hmac } from '@noble/hashes/hmac.js'; import { concatBytes, hexToBytes, isBytes, randomBytes } from '@noble/hashes/utils.js'; import { isProbablePrime } from "./primality.js"; import { bytesToNumber, getFieldBytesLength, getMinHashLength, I2OSP, invert, mapHashToField, mod, numberToBytes, OS2IP, pow, } from "./utils.js"; export class DERErr extends Error { constructor(m = '') { super(m); } } export const DER = { // asn.1 DER encoding utils Err: DERErr, _parseInt(data) { const { Err: E } = DER; if (data.length < 2 || data[0] !== 0x02) throw new E('Invalid signature integer tag'); const len = data[1]; const res = data.subarray(2, len + 2); if (!len || res.length !== len) throw new E('Invalid signature integer: wrong length'); // https://crypto.stackexchange.com/a/57734 Leftmost bit of first byte is 'negative' flag, // since we always use positive integers here. It must always be empty: // - add zero byte if exists // - if next byte doesn't have a flag, leading zero is not allowed (minimal encoding) if (res[0] & 0b10000000) throw new E('Invalid signature integer: negative'); if (res[0] === 0x00 && !(res[1] & 0b10000000)) throw new E('Invalid signature integer: unnecessary leading zero'); return { d: bytesToNumber(res), l: data.subarray(len + 2) }; // d is data, l is left }, toSig(hex) { // parse DER signature const { Err: E } = DER; const data = typeof hex === 'string' ? hexToBytes(hex) : hex; //ut.abytes(data); let l = data.length; if (l < 2 || data[0] != 0x30) throw new E('Invalid signature tag'); if (data[1] !== l - 2) throw new E('Invalid signature: incorrect length'); const { d: r, l: sBytes } = DER._parseInt(data.subarray(2)); const { d: s, l: rBytesLeft } = DER._parseInt(sBytes); if (rBytesLeft.length) throw new E('Invalid signature: left bytes after parsing'); return { r, s }; }, hexFromSig(sig) { // Add leading zero if first byte has negative bit enabled. More details in '_parseInt' const slice = (s) => (Number.parseInt(s[0], 16) & 0b1000 ? '00' + s : s); const h = (num) => { const hex = num.toString(16); return hex.length & 1 ? `0${hex}` : hex; }; const s = slice(h(sig.s)); const r = slice(h(sig.r)); const shl = s.length / 2; const rhl = r.length / 2; const sl = h(shl); const rl = h(rhl); return `30${h(rhl + shl + 4)}02${rl}${r}02${sl}${s}`; }, }; // Table C.1. Minimum number of Miller-Rabin iterations for DSA const isProbablePrimeDSA_P = (L, n, randFn = randomBytes) => isProbablePrime(n, L === 3072 ? 2 : 3, randFn); const isProbablePrimeDSA_Q = (N, n, randFn = randomBytes) => isProbablePrime(n, N === 160 ? 19 : N === 224 ? 24 : 27, randFn); /** * Based on FIPS186-4 (A.1.1.2 Generation of the Probable Primes p and q Using an Approved Hash Function) * @param L - The desired length of the prime p (in bits). * @param N - The desired length of the prime q (in bits). * @param seed - seed: Uint8Array or length in bits (greater or equal to N) * @param hash - hash function */ function genDSAPrimes(L, N, hash, seed, randFn = randomBytes) { if (!Number.isSafeInteger(L) || !Number.isSafeInteger(N)) throw new Error('wrong L/N params'); // From FIPS186-4: 4.2 Selection of Parameter Sizes and Hash Functions for DSA const pairs = { 1024: [160], 2048: [224, 256], 3072: [256] }; if (!pairs[L].includes(N)) throw new Error(`Invalid L/N pair: possible N=${pairs[L]}`); const outlen = hash.outputLen * 8; // NOTE: we ask user to provide seed instead if (!Number.isSafeInteger(seed) && !isBytes(seed) && seed !== undefined) throw new Error('wrong seed: should be number of bits or Uint8Array'); const seedOrLen = seed || N; const seedlen = isBytes(seedOrLen) ? seedOrLen.length * 8 : seedOrLen; if (seedlen < N || seedlen % 8 !== 0) throw new Error('invalid seedlen'); const seedBytesLen = seedlen / 8; const n = Math.ceil(L / outlen) - 1; // 3 const b = L - 1 - n * outlen; // 4 const mask = 2n ** BigInt(N - 1); while (true) { const domainParameterSeed = isBytes(seedOrLen) ? seedOrLen : randFn(seedBytesLen); const U = bytesToNumber(hash(domainParameterSeed)) % mask; // 6 let q = mask + U + 1n - (U % 2n); // 7 if (!isProbablePrimeDSA_Q(N, q, randFn)) { if (isBytes(seed)) throw new Error('Fixed seed, Q is not prime'); continue; // 9 } let offset = 1n; // 10 for (let counter = 0; counter < 4 * L; counter++) { // 11.1 const V = []; for (let j = 0; j <= n; j++) { const seedWithOffset = bytesToNumber(domainParameterSeed) + offset + (BigInt(j) % 2n ** BigInt(seedlen)); V.push(bytesToNumber(hash(numberToBytes(seedWithOffset, seedBytesLen)))); } let W = V[0]; for (let i = 1; i < n; i++) W += V[i] * 2n ** BigInt(i * outlen); W += (V[n] % 2n ** BigInt(b)) * 2n ** BigInt(n * outlen); // 11.2 const X = W + 2n ** BigInt(L - 1); // 11.3: 0 ≤ W < 2L–1; hence, 2L–1 ≤ X < 2L const c = X % (2n * q); // 11.4 const p = X - (c - 1n); // 11.5: p ≡ 1 (mod 2q). if (p >= 2n ** BigInt(L - 1) && isProbablePrimeDSA_P(L, p, randFn)) { return { p, q, domainParameterSeed, counter, hash }; } offset += BigInt(n) + 1n; // 11.9 } } } /** * Based on FIPS186-4: A.2.3 Verifiable Canonical Generation of the Generator g * @param res - result of genDSAPrimes * @param hash - hash algorihm function * @param index - index (key separation, for example: index = 1 for digital signatures and with index = 2 for key establishment.) */ function genDSAGenerator(res, index) { if (!Number.isSafeInteger(index) || index < 1 || index > 255) throw new Error('invalid index'); const { p, q, domainParameterSeed, hash } = res; if (typeof p !== 'bigint' || typeof q !== 'bigint' || !isBytes(domainParameterSeed) || typeof hash !== 'function') { throw new Error('wrong params'); } const e = (p - 1n) / q; // Step 3 for (let count = 0;;) { count++; // Step 5 count &= 0xffff; // 16 bit integer if (count === 0) throw new Error('counter wrapped'); // Step 6 const U = concatBytes(domainParameterSeed, hexToBytes('6767656e'), // 'ggen' in ascii new Uint8Array([index]), new Uint8Array([count >> 8, count & 0xff])); // Step 7 const W = bytesToNumber(hash(U)); // Step 8 const g = pow(W, e, p); // W ** e % P if (g >= 2n) return g; } } /** * * @param L - The desired length of the prime p (in bits). * @param N - The desired length of the prime q (in bits). * @param hash - hash function * @param index - index (key separation, for example: index = 1 for digital signatures and with index = 2 for key establishment.) * @param seed - seed: Uint8Array or length in bits (greater or equal to N) * @example * const params = genDSAParams(3072, 256, sha256, 1); // Generate random params * @example * const params = genDSAParams(3072, 256, sha256, 1, new Uint8Array([...])); // Generate params from known seed */ export function genDSAParams(L, N, hash, index, seed, randFn = randomBytes) { if (typeof hash !== 'function') throw new Error('wrong hash'); const res = genDSAPrimes(L, N, hash, seed, randFn); const g = genDSAGenerator(res, index); return { ...res, index, g }; } /** * Minimal HMAC-DRBG from NIST 800-90 for RFC6979 sigs. * @returns function that will call DRBG until 2nd arg returns something meaningful * @example * const drbg = createHmacDRBG<Key>(32, 32, hmac); * drbg(seed, bytesToKey); // bytesToKey must return Key or undefined */ export function createHmacDrbg(hashLen, qByteLen, hmacFn) { if (typeof hashLen !== 'number' || hashLen < 2) throw new Error('hashLen must be a number'); if (typeof qByteLen !== 'number' || qByteLen < 2) throw new Error('qByteLen must be a number'); if (typeof hmacFn !== 'function') throw new Error('hmacFn must be a function'); const NULL = Uint8Array.of(); const byte0 = Uint8Array.of(0); const byte1 = Uint8Array.of(1); const maxDrbgIters = 1000; // Step B, Step C: set hashLen to 8*ceil(hlen/8) let v = new Uint8Array(hashLen); // Minimal non-full-spec HMAC-DRBG from NIST 800-90 for RFC6979 sigs. let k = new Uint8Array(hashLen); // Steps B and C of RFC6979 3.2: set hashLen, in our case always same let i = 0; // Iterations counter, will throw when over 1000 const reset = () => { v.fill(1); k.fill(0); i = 0; }; const h = (...b) => hmacFn(k, v, ...b); // hmac(k)(v, ...values) const reseed = (seed = NULL) => { // HMAC-DRBG reseed() function. Steps D-G k = h(byte0, seed); // k = hmac(k || v || 0x00 || seed) v = h(); // v = hmac(k || v) if (seed.length === 0) return; k = h(byte1, seed); // k = hmac(k || v || 0x01 || seed) v = h(); // v = hmac(k || v) }; const gen = () => { // HMAC-DRBG generate() function if (i++ >= maxDrbgIters) throw new Error('drbg: tried max iterations'); let len = 0; const out = []; while (len < qByteLen) { v = h(); const sl = v.slice(); out.push(sl); len += v.length; } return concatBytes(...out); }; const genUntil = (seed, pred) => { reset(); reseed(seed); // Steps D-G let res = undefined; // Step H: grind until k is in [1..n-1] while (!(res = pred(gen()))) reseed(); reset(); return res; }; return genUntil; } /** * Simplified DSA implementation focusing on simplicity and basic functionality. * @param params - DSA parameters {p, q, g} * @returns DSA key generation, signing, and verification functions */ export const DSA = (params) => { const { p, q, g, hash } = params; if (typeof p !== 'bigint' || typeof q !== 'bigint' || typeof g !== 'bigint') throw new Error('wrong DSAParams'); if (typeof hash !== 'function') throw new Error('wrong hash'); const fieldBytes = getFieldBytesLength(q); const fieldBits = q.toString(2).length; // RFC6979: ensure ECDSA msg is X bytes and < N. RFC suggests optional truncating via bits2octets. // FIPS 186-4 4.6 suggests the leftmost min(nBitLen, outLen) bits, which matches bits2int. // bits2int can produce res>N, we can do mod(res, N) since the bitLen is the same. // int2octets can't be used; pads small msgs with 0: unacceptatble for trunc as per RFC vectors const bits2int = function (bytes) { // For curves with nBitLength % 8 !== 0: bits2octets(bits2octets(m)) !== bits2octets(m) // for some cases, since bytes.length * 8 is not actual bitLength. const num = bytesToNumber(bytes); // check for == u8 done here const delta = bytes.length * 8 - fieldBits; // truncate to fieldBits leftmost bits return delta > 0 ? num >> BigInt(delta) : num; }; return { randomPrivateKey() { return bytesToNumber(mapHashToField(randomBytes(getMinHashLength(q)), q)); }, getPublicKey: (privateKey) => { return pow(g, privateKey, p); }, sign: (privateKey, message) => { const mHash = hash(message); const hmacFn = (key, ...msgs) => hmac(hash, key, concatBytes(...msgs)); const drbg = createHmacDrbg(hash.outputLen, fieldBytes, hmacFn); const h = mod(bits2int(mHash), q); const seed = concatBytes(I2OSP(privateKey % q, fieldBytes), I2OSP(h, fieldBytes)); // Step D of RFC6979 3.2 const k = drbg(seed, (kBytes) => { kBytes = kBytes.subarray(0, fieldBytes); // hash can be bigger than fieldBytes const k = OS2IP(kBytes); if (1n < k && k < q - 1n) return k; return; }); // Steps B, C, D, E, F, G const r = pow(g, k, p) % q; // (g^k % p) % q const ik = invert(k, q); // k^-1 mod n const s = mod(ik * mod(h + r * privateKey, q), q); // compact (P1363) const res = concatBytes(numberToBytes(r, fieldBytes), numberToBytes(s, fieldBytes)); return res; }, verify: (publicKey, msg, sig) => { let r, s; // Signature can be represented in 2 ways: compact (2*nByteLength) & DER (variable-length). // Since DER can also be 2*nByteLength bytes, we check for it first. try { ({ r, s } = DER.toSig(sig)); } catch (derError) { if (!(derError instanceof DER.Err)) throw derError; r = bytesToNumber(sig.slice(0, fieldBytes)); s = bytesToNumber(sig.slice(fieldBytes, 2 * fieldBytes)); } if (r <= 0n || r >= q || s <= 0n || s >= q) return false; const h = mod(bits2int(hash(msg)), q); const is = invert(s, q); // s^-1 const u1 = mod(h * is, q); // u1 = hs^-1 mod n const u2 = mod(r * is, q); // u2 = rs^-1 mod n const t0 = pow(g, u1, p); const t1 = pow(publicKey, u2, p); const v = ((t0 * t1) % p) % q; return v === r; }, }; }; export const _TEST = { genDSAPrimes, genDSAGenerator, }; //# sourceMappingURL=dsa.js.map