Skip to content

Latest commit

 

History

History
219 lines (162 loc) · 10.6 KB

File metadata and controls

219 lines (162 loc) · 10.6 KB

PQC Registry

Overview

The x/pqc module exposes a post-quantum (PQ) account registry so that user accounts can publish Dilithium public keys alongside their legacy Ed25519 keys. Every transaction signed by an externally owned account (EOA) must include a Dilithium signature. The only exception is a narrow IBC relayer-core bypass driven by ibc_relayer_allowlist: allowlisted relayer addresses may submit relayer/core IBC transactions without the extra PQC extension, but user-facing messages such as MsgTransfer still require PQC. Module accounts never sign transactions, so they are unaffected. The registry feeds the dual-sign ante decorator and the policy is always enforced as REQUIRED (governance parameters remain for backwards compatibility but are clamped in-memory).

Account Registry

  • Each account may register a single Dilithium key via MsgLinkAccountPQC.
  • The stored record contains the scheme identifier (currently dilithium3), the SHA-256 hash of the public key, and the timestamp (added_at). Full keys never live on-chain; they are provided alongside signatures and hashed/verified on demand.
  • Rotation is permanently disabled; relinking with the exact same key is accepted as a no-op, but any attempt to replace the hash is rejected with ErrAccountRotationDisabled.
  • Linking is gated by two guards:
    1. min_balance_for_link – required spendable ULUMEN balance before broadcasting MsgLinkAccountPQC.
    2. pow_difficulty_bits – the transaction must include a nonce such that sha256(pubkey || nonce) has at least this many leading zero bits.

Events emitted on successful linkage include the account address, scheme, and SHA-256 hash of the public key so that operators can index registry updates without storing full keys on-chain.

CLI

# Link the Dilithium3 key for the --from account (min-balance + PoW enforced automatically)
lumen tx pqc link-account --scheme dilithium3 --pubkey <hex|base64>

# Query the registered key for an address
lumen q pqc account <bech32-address>

# Inspect module parameters
lumen q pqc params

The CLI validates that the provided scheme matches the active backend (crypto/pqc/dilithium.Default()), refuses to link if the account balance is below min_balance_for_link, and mines an appropriate nonce to satisfy pow_difficulty_bits before broadcasting.

Operator workflow helpers

Production builds now expose the PQC helper commands under lumend keys so operators never have to rebuild custom binaries:

# Generate a Dilithium3 keypair, store it locally, and link it to the "validator" keyring entry:
lumend keys pqc-generate --name validator-pqc --link-from validator \
  --pqc-passphrase-file ~/.config/lumen/pqc_passphrase

# List stored keys and local bindings:
lumend keys pqc-list

# Produce the JSON snippet for genesis (app_state.pqc.accounts):
lumend keys pqc-genesis-entry --from validator \
  --output validator-pqc.json \
  --write-genesis ~/.lumen/config/genesis.json

