Skip to content

Post-Quantum Security

A sufficiently large quantum computer running Shor’s algorithm breaks the discrete-log and integer-factorization problems behind Ed25519, ECDSA, and RSA in polynomial time. The audit-defensible posture is to ship NIST-standard post-quantum primitives before the cryptographically-relevant quantum era — not after — because customer audit trails are long-lived and adversaries can harvest now, decrypt later.


Smart Engines uses the @noble/post-quantum library (pinned at 0.4.1), a TypeScript implementation of the NIST PQC standards. Two FIPS standards are in scope:

Standard Algorithm Role Status in platform
FIPS 204 ML-DSA-87 (Dilithium5) Digital signatures — NIST Level 5 In use, partly. Validators mint quorum certificates for a few internal record types today; most attested surfaces are wired but do not yet mint them (see below). A certificate that is present but fails verification is rejected; a missing certificate is logged and the artifact is still accepted.
FIPS 203 ML-KEM-768 / ML-KEM-1024 Key encapsulation for encryption In use for eight internal surfaces, each sealed with ML-KEM plus AES-256-GCM. New writes in the older, pre-PQC format are refused; older envelopes can still be read during a compatibility window.

Why the top tier (ML-DSA-87 / Level 5)? The platform uses one signature level for every certificate rather than mixing tiers, and at the platform’s quorum sizes the extra cost of the highest level is small.


The platform’s attestation is a quorum certificate in the PqcCertV1 shape. The validator that initiates an attestation collects ML-DSA-87 signatures from a validator quorum over the cluster’s internal messaging and assembles one certificate. A validator co-signs only content it has admitted itself, and only events signed with an enrolled validator key can be witnessed.

Today certificates reach a quorum for a few internal record types, such as validator membership events and smart-app runtime status. For the other attested surfaces — on-chain shadow certificates, BaaS receipts, license envelopes, marketplace listings and DKG transcripts among them — the attestation path is built, but the event that would trigger it is not yet signed with an enrolled key, so no certificate is minted. A verifier asked about one of those artifacts sees a missing certificate.

export type PqcCertV1 = {
readonly pqcCertVersion: 1;
readonly algorithm: 'ml-dsa-87';
readonly consumer: string; // e.g. 'license-envelope', 'baas-storage', 'onchain-hedera'
readonly contentHash: string; // hex sha256 of canonical artifact payload
readonly payloadHash?: string; // optional; must equal contentHash when present
readonly threshold: number; // quorum threshold (k of n)
readonly signers: readonly {
readonly nodeId: string;
readonly dilithium5PublicKey: string;
readonly signature: string;
}[];
readonly ts: number; // unix-ms of cert assembly
readonly topicMessageId?: string; // HCS coordinate; UNSIGNED, see policy.expectedTopicMessageId
};

It is a quorum cert, not a single signature: each signers[] entry is one validator’s ML-DSA-87 signature, and the cert is full only when the verified-signer count meets threshold (status: 'partial' is returned if fewer than threshold sign before timeout).

%%{init: {'sequence':{'showSequenceNumbers':false}}}%%
sequenceDiagram
    autonumber
    participant Art as Platform artifact
    participant Pub as Initiating validator
    participant Peers as Validator peers
    participant Cust as Customer (offline)
    Art->>Pub: contentHash = sha256(canonical payload)
    Pub->>Peers: request signatures over the content hash
    Peers->>Peers: each signs hash with its ML-DSA-87 key
    Peers-->>Pub: ML-DSA-87 signature + pubkey
    Pub->>Pub: assemble PqcCertV1 when ≥ threshold collected
    Note over Pub: cert ships inline or is published to an HCS attestation topic
    Cust->>Cust: verifyPqcAttestation(cert, payload, registry, { network, maxAgeMs }) — offline after one Mirror read
    Note over Cust: schema → content-hash → anchored registry is mandatory → per-signer key binding → reject duplicates → quorum floor

