Composite sign for PQ migration

Problem: You’re issuing signatures today that need to remain valid for 10+ years. ECDSA might be broken by quantum computers in that window. You want to start signing with BOTH classical and post-quantum algorithms so the signature stays valid if EITHER algorithm holds.

Solution

Composite signatures (IETF draft) bundle multiple per-algorithm signatures into one envelope. Verifiers check all components; the signature is valid if all pass.

Current state

Confium’s composite signatures support:

  • Ed25519 (classical) — confium_composite::build_ed25519_component
  • ECDSA-P256 (classical) — confium_composite::build_p256_component

PQ algorithms (ML-DSA-65, SLH-DSA) land when the underlying FIPS 204/205 crates ship. Today you can deploy hybrid classical-classical (Ed25519 + P256) to bridge between systems that only support one.

Quickstart

Generate keys (one of each algorithm)

# Generate an Ed25519 keypair
openssl genpkey -algorithm ed25519 -out ed25519.key
openssl pkey -in ed25519.key -pubout -out ed25519.pub

# Generate an ECDSA-P256 keypair
openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-256 -out p256.key
openssl pkey -in p256.key -pubout -out p256.pub

Composite-sign via CLI

# Extract raw key bytes for the CLI (in production, keep keys in HSM)
openssl pkey -in ed25519.key -outform DER 2>/dev/null | tail -c 32 > ed25519.key.bin
openssl ec -in p256.key -outform DER 2>/dev/null | tail -c 32 > p256.key.bin

confium pki composite-sign \
    --message "release-v1.0.0.tar.gz" \
    --ed25519-key ed25519.key.bin \
    --p256-key p256.key.bin \
    --out composite-sig.hex

Verify

# Verify the Ed25519 component
confium verify composite \
    --signature composite-sig.hex \
    --algorithm ed25519 \
    --message "release-v1.0.0.tar.gz" \
    --public-key ed25519.pub

# Verify the ECDSA-P256 component
confium verify composite \
    --signature composite-sig.hex \
    --algorithm ecdsa-p256 \
    --message "release-v1.0.0.tar.gz" \
    --public-key p256.pub

Both components must verify for the composite to be considered valid.

When to use composite

Scenario Use composite?
Long-lived signatures (5+ years) Yes — PQ migration insurance
Short-lived signatures (minutes) No — overhead not worth it
Cross-system interop Yes — bridge old + new systems
Performance-critical No — composites are 2×+ larger

Verifier policy

By default, verifiers require ALL components to pass (strict). For migration scenarios where you want to accept EITHER component, you can configure the verifier:

// Rust: relaxed policy (either component verifies = overall valid)
use confium_composite::CompositeSignature;

let result = sig.verify(&message, |alg, pk, msg, sig| {
    // Try Ed25519 first, fall back to P256
    if confium_composite::ed25519_verifier(alg, pk, msg, sig).is_ok() {
        return Ok(());
    }
    confium_composite::p256_verifier(alg, pk, msg, sig)
});

This is less secure — only use it for transitional scenarios where one algorithm might be deprecated soon.

Storage

Composite signatures are JSON-serialized for portability:

{
  "components": [
    {
      "algorithm": "Ed25519",
      "public_key": "hex...",
      "signature": "hex..."
    },
    {
      "algorithm": "ECDSA-P256",
      "public_key": "hex...",
      "signature": "hex..."
    }
  ]
}

Each component carries its own public key. Verifiers don’t need to know which key belongs to which algorithm in advance.

Future: ML-DSA composites

When FIPS 204 (ML-DSA) crates ship, the API will extend to:

let composite = CompositeSignature::new(vec![
    build_ed25519_component(&classical_key, &message)?,
    build_ml_dsa_65_component(&pq_key, &message)?,
]);

The verifier API stays the same. Your existing composite-sign tooling will work unchanged — just add the PQ component.

See also