WebBuf WebBuf
Docs

Hybrid post-quantum

@webbuf/sig-ed25519-mldsa

Composite Ed25519 + ML-DSA-65 signatures (OpenPGP-style hybrid: classical + post-quantum)

Install

npm install @webbuf/sig-ed25519-mldsa

Usage

import {
  sigEd25519MldsaSign,
  sigEd25519MldsaVerify,
} from "@webbuf/sig-ed25519-mldsa";
import { ed25519PublicKeyCreate } from "@webbuf/ed25519";
import { mlDsa65KeyPair } from "@webbuf/mldsa";
import { FixedBuf } from "@webbuf/fixedbuf";
import { WebBuf } from "@webbuf/webbuf";

// Each party generates two independent keys: a 32-byte Ed25519 seed and an
// ML-DSA-65 keypair.
const ed25519Priv = FixedBuf.fromRandom<32>(32);
const ed25519Pub = ed25519PublicKeyCreate(ed25519Priv);

const { signingKey: mldsaSigningKey, verifyingKey: mldsaVerifyingKey } =
  mlDsa65KeyPair();

// Sign a message — produces a 3374-byte composite signature.
const message = WebBuf.fromUtf8("composite signature");
const signature = sigEd25519MldsaSign(ed25519Priv, mldsaSigningKey, message);

// Verify — both halves must verify against their respective public keys.
const ok = sigEd25519MldsaVerify(
  ed25519Pub,
  mldsaVerifyingKey,
  message,
  signature,
);
// ok === true

API reference (4 exports)

Constants

SIG_ED25519_MLDSA

const
SIG_ED25519_MLDSA: { readonly versionByte: 1; readonly ed25519SignatureSize: 64; readonly mldsaSignatureSize: 3309; readonly fixedSize: number; readonly ed25519PublicKeySize: 32; readonly ed25519PrivateKeySize: 32; readonly mldsaVerifyingKeySize: 1952; readonly mldsaSigningKeySize: 4032; }

Functions

_sigEd25519MldsaSignDeterministic

function

Test/internal-only: sign with deterministic ML-DSA-65 (FIPS 204 `Sign`, no per-call randomness). Used by KAT regression tests. Application code should never call this directly — the leading underscore signals deterministic randomness, which is unsafe in production per issue 0003. Use `sigEd25519MldsaSign` instead, which uses ML-DSA-65's hedged-signing default.

_sigEd25519MldsaSignDeterministic(ed25519Priv: FixedBuf<32>, mldsaSigningKey: FixedBuf<4032>, message: WebBuf): FixedBuf<3374>

sigEd25519MldsaSign

function

Composite Ed25519 + ML-DSA-65 signature over a message. Signs the raw message bytes with both PureEdDSA (RFC 8032 §5.1.6) and FIPS 204 ML-DSA-65 Sign. Both signers consume the message verbatim — no prehash, no digest indirection. Returns the wire-format concatenation: `version || ed25519_sig (64) || mldsa_sig (3309)` = 3374 bytes. Determinism: PureEdDSA is RFC-deterministic; ML-DSA-65 is hedged by default (issue 0003). The composite signature is therefore non-deterministic by default — the Ed25519 half is stable for a given (seed, message), but the ML-DSA half varies per call.

sigEd25519MldsaSign(ed25519Priv: FixedBuf<32>, mldsaSigningKey: FixedBuf<4032>, message: WebBuf): FixedBuf<3374>

sigEd25519MldsaVerify

function

Composite Ed25519 + ML-DSA-65 signature verification. Both halves must verify against their respective public keys for the composite to verify. Returns `true` iff both pass; returns `false` for any rejection (wrong key on either side, tampered message, tampered signature, version-byte mismatch, malformed Ed25519 point, non-canonical Ed25519 S, etc.). Throws **only** on input-length errors at the top level. Strict Ed25519 verification (`verify_strict` under the hood) is enforced via `@webbuf/ed25519` — small-order Ed25519 public keys and non-canonical S are rejected, closing the universal-forgery hole that fooled the experiment-3 wrapper before the Codex fix. Both halves are verified regardless of either half's individual result, so this does not short-circuit. Neither primitive's underlying verifier is constant-time, however — the abstraction does not add timing safety we don't already have at the primitive layer.

sigEd25519MldsaVerify(ed25519Pub: FixedBuf<32>, mldsaVerifyingKey: FixedBuf<1952>, message: WebBuf, signature: FixedBuf<3374>): boolean