Customers verify with a pure, no-network SDK primitive — verifyPqcAttestation(cert, payload, registry, { network, maxAgeMs }) from @hsuite/smart-engines-sdk/pqc-verify, where registry = await fetchRegistrySnapshotFromMirror({ network }) is the one network read. It performs these checks, all offline:

  1. Schema — pqcCertVersion === 1, algorithm === 'ml-dsa-87'; if a payloadHash is present it must equal contentHash (payload-hash-mismatch).
  2. Content-hash match — sha256(canonical(payload)) === cert.contentHash.
  3. Anchored registry — mandatory for every consumer. The registry is the validators’ own signed HCS rows: each validator publishes its entry to the public validator-registry topic under its Ed25519 key, and the entry carries a SHA-256 commitment to its ML-DSA-87 key. fetchRegistrySnapshotFromMirror({ network }) reads those rows from any public Mirror node (rows from every answering mirror are unioned), and the verifier authenticates each one against the Ed25519 pins and topic id the SDK ships for that network — or against an anchor the caller supplies, which the result reports as anchor.source: 'caller'. A Mirror can withhold rows (that validator is not counted) or serve old ones (each signer’s own binding must be younger than maxRegistryAgeMs, default 72 h), but it cannot forge one. Omit the registry and the result is registry-snapshot-required; pass a served {snapshotTs, validators} snapshot and it is registry-snapshot-unanchored, because whoever serves a snapshot chooses its keys. hsuite verify reads the Mirror under the --network preset by default; --registry <file> verifies saved rows and --registry-anchor <file> replaces the SDK’s pins. No discovery seed, gateway or host is a trust input. When no mirror answers, the fetch throws RegistryUnavailableError, so “registry unreachable” can never degrade into “verified against the attacker’s keys.” Known limit: the binding between a validator and its ML-DSA-87 key is authenticated by a classical Ed25519 signature on the registry row. Optional pins on the ML-DSA identities themselves exist in the anchor format but are not yet filled.
  4. Per-signer key binding — a signer no pin names (registry-unknown-signer), or whose key’s sha256 is not the commitment its anchored entry binds (registry-pubkey-mismatch), refuses the cert. A signer whose newest entry is a leave, whose binding is stale, or whose binding is missing, duplicated or equivocated (registry-inactive-signer, registry-evidence-stale, registry-unkeyed-signer) does not count. Each counted signature verifies under the now-bound key over the domain-separated digest of (consumer, contentHash, ts).
  5. No duplicate signers — the same nodeId, or the same public key under two nodeIds, is rejected outright (duplicate-signer), never silently de-duplicated. A repeat is never a legitimate quorum.
  6. Quorum floor — verified-signer count >= the effective threshold: ⌈2N/3⌉ of the anchor’s N pinned validators (a Mirror’s omissions and a hostile leave never lower N), never below 2 (attestationQuorumThreshold), raised further by a higher cert.threshold or policy.minSigners; a cert cannot self-declare its way below it (threshold-not-met). The platform’s own verifier and emitter use the same function.
  7. Freshness — a decision every caller makes. A quorum cert never expires on its own, so policy.maxAgeMs is required: a finite bound rejects stale (stale-attestation) and future-dated (future-dated-attestation) certs, and maxAgeMs: Infinity verifies a historical record (an archived artifact) deliberately, with no time bound. Omit it and the result is { valid: false, reason: 'freshness-policy-required' }; hsuite verify likewise requires --max-age <ms|infinity>.

There is also a hsuite verify CLI subcommand, and the host tier can serve certificates for lookup. It serves certificates only, never the validator key set they are checked against.


This is the honest boundary — it matters for audits.

The on-chain bytes a blockchain actually stores stay on that chain’s native curve (Ed25519 on the live chains, Hedera and XRPL), because the chain’s own consensus must verify them. The design pairs each on-chain transaction with a shadow cert: an ML-DSA-87 quorum certificate that rides alongside the native signature, off chain, and stays soft-on. The chain adapters include a shadow-cert emit step, but its event is not yet signed with an enrolled key, so no shadow cert is minted today. Labels are also reserved for Stellar, Polkadot, Solana and Bitcoin, which are coming next.

%%{init: {'flowchart':{'htmlLabels':true}}}%%
flowchart LR
    Tx["Transaction"] --> Native["Native chain signature<br/><i>Ed25519<br/>(NOT yet quantum-safe on chain)</i>"]
    Tx --> Shadow["Shadow PQC cert<br/><i>ML-DSA-87, off-chain<br/>built, not yet minted</i>"]
    Native --> Ledger["On-chain"]
    Shadow --> Audit["PQ-safe audit trail"]
  • Signature certificates (ML-DSA-87) — every consumer runs soft-on today: a present certificate that fails verification is rejected, while a missing certificate is logged and the artifact accepted. Moving a consumer to hard-on (reject when missing) is a per-consumer step, planned once certificate coverage is complete. Where an internal consumer’s PQC verifier is not wired at all, the consumer refuses the event rather than skipping the check.
  • BaaS receipts (baas-*) and on-chain shadow certs (onchain-*) are soft-on by design: the customer SDK, not the validator, decides whether to enforce them. None of them is minted today, for the reason above.
  • Encryption (ML-KEM) — enforced on its eight internal surfaces for new writes, with the cluster-key caveat above. Per-app threshold keys are built, not yet enabled.

topicMessageId (<topicId>@<consensusTimestamp>) is outside the signed digest in pqcCertVersion: 1: it is assigned after consensus, when the quorum has already signed. The verifier therefore refuses a cert that carries one (topic-message-id-unauthenticated) unless the caller passes the same value as policy.expectedTopicMessageId, read from a Mirror node; a different value is topic-message-id-mismatch. Certs the platform emits carry none. Binding the coordinate into the signatures would need pqcCertVersion: 2 with a post-publication signing round.

pqcCertVersion: 1 is the locked customer API contract. Additive changes stay v1; breaking changes bump pqcCertVersion and ship a co-released dual-accepting SDK. A cryptographic-algorithm change (post-Dilithium5) is a breaking change.