Compliance signing: provider-selection routine & production handoff

Audience. Operators and implementers wiring production signing for compliance evidence (decision capsules, SoA/export bundles, verdict-ledger/attestations, regulatory payloads). This is the handoff doc for SPRINT_20260530_024 (production signing): it states exactly what to set so production signing is a configuration/selection exercise, not a code change.

Authored by SPRINT_20260530_023 (compliance signing through the crypto abstraction

  • provider-selection routine).

1) The contract

Every compliance-evidence signing path routes through the Stella Ops cryptographic abstraction — ICryptoProviderRegistryICryptoProviderICryptoSigner (src/__Libraries/StellaOps.Cryptography/). No call site performs a raw/direct crypto call to produce compliance evidence, and no call site hard-codes a provider. The specific provider is chosen by configuration/selection.

There are two orthogonal selection inputs:

InputConfig key(s)What it picks
Registry profileStellaOps:Crypto:Registry:ActiveProfile + :PreferredProviders (env STELLAOPS__CRYPTO__REGISTRY__ACTIVEPROFILE); regional bundles via STELLAOPS_CRYPTO_PROFILEthe provider (which ICryptoProvider signs)
Compliance profileCrypto:Compliance:ProfileId (env STELLAOPS_CRYPTO_COMPLIANCE_PROFILE); world/fips/gost/sm/kcmvp/eidasthe algorithm family for a signing purpose

See docs/security/crypto-profile-configuration.md for the full region/profile table and the CLI selection surface (stella crypto profiles select <profile>).


2) The provider-selection routine

The single routine lives in the core library:

SignatureProviderSelection.Resolve(
    new SignatureSelectionRequest(
        Purpose:           SignaturePurpose.CapsuleSeal,
        ProfileId:         "<active compliance profile>",   // optional
        AlgorithmOverride: "<configured Signing:Algorithm>", // optional
        ProviderOverride:  "<configured Signing:Provider>")) // optional
  => SignatureSelection(AlgorithmId, ProviderHint?)

A call site requests a signer by purpose; the routine resolves the canonical algorithm for (profile + purpose), threads any explicit override, and the registry then resolves the concrete provider from the active profile’s preferred order. This is the asymmetric-signing analogue of ComplianceProfile.GetAlgorithmForPurpose (hashing) and GetHmacAlgorithmForPurpose (HMAC).

Algorithm defaults (behaviour-preserving)

Purposeworld / international / kcmvpeidasfipsgostsmpq
capsule-sealEd25519 (deterministic audit-chain default)ES256ES256GOST12-256SM2Dilithium3
all othersES256ES256ES256GOST12-256SM2Dilithium3

These reproduce what the call sites resolve today (e.g. CryptoDsseSigner.TryResolveProfileAlgorithm, the live EvidenceLocker capsule config’s ED25519), so swapping in a production provider does not change the dev-profile signature shape (golden-path determinism preserved).

On-prem-first guard (non-negotiable)

ProviderHint is only non-null when an operator passes an explicit ProviderOverride. The routine never defaults to a cloud-managed KMS. Sovereign software providers (eu.eidas.soft, fips.ecdsa.soft, GOST, SM), an in-process PKCS#11 HSM, or the local bouncycastle.ed25519 dev provider are the only defaults. A cloud KMS, if ever supported, is an explicit opt-in provider plugin selected via ProviderOverride / registry preferred order — never a default. (MEMORY: feedback_on_prem_first_no_cloud_defaults; Vault is the flagship KEK/secret backend.)

The deploy-time pack/config contract is static and local: devops/etc/crypto-plugins-manifest.json names runtime provider IDs and, when different, the signed packId; devops/etc/plugins/crypto/registry.yaml names profile-level provider packs; and devops/etc/appsettings.crypto.*.yaml selects runtime provider IDs such as offline.verification, ru.openssl.gost, ru.pkcs11, ru.cryptopro.csp, ru.winecsp.http, cn.sm.soft, cn.sm.remote.http, eu.eidas.soft, fips.ecdsa.soft, pq.soft, and sim.crypto.remote.

Dev fallback reached through the routine

The dev bouncycastle.ed25519 ephemeral path is a fallback reached through the routine, not a hard-coded branch: on the world/international profile the routine resolves Ed25519 for capsule-seal, and the registry routes Ed25519 to the BouncyCastle Ed25519 provider. So moving to production is purely a profile/provider selection change.


3) Where the routine is consumed

CryptoRegistryCapsuleSigner (EvidenceLocker capsule sealing) resolves its signing algorithm + provider through SignatureProviderSelection.Resolve(...) with the configured Signing:Algorithm / Signing:Provider as explicit overrides (SPRINT_20260530_023 TASK-023-3). The other compliance signers (EvidenceSignatureService, Nis2SoaCryptoProviderSigner, CraTechFileCryptoProviderSigner, AttestorSigningService, CryptoDsseSigner, DoraInfoSharingSignerService, VexOverrideSigningKeyProvider) already resolve the provider through ICryptoProviderRegistry.ResolveSigner and may adopt the routine for their algorithm decision over time. See the call-site routing table in docs/implplan/SPRINT_20260530_023_*.md.

Legacy bypass (contained, opt-in only)

The legacy raw-crypto capsule signers Ed25519CapsuleSigner / EcdsaCapsuleSigner (src/EvidenceLocker/StellaOps.EvidenceLocker/Capsules/CapsuleSigner.cs) call BCL crypto directly and do not route through the abstraction. They are reachable only when EvidenceLocker:Signing:UseCryptoRegistry=false AND raw key material (KeyMaterial:Ed25519PrivateKeyBase64 / :EcPrivateKeyPem) is configured — an explicit operator opt-in, retained for backward-compat verification of previously-written envelopes. The live golden-path config sets UseCryptoRegistry=true (so the live chain uses CryptoRegistryCapsuleSigner), and the production startup gate (EvidenceLockerRuntimeConfigurationValidator.ValidateCapsuleSigner) refuses to ship unsigned capsules. Production deployments MUST keep UseCryptoRegistry=trueso no compliance signing path bypasses the abstraction.


4) Production-signing handoff (what SPRINT_20260530_024 must set)

To plug in a real production provider/key, you change configuration and key provisioning, not signer code. For each region:

4.1 Pick the profiles

  1. Crypto:Compliance:ProfileId (or STELLAOPS_CRYPTO_COMPLIANCE_PROFILE) — the compliance profile (fips / eidas / gost / sm / kcmvp / world). Drives the algorithm family the routine resolves.
  2. StellaOps:Crypto:Registry:ActiveProfile + :PreferredProviders — order the active region’s providers first (e.g. eu.eidas.soft, fips.ecdsa.soft, ru.openssl.gost, ru.pkcs11, cn.sm.remote.http, an HSM provider). The bundled regional compose files (devops/compose/docker-compose.compliance-{eu,china,russia}.yml) set STELLAOPS_CRYPTO_PROFILE and the regional gates.

4.2 Enable the chosen provider’s gate

The software compliance providers are gated off by default and must be explicitly allowed (do not enable for a profile you are not certifying):

ProviderEnv gateAlgorithms
eu.eidas.softEIDAS_SOFT_ALLOWED=1ES256/ES384
fips.ecdsa.softFIPS_SOFT_ALLOWED=1ES256/384/512
cn.sm.softSM_SOFT_ALLOWED=1SM2
ru.openssl.gostSTELLAOPS_CRYPTO_ENABLE_RU_OPENSSL (default on Linux)GOST12-256/512
PQ softPQ_SOFT_ALLOWED=1Dilithium3 / Falcon512

These software providers are non-certified (QSCD / FIPS module not enforced). Making any provider “qualified eIDAS” / certified-FIPS is a separate decision, out of scope here. Keep production=false labels for dev/sim providers.

4.3 Provision the key on the provider (not raw bytes in app config)

Per-service the signer reads a logical KeyId + optional Provider hint and asks the registry for that key. The production pattern is to pre-provision the key on the chosen provider/HSM (Vault-wrapped or PKCS#11), not to paste raw private bytes into config. For EvidenceLocker capsule sealing the relevant keys are:

For the other signers, the analogous keys live under their own option sections (<service>:Signing:KeyId|Provider|Algorithm); the resolution semantics are identical because they all go through ICryptoProviderRegistry.ResolveSigner.

4.4 Production preconditions checklist