PQ Provider Options Design
Audience: Security Guild, Scanner/Policy/Attestor engineers, crypto maintainers. Scope: The current shape of the post-quantum signing provider (
pq.soft) — supported algorithms, activation gate, options, key handling, and the gap between design intent and what is implemented today.
Last updated: 2026-05-30 · Owners: Security Guild · Scanner Guild · Policy Guild
Reconciled against the implementation on 2026-05-30. The post-quantum signing provider is implemented as the
pq.softcrypto plugin (StellaOps.Cryptography.Plugin.PqSoft), a software-only provider built on BouncyCastle PQC primitives. Several items in earlier drafts of this design (aCrypto:Providersappsettings registry, per-featureSigningProvideroverrides, BLAKE3 hashing, PEM key files, liboqs/oqs-provider bindings) were never implemented; they are now marked NOT IMPLEMENTED or Draft / roadmap below rather than described as current behavior.
Goals
- Allow DSSE/attestation flows to choose a post-quantum (PQ) signing profile (Dilithium3/Falcon512) via the existing
ICryptoProvider/ crypto provider registry surface without breaking deterministic outputs. - Keep hash inputs stable across providers; only the signature algorithm changes.
- Remain offline-friendly and configurable per environment (plugin manifest entry
PqSoftProviderOptions).
Provider implementation (current)
The PQ provider is a single ICryptoProvider implementation:
- Plugin id / provider
Name:pq.soft(PqSoftCryptoProvider.Namereturns the literal"pq.soft"). - Assembly:
StellaOps.Cryptography.Plugin.PqSoft.dll, typeStellaOps.Cryptography.Plugin.PqSoft.PqSoftCryptoProvider. - It exposes signing and verification only.
GetHasherandGetPasswordHasherthrowNotSupportedException— the provider does not expose hashing or password hashing.
There is no separate
pq-dilithium3/pq-falcon512provider. Earlier drafts named two distinct providers; in the implementation there is one provider (pq.soft) that supports two signature algorithms, selected per key.
Supported algorithms
PqSoftCryptoProvider.Supports(...) returns true only for the Signing/Verification capabilities and only for these two algorithm identifiers (from StellaOps.Cryptography.SignatureAlgorithms):
| Algorithm id (constant) | Literal value | BouncyCastle primitive |
|---|---|---|
SignatureAlgorithms.Dilithium3 | DILITHIUM3 | MLDsaParameters.ml_dsa_65 (FIPS 204 ML-DSA-65), MLDsaSigner(deterministic: true) |
SignatureAlgorithms.Falcon512 | FALCON512 | FalconParameters.falcon_512, FalconSigner |
Notes:
- Algorithm ids are matched case-insensitively (
StringComparer.OrdinalIgnoreCase);UpsertSigningKeyupper-cases the id before lookup. - The legacy name
DILITHIUM3maps to the standardized ML-DSA-65 parameter set; the implementation usesMLDsaParameters.ml_dsa_65and theMLDsaSigner/MLDsaKeyPairGeneratorclasses. The exported JWK usescrv = "Dilithium3"/alg = "DILITHIUM3"for the public key, andcrv = "Falcon512"/alg = "FALCON512"for Falcon.
Inconsistency to be aware of — the crypto plugin manifest (
devops/etc/crypto-plugins-manifest.json, idpq.soft) advertises a broader capability set (ML-DSA-44/65/87,SLH-DSA-SHA2/SHAKE,SHA3-256/512,SHAKE256; complianceNIST-PQC,FIPS-203,FIPS-205). Those manifest capabilities are aspirational/inaccurate: the code only acceptsDILITHIUM3andFALCON512for signing, supports no hashing, and provides no SLH-DSA / SHA3 implementation. Treat the C#Supports(...)result as ground truth, not the manifest capability strings.
Activation gate
PQ signing is gated off by default. The provider is enabled only when:
PqSoftProviderOptions.RequireEnvironmentGateisfalse, or- the
PQ_SOFT_ALLOWEDenvironment variable is set to1ortrue(case-insensitive; any other value, or unset, leaves the provider disabled).
When the gate is closed, Supports(...) returns false for all algorithms and GetSigner / UpsertSigningKey throw InvalidOperationException ("Provider 'pq.soft' is disabled. Set PQ_SOFT_ALLOWED=1 or disable RequireEnvironmentGate to enable.").
Provider options (current shape)
The provider binds PqSoftProviderOptions:
{
// PqSoftProviderOptions
"RequireEnvironmentGate": true, // default; gate via PQ_SOFT_ALLOWED
"Keys": [
{
// PqSoftKeyOptions
"KeyId": "pq-key-1",
"Algorithm": "DILITHIUM3", // or "FALCON512"; default DILITHIUM3
"PrivateKeyPath": "secrets/pq/dilithium3.key",
"PublicKeyPath": "secrets/pq/dilithium3.pub" // optional
}
]
}
There is no top-level Crypto.DefaultProvider / Crypto.Providers registry binding, no Type: "PostQuantum" discriminator, no Hash field, no CertPath, and no UseBlake3 field in the implementation. The only options bound by the PQ provider are RequireEnvironmentGate and a Keys list of PqSoftKeyOptions (KeyId, Algorithm, PrivateKeyPath, PublicKeyPath). Plugin discovery and priority (priority 70) come from the manifest entry, not from a Crypto appsettings section.
Key handling (current)
- Keys are supplied as raw private-key bytes, not PEM.
UpsertSigningKeyrejects an empty private key ("PQ provider requires raw private key bytes."). - The private-key bytes are used as deterministic seed material: the provider feeds them through a
DigestRandomGenerator(Sha512Digest)/SecureRandomand runs the BouncyCastle key-pair generator (CreateSeededRandom→MLDsaKeyPairGenerator/FalconKeyPairGenerator). The same seed therefore reproduces the same key pair, which keeps tests/fixtures deterministic. - On-disk keys (
PqSoftKeyOptions.PrivateKeyPath/PublicKeyPath) are read withFile.ReadAllBytesat construction time viaTryLoadKeyFromFile. Missing files are skipped silently (the key simply is not registered); load failures are logged type-only (exception type, never the message/path/key bytes) to avoid echoing key material — see theLogWarnExceptionTypeOnlyhelper (SPRINT_20260528_037 / S037-002). - A registered key validates that its stored algorithm matches the algorithm requested at
GetSignertime, otherwiseGetSignerthrows ("... is registered for algorithm '...', not '...'."). DescribeKeys()emits descriptors taggedcertified=false,simulation=software— this is explicitly a software PQ provider, not a certified/HSM-backed one.
Selection rules
- The provider is selected through the crypto provider registry (
ICryptoProviderRegistry/CryptoProviderRegistry/TenantAwareCryptoProviderRegistry) by requesting a supported algorithm (DILITHIUM3/FALCON512) for theSigning/Verificationcapability while the gate is open. - Determinism: hash inputs (payload canonicalisation) are unchanged by provider selection — only the signature material differs. Both algorithms sign deterministically (
MLDsaSigner(deterministic: true); Falcon as implemented), so signing the same payload with the same key yields the same signature.
NOT IMPLEMENTED (Draft / roadmap). The following per-feature override keys referenced in earlier drafts do not exist anywhere in the codebase and are not consumed by Scanner, Policy, or Attestor:
Crypto:DefaultProviderDSSE:SigningProviderPolicyEngine:SigningProviderScanner:SigningProviderThere is likewise no “fall back to
DefaultProviderand emit a warning” behavior for a missing PQ provider; a missing/unregistered key throwsKeyNotFoundExceptionand a closed gate throwsInvalidOperationException.
Hash strategy
NOT IMPLEMENTED (Draft / roadmap). The PQ provider does not expose any hasher (
GetHasherthrowsNotSupportedException). There is noUseBlake3flag and noDeterministicHashVersionsetting in the implementation; searches acrosssrc/**return no matches for either. Hashing for DSSE/attestation payloads continues to use the platform’s existing hash strategy (SHA-256), independent of the PQ signing provider. BLAKE3 selection and aDeterministicHashVersion = 2switch are aspirational only.
Key formats
Corrected. PQ keys are not stored as PEM (
BEGIN PUBLIC KEY/BEGIN PRIVATE KEY) and there is no liboqs / OpenQuantumSafe toolchain dependency. The implementation uses BouncyCastle (Org.BouncyCastle.*,Pqc.Crypto.Falcon, ML-DSA) and reads keys as raw bytes viaFile.ReadAllBytes. Private-key bytes act as deterministic seed material for key-pair generation (see “Key handling” above). The provider validates that a key’s stored algorithm matches the requested algorithm at sign time.
Testing plan
Implemented tests:
StellaOps.Cryptography.Tests/PqSoftCryptoProviderTests.cs:GateBehavior_RespectsEnvironmentFlag—SupportsisfalseuntilPQ_SOFT_ALLOWED=1.Constructor_SkipsMissingKeyFile— missing key path → key not registered.UpsertSigningKey_RegistersSigner— upsert thenGetSignerround-trip forDILITHIUM3.
- Additional PQ-related coverage lives in
StellaOps.Cryptography.Tests/SimRemoteCapabilityDetectionTests.PostQuantum.csandPqSoftCryptoProviderTests.cs(a duplicate undersrc/__Libraries/StellaOps.Cryptography.Tests/).
Orphaned reference. Earlier drafts scoped this work to
SCANNER-CRYPTO-90-002/003; those task ids do not exist anywhere in the repo and have been removed as the authoritative tracking reference. The env-gated integration harnessops/crypto/run-rootpack-ru-tests.shreferenced below also does not exist (noops/crypto/directory). Sign-and-verify round-trips, oqs/liboqs-compatible interop verification, and SBOM/Policy attestation integration tests are not yet implemented and remain roadmap items.
Planned (Draft / roadmap, not yet implemented):
- Integration (env-gated): sign sample SBOM attestations and Policy bundles with Dilithium3 and Falcon512; verify with an interop verifier.
- Determinism check: sign the same payload twice → identical signatures (both algorithms sign deterministically as implemented); record hashes in fixtures.
Rollout status
- Done — provider classes implemented under
StellaOps.Cryptography.Plugin.PqSoft(BouncyCastle PQC, not oqs bindings). - Done (partial) — provider is discovered via the plugin manifest (
devops/etc/crypto-plugins-manifest.json, idpq.soft) and bindsPqSoftProviderOptions. There is noCrypto:Providersappsettings registry. - NOT IMPLEMENTED — DSSE signing-option plumbing in Scanner/Policy/Attestor hosts via a
SigningProvideroverride (no such override keys exist). - NOT IMPLEMENTED — env-gated tests in
ops/crypto/run-rootpack-ru-tests.sh(script and directory absent). - NOT IMPLEMENTED — operator guidance in
docs/dev/crypto.md(file absent) and RootPack notes.
Risks / mitigations
- Manifest vs. code drift — the
pq.softmanifest entry advertises ML-DSA-44/87, SLH-DSA, and SHA3/SHAKE capabilities the code does not implement. Reconcile the manifest withPqSoftCryptoProvider.Supports(...)or implement the advertised algorithms before relying on the manifest for capability negotiation. - Software-only, not certified — keys are tagged
certified=false,simulation=software. This provider is not a substitute for an HSM- or CSP-backed PQ provider where certification is required. - Interop risk — consumers may not understand Dilithium3/Falcon512 signatures. Dual-signing (classical + PQ) during transition is a design intent but is not wired through a
SigningProvidertoggle today. - Performance — larger PQ signatures could affect payload size; benchmark during any rollout.
- Supply / offline — the provider depends on BouncyCastle (already a vendored dependency), not liboqs; no additional native PQ libraries need mirroring for offline installs.
