@noble/post-quantum
Version:
Auditable & minimal JS implementation of post-quantum cryptography: FIPS 203, 204, 205, Falcon
281 lines • 13 kB
TypeScript
/**
* 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