Files

281 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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