UNPKG

@noble/post-quantum

Version:

Auditable & minimal JS implementation of post-quantum cryptography: FIPS 203, 204, 205, Falcon

281 lines 13 kB
/** * Post-Quantum Hybrid Cryptography * * The current implementation is flawed and likely redundant. We should offer * a small, generic API to compose hybrid schemes instead of reimplementing * protocol-specific logic (SSH, GPG, etc.) with ad hoc encodings. * * 1. Core Issues * - sign/verify: implemented as two separate operations with different keys. * - EC getSharedSecret: could be refactored into a proper KEM. * - Multiple calls: keys, signatures, and shared secrets could be * concatenated to reduce the number of API invocations. * - Reinvention: most libraries add strange domain separations and * encodings instead of simple byte concatenation. * * 2. API Goals * - Provide primitives to build hybrids generically. * - Avoid embedding SSH- or GPG-specific formats in the core API. * * 3. Edge Cases * • Variable-length signatures: * - DER-encoded (Weierstrass curves). * - Falcon (unpadded). * - Concatenation works only if length is fixed; otherwise a length * prefix is required (but that breaks compatibility). * * • getSharedSecret: * - Default: non-KEM (authenticated ECDH). * - KEM conversion: generate a random SK to remove implicit auth. * * 4. Common Pitfalls * - Seed expansion: * • Expanding a small seed into multiple keys reduces entropy. * • API should allow identity mapping (no expansion). * * - Skipping full point encoding: * • Some omit the compression byte (parity) for WebCrypto compatibility. * • Better: hash the raw secret; coordinate output is already non-uniform. * • Some curves (e.g., X448) produce secrets that must be re-hashed to match * symmetric-key lengths. * * - Combiner inconsistencies: * • Different domain separations and encodings across libraries. * • Should live at the application layer, since key lengths vary. * * 5. Protocol Examples * - SSH: * • Concatenate keys. * • Combiner: SHA-512. * * - GPG: * • Concatenate keys. * • Combiner: * SHA3-256(kemShare || ecdhShare || ciphertext || pubKey || algId || domSep || len(domSep)) * * - TLS: * • Transcript-based derivation (HKDF). * * 6. Relevant Specs & Implementations * - IETF Hybrid KEM drafts: * • draft-irtf-cfrg-hybrid-kems * • draft-connolly-cfrg-xwing-kem * • draft-westerbaan-tls-xyber768d00 * * - PQC Libraries: * • superdilithium (cyph/pqcrypto.js) – low adoption. * • hybrid-pqc (DogeProtocol, quantumcoinproject) – complex encodings. * * 7. Signatures * - Ed25519: fixed-size, easy to support. * - Variable-size: introduces custom format requirements; best left to * higher-level code. * * @module */ /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */ import { type EdDSA } from '@noble/curves/abstract/edwards.js'; import { type MontgomeryECDH } from '@noble/curves/abstract/montgomery.js'; import { type ECDSA } from '@noble/curves/abstract/weierstrass.js'; import { type CHash, type CHashXOF } from '@noble/hashes/utils.js'; import { type KEM, type Signer, type TArg, type TRet } from './utils.ts'; type CurveECDH = ECDSA | MontgomeryECDH; type CurveSign = ECDSA | EdDSA; /** * Wraps an ECDH-capable curve as a KEM. * Shared secrets stay in the wrapped curve's raw ECDH byte format with no built-in KDF. * On SEC 1 / Weierstrass curves, that means the compressed shared-point body without the * 1-byte `0x02` / `0x03` prefix. * The X25519 path also leaves RFC 7748's optional all-zero shared-secret check to callers. * @param curve - Curve with `getSharedSecret`. * @param allowZeroKey - Legacy vector-matching toggle for Weierstrass keygen. * On Weierstrass curves this removes the usual post-reduction `+1` shift, changing seeded scalar * reduction from `[1, ORDER)` to direct reduction into `[0, ORDER)`. It does not make scalar zero * valid: an all-zero seed still derives scalar `0` and throws in `curve.getPublicKey(...)`. * Only supported on Weierstrass/ECDSA curves. * @returns KEM wrapper over the curve. * @throws If the curve does not expose `getSharedSecret`. {@link Error} * @example * Wrap an ECDH-capable curve as a generic KEM. * ```ts * import { x25519 } from '@noble/curves/ed25519.js'; * import { ecdhKem } from '@noble/post-quantum/hybrid.js'; * const kem = ecdhKem(x25519); * const publicKeyLen = kem.lengths.publicKey; * ``` */ export declare function ecdhKem(curve: CurveECDH, allowZeroKey?: boolean): TRet<KEM>; /** * Wraps a curve signer as a generic `Signer`. * Signatures stay in the wrapped curve's native byte encoding. * This wrapper does not normalize or document which per-curve signing options are meaningful. * @param curve - Curve with `sign` and `verify`. * @param allowZeroKey - Legacy vector-matching toggle for Weierstrass keygen. * On Weierstrass curves this removes the usual post-reduction `+1` shift, changing seeded scalar * reduction from `[1, ORDER)` to direct reduction into `[0, ORDER)`. It does not make scalar zero * valid: an all-zero seed still derives scalar `0` and throws in `curve.getPublicKey(...)`. * Only supported on Weierstrass/ECDSA curves. * @returns Signer wrapper over the curve. * @throws If the curve does not expose `sign` and `verify`. {@link Error} * @example * Wrap a curve signer as a generic signer. * ```ts * import { ed25519 } from '@noble/curves/ed25519.js'; * import { ecSigner } from '@noble/post-quantum/hybrid.js'; * const signer = ecSigner(ed25519); * const sigLen = signer.lengths.signature; * ``` */ export declare function ecSigner(curve: CurveSign, allowZeroKey?: boolean): TRet<Signer>; /** Seed-expansion callback used by the hybrid combiners. */ export type ExpandSeed = (seed: TArg<Uint8Array>, len: number) => TRet<Uint8Array>; type XOF = CHashXOF<any, { dkLen: number; }>; /** * Adapts an XOF into an `ExpandSeed` callback. * The returned callback interprets its second argument as an output byte length passed as `dkLen`. * @param xof - Extendable-output hash function. * @returns Seed expander using `dkLen`. * @example * Adapt an XOF into a seed expander. * ```ts * import { shake256 } from '@noble/hashes/sha3.js'; * import { expandSeedXof } from '@noble/post-quantum/hybrid.js'; * const expandSeed = expandSeedXof(shake256); * const seed = expandSeed(new Uint8Array([1]), 4); * ``` */ export declare function expandSeedXof(xof: TArg<XOF>): TRet<ExpandSeed>; /** Combines public keys, ciphertexts, and shared secrets into one shared secret. */ export type Combiner = (publicKeys: TArg<Uint8Array[]>, cipherTexts: TArg<Uint8Array[]>, sharedSecrets: TArg<Uint8Array[]>) => TRet<Uint8Array>; /** * Combines multiple KEMs into one composite KEM. * @param realSeedLen - Input seed length expected by `expandSeed`. * @param realMsgLen - Shared-secret length returned by `combiner`. * @param expandSeed - Seed expander used to derive per-KEM seeds. * @param combiner - Combines the per-KEM outputs into one shared secret. * @param kems - KEM implementations to combine. * @returns Composite KEM. * @example * Combine multiple KEMs into one composite KEM. * ```ts * import { shake256 } from '@noble/hashes/sha3.js'; * import { combineKEMS, expandSeedXof } from '@noble/post-quantum/hybrid.js'; * import { ml_kem768 } from '@noble/post-quantum/ml-kem.js'; * const hybrid = combineKEMS( * 32, * 32, * expandSeedXof(shake256), * (_pk, _ct, sharedSecrets) => sharedSecrets[0], * ml_kem768, * ml_kem768 * ); * const { publicKey } = hybrid.keygen(); * ``` */ export declare function combineKEMS(realSeedLen: number | undefined, // how much bytes expandSeed expects realMsgLen: number | undefined, // how much bytes combiner returns expandSeed: TArg<ExpandSeed>, combiner: TArg<Combiner>, ...kems: TArg<KEM[]>): TRet<KEM>; /** * Combines multiple signers into one composite signer. * @param realSeedLen - Input seed length expected by `expandSeed`. * @param expandSeed - Seed expander used to derive per-signer seeds. * @param signers - Signers to combine. * @returns Composite signer. * @example * Combine multiple signers into one composite signer. * ```ts * import { shake256 } from '@noble/hashes/sha3.js'; * import { combineSigners, expandSeedXof } from '@noble/post-quantum/hybrid.js'; * import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js'; * const hybrid = combineSigners(32, expandSeedXof(shake256), ml_dsa44, ml_dsa44); * const { publicKey } = hybrid.keygen(); * ``` */ export declare function combineSigners(realSeedLen: number | undefined, expandSeed: TArg<ExpandSeed>, ...signers: TArg<Signer[]>): TRet<Signer>; /** * Builds a QSF hybrid KEM preset from a PQ KEM and an elliptic-curve KEM. * The combined shared-secret length follows `kdf.outputLen`; the built-in presets use 32-byte * SHA3-256 output, while custom `kdf` choices inherit their own digest size. * Its combiner hashes `ss0 || ss1 || ct1 || pk1 || label`, not the full * `(c1, c2, ek1, ek2)` example input shape from SP 800-227 equation (15). * Labels are encoded with `asciiToBytes()`, so non-ASCII labels are rejected. * @param label - Domain-separation label. * @param pqc - Post-quantum KEM. * @param curveKEM - Classical curve KEM. * @param xof - XOF used for seed expansion. * @param kdf - Hash used for the final combiner. * @returns Hybrid KEM. * @example * Build a QSF hybrid KEM preset from a PQ KEM and an elliptic-curve KEM. * ```ts * import { p256 } from '@noble/curves/nist.js'; * import { sha3_256, shake256 } from '@noble/hashes/sha3.js'; * import { QSF, ecdhKem } from '@noble/post-quantum/hybrid.js'; * import { ml_kem768 } from '@noble/post-quantum/ml-kem.js'; * const kem = QSF('example', ml_kem768, ecdhKem(p256, true), shake256, sha3_256); * const publicKeyLen = kem.lengths.publicKey; * ``` */ export declare function QSF(label: string, pqc: TArg<KEM>, curveKEM: TArg<KEM>, xof: TArg<XOF>, kdf: CHash): TRet<KEM>; /** QSF preset combining ML-KEM-768 with P-256. */ export declare const QSF_ml_kem768_p256: TRet<KEM>; /** QSF preset combining ML-KEM-1024 with P-384. */ export declare const QSF_ml_kem1024_p384: TRet<KEM>; /** * Builds the "KitchenSink" hybrid KEM combiner. * The current builder always derives a fixed 32-byte output, * regardless of the hash's native output size. * Its HKDF extract step uses implicit zero salt with IKM * `hybrid_prk || ss0 || ss1 || ct0 || pk0 || ct1 || pk1 || label`. * Its HKDF expand step fixes `info` to `len || 'shared_secret' || ''`. * Labels are encoded with `asciiToBytes()`, so non-ASCII labels are rejected. * @param label - Domain-separation label. * @param pqc - Post-quantum KEM. * @param curveKEM - Classical curve KEM. * @param xof - XOF used for seed expansion. * @param hash - Hash used for HKDF extraction and expansion. * @returns Hybrid KEM. * @example * Build the "KitchenSink" hybrid KEM combiner. * ```ts * import { sha256 } from '@noble/hashes/sha2.js'; * import { shake256 } from '@noble/hashes/sha3.js'; * import { createKitchenSink, ecdhKem } from '@noble/post-quantum/hybrid.js'; * import { ml_kem768 } from '@noble/post-quantum/ml-kem.js'; * import { x25519 } from '@noble/curves/ed25519.js'; * const kem = createKitchenSink('example', ml_kem768, ecdhKem(x25519), shake256, sha256); * const publicKeyLen = kem.lengths.publicKey; * ``` */ export declare function createKitchenSink(label: string, pqc: TArg<KEM>, curveKEM: TArg<KEM>, xof: TArg<XOF>, hash: CHash): TRet<KEM>; /** KitchenSink preset combining ML-KEM-768 with X25519. * Caller randomness splits into 32 ML-KEM coins plus a 32-byte X25519 ephemeral-secret seed. */ export declare const KitchenSink_ml_kem768_x25519: TRet<KEM>; /** X25519 + ML-KEM-768 hybrid preset. * Uses the hard-coded domain-separation label `\\.//^\\` and hashes only `ct1 || pk1` * from the X25519 side in addition to the two component shared secrets. */ export declare const ml_kem768_x25519: TRet<KEM>; /** P-256 + ML-KEM-768 hybrid preset. */ export declare const ml_kem768_p256: TRet<KEM>; /** P-384 + ML-KEM-1024 hybrid preset. */ export declare const ml_kem1024_p384: TRet<KEM>; /** Legacy alias for `ml_kem768_x25519`. */ export declare const XWing: TRet<KEM>; /** Legacy alias for `ml_kem768_x25519`. */ export declare const MLKEM768X25519: TRet<KEM>; /** Legacy alias for `ml_kem768_p256`. */ export declare const MLKEM768P256: TRet<KEM>; /** Legacy alias for `ml_kem1024_p384`. */ export declare const MLKEM1024P384: TRet<KEM>; /** Legacy alias for `QSF_ml_kem768_p256`. */ export declare const QSFMLKEM768P256: TRet<KEM>; /** Legacy alias for `QSF_ml_kem1024_p384`. */ export declare const QSFMLKEM1024P384: TRet<KEM>; /** Legacy alias for `KitchenSink_ml_kem768_x25519`. */ export declare const KitchenSinkMLKEM768X25519: TRet<KEM>; export {}; //# sourceMappingURL=hybrid.d.ts.map