Cryptography — Architecture

Audience: crypto/platform engineers and compliance reviewers wiring Stella Ops for regional or sovereign deployments.

Cryptography is the Stella Ops library layer that supplies algorithm-agile, compliance-profile-driven primitives (hashing, MAC, signing, KEK management). It lets every service request operations by purpose and resolve the concrete algorithm from the active regional profile (eIDAS, FIPS, GOST, SM, KCMVP, PQ), while keeping signing deterministic and reproducible. It is on-prem first: HashiCorp Vault is the flagship KEK backend, and no cloud-managed crypto service is a default.


0) Mission & boundaries

Mission. Provide algorithm-agile, compliance-profile-driven cryptographic primitives that support regional requirements (eIDAS/FIPS/GOST/SM/KCMVP/PQ) while ensuring deterministic, reproducible signing operations across all StellaOps services. Components request hashing/MAC by purpose (graph, symbol, content, merkle, attestation, interop, secret) and the active compliance profile resolves the concrete algorithm — see §4.

Boundaries.


1) Solution & project layout

Canonical-assembly note (SPRINT_20260430_017). The core crypto types live in src/__Libraries/StellaOps.Cryptography/, NOT in src/Cryptography/StellaOps.Cryptography/. The latter was a SPRINT_000 scaffold whose AssemblyName collision broke Auth.ServerIntegration; it was deleted and everything consolidated into the canonical assembly. The plugin projects under src/Cryptography/ reference the canonical project. Public namespaces are unchanged (StellaOps.Cryptography, StellaOps.Cryptography.Plugin*). See src/Cryptography/AGENTS.md.

Canonical core assembly — src/__Libraries/StellaOps.Cryptography/:

src/__Libraries/StellaOps.Cryptography/
 ├─ CryptoProvider.cs              # ICryptoProvider, ICryptoProviderRegistry, ICryptoHasher,
 │                                 #   CryptoCapability, CryptoKeyReference, *Resolution records
 ├─ CryptoProviderRegistry.cs      # Default ICryptoProviderRegistry (deterministic ordering, fail-closed hints)
 ├─ DefaultCryptoProvider.cs       # Name="default" — BCL ES*/RS*/PS*/Ed25519 + SHA hashing
 ├─ LibsodiumCryptoProvider.cs     # Name="libsodium" (STELLAOPS_CRYPTO_SODIUM build)
 ├─ CompliancePolicyCryptoProviders.cs  # fips.ecdsa.soft / eu.eidas.soft / kr.kcmvp.hash
 ├─ ComplianceProfile.cs           # Per-purpose algorithm map (the regional routing model)
 ├─ ComplianceProfiles.cs          # world / fips / gost / sm / kcmvp / eidas built-ins
 ├─ CryptoComplianceService.cs     # ICryptoComplianceService — resolve algorithm by purpose
 ├─ HashPurpose.cs / HashAlgorithms.cs / HmacAlgorithms.cs / SignatureAlgorithms.cs
 ├─ ICryptoHash.cs / ICryptoHmac.cs / ICryptoRandom.cs / ICryptoSigner.cs
 ├─ IHmacAlgorithm.cs / DefaultHmacAlgorithm.cs / FipsHmacAlgorithm.cs
 ├─ Argon2idPasswordHasher.cs (+ .Sodium / .BouncyCastle) / Pbkdf2PasswordHasher.cs
 ├─ MultiProfileSigner.cs / SignatureProfile.cs / EcdsaSigner.cs
 └─ Models/, Digests/, KeyEscrow/, Audit/, Interop/

Provider plugin projects — src/Cryptography/ (reference the canonical assembly):

src/Cryptography/
 ├─ StellaOps.Cryptography.Plugin.Fips/      # FipsPlugin (OS FIPS-mode probe, FIPS symmetric/HMAC)
 ├─ StellaOps.Cryptography.Plugin.Eidas/     # EidasPlugin, CAdES-B/-T/-LT/-LTA builder, TSL service
 ├─ StellaOps.Cryptography.Plugin.Gost/      # In-process BouncyCastle GOST (hash + 256-bit gen test signing)
 ├─ StellaOps.Cryptography.Plugin.Sm/        # BouncyCastle SM2/SM3/SM4
 ├─ StellaOps.Cryptography.Plugin.Hsm/       # HsmPlugin + Pkcs11HsmClientImpl (Pkcs11Interop)
 ├─ StellaOps.Cryptography.Plugin.OpenPgp/   # OpenPgpEncryptionProvider/v1 (email handoff)
 ├─ StellaOps.Cryptography.Profiles.Ecdsa/   # ECDSA signing profile
 ├─ StellaOps.Cryptography.Profiles.EdDsa/   # EdDSA signing profile
 ├─ plugins/                                 # external plugin drop directory
 └─ StellaOps.Cryptography.sln

Additional provider/KMS/KEK libraries — src/__Libraries/:

src/__Libraries/
 ├─ StellaOps.Cryptography.DependencyInjection/   # AddStellaOpsCrypto(...) + provider registration
 ├─ StellaOps.Cryptography.Plugin.EIDAS/          # eu.eidas — remote TSP/QSCD HTTP adapter
 ├─ StellaOps.Cryptography.Plugin.CryptoPro/      # ru.cryptopro.csp (Windows CryptoPro CSP GOST)
 ├─ StellaOps.Cryptography.Plugin.OpenSslGost/    # ru.openssl.gost
 ├─ StellaOps.Cryptography.Plugin.Pkcs11Gost/     # ru.pkcs11 (GOST via PKCS#11)
 ├─ StellaOps.Cryptography.Plugin.SmSoft/         # cn.sm.soft (software SM2/SM3; signed crypto-sm pack, §4.3b)
 ├─ StellaOps.Cryptography.Sm.Contracts/          # host-owned HTTP transport (cn.sm.remote.http): SmRemoteHttpClient/Provider/Signer (§4.3b)
 ├─ StellaOps.Cryptography.Plugin.SmRemote/       # thin plug-in wrapper that references Sm.Contracts (§4.3b)
 ├─ StellaOps.Cryptography.Plugin.SimRemote/      # sim.crypto.remote (test-only universal simulator)
 ├─ StellaOps.Cryptography.Plugin.PqSoft/         # pq.soft (DILITHIUM3 / FALCON512, experimental)
 ├─ StellaOps.Cryptography.Plugin.BouncyCastle/   # bouncycastle.ed25519
 ├─ StellaOps.Cryptography.Plugin.WineCsp/        # Wine-hosted CryptoPro CSP bridge
 ├─ StellaOps.Cryptography.Providers.OfflineVerification/  # offline.verification
 ├─ StellaOps.Cryptography.Plugin.BouncyCastleGost/ # BouncyCastle GOST (distinct from src/Cryptography/…Plugin.Gost)
 ├─ StellaOps.Cryptography.Plugin.OfflineVerification/ # distinct from Providers.OfflineVerification below
 ├─ StellaOps.Cryptography.Kms/                   # IKmsClient signing facade (File/FIDO2/Pkcs11/AWS/GCP — see §4.9)
 ├─ StellaOps.Cryptography.CredentialStore/       # KEK sources + secret-provider/registry contracts + AEAD/derived-tenant-keys (see §4.9)
 ├─ StellaOps.Cryptography.CredentialStore.Vault/ # VaultKekSource (flagship KEK source)
 ├─ StellaOps.Cryptography.CredentialStore.Hsm/   # HsmKekSource
 ├─ StellaOps.Cryptography.CredentialStore.Persistence/   # Postgres secret-provider registry + crypto.kek_versions migration/bootstrap
 ├─ StellaOps.Cryptography.CredentialStore.PluginRouter/  # secret-provider routing
 ├─ StellaOps.Cryptography.CredentialStore.ReSeal/        # KEK re-seal machinery (§11 --reseal)
 ├─ StellaOps.Cryptography.CertificateStatus/ (+ .Abstractions) # OCSP/CRL status layer (eIDAS LT, §4.8)
 ├─ StellaOps.Cryptography.Dsse/                  # DSSE PAE / envelope verification primitives
 ├─ StellaOps.Cryptography.HostProviders/         # RegionalCryptoRegistration / HostCryptoProfiles
 ├─ StellaOps.Doctor.Plugins.Cryptography/        # Doctor checks for this module
 └─ StellaOps.Cryptography.PluginLoader/

The in-process provider project src/Cryptography/StellaOps.Cryptography.Plugin/ ships CryptoProviderPackCatalog, CryptoProviderPackProbeReporter, CryptoPluginBase, and CredentialStoreInlineKey (cited by §4.3b without naming the owning project).

The AddStellaOpsCrypto(...) entry point (StellaOps.Cryptography.DependencyInjection) registers DefaultCryptoProvider, ICryptoHash/ICryptoHmac/ICryptoRandom, and the ICryptoProviderRegistry. The dependency-injection project is base-safe by default: regional, simulator, and proprietary provider source files plus their project references are compiled only when StellaOpsEnableOptionalCryptoProviders=true is set for an explicit provider-pack build. Base services may use AddStellaOpsCrypto(...) without pulling GOST/SM/PQ/simulator/CSP provider DLLs into their publish graph.