pqc-generate prints the public/private key hex by default and supports --pub-out / --priv-out to write them to files, as well as --force to overwrite existing outputs. The generated key can be stored (--name) and linked to a Cosmos key in one shot (--link-from). En ajoutant --pqc-passphrase-file ou la variable LUMEN_PQC_PASSPHRASE_FILE, le keystore local (~/.lumen/pqc_keys/*.json) est chiffré ; sans passphrase, les clés restent en clair.

pqc-import importe toujours un couple fourni par un autre outil, et pqc-link relie un compte Cosmos à une clé locale. pqc-genesis-entry inspecte la clé liée (ou celle passée via --pqc) et produit l’entrée JSON prête à être insérée dans app_state.pqc.accounts. Avec --write-genesis, l’entrée est injectée directement dans le genesis.json (un backup .bak est créé automatiquement).

Module Parameters

Parameter Default Description
policy REQUIRED Runtime policy is forced to REQUIRED; other enum values are ignored and rejected in SetParams.
min_scheme dilithium3 Minimum scheme identifier accepted for incoming signatures.
min_balance_for_link 1000ulmn Accounts must hold at least this spendable balance before linking.
pow_difficulty_bits 21 Required number of leading zero bits for `sha256(pubkey
ibc_relayer_allowlist [] Relayer addresses allowed to bypass PQC for relayer/core IBC transactions only.

Parameter updates are validated against the list of supported schemes exposed by the active backend. Account rotation is permanently disabled; attempting to re-link with a different key returns ErrAccountRotationDisabled. The PoW guard uses big-endian encodings of the nonce bytes; clients increment the nonce until the digest carries enough leading zeros.

Governance does not reopen the full MsgUpdateParams surface for x/pqc. The only governable PQC mutations are the dedicated MsgAddIBCRelayer and MsgRemoveIBCRelayer messages used to maintain the IBC relayer allowlist.

Dual-Sign Ante Enforcement

The PQCDualSignDecorator runs immediately after Ed25519 signature verification. If the transaction is composed only of relayer/core IBC messages and every signer is in ibc_relayer_allowlist, the decorator bypasses the PQC extension requirement. Otherwise, every EOA signature must pass the PQC checks. For each signer in the transaction:

  1. The account’s registered PQC key is fetched from the x/pqc keeper.

  2. The decorator attempts to locate a matching PQC signature in the transaction’s TxBody.extension_options.
    The payload must be encoded as a lumen.pqc.v1.PQCSignatures message where each entry contains:

    message PQCSignatureEntry {
      string addr      = 1; // bech32 signer address
      string scheme    = 2; // e.g. "dilithium3"
      bytes signature  = 3; // Dilithium signature over the PQC payload
      bytes pub_key    = 4; // Raw Dilithium public key (hash checked against registry)
    }
  3. The signature is verified against the Dilithium backend (crypto/pqc/dilithium.Default()).

Sign Bytes Format

The PQC payload reuses the standard Cosmos SDK SignDoc, but with a domain separator:

PQCv1: || SignDoc{
  body_bytes:     sanitized_body_bytes, // TxBody with any PQC signature extensions stripped
  auth_info_bytes: tx_raw.auth_info_bytes,
  chain_id:        ctx.ChainID(),
  account_number:  signer_account_number,
}

Transactions should first be assembled without the PQC signature, the payload computed as above, and only then should the final signature be inserted into the PQCSignatures extension.

Enforcement Outcomes

Runtime policy is always REQUIRED:

Situation Behaviour
Missing key Reject (ErrPQCRequired)
Missing signature Reject (ErrMissingExtension / ErrPQCRequired)
Invalid signature Reject (ErrPQCVerifyFailed)

Successful verifications emit a pqc.verified event containing the signer address and scheme.

Client Setup

A lightweight keystore stores Dilithium testing keys under ~/.lumen/pqc_keys. The keystore is intentionally plaintext (for operator convenience); ensure the directory is protected at the OS level or migrate the keys to an HSM when you harden your environment. The following helpers are always available:

# Generate and link a new Dilithium keypair
lumend keys pqc-generate --name validator-pqc --link-from validator \
  --pqc-passphrase-file ~/.config/lumen/pqc_passphrase

# Import a Dilithium keypair (private key provided as hex/base64)
lumend keys pqc-import --name local-dilithium --scheme dilithium3 \
  --pubkey <hex-or-base64> --privkey <hex-or-base64>

# Show the imported keys and local bindings
lumend keys pqc-list

# Display a single key (prints the fingerprint and raw bytes)
lumend keys pqc-show local-dilithium

# Link a cosmos keyring entry to the imported PQC key
lumend keys pqc-link --from validator --pqc local-dilithium

# Produce the JSON entry for app_state.pqc.accounts
lumend keys pqc-genesis-entry --from validator --output validator-pqc.json \
  --write-genesis ~/.lumen/config/genesis.json

During transaction assembly, the CLI signs Ed25519 first and then augments the tx with PQC signatures. PQC signing is enabled by default and controlled globally via the tx command’s persistent flags:

Flag Default Purpose
--pqc-enable true Toggle PQC signing for the current transaction.
--pqc-scheme dilithium3 Select the target scheme (must match on-chain params).
--pqc-from + --pqc-key [] Optional override mapping of signer addresses to local PQC key names.

Example bank transfer (the PQC signature is embedded automatically; no manual extension management is required):

lumend tx bank send alice bob 10ulmn \
  --pqc-enable \
  --pqc-key local-dilithium

The signer-specific overrides are useful for multi-signer flows; otherwise the CLI falls back to the local binding established via keys pqc-link. When --pqc-enable=false, the CLI omits the PQC extension entirely.

End-to-end PQC test

make build
HOME=$(mktemp -d) make e2e

# or run the single flow
make e2e-pqc

This script generates a Dilithium3 key via CIRCL, imports it into the local PQC keystore, links the key on-chain, sends a PQC-signed bank transaction (expected to pass), then retries the same transfer with --pqc-enable=false which should fail when PqcPolicy=REQUIRED. Disable the client-side injector explicitly only when you need to exercise failure paths or when the chain policy is permissive.

Production Backends

Two production-grade backends are available under crypto/pqc/dilithium:

  • Circl (default) – pure Go implementation provided by github.com/cloudflare/circl/sign/dilithium.
  • PQClean fallback – builds on the AES-enhanced Dilithium3 variant (compiled when -tags pqc_oqs is supplied).

Default() tries Circl first; if that backend is unavailable it falls back to the PQClean path when pqc_oqs is enabled, and otherwise panics. CI workflows enforce that pqc_testonly/noop symbols never appear in release binaries, and cmd/lumend panics during init() if an unexpected backend name is detected.

End-to-end Tests

All CLI and E2E scripts run with PQC enabled by default. The only supported way to bypass the decorator is to pass --pqc-enable=false on a single transaction, which should then fail on-chain when policy=REQUIRED. Test harnesses may flip that flag to ensure the failure path is exercised, but there is no global “disable PQC” environment variable for the application.