SmRemote runtime boundary (2026-06-05; contracts-only split completed 2026-06-07, d2e24267aa). src/SmRemote/StellaOps.SmRemote.Service does not consume AddStellaOpsCrypto(...); it constructs the narrow provider registry it needs for the remote SM2/SM4 API. The service host references the canonical crypto assembly plus the host-owned HTTP transport in StellaOps.Cryptography.Sm.Contracts (the production cn.sm.remote.http forwarder). As of the contracts-only split it no longer ProjectReferences StellaOps.Cryptography.Plugin.SmRemote or StellaOps.Cryptography.Plugin.SmSoft— the in-process cn.sm.soft provider is mounted as a signed pack (Production) or reached via a reflective Dev/Testing fallback (§4.3b). It does not reference the broad crypto DI package, sim.crypto.remote, PQ, GOST, CSP, or other optional regional provider payloads directly. A follow-up Authority slice removed the unused Auth.ServerIntegration -> StellaOps.Configuration project reference, so freshly built Auth.ServerIntegration restored assets no longer carry StellaOps.Configuration, StellaOps.Cryptography.DependencyInjection, or the optional provider assemblies. Full extraction of cn.sm.soft to a mounted provider pack is complete (ADR-024, 2026-06-07): the com.stellaops.crypto.sm pack and cn.sm.soft runtime provider IDs are reconciled, and the in-process software provider loads via the fail-closed MountedSmCryptoRuntimePluginLoader (crypto:provider:sm, runtime-bundle.v1, AllowUnsigned=false). cn.sm.soft stays test/local-harness only by default; Production startup rejects it unless the operator opts in with SMREMOTE_ALLOW_SIGNED_SOFT_PACK_IN_PROD AND an admitted signed pack supplies it (fail-closed otherwise — see §4.3a). StellaOps.Configuration can expose registry options without pulling optional provider payloads because the shared crypto DI project now keeps optional provider registrations behind an explicit opt-in build property.


1.1 Provider catalog (DB-driven, SPRINT_20260503_008)

The set of well-known providers surfaced by the Platform admin UI is held in platform.crypto_provider_catalog(Postgres). Each row is metadata only — provider id, display name, health endpoint, advertised algorithms, supported compliance profiles, production / test-only flags, and the linked plug-in id. Catalog rows are not executable code; the actual ICryptoProvider implementations still ship as code under src/__Libraries/StellaOps.Cryptography.Plugin.* and src/Cryptography/....

The boundary is non-negotiable: catalog identity is a foreign key into the registered-plug-in set, not a substitute for it. Loading arbitrary DLLs from a DB row would punch a hole in supply-chain integrity.

The migration (068_CryptoProviderCatalog.sql) auto-applies on startup (per CoC §2.7) and the catalog is seeded from src/Platform/StellaOps.Platform.WebService/Configuration/crypto-provider-catalog.seed.yaml on first boot only — subsequent updates flow through the /api/v1/admin/crypto-providers/catalog admin endpoints with audit emission (AuditActions.Platform.{Create,Update,Delete}CryptoCatalogEntry).

The seed YAML preserves the previously hard-coded list of smremote, cryptopro, and crypto-sim byte-for-byte so first-boot behaviour is unchanged. Operators add a new well-known provider (eIDAS QTSP, HSM PKCS#11, GOST CryptoPro/OpenSSL, …) by either inserting a catalog row directly or calling POST /api/v1/admin/crypto-providers/catalog with the crypto:admin scope (catalog write endpoints are POST/PUT/DELETE /api/v1/admin/crypto-providers/catalog[/{providerId}]; the catalog group requires authorization but is not tenant-scoped because platform.crypto_provider_catalog is a platform-global resource). (A cloud signing-KMS adapter such as AWS/GCP can be catalogued too, but it is an explicit operator opt-in, not a default — see §4.9 on the on-prem-first posture.)

2) External dependencies


3) Contracts & data model

3.1 Core provider model (ICryptoProvider / ICryptoProviderRegistry)

The crypto core is capability- and algorithm-id based, not a per-profile ISignatureProvider. A provider declares which (capability, algorithmId) pairs it supports; the registry resolves the first capable provider in a deterministic preferred order. (Source: CryptoProvider.cs, CryptoProviderRegistry.cs.)

public enum CryptoCapability
{
    PasswordHashing, Signing, Verification,
    SymmetricEncryption, KeyDerivation, ContentHashing
}

public sealed record CryptoKeyReference(string KeyId, string? ProviderHint = null);

public interface ICryptoProvider
{
    string Name { get; }                                        // e.g. "default", "fips.ecdsa.soft"
    bool Supports(CryptoCapability capability, string algorithmId);

    IPasswordHasher GetPasswordHasher(string algorithmId);
    ICryptoHasher   GetHasher(string algorithmId);              // content hashing
    ICryptoSigner   GetSigner(string algorithmId, CryptoKeyReference keyReference);
    ICryptoSigner   CreateEphemeralVerifier(string algorithmId, ReadOnlySpan<byte> publicKeyBytes);

    void UpsertSigningKey(CryptoSigningKey signingKey);
    bool RemoveSigningKey(string keyId);
    IReadOnlyCollection<CryptoSigningKey> GetSigningKeys();
}

public interface ICryptoProviderRegistry
{
    IReadOnlyCollection<ICryptoProvider> Providers { get; }
    bool TryResolve(string preferredProvider, out ICryptoProvider provider);
    ICryptoProvider ResolveOrThrow(CryptoCapability capability, string algorithmId);

    // Explicit provider hints are fail-closed: an unknown/unsupported hint throws,
    // it does NOT fall back to another provider (see §4.4).
    CryptoSignerResolution ResolveSigner(CryptoCapability capability, string algorithmId,
        CryptoKeyReference keyReference, string? preferredProvider = null);
    CryptoHasherResolution ResolveHasher(string algorithmId, string? preferredProvider = null);
}

3.2 Hash computation (ICryptoHash / ICryptoHasher)

ICryptoHash is purpose-driven — callers request a hash for a HashPurposeand the active compliance profile resolves the concrete algorithm. It does not expose ComputeSha256/ComputeBlake3 convenience methods, and there is no HashAlgorithm enum (algorithm ids are the string constants in HashAlgorithms). (Source: ICryptoHash.cs, HashAlgorithms.cs.)

public interface ICryptoHash
{
    // Algorithm-based (backward compatible) — algorithmId is null ⇒ default algorithm
    byte[] ComputeHash(ReadOnlySpan<byte> data, string? algorithmId = null);
    string ComputeHashHex(ReadOnlySpan<byte> data, string? algorithmId = null);
    string ComputeHashBase64(ReadOnlySpan<byte> data, string? algorithmId = null);
    ValueTask<byte[]> ComputeHashAsync(Stream stream, string? algorithmId = null, CancellationToken ct = default);

    // Purpose-based (preferred for compliance)
    byte[] ComputeHashForPurpose(ReadOnlySpan<byte> data, string purpose);
    string ComputeHashHexForPurpose(ReadOnlySpan<byte> data, string purpose);
    string ComputePrefixedHashForPurpose(ReadOnlySpan<byte> data, string purpose); // e.g. "blake3:abc…"

    // Metadata
    string GetAlgorithmForPurpose(string purpose);
    string GetHashPrefix(string purpose);                       // "blake3:" / "sha256:" / "gost3411:" / "sm3:"
}

// Single-algorithm hasher returned by ICryptoProvider.GetHasher(...)
public interface ICryptoHasher
{
    string AlgorithmId { get; }
    byte[] ComputeHash(ReadOnlySpan<byte> data);
    string ComputeHashHex(ReadOnlySpan<byte> data);
}

Algorithm-id constants (HashAlgorithms.cs): SHA256, SHA384, SHA512, GOST3411-2012-256, GOST3411-2012-512, BLAKE3-256, SM3. There is no Blake3-512 and no Streebog* token — GOST Stribog is spelled GOST3411-2012-256/512.

Signature algorithm-id constants (SignatureAlgorithms.cs): ES256/ES384/ ES512, RS256/RS384/RS512, PS256/PS384/PS512, ED25519, EdDSA, GOST12-256, GOST12-512, SM2, DILITHIUM3, FALCON512.

3.2.1 VEX signature scheme verification (Excititor consumer)

Excititor’s ProductionVexSignatureVerifier (src/Concelier/__Libraries/StellaOps.Excititor.Core/Verification/ProductionVexSignatureVerifier.cs) is the reference consumer of the cryptography registry for VEX signature verification. Sprint 502_004 EXCITITOR-CRYPTO-AUDIT-00 (2026-05-02) audited this integration and recorded the binding shape; the dossier captures the conclusions here so future readers don’t repeat the audit.

The integration uses JOSE algorithm IDs, not signature scheme names. When the verifier resolves a provider it calls:

provider = _cryptoProviders.ResolveOrThrow(CryptoCapability.Signing, algorithm);
verifier = provider.CreateEphemeralVerifier(algorithm, key.PublicKey.Span);

…where algorithm is one of the JOSE constants from src/__Libraries/StellaOps.Cryptography/SignatureAlgorithms.cs (ES256, ES384, ES512, RS256, RS384, RS512, ED25519, EdDSA). These primitives are already shipped by DefaultCryptoProvider, EcdsaSigner, LibsodiumCryptoProvider, and CompliancePolicyCryptoProviders.

Signature schemes (cosign-detached, cosign-keyless, pgp-detached, dsse) are NOT registry algorithm IDs. They are scheme names that the verifier represents as VerificationMethod enum values, selected by ExtractSignatureInfo reading document metadata (signature-type, cosign-keyid, pgp-keyid, DSSE envelope JSON). The schemes wrap the underlying JOSE primitives — cosign signs with ECDSA-P256, PGP with RSA or ECDSA, DSSE envelopes any of the above. Operator-facing config (e.g. etc/excititor.worker.yaml.sample per-connector signature.profile) uses the scheme names as routing labels; they do not need round-tripping with the registry.

Per-issuer routing table (binding): see Excititor architecture §4.2.1 “Connector signature sources” for the canonical Cisco→cosign-keyless / Oracle→pgp-detached / RedHat→cosign-detached (with pgp-detached fallback) / Ubuntu→pgp-detached / Rancher→dsse / MSRC→quarantine mapping. The cryptography dossier deliberately does NOT duplicate that table to avoid drift.

Open work — scheme-level verifier methods (owned by Excititor, not this dossier). Several ProductionVexSignatureVerifier scheme strategies (PGP-detached, cosign-keyless, DSSE-keyless, X.509-chain) are scheme-level wirings inside the Excititor verifier — Fulcio/PGP/X.509 chain handling on top of the registry primitives (ECDSA, RSA, Ed25519), all of which already exist in this module. Because that is another module’s implementation state (with volatile per-method line numbers that rot silently — §1.5), the authoritative, dated tracking lives in the Excititor dossier / verification sprint, not here. This cryptography dossier keeps only the binding shape above (JOSE algorithm IDs vs. scheme names). Worker host wiring of AllowUnsigned=false and the per-connector profile configuration ships as part of sprint 502_001 EXCITITOR-WORKER-VRF-01…02; the YAML sample at etc/excititor.worker.yaml.sample is the binding configuration shape.

License posture (CLAUDE.md §2.6): the audit confirmed no new external dependencies are required for the VEX signature scheme work. Bouncy Castle (Apache-2.0, already vendored under StellaOps.Cryptography.Plugin.OpenPgp and third-party-licenses/) covers PGP packet parsing and ECDSA primitives where the BCL is insufficient. Sigstore-go bindings, Sigstore.NET, or Cosign-net are NOT introduced; cosign signature math is implemented against the existing ECDsa primitives directly.

3.3 MAC primitives (IHmacAlgorithm)

public interface IHmacAlgorithm
{
    byte[] ComputeMac(
        ReadOnlyMemory<byte> key,
        ReadOnlyMemory<byte> data,
        HmacAlgorithmName algorithm = default);

    bool Verify(
        ReadOnlyMemory<byte> key,
        ReadOnlyMemory<byte> data,
        ReadOnlySpan<byte> expectedMac,
        HmacAlgorithmName algorithm = default);
}

IHmacAlgorithm is the only sanctioned HMAC API for production code in the StellaOps repository. Direct calls to System.Security.Cryptography.HMACSHA256, HMACSHA384, or HMACSHA512 are forbidden outside the canonical crypto assembly (src/__Libraries/StellaOps.Cryptography/) — specifically DefaultHmacAlgorithm.cs and FipsHmacAlgorithm.cs, which are the only sanctioned BCL call sites — plus the legacy FipsPlugin.HashAsync HMAC switch in src/Cryptography/StellaOps.Cryptography.Plugin.Fips/FipsPlugin.cs. The architecture-conformance test in src/Cryptography/__Tests/StellaOps.Cryptography.Tests/HmacBclCallSiteConformanceTests.cs enforces this rule.

Note (SPRINT_20260430_017): IHmacAlgorithm and its types (HmacAlgorithmName, DefaultHmacAlgorithm, FipsHmacAlgorithm) live in the canonical legacy assembly (src/__Libraries/StellaOps.Cryptography/), not in the SPRINT_000 scaffolding project (src/Cryptography/StellaOps.Cryptography/) which has been deleted. Public namespaces are unchanged (StellaOps.Cryptography, StellaOps.Cryptography.Plugin, StellaOps.Cryptography.Plugin.Fips); consumers reference the canonical assembly via __Libraries/StellaOps.Cryptography.csproj. The SPRINT_000-introduced StellaOps.Cryptography.dll AssemblyName collision that broke Auth.ServerIntegration is retired.

Algorithm names

HmacAlgorithmName is a value type modelled on the BCL HashAlgorithmName style. The repository ships three well-known names:

Name tokenStatic memberOutput bytes
HMAC-SHA256HmacAlgorithmName.HmacSha256 (default)32
HMAC-SHA384HmacAlgorithmName.HmacSha38448
HMAC-SHA512HmacAlgorithmName.HmacSha51264

default(HmacAlgorithmName) resolves to HmacSha256 so callers can omit the algorithm parameter and still get a documented MAC algorithm.

Implementations

ImplementationNamespaceFile location (canonical assembly)Use
DefaultHmacAlgorithmStellaOps.Cryptography.Pluginsrc/__Libraries/StellaOps.Cryptography/DefaultHmacAlgorithm.csDefault for non-regional deployments. Backed by BCL one-shots.
FipsHmacAlgorithmStellaOps.Cryptography.Plugin.Fipssrc/__Libraries/StellaOps.Cryptography/FipsHmacAlgorithm.csFIPS-validated path; lifted from the legacy FipsPlugin.HashAsync HMAC switch.

Both implementations call the same BCL one-shot static methods (HMACSHA256.HashData, HMACSHA384.HashData, HMACSHA512.HashData); they delegate to the FIPS-validated provider on systems where FIPS mode is enabled. The split exists so that the FIPS plugin can advertise a distinct, attested implementation in the plugin manifest, and so that key handling differs:

Regional plugin extension

GOST and SM regional plugins may register their own MAC algorithms by:

  1. Introducing additional HmacAlgorithmName tokens (for example HMAC-Streebog256 or HMAC-SM3) — pattern follows the HmacSha256 / HmacSha384 / HmacSha512 static members in src/__Libraries/StellaOps.Cryptography/HmacAlgorithmName.cs.
  2. Implementing IHmacAlgorithm in the regional plugin assembly (StellaOps.Cryptography.Plugin.Gost, StellaOps.Cryptography.Plugin.Sm, etc.) — same constructor pattern as FipsHmacAlgorithm.
  3. Updating the architecture-conformance allow-list in src/Cryptography/__Tests/StellaOps.Cryptography.Tests/HmacBclCallSiteConformanceTests.cs to cover the new plugin path.

The regional plugin MAC implementations are out of scope for the foundational sprint that introduced IHmacAlgorithm; they are scheduled in their own follow-up sprints once the corresponding hash primitives (Streebog, SM3) are exposed.

3.3.1 Cryptographic RNG primitives (ICryptoRandom)

public interface ICryptoRandom
{
    void Fill(Span<byte> destination);
    byte[] GetBytes(int count);
}

ICryptoRandom is the only sanctioned cryptographic-RNG API for production code in the StellaOps repository. Direct calls to System.Security.Cryptography.RandomNumberGenerator (Fill, GetBytes, Create) and the legacy RNGCryptoServiceProvider are forbidden outside the canonical crypto assembly (src/__Libraries/StellaOps.Cryptography/) and the regional crypto plugins. The architecture-conformance test in src/Cryptography/__Tests/StellaOps.Cryptography.Tests/RandomBclCallSiteConformanceTests.cs enforces this rule (active enforcement as of Sprint 503_011).

The non-cryptographic System.Random.Shared surface is covered separately by the determinism analyzer (STELLA0114); ICryptoRandom intentionally focuses on cryptographic-grade randomness only.

Implementations

ImplementationNamespaceFile locationUse
DefaultCryptoRandomStellaOps.Cryptographysrc/__Libraries/StellaOps.Cryptography/DefaultCryptoRandom.csDefault for all deployments. Backed by BCL RandomNumberGenerator.Fill.

AddStellaOpsCrypto(...) registers ICryptoRandomDefaultCryptoRandom as a singleton. Regional plugins (FIPS / HSM / CryptoPro) wrap their own CSPRNG access internally through their plugin entry points; the shared abstraction does not need to be plugin-aware in the same way ICryptoHmac is. If a future regional plugin needs to plug its own RNG into ICryptoProviderRegistry, the default can be superseded via the standard DI override.

3.3.2 AEAD primitives (IAeadAlgorithm)

The canonical assembly also defines an authenticated-encryption surface — IAeadAlgorithm, AeadAlgorithmName, and DefaultAeadAlgorithm (src/__Libraries/StellaOps.Cryptography/). This is the sanctioned AEAD API that the §11 re-seal profiles (AES-256-GCM / SM4-GCM / GOST-GCM) and CredentialStore.CredentialAead route sealing through, in the same “one sanctioned primitive” spirit as IHmacAlgorithm (§3.3) and ICryptoRandom (§3.3.1). AeadAlgorithmName enumerates the supported AEAD algorithm identifiers; production code seals/unseals via IAeadAlgorithm rather than constructing BCL AEAD ciphers directly.

Migration narrative (Sprint 503_011)

Sprint 503_011 (SPRINT_20260503_011_Cryptography_icryptorandom_abstraction) introduced the abstraction and migrated the production-code snapshot recorded by Sprint 503_005 / B-CRYPTO-004. The pattern:

Test fixtures, oracles, and benchmark seeds remain on the BCL by design and stay in RandomBclCallSiteConformanceTests.KnownLegacyOffenders as permanent annotations (same convention as HmacBclCallSiteConformanceTests).

3.4 Compliance profile (ComplianceProfile)

The regional routing primitive is ComplianceProfile(not a CryptoProfile record — that type does not exist). A profile maps each HashPurpose to a hash algorithm + display prefix and each HmacPurpose to an HMAC algorithm. ICryptoComplianceService.GetAlgorithmForPurpose(purpose) resolves the algorithm for the active profile; ValidateAlgorithm(...) enforces it (fail-closed when StrictValidation=true). (Source: ComplianceProfile.cs, CryptoComplianceService.cs.)

public sealed class ComplianceProfile
{
    public required string ProfileId { get; init; }       // "world" | "fips" | "gost" | "sm" | "kcmvp" | "eidas"
    public required string StandardName { get; init; }    // e.g. "FIPS 140-3"
    public string? Description { get; init; }
    public required IReadOnlyDictionary<string, string> PurposeAlgorithms { get; init; }
    public required IReadOnlyDictionary<string, string> HashPrefixes { get; init; }
    public IReadOnlyDictionary<string, string>? HmacPurposeAlgorithms { get; init; }
    public bool AllowInteropOverride { get; init; } = true;  // interop purpose may stay SHA-256

    public string GetAlgorithmForPurpose(string purpose);
    public string GetHashPrefix(string purpose);
    public string GetHmacAlgorithmForPurpose(string purpose);
    public bool IsCompliant(string purpose, string algorithmId);
}

public interface ICryptoComplianceService
{
    ComplianceProfile ActiveProfile { get; }
    string GetAlgorithmForPurpose(string purpose);
    string GetHashPrefix(string purpose);
    void ValidateAlgorithm(string? purpose, string requestedAlgorithm); // throws CryptoComplianceException when strict
    bool IsAlgorithmCompliant(string algorithmId);
}

3.5 OpenPGP Email Encryption Provider

OpenPGP public-key encryption for operator-controlled email handoffs is exposed through StellaOps.Cryptography.OpenPgpEncryptionProvider/v1, implemented by src/Cryptography/StellaOps.Cryptography.Plugin.OpenPgp/. Product modules call this provider boundary instead of using OpenPGP libraries or external gpg commands directly.

The provider validates the supplied full recipient fingerprint against the local public key ring, rejects revoked or expired primary/encryption keys, checks encryption-key capability and key flags when present, emits ASCII-armored ciphertext, and records SHA-256 evidence for the exact ciphertext plus provider/key identity metadata. It requires the caller to supply the evaluation time so expiry checks remain deterministic and testable offline.

Contract doc: docs/contracts/openpgp-encryption-provider-v1.md.


4) Supported Profiles

4.1 Compliance profiles (regional routing)

The active compliance profile is the primary regional-routing mechanism. It is a per-tenant preference (Crypto:Compliance:ProfileId, default world, overridable via STELLAOPS_CRYPTO_COMPLIANCE_PROFILE). Each profile maps the seven HashPurpose values to a concrete hash algorithm, and the three HmacPurpose values to an HMAC algorithm. Components hash/MAC by purpose; the profile resolves the algorithm. (Source: ComplianceProfiles.cs, HashPurpose.cs, HmacAlgorithms.cs.)

The six built-in profiles (ComplianceProfiles.All):

ProfileStandardNamegraphsymbol/content/merkle/attestationinteropsecret (password)HMAC (signing/auth)
worldISO/DefaultBLAKE3-256SHA256SHA256argon2idHMAC-SHA256
fipsFIPS 140-3SHA256 (BLAKE3 not FIPS-approved)SHA256SHA256pbkdf2-sha256HMAC-SHA256
gostGOST R 34.11-2012GOST3411-2012-256GOST3411-2012-256SHA256argon2idHMAC-GOST3411
smGB/T (SM3)SM3SM3SHA256argon2idHMAC-SM3
kcmvpKCMVP (Korea)SHA256SHA256SHA256argon2idHMAC-SHA256
eidaseIDAS/ETSI TS 119 312SHA256SHA256SHA256argon2idHMAC-SHA256

Notes:

4.2 Registered providers (provider-id inventory)

ICryptoProvider.Name is the stable provider id used in fail-closed routing hints and the platform catalog. Verified registered ids (from source Name => declarations):

Provider idType / projectCapabilityGate / posture
defaultDefaultCryptoProviderES*/RS*/PS*/Ed25519 sign+verify, SHA hashingalways registered
libsodiumLibsodiumCryptoProviderEd25519, Argon2idSTELLAOPS_CRYPTO_SODIUM build
fips.ecdsa.softFipsSoftCryptoProviderES256/384/512, SHA256/384/512env gate FIPS_SOFT_ALLOWED (software, non-certified)
eu.eidas.softEidasSoftCryptoProviderES256/384, SHA256/384env gate EIDAS_SOFT_ALLOWED (non-QSCD software)
kr.kcmvp.hashKcmvpHashOnlyProviderSHA256 hashing onlyenv gate KCMVP_HASH_ALLOWED
eidasEidasCryptoProvider (Plugin.EIDAS)RSA/CMS sign+verify, CAdESremote TSP/QSCD HTTP; fail-closed above CAdES-B
cn.sm.softSmSoftCryptoProvider; host-owned verification-only facade in ProductionSM2/SM3 software outside Production; SM2 public-key verification only in Production DSSE hostsDev/Test signing by default; Production SmRemote signed-pack opt-in is restricted to the sidecar harness (SMREMOTE_ALLOW_SIGNED_SOFT_PACK_IN_PROD + admitted pack, §4.3a) and does not enable host software signing
cn.sm.remote.httpSmRemoteHttpProviderSM2 over HTTPproduction SM via operator HSM adapter
ru.openssl.gostOpenSslGostProviderGOST R 34.10-2012configured OpenSSL GOST
ru.cryptopro.cspCryptoProGostCryptoProviderGOST via CryptoPro CSPlicensed CryptoPro
ru.pkcs11Pkcs11GostCryptoProviderGOST via PKCS#11configured PKCS#11 GOST
pq.softPqSoftCryptoProviderDILITHIUM3 / FALCON512experimental
bouncycastle.ed25519BouncyCastleEd25519CryptoProviderEd25519optional
kmsKmsCryptoProvider (Kms)signing via IKmsClientFile/FIDO2 default; PKCS#11 (HSM) on-prem backend; AWS/GCP optional, non-default
sim.crypto.remoteSimRemoteProvideruniversal simulatortest-only; STELLAOPS_ALLOW_UNSAFE_CRYPTO_SIM=1
offline.verificationOfflineVerificationCryptoProviderverification onlyair-gap verification

For a confirmed regional Production profile, StellaOps.Cryptography.HostProviders composes the portable holders directly into the canonical default-ALC ICryptoProvider registry: ru.pkcs11 from StellaOps:Crypto:Pkcs11 and cn.sm.remote.http from StellaOps:Crypto:SmRemote. The SM production composition also registers a separate verification-only cn.sm.soft facade; environment variables or software options cannot promote that facade to a signer. Production SmRemote composition forces a live vendor-contract probe and explicit key mapping (SkipProbe=false, RequireVendorAdapterContract=true, AllowImplicitKeyMapping=false), and the provider validates the status envelope before advertising SM2. ru.cryptopro.csp remains a licensed, distribution-specific registration. See regional signing.

The in-process HSM provider (HsmPlugin, src/Cryptography/StellaOps.Cryptography.Plugin.Hsm/) and the in-process BouncyCastle GOST/SM plugins (StellaOps.Cryptography.Plugin.Gost / .Sm) register through the plugin loader rather than AddStellaOpsCrypto’s default set.

4.3 Signature profiles (signing-side curve/algorithm selection)

Separately from the compliance (hashing) profiles above, signing services select a signature profile. The audit/evidence-chain default is EdDSA Ed25519:

ProfileHashSignatureUse Case
eddsa-ed25519SHA-512Ed25519Audit/evidence chain default
ecdsa-p256SHA-256ECDSA P-256 (ES256)Regional interop / FIPS profile; not evidence default
ecdsa-p384SHA-384ECDSA P-384 (ES384)Higher security
ecdsa-secp256k1SHA-256ECDSA secp256k1Blockchain compat

Audit bundles, evidence capsules, release decision capsules, and other evidence-chain signatures resolve eddsa-ed25519 unless a service is explicitly signing for a regional interop profile. ECDSA-P256 remains available for FIPS/eIDAS/legacy interoperability, but it must not be bound as the default audit-chain signer because the current P-256 signer is non-deterministic.

Regional signature algorithm ids (advertised by the regional providers above): GOST12-256/GOST12-512 (GOST R 34.10-2012), SM2, plus DILITHIUM3 / FALCON512 (PQ, experimental). The eIDAS plugin signs CMS/CAdES with RSA/ECDSA.

4.3a SM Remote Runtime Guard

SM Remote exposes SM2/SM3/SM4 over HTTP for services that cannot run the SM provider in-process. Its production posture is fail closed:

4.3b SM crypto contracts-only split (ADR-024, d2e24267aa)

The SmRemote service image must not statically link the in-process SM crypto provider plug-ins. The boundary is enforced by a project-reference boundary test (SmRemoteRuntimePluginBoundaryTests, flipped ContainsDoesNotContain): StellaOps.SmRemote.Service no longer ProjectReferences StellaOps.Cryptography.Plugin.SmRemote or StellaOps.Cryptography.Plugin.SmSoft. This closed the last two hard source-coupling violations (repo audit violationCount → 0).

The decomposition follows the same shape as the GOST runtime-plugin split — host-owned transport vs signed provider pack — but with one deliberate asymmetry: the SM remote path carries no in-process crypto material, so it stays host-owned rather than becoming a mounted pack.

Host-owned HTTP transport — StellaOps.Cryptography.Sm.Contracts. The HTTP forwarder and its DTOs were extracted into a new contracts-only library that is its own project at src/__Libraries/StellaOps.Cryptography.Sm.Contracts/(NOT under src/Cryptography/, and NOT hosted inside Plugin.SmRemote). It contains SmRemoteHttpClient (incl. the request/response DTOs and SmRemoteVendorContract), SmRemoteHttpProvider, SmRemoteProviderOptions, and SmRemoteSigner. The thin src/__Libraries/StellaOps.Cryptography.Plugin.SmRemote/ project merely references Sm.Contracts; the SmRemote service references Sm.Contracts directly (verified in src/SmRemote/StellaOps.SmRemote.Service/StellaOps.SmRemote.Service.csproj) — that direct reference is the point of ADR-024. The types were moved with their namespaces preserved, so Cli, StellaOps.Cryptography.DependencyInjection, and Attestor compile unchanged. cn.sm.remote.http is a pure forwarder with no in-process crypto material, so it is woven directly into the SmRemote service Program.cs endpoints and stays in-process — it is not a signed mounted pack and not a base-image crypto default. In CryptoProviderPackCatalog it is catalogued ownership=HostOwned, requiresSignedPack=false, cloudManaged=false, productionAllowed=true.

Signed mounted pack — cn.sm.soft. Only the in-process SM2/SM3 software provider (StellaOps.Cryptography.Plugin.SmSoft) becomes a signed mounted crypto pack. It is catalogued ownership=RuntimePlugin, requiresSignedPack=true, profiles=[crypto-sm, sm], productionAllowed=true with the decision text “the in-process SM software provider carries real SM2/SM3 material and must be mounted as a signed crypto pack, never compile-time referenced by the SmRemote service image.” BouncyCastle SM3/SM4 remain host-owned — the violation was the project references, not the NuGet package.

MountedSmCryptoRuntimePluginLoader (src/SmRemote/StellaOps.SmRemote.Service/Plugins/). A new long-lived loader (NOT the GOST probe-and-unload loader) that instantiates the SM provider from the mounted signed pack and feeds it to the host ICryptoProviderRegistry. It reuses the shared SignedRuntimePluginAdmission (module crypto, capability crypto:provider:sm, contract runtime-bundle.v1, detached RSA-PKCS1-SHA256, AllowUnsigned = false) for fail-closed admission, plus a Default-ALC Resolving hook that serves the transitive StellaOps.* dependencies the mounted provider assemblies carry (the TPA-list trap — without the hook, activation throws FileNotFoundException; BouncyCastle is already on the host TPA). Crucially it ActivatorUtilities-activates the ICryptoProvider, so the host-registered SmRemoteHttpClient, IOptions<T>, and ILogger<T> constructor dependencies are injected. This host-DI activation is the operational gap the Signer GOST probe loader cannot fill: a probe loader uses a collectible ALC and a parameterless activator, so it can never serve a live provider for /sign and /verify. Providers are de-duplicated by ICryptoProvider.Name so a bundle mounted twice registers once and a shadowed provider is surfaced as rejected (no silent-green).

crypto-sm bundle producer profile. devops/build/package-runtime-plugins.ps1 gains a crypto-sm profile (alongside crypto-gost) that stages a signed cn.sm.soft bundle from StellaOps.Cryptography.Plugin.SmSoft.csproj with capability crypto:provider:sm. Mount it at devops/plugins/crypto/crypto-sm/app/plugins/crypto/crypto-sm plus the trust root, mirroring crypto-gost. The Production runtime guard, opt-in flag, and fail-closed-without-pack semantics are documented in §4.3a; the compose mount is the live-lane follow-up (the admission/activation paths are covered by SmRuntimePluginLoaderAdmissionTests, with every reject path admitting zero providers).

Dev/Testing fail-open-to-host fallback. Outside Production, the SmRemote service Program.cs falls back to the in-process cn.sm.soft provider via a reflective SmHostDevFallbackProviderFactory (no compile-time SmSoft reference, preserving the boundary) so local harnesses keep working. Production has no fallback: an empty provider set faults startup (asserted fail-closed).

4.4 Provider Maturity And Fail-Closed Selection

Regional providers must not silently upgrade an unconfigured deployment into a signing authority:

Provider selection therefore means “resolve a configured capable provider” rather than “fall back to a generated local provider.” If the requested regional profile lacks a configured provider, callers must surface unsupported or unavailable and fail the verification/signing path closed.

4.5 Provider Implementation Inventory (Local Inspection, 2026-04-27)

This map is based only on local source, docs, and devops files. It separates production-capable provider code from service adapters, simulator harnesses, and deployment/provider-pack material.

ProfileProduction provider codeService or microservice adapterSimulator/local harnessDeployment/provider material and route tests
FIPSsrc/Cryptography/StellaOps.Cryptography.Plugin.Fips/ and src/__Libraries/StellaOps.Cryptography/CompliancePolicyCryptoProviders.cs (fips.ecdsa.soft) provide RSA/ECDSA/AES/SHA paths gated by configured keys and FIPS_SOFT_ALLOWED for software profile use.No FIPS-specific crypto microservice exists locally; HSM-backed FIPS deployments should use the HSM provider path.GenerateKeysOnInit and derived symmetric keys are explicit harness flags; sim.crypto.remote advertises fips.sim through the simulator manifest.Deployment profiles supply configured keys, HSM, or KMS material for Signer, Authority, and Attestor. OS FIPS-mode/provider proof is deployment evidence, not a reason to create another FIPS provider.
eIDASsrc/Cryptography/StellaOps.Cryptography.Plugin.Eidas/ contains the eIDAS plugin, RSA/CMS signing, CAdES builder, timestamp policy selector, and EU trust-list service. CAdES-B can be produced with configured certificate material; CAdES-T/LT/LTA and upgrade paths now fail closed instead of adding placeholder timestamp/revocation attributes. src/__Libraries/StellaOps.Cryptography.Plugin.EIDAS/ now performs real HTTP sign/verify calls to a configured TSP/QSCD adapter instead of fabricating remote signatures, and public JWK export is derived from configured certificate material. Local keys export from the local keystore certificate; TSP/QSCD keys must expose a public certificate in configuration or fail closed. The active provider exposes `EvidenceProfile=BaselineBBaselineTBaselineLTBaselineLTA; profiles aboveBaselineBfail closed unless timestamp/revocation/archive/TSL evidence is present, and still reject signing until evidence embedding is enabled by a real provider pack.EidasSealedFixturePackValidatorpreflights future vendor fixture packs atEidas:TrustedList:FixturePackManifestPathby checking schema, SHA-256s, expiry, jurisdiction/profile, required roles, and expected accepted/rejected verdicts, but reportsproductionCryptographicValidation=false. Attestor ProofChain also rejects structural-only timestamp tokens so policy resolution cannot bypass the provider evidence gate.eu.eidas.softis a non-QSCD software ECDSA provider gated byEIDAS_SOFT_ALLOWED`.
GOSTsrc/__Libraries/StellaOps.Cryptography.Plugin.OpenSslGost/, src/__Libraries/StellaOps.Cryptography.Plugin.Pkcs11Gost/, and src/__Libraries/StellaOps.Cryptography.Plugin.CryptoPro/ are real provider libraries for OpenSSL GOST, PKCS#11 GOST, and Windows CryptoPro-backed GOST.devops/compose/docker-compose.crypto-provider.cryptopro.yml declares a cryptopro-csp service; devops/services/cryptopro/linux-csp-service/ provides the fail-closed Linux wrapper for customer-provided CryptoPro CSP packages.src/Cryptography/StellaOps.Cryptography.Plugin.Gost/ is an in-process BouncyCastle provider suitable for hashes and explicit local 256-bit generated signing tests; it does not advertise generated GOST-R34.10-2012-512 signing. sim.crypto.remote and docker-compose.crypto-provider.crypto-sim.yml advertise GOST simulation.Deployment packs provide licensed CryptoPro/OpenSSL/PKCS#11 configuration, vectors, license evidence, and Attestor/Signer/Authority route tests. GOST is existing provider code and must not be planned as a blank implementation.
SMsrc/__Libraries/StellaOps.Cryptography.Plugin.SmSoft/ provides gated software SM2/SM3 (now also packaged as the signed cn.sm.soft crypto pack on profile crypto-sm, mounted via MountedSmCryptoRuntimePluginLoader — see §4.3b); src/Cryptography/StellaOps.Cryptography.Plugin.Sm/ provides BouncyCastle SM2/SM3/SM4 with configured keys or explicit local generated keys.The host-owned HTTP transport (SmRemoteHttpClient, DTOs, SmRemoteHttpProvider, cn.sm.remote.http) now lives in the contracts-only StellaOps.Cryptography.Sm.Contracts library (at src/__Libraries/StellaOps.Cryptography.Sm.Contracts/, per §4.3b — the thin src/Cryptography/StellaOps.Cryptography.Plugin.SmRemote/ project merely references it); the SmRemote service no longer compile-time references the SM provider plug-ins (ADR-024, §4.3b). src/SmRemote/ is the SM Remote web service with /hash, /encrypt, /decrypt, /sign, /verify, auth scopes, and tenant requirements. Production can now register cn.sm.remote.http against an operator-controlled HSM adapter using SM_REMOTE_HSM_URL, optional SM_REMOTE_HSM_API_KEY, and explicit SMREMOTE_KEYS mappings. devops/compose/docker-compose.sm-remote.yml builds the standalone service with production fail-closed defaults.src/__Libraries/StellaOps.Cryptography.Plugin.SimRemote/ and docker-compose.crypto-provider.crypto-sim.yml simulate SM algorithms for tests; SmRemote Testing/Development may use software and ephemeral keys only with explicit local harness gates.Deployment packs add vendor-specific HSM, SDF, or PKCS#11 adapters behind the stable SM Remote HTTP contract and route coverage through Signer/Authority/Attestor. The service host has a production path without falling back to simulator or software-only keys.
HSMsrc/Cryptography/StellaOps.Cryptography.Plugin.Hsm/ contains HsmPlugin and Pkcs11HsmClientImpl using Pkcs11Interop, with session pooling, slot failover, key lookup, sign/verify, AES-GCM encrypt/decrypt, and key metadata.No standalone HSM microservice exists locally; HSM is an in-process provider.The internal SimulatedHsmClient is available only with AllowSimulation=true in Development/Testing.Deployment profiles supply PKCS#11 library path, slot, PIN secret, token labels, and key IDs. SoftHSM/device integration coverage proves the route without making the simulator release evidence.
Universal simulatorsrc/__Libraries/StellaOps.Cryptography.Plugin.SimRemote/ is the client/provider adapter for sim.crypto.remote; devops/etc/crypto-plugins-manifest.json classifies it under compliance: ["test-only"], sets requiresExplicitUnsafeOptIn: true, and disables it by default, while devops/compose/docker-compose.crypto-provider.crypto-sim.yml requires STELLAOPS_ALLOW_UNSAFE_CRYPTO_SIM=1.The Compose overlay declares sim-crypto, devops/services/crypto/sim-crypto-service/ provides the deterministic local service, and src/Platform/StellaOps.Platform.WebService/Services/CryptoProviderHealthService.cs probes crypto-sim.The compose overlay and provider options cover GOST, SM, PQ, FIPS, eIDAS, KCMVP, and world simulation aliases. The simulator uses deterministic HMAC signatures for smoke tests only and reports provider=sim.crypto.remote, keyId=sim-key, production=false, testOnly=true, and releaseEvidence=false from readiness and operation payloads. The default devops/services/crypto/sim-crypto-smoke run covers SM, GOST, eIDAS, FIPS, and PQ labels and rejects tampered payloads.Keep simulator use explicitly opt-in with STELLAOPS_ALLOW_UNSAFE_CRYPTO_SIM=1. Provider-route tests through Signer, Authority, Attestor, and Platform health may use it only as Development/Testing evidence.

Planning consequence: regional-crypto continuation is provider selection, configuration, health, deployment overlays, and end-to-end route tests against existing provider surfaces. Real eIDAS QSCD/QTSP/TSA/OCSP/CRL/TSL material, licensed GOST provider evidence, vendor SM HSM/SDF adapters, and physical HSM details are expected in deployment-specific provider packs. They are not simulator defaults and they do not justify fake success; unsupported advanced evidence levels fail closed until a selected pack supplies the required material. Base/default Signer crypto and offline verification are host-owned runtime capabilities, so the default recommended and harness compose profiles must not declare empty /app/plugins/crypto/{base,recommended,harness} executable bundle mounts. Explicit regional crypto overlays are the place for signed provider-pack mounts and should remain non-green when selected without real pack and trust-root material. The local crypto-gost profile is packaged as a signed runtime bundle under devops/plugins/crypto/crypto-gost/com.stellaops.crypto.gost/; the fips-hsm profile is packaged under devops/plugins/crypto/fips-hsm/ with signed com.stellaops.crypto.fips and com.stellaops.crypto.hsm packs. Signer’s plugin probe endpoint validates mounted manifest/checksum/signature/capability metadata before any provider probe. Full profile acceptance still requires live container proof with the overlay and trust-root mount, plus image audit evidence.

4.5a Provider-Pack Manifest And Registry Contract (2026-06-12)

The source catalog, manifest, registry, and profile appsettings are a static gate for the pluginized crypto split:

SurfaceSigned pack/profile IDRuntime provider IDsDefault posture
Base defaultdefaultdefaultHost-owned, base included.
Offline verificationoffline.verificationoffline.verificationHost-owned, base included, air-gap verification.
FIPScom.stellaops.crypto.fipsfips.ecdsa.softOptional signed pack; software route gated and non-certified.
HSMcom.stellaops.crypto.hsmHSM/PKCS#11 key routesOptional signed pack; operator supplies PKCS#11 material.
eIDAScom.stellaops.crypto.eidaseu.eidas.soft plus QSCD/TSP routesOptional signed pack; qualified evidence requires sealed operator material.
GOSTcom.stellaops.crypto.gostru.openssl.gost, ru.pkcs11, ru.cryptopro.csp, ru.winecsp.httpOptional regional pack; no GOST payload in base images.
SMcom.stellaops.crypto.sm, cn.sm.softcn.sm.soft, cn.sm.remote.httpOptional regional pack; production should prefer remote/HSM material.
KCMVPkr.kcmvp.hashkr.kcmvp.hashOptional hash-only route.
PQpq.softpq.softExperimental optional pack; not production release evidence.
Simulatorsim.crypto.remotesim.crypto.remoteTest-only; requires explicit unsafe flag.

devops/etc/crypto-plugins-manifest.json uses packId when the signed bundle ID differs from the runtime provider ID. devops/etc/plugins/crypto/registry.yaml exposes profile pack rows and backs each active row with providers/*.yaml. The appsettings.crypto.*.yaml profiles use runtime provider IDs. AWS/GCP/Azure KMS adapters remain future-only in source and are not present in the default/base registry profile.

For eIDAS, “provider pack” is a compliance boundary, not just a code plugin. It means either a selected QTSP/QSCD-backed deployment pack or a compliance-owned sealed pack containing RFC 3161 timestamp tokens, OCSP/CRL evidence, archive timestamp tokens, signing and TSA certificate chains, and sealed TSL/LOTL snapshots. BaselineT needs timestamp evidence, BaselineLT needs timestamp plus revocation evidence, and BaselineLTA needs those plus archive timestamp evidence. The default provider cannot make these profiles release-evidence eligible from simulator bytes or live trust-list lookups because those paths are not deterministic, not air-gap replayable, and do not prove qualified trust status. Provider-pack intake, legal review, and offline validation are tracked in docs/implplan/SPRINT_20260427_018_Cryptography_eidas_production_evidence.md.

4.6 Provider And Simulator Placement Contract

Use these locations when adding or reviewing regional crypto work:

4.7 Development/Testing Regional Fixture Policy

Regional fixture work has a simulator-backed Development/Testing path. Development and Testing can use sim.crypto.remote as the local fixture provider when STELLAOPS_ALLOW_UNSAFE_CRYPTO_SIM=1 is set. The simulator-backed path covers:

Profile familyDev/test simulator label coverageProduction provider-pack material
eIDASeidas.sim smoke signing and tamper rejection. Sealed fixture-pack preflight remains available for deterministic LT/LTA pack completeness checks.QSCD/QTSP/TSA bridge, qualified certificate chain, OCSP/CRL, TSL/LOTL, archive timestamp, and local cryptographic validation must come from real provider fixtures.
GOSTGOST12-256, GOST12-512, ru.magma.sim, and ru.kuznyechik.sim smoke coverage.Licensed CryptoPro CSP, OpenSSL GOST, or PKCS#11 route with operator key/container evidence.
SMSM2, sm.sim, and sm2.sim smoke coverage.cn.sm.remote.http through SmRemote backed by an SM HSM, SDF service, or PKCS#11 adapter with explicit key mapping.
FIPS/HSMfips.sim smoke coverage for route behavior. SoftHSM is a test-only PKCS#11 harness when configured separately.OS/provider FIPS-mode proof, certified HSM/KMS route, or vendor PKCS#11 evidence with testOnly=false.
PQpq.sim, DILITHIUM3, and FALCON512 smoke coverage for label routing only.Production PQ providers require an approved provider pack, vectors, and policy approval before release evidence eligibility.

Simulator smoke output may be stored as test evidence only. Production release decisions must reject simulator identifiers, testOnly=true responses, production=false provider payloads, and any payload that does not explicitly prove release-evidence eligibility through a real provider route.

4.8 Runtime Loader And Claim Control (eIDAS)

The eIDAS plugin (src/Cryptography/StellaOps.Cryptography.Plugin.Eidas/) ships two fail-closed loaders that gate any production CAdES-T / CAdES-LT / CAdES-LTA path. Both loaders are vendor-independent and are operative regardless of the upstream QTSP-vendor-agreement outcome (sprint SPRINT_20260502_005_Cryptography_eidas_qtsp_vendor_intake.md).

The plugin gates SignAsync and VerifyAsync for any algorithm name containing CAdES-T, CAdES-LT, or CAdES-LTA. CAdES-BES paths are unaffected — they are the simulator-success scenario allowed without the loaders being satisfied. When the gate is closed (any of the four Loader 1 branches or any of the three Loader 2 branches above), both methods throw EidasEvidenceNotProductionReadyException carrying the structured Reason enum.

The runtime emits a counsel-cleared wire-form claim state (EidasRuntimeClaimState) consumable by Doctor and evidence envelopes:

Wire stringMeaning
not-claimed:eidas-pack-not-loadedLoader 1 manifest path unset or missing.
not-claimed:eidas-pack-expiredPack validUntilUtc has passed.
not-claimed:eidas-operator-agreement-not-loadedQTSP agreement absent or expired.
not-claimed:eidas-transaction-evidence-missingLoader 2 evidence-manifest unavailable.
claimed-against:<qtsp-pack-version>Loader 1 + Loader 2 both satisfied; current production.
claimed-against:<qtsp-pack-version>:historical-validationPre-TLv6 pack consumed under the operator’s opted-in historical-validation use-case.

Claim-control wording. Per eIDAS Articles 33 / 34 / 40, runtime output paths must NOT use “qualified validation service” / “qualified preservation service” / “qualified timestamping service” / “claimed-against” affirmative claims unless all loader gates are closed AND a current TLv6 pack (or an explicit historical-validation exception) is in force. The plugin exposes the verbatim safer-wording counsel cleared in docs/legal/decisions/lawyer-response2.md §“Revised vendor-facing claim-control wording” via EidasClaimWording.SaferWording. The role description Stella Ops MAY use without a QTSP agreement is exposed via EidasClaimWording.RoleDescription. Both constants are character-for-character verbatim; tests assert the equality and will break if either is paraphrased.

4.8.1 CAdES-T / -LT / -LTA production readiness (EIDAS-EVIDENCE-018-03)

CadesSignatureBuilder produces real CAdES-T / -LT / -LTA outputs that embed the corresponding ETSI EN 319 122-1 unsigned attributes when an IEidasFixturePack is wired in. The pack is operator-supplied at deploy time (production) or supplied by the sealed deterministic test fixture (CI). When no pack is wired in (DI returned null), the higher levels stay fail-closed and the builder throws NotSupportedException whose message points to the QTSP/QSCD bridge. The plugin-level gate (EidasPlugin.SignAsync / EidasPlugin.VerifyAsync) still throws EidasEvidenceNotProductionReadyException first when Loader 1 / Loader 2 are not both satisfied — that gate fires regardless of whether a pack is wired in.

ProfileProduction-ready?Embedded unsigned attributes
CAdES-BESYes (always; CMS SignedData baseline).None — base CMS SignedData.
CAdES-TYes when (a) an IEidasFixturePack is wired AND (b) the loader gate is closed.id-aa-signatureTimeStampToken (OID 1.2.840.113549.1.9.16.2.14) — RFC 3161 TimeStampToken over SHA-256(SignerInfo.Signature).
CAdES-LTYes when (a) an IEidasFixturePack is wired AND (b) the loader gate is closed.CAdES-T attribute, plus id-aa-ets-revocationValues (OID 1.2.840.113549.1.9.16.2.24) — RevocationValues SEQUENCE wrapping the pack’s BasicOCSPResponse and CRL bytes.
CAdES-LTAYes when (a) an IEidasFixturePack is wired AND (b) the loader gate is closed.CAdES-T + CAdES-LT attributes, plus id-aa-ets-archiveTimestamp v3 (OID 1.2.840.113549.1.9.16.2.48) — RFC 3161 TimeStampToken over the SHA-256 of the SignedData bytes after the T+LT attributes were added.

Fail-closed posture (binding):

Sealed-test-fixture story. CI uses the deterministic in-repo pack authored under EIDAS-EVIDENCE-018-01 at src/Cryptography/__Tests/StellaOps.Cryptography.Tests/Eidas/Fixtures/sealed-pack/. The test-side SealedFixturePack (in src/Cryptography/__Tests/StellaOps.Cryptography.Tests/Eidas/SealedFixturePack.cs) implements IEidasFixturePack against that pack. Production deployments are expected to wire an operator-supplied implementation backed by the QTSP-supplied pack at deploy time; the eIDAS plugin’s DI registration intentionally does NOT register a default IEidasFixturePack so the simulator-success scenario cannot occur by accident.

Synthetic fixture allowance. The sealed fixture pack may be used to prove loader, parser, CAdES, and future QES signer-certificate wiring while the real eIDAS qualified-provider pack is unavailable. It is test material only: do not mount it in production, do not ship it in Offline Kit/provider-pack distributions, and do not use it as evidence for a qualified-service claim. Real pack ingestion is documented in docs/modules/cryptography/eidas-qualified-provider-pack-ingestion.md; production QES remains fail-closed until the operator supplies a real pack through Eidas:TrustedList:OfflineTrustMaterialPackPath, an unexpired agreement through Eidas:TrustedList:QtspAgreementPath, and per-transaction evidence through Eidas:TransactionEvidence:RootPath.

Round-trip verification. Produced CAdES-T signatures are round-trip-verified against the same SealedPackEvidenceValidator used by EIDAS-EVIDENCE-018-02 (RFC 3161 CMS signature + pinned-chain walk + message-imprint match + gen-time window). Produced CAdES-LT signatures additionally OCSP-validate via SealedPackEvidenceValidator.ValidateBaselineLT. Produced CAdES-LTA signatures additionally archive-TST-validate via the same RFC 3161 validation path used for the signature TST. See CadesEmbedTests.cs for the round-trip assertions.

4.9 Master KEK lifecycle (IKekSource) — Vault flagship, on-prem first

The master key-encryption key (KEK) that wraps connector credentials, Integrations secrets, and deployment-bundle ciphertext is resolved through the IKekSourceabstraction (src/__Libraries/StellaOps.Cryptography.CredentialStore/IKekSource.cs). The operator selects the source kind via the config key Crypto:Kek:Source; allowed values are env (default), file, vault, hsm — an unrecognised value throws at registration (fail-fast over a quiet downgrade).

Source kindSourceKindProjectNotes
envenvCredentialStore (EnvKekSource)Sprint-027 fallback chain; not recommended in production.
filefileCredentialStore (FileKekSource)reads Crypto:Kek:File:Path.
vaultvaultCredentialStore.Vault (VaultKekSource)flagship for self-hosted deployments.
hsmhsmCredentialStore.Hsm (HsmKekSource)PKCS#11-backed master KEK.

Vault is the flagship and the recommended production source. Operators already run HashiCorp Vault for pull-mode AuthRef; VaultKekSource reuses that surface and gates the master KEK behind Vault-managed access policy. Config keys (VaultKekSourceOptions): Crypto:Kek:Vault:Address, …:Mount, …:Path, …:Field, …:Encoding (utf8|base64|hex), …:StartupRetryWindow.

No silent fallback (compliance posture). Every IKekSource MUST throw when its source is unavailable; silently downgrading to env-var would mask the operator’s chosen posture. VaultKekSource retries with capped exponential backoff for the configured StartupRetryWindow, then fails startup with a sanitised error (it never logs KEK bytes, encoded values, or Vault auth fragments). Resolved KEK material is cached in-process for the host lifetime; InvalidateCache() forces a re-resolve during rotation.

Cloud KMS for the master KEK is deliberately out of scope. Per the IKekSource design comment (2026-05-27 decision): “Env + File + Vault + HSM are in scope. AWS / Azure KMS are deliberately out of scope (no stubs); the interface stays open so a downstream plugin can implement them if/when needed.” This is the on-prem-first posture — there is no cloud-KMS default for the master KEK.

CredentialStore is more than KEK sources. The IKekSource implementations above are only part of the StellaOps.Cryptography.CredentialStore project. It also owns the secret-provider abstraction (ISecretProvider, BuiltinSecretProvider, RoutingSecretProvider, SecretProviderServiceCollectionExtensions) and the persistence-neutral runtime provider registry contract (ISecretProviderConfigStore, SecretProviderConfigRecord). CredentialStore.Persistence supplies the Postgres implementation; Platform consumes only the neutral contract. The core project also owns the asymmetric KEK source (AsymmetricKekSource), per-tenant derived keys (DerivedTenantKeyProvider, MasterKeyProvisioner), AEAD sealing (CredentialAead), and the compliance-driven algorithm selection (CredentialAlgorithmProfileMap, TenantComplianceProfileCredentialAlgorithmSelector). The KEK-source subset is documented in full here; the secret-provider/derived-key/AEAD surfaces are owned by this library even where other module docs describe their use.

KEK versions / rotation. CredentialStore.Persistence ships the crypto.kek_versions migration (001_kek_versions.sql) and KekVersionBootstrapHostedService, which stamps the active KEK fingerprint and source (one of env|file|vault|hsm) on startup. KekMaterial carries KekId, KekVersion, the secret bytes, and an optional ExpiresAt; the rotation audit trail tracks only fingerprints + versions, never the secret.

KEK control-plane scopes (SPRINT_20260529_028 / RP-028-005). KEK read and rotation are gated by dedicated OAuth scopes distinct from crypto:admin so an operator can grant rotation capability separately from provider-catalogue mutation: crypto:kek:read (StellaOpsScopes.PlatformCryptoKekRead) and crypto:kek:rotate (StellaOpsScopes.PlatformCryptoKekRotate). The CLI exposes the stella crypto kek subgroup (and stella crypto provider set / stella crypto algorithm for installation-level provider/algorithm settings).

Signing KMS vs. master KEK (do not conflate). The separate StellaOps.Cryptography.Kms library exposes an IKmsClient signing facade (SignAsync/VerifyAsync/RotateAsync/RevokeAsync/ExportAsync) surfaced as the kms provider. Its default backends are FileKmsClient and Fido2KmsClient; AwsKmsClient/GcpKmsClient exist as optional adapters but are not wired by default and must be opted into explicitly. This is a distinct concern from the master-KEK IKekSource above.


5) Plugin Architecture

5.1 Plugin Discovery

Plugins are loaded from plugins/ directory:

plugins/
  ├─ gost/
  │   ├─ manifest.json
  │   └─ StellaOps.Cryptography.Plugin.Gost.dll
  └─ sm/
      ├─ manifest.json
      └─ StellaOps.Cryptography.Plugin.Sm.dll

5.2 Plugin Manifest

{
  "pluginId": "stellaops.crypto.gost",
  "version": "1.0.0",
  "profiles": ["gost-2012-256", "gost-2012-512"],
  "dependencies": {
    "cryptopro": "5.0+"
  },
  "platforms": ["win-x64", "linux-x64"]
}

5.3 Plugin Interface

Reconciliation note (2026-05-30). There is no ICryptoPlugin type in the codebase, and no GetSignatureProvider/GetHashProvider pair. A crypto plugin is just an ICryptoProviderimplementation (see §3.1) discovered by StellaOps.Cryptography.PluginLoader and added to the ICryptoProviderRegistry. The provider advertises support via Supports(CryptoCapability, algorithmId) and returns ICryptoSigner / ICryptoHasher instances on demand. The illustrative shape below is retained only as a conceptual sketch; the binding contract is ICryptoProvider.

// Conceptual sketch only — the real contract is ICryptoProvider (§3.1).
public interface ICryptoProvider
{
    string Name { get; }
    bool Supports(CryptoCapability capability, string algorithmId);
    ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference);
    ICryptoHasher GetHasher(string algorithmId);
    // …UpsertSigningKey / RemoveSigningKey / GetSigningKeys / CreateEphemeralVerifier
}

6) Configuration (YAML)

Config keys below are grounded in the actual options types (CryptoComplianceOptionsCrypto:Compliance, KekSourceServiceCollectionExtensions

Crypto:
  # Compliance (regional hashing/MAC routing). See §4.1.
  Compliance:
    ProfileId: "world"        # world | fips | gost | sm | kcmvp | eidas (default: world)
    StrictValidation: true    # fail closed on non-compliant algorithm-for-purpose
    WarnOnNonCompliant: true  # used when StrictValidation=false
    AllowInteropOverride: true
    # PurposeOverrides: { graph: "SHA256" }   # optional per-purpose overrides
    # Env overrides: STELLAOPS_CRYPTO_COMPLIANCE_PROFILE, STELLAOPS_CRYPTO_STRICT_VALIDATION

  # Master KEK source. On-prem first; Vault is the flagship. See §4.9.
  Kek:
    Source: "vault"           # env (default) | file | vault | hsm  (unknown ⇒ fail-fast)
    Vault:                    # required when Source=vault
      Address: "https://vault.internal:8200"
      Mount: "secret"
      Path: "stellaops/kek/master"
      Field: "kek"
      Encoding: "base64"      # utf8 | base64 | hex
      StartupRetryWindow: "00:00:30"   # fails startup if Vault unreachable after window
    File:                     # required when Source=file
      Path: "/etc/stellaops/kek.key"
    # NOTE: AWS/Azure KMS are deliberately NOT a KEK source (no cloud default).

# In-process HSM provider (PKCS#11) — opt-in, no production simulation.
Hsm:
  LibraryPath: "/usr/lib/softhsm/libsofthsm2.so"  # native PKCS#11 module (required)
  SlotId: 0
  AllowSimulation: false      # true only in Development/Testing harnesses

# Signing KMS facade (IKmsClient) — distinct from the master KEK above.
# Default backends are file/FIDO2; AWS/GCP adapters are optional and NOT
# wired by default (on-prem posture).

Posture note (2026-05-30 reconciliation). A prior revision of this dossier showed Kms: Provider: "aws", Region: "us-east-1" as the configuration example. That contradicted the on-prem-first posture: cloud KMS is not a master-KEK source and is not a default. The example above leads with Vault.


7) Security considerations


8) Performance targets


9) Testing matrix


10) Offline Operation

For air-gapped deployments:

  1. Offline key bundles: Pre-exported public keys for verification (the offline.verification provider is verification-only).
  2. Local plugins: All providers loaded from the local filesystem; no network calls in crypto paths (per src/Cryptography/AGENTS.md).
  3. No external crypto-service calls: All operations use local keys, the in-process PKCS#11 HSM, or a master KEK from a local/Vault source. The KEK env/file sources need no network; the Vault source talks only to the operator’s in-estate Vault. No cloud-KMS dependency exists in any path.
  4. Trust bundles: Pre-configured trust roots; eIDAS advanced evidence uses sealed offline trust-material packs (§4.8), never live trust-list lookups.

11) Compliance profile preference persistence (CLI / UI parity)

The active compliance profile (world, fips, eidas, gost, sm, kcmvp) is a per-tenant preference. As of SPRINT_20260503_007 / B-FECRYPTO-002 the profile is persisted in a distinct table from provider routing:

ConcernTableEndpointAudit actionOAuth scopeAuth policy
Provider routingplatform.tenant_crypto_preferencesPUT /api/v1/admin/crypto-providers/preferencesupdate_crypto_preferencecrypto:adminplatform.crypto.admin
Compliance profileplatform.tenant_compliance_profilePUT /api/v1/admin/crypto-providers/compliance-profileupdate_compliance_profilecrypto:profile:adminplatform.crypto.profile.admin

Scope vs. policy (2026-05-30 reconciliation). A prior revision listed the ASP.NET authorization policy names (platform.crypto.admin / platform.crypto.profile.admin) in the “Scope” column. The canonical OAuth scope strings in the scope catalog (StellaOpsScopes.PlatformCryptoAdmin / PlatformCryptoProfileAdmin) are crypto:admin and crypto:profile:admin; the colon-delimited policy constants in PlatformPolicies.cs (platform.crypto.admin, …) are the distinct policy registration keys. The read scope is crypto:read (policy platform.crypto.read). The Audited(...) filter records the audit action under the platform module (AuditActions.Platform.UpdateCryptoPreference = update_crypto_preference, etc.); the endpoint description renders it platform.update_compliance_profile when module-qualified.

The two are kept orthogonal because changing the profile changes algorithm selection across hashing/MAC/AEAD for the entire tenant, which is more sensitive than provider routing — operators may grant the routing scope to a deployment engineer while reserving profile changes for a security officer.

The compliance-profile table is single-row-per-tenant and captures the prior value into previous_profile on every upsert, powering the audit payload’s before/after fields without an extra ledger lookup. Migration 069_TenantComplianceProfile.sql auto-applies via the existing AddStartupMigrations wiring per CoC §2.7.

The Console panel under src/Web/StellaOps.Web/src/app/features/settings/crypto-providers/ calls the new endpoint after a typed-confirmation dialog (the operator must type the target profile id to enable submit), surfaces the algorithm delta between current and target profile, and warns when the target profile is not in the compatibility matrix for the currently active provider.

CLI surface (2026-05-30 reconciliation). Two profile commands now coexist in src/Cli/StellaOps.Cli/Commands/CryptoCommandGroup.cs:

See docs/modules/cli/architecture.md §23 and the parity row in docs/modules/cli/cli-vs-ui-parity.md §5b for the CLI contract details.