Offline Verification Crypto Provider - Security Guide

Document Version: 1.1 Last Updated: 2026-05-30 Status: Active (verification-only; signing path is a non-functional stub — see warning below) Audience: Security Engineers, Platform Operators, DevOps Teams Sprint: SPRINT_1000_0007_0002

CRITICAL — read before relying on this provider. As implemented today (src/__Libraries/StellaOps.Cryptography.Plugin.OfflineVerification/OfflineVerificationCryptoProvider.cs), this provider is a verification-only component. Its real, tested capability is CreateEphemeralVerifier(...) (signature verification from a raw public key).

  • Signing does not work. GetSigner(...) returns EcdsaSigner/RsaSigner whose SignAsync/VerifyAsync generate a fresh ephemeral key on every call (ECDsa.Create(curve) / RSA.Create()) and never load key material from the supplied CryptoKeyReference. A signature produced this way cannot be verified by anyone, and the in-code comments say so (“In a real implementation, would load key material”). Do not use the signing path for any real signature.
  • Key management is not supported. UpsertSigningKey and RemoveSigningKey throw NotSupportedException; GetSigningKeys returns an empty collection.
  • ExportPublicJsonWebKey() throws NotSupportedExceptionon every signer/verifier this provider returns.

The signing and key-management examples in this guide are therefore marked NOT IMPLEMENTED where they appear, and describe the intended (roadmap) shape, not current behavior.

Table of Contents

  1. Overview
  2. Architecture
  3. Security Model
  4. Algorithm Support
  5. Deployment Scenarios
  6. API Reference
  7. Trust Establishment
  8. Threat Model
  9. Compliance
  10. Best Practices
  11. Troubleshooting

Overview

The OfflineVerificationCryptoProvider is a cryptographic abstraction layer that wraps .NET BCL (System.Security.Cryptography) to enable configuration-driven cryptography in offline, air-gapped, and sovereignty-constrained environments.

Purpose

Key Features

NOT IMPLEMENTED: ECDSA/RSA signing with managed keys. GetSigner(...) exists but its signers do not load key material (see the warning at the top of this document). Treat this provider as verification-only.


Architecture

Component Hierarchy

┌─────────────────────────────────────────────────────────┐
│  Production Code (AirGap, Scanner, Attestor)            │
│  ├── Uses: ICryptoProvider abstraction                  │
│  └── Never touches: System.Security.Cryptography        │
└─────────────────────────────────────────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────────┐
│  StellaOps.Cryptography (Core Abstraction)              │
│  ├── ICryptoProvider interface                          │
│  ├── ICryptoSigner interface                            │
│  ├── ICryptoHasher interface                            │
│  └── CryptoProviderRegistry                             │
└─────────────────────────────────────────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────────┐
│  OfflineVerificationCryptoProvider (Plugin)             │
│  ├── BclHasher (SHA-256/384/512)                        │
│  ├── EcdsaSigner (ES256/384/512)                        │
│  ├── RsaSigner (RS/PS 256/384/512)                      │
│  ├── EcdsaEphemeralVerifier (public-key-only)           │
│  └── RsaEphemeralVerifier (public-key-only)             │
└─────────────────────────────────────────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────────┐
│  System.Security.Cryptography (.NET BCL)                │
│  ├── ECDsa (NIST P-256/384/521)                         │
│  ├── RSA (2048/3072/4096-bit)                           │
│  └── SHA256/SHA384/SHA512                               │
└─────────────────────────────────────────────────────────┘

Isolation Boundaries

Crypto Operations Allowed:

Enforcement Mechanisms (intended design — see status notes):

  1. Static Analysis: scripts/audit-crypto-usage.ps1NOT IMPLEMENTED. This script does not exist in the repository. The only present audit script is tools/scripts/audit-reconciliation.ps1 (unrelated). The boundary is currently a convention, not an automated gate.
  2. CI Validation: .gitea/workflows/crypto-compliance.ymlNOT ACTIVE. The workflow file exists only under .gitea/workflows-archived/crypto-compliance.yml and references the missing audit script above, so it cannot run as written.
  3. Code Review: Manual review on pull requests (no automated crypto-usage check is wired today).

Security Model

Threat Categories

ThreatLikelihoodImpactMitigation
Key ExtractionMediumHighIn-memory keys only, minimize key lifetime
Side-Channel (Timing)LowMedium.NET BCL uses constant-time primitives
Algorithm DowngradeVery LowCriticalRuntime algorithm allowlist (Supports/GetHasher/CreateEphemeralVerifier switch throws NotSupportedException for unlisted IDs)
Public Key SubstitutionMediumCriticalFingerprint verification, out-of-band trust (must be done by the caller; this provider does NOT validate keys against a trust store)
Replay AttackMediumMediumInclude timestamps in signed payloads (caller responsibility)
Man-in-the-MiddleLow (offline)N/APhysical media transport

Note on the allowlist: algorithm gating is a runtime switch in the provider, not a compile-time check. Unsupported algorithm IDs throw NotSupportedException when a hasher, signer, or ephemeral verifier is requested.

Trust Boundaries

┌────────────────────────────────────────────────────────┐
│  Trusted Computing Base (TCB)                          │
│  ├── .NET Runtime (Microsoft-signed)                   │
│  ├── OfflineVerificationCryptoProvider (BUSL-1.1)    │
│  └── Pre-distributed Public Key Fingerprints           │
└────────────────────────────────────────────────────────┘
                        ▲
                        │ Trust Anchor
                        │
┌────────────────────────────────────────────────────────┐
│  Untrusted Zone                                        │
│  ├── Container Images (to be verified)                 │
│  ├── SBOMs (to be verified)                            │
│  └── VEX Documents (to be verified)                    │
└────────────────────────────────────────────────────────┘

Trust Establishment:

  1. Pre-distribution: Public key fingerprints embedded in airgap bundle
  2. Out-of-Band Verification: Manual verification via secure channel
  3. Chain of Trust: Each signature verified against trusted fingerprints

Algorithm Support

Verification (and signing algorithm IDs)

The algorithm IDs below are what Supports(...) reports and what CreateEphemeralVerifier(...) accepts. Verification (CreateEphemeralVerifier) is implemented for all nine IDs. The signing path (GetSigner) accepts the same IDs but is a non-functional stub (see the warning at the top of this document), so the “Use Case” column describes the intended role.

AlgorithmCurve/Key SizeHashPaddingEphemeral verifyUse Case (intended)
ES256NIST P-256SHA-256N/ADSSE envelopes, in-toto attestations
ES384NIST P-384SHA-384N/AHigh-security SBOM signatures
ES512NIST P-521SHA-512N/ALong-term archival signatures
RS2562048+ bitsSHA-256PKCS1Legacy compatibility
RS3842048+ bitsSHA-384PKCS1Legacy compatibility
RS5122048+ bitsSHA-512PKCS1Legacy compatibility
PS2562048+ bitsSHA-256PSSRecommended RSA
PS3842048+ bitsSHA-384PSSRecommended RSA
PS5122048+ bitsSHA-512PSSRecommended RSA

Manifest gap (flagged): etc/crypto-plugins-manifest.json lists verification: capabilities only for ES256/384/512 and RS256/384/512 — it omits verification:PS256/384/512 even though the code’s Supports(CryptoCapability.Verification, ...) returns true for PS algorithms and CreateEphemeralVerifier handles them. The plugin’s own README declares the PS verification capabilities; the manifest does not. Treat PS verification as supported (code is source of truth), and note the manifest needs the three verification:PS* lines added.

ES256/384/512 map to NIST P-256/P-384/P-521 respectively (note: ES512 uses curve P-521, not P-512).

Content Hashing

AlgorithmOutput SizePerformanceUse Case
SHA-256256 bitsFastDefault for most use cases
SHA-384384 bitsMediumMedium-security requirements
SHA-512512 bitsMediumHigh-security requirements

Normalization: Both SHA-256 and SHA256 formats accepted, normalized to SHA-256.

Password Hashing

Not functional. GetPasswordHasher(...) always throws NotSupportedException (“Password hashing is not supported by the offline verification provider.”).

Source inconsistency (flagged): Supports(CryptoCapability.PasswordHashing, "PBKDF2") and Supports(..., "Argon2id") both return true, which contradicts GetPasswordHasher always throwing. A caller that branches on Supports(...) before calling GetPasswordHasher(...) will hit an unexpected exception. The Supports switch for PasswordHashing should return false for this provider, or GetPasswordHasher should be implemented. Until reconciled, treat password hashing as unsupported.

Use dedicated password hashers instead:


Deployment Scenarios

Scenario 1: Air-Gapped Container Scanning

Environment: Offline network segment, no internet access

Configuration:

{
  "cryptoProvider": "offline-verification",
  "algorithms": {
    "signing": "ES256",
    "hashing": "SHA-256"
  },
  "trustRoots": {
    "fingerprints": [
      "sha256:a1b2c3d4e5f6....",
      "sha256:f6e5d4c3b2a1...."
    ]
  }
}

Trust Establishment:

  1. Pre-distribute trust bundle via USB/DVD: offline-kit.tar.gz
  2. Bundle contains:
    • Public key fingerprints (trust-anchors.json)
    • Root CA certificates (if applicable)
    • Offline crypto provider plugin
  3. Operator verifies bundle signature using out-of-band channel

Workflow:

┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
│ Scan     │──▶│ Generate │──▶│ Sign     │──▶│ Verify   │
│ Container│   │ SBOM     │   │ with ES256│  │ Signature│
└──────────┘   └──────────┘   └──────────┘   └──────────┘
                                     │              │
                                     ▼              ▼
                          OfflineVerificationCryptoProvider

Scenario 2: Sovereign Cloud Deployment

Environment: National cloud with data residency requirements

Configuration:

{
  "cryptoProvider": "offline-verification",
  "jurisdiction": "world",
  "compliance": ["NIST", "offline-airgap"],
  "keyRotation": {
    "enabled": true,
    "intervalDays": 90
  }
}

Key Considerations:

Scenario 3: CI/CD Pipeline — Hashing and Verification

Environment: Build server

NOT IMPLEMENTED — deterministic signing. This provider performs no signing (the signing path is a non-functional stub) and has no RFC 6979 / deterministic-ECDSA support. There is no BouncyCastle dependency: the project references only StellaOps.Cryptography and Microsoft.IdentityModel.Tokens (see StellaOps.Cryptography.Plugin.OfflineVerification.csproj). Standard .NET ECDsa.SignData is non-deterministic. The deterministicSigning config key below is not honored by any code in this provider.

What this provider can actually contribute in CI/CD:

  1. Content hashing of artifacts via GetHasher("SHA-256") (deterministic, known-answer tested).
  2. Signature verification of artifacts signed elsewhere, via CreateEphemeralVerifier(...) given the signer’s public key in SPKI/DER form.

For actual signing in CI/CD, use a key-managing provider (e.g., the default/FIPS/eIDAS providers in etc/crypto-plugins-manifest.json) or an HSM/KMS-backed provider — not this one.

Illustrative (NOT honored) configuration:

{
  "cryptoProvider": "offline-verification",
  "deterministicSigning": true,
  "algorithms": {
    "signing": "ES256",
    "hashing": "SHA-256"
  }
}

API Reference

ICryptoProvider.CreateEphemeralVerifier

CreateEphemeralVerifier is a default method on the ICryptoProvider interface (the base implementation throws NotSupportedException); the offline provider overrides it with a working implementation. It is the only fully functional asymmetric operation this provider offers.

Signature:

ICryptoSigner CreateEphemeralVerifier(
    string algorithmId,
    ReadOnlySpan<byte> publicKeyBytes)

Purpose: Create a verification-only signer from raw public key bytes, without key persistence or management overhead.

Parameters:

Returns: ICryptoSigner (an EcdsaEphemeralVerifier or RsaEphemeralVerifier) with:

Throws:

Usage Example:

// DSSE envelope verification
var envelope = DsseEnvelope.Parse(envelopeJson);
var trustRoots = LoadTrustRoots();

foreach (var signature in envelope.Signatures)
{
    // Get public key from trust store
    if (!trustRoots.PublicKeys.TryGetValue(signature.KeyId, out var publicKeyBytes))
        continue;

    // Verify fingerprint
    var fingerprint = ComputeFingerprint(publicKeyBytes);
    if (!trustRoots.TrustedFingerprints.Contains(fingerprint))
        continue;

    // Create ephemeral verifier
    var verifier = cryptoProvider.CreateEphemeralVerifier("PS256", publicKeyBytes);

    // Build pre-authentication encoding (PAE)
    var pae = BuildPAE(envelope.PayloadType, envelope.Payload);

    // Verify signature
    var isValid = await verifier.VerifyAsync(pae, Convert.FromBase64String(signature.Signature));

    if (isValid)
        return ValidationResult.Success();
}

return ValidationResult.Failure("No valid signature found");

ICryptoHasher.ComputeHash

Signature:

byte[] ComputeHash(ReadOnlySpan<byte> data)

Usage Example:

var hasher = cryptoProvider.GetHasher("SHA-256");
var hash = hasher.ComputeHash(fileBytes);
var hex = Convert.ToHexString(hash).ToLowerInvariant();

ICryptoSigner.SignAsync / VerifyAsync (NOT IMPLEMENTED for managed-key signing)

Signatures:

ValueTask<byte[]> SignAsync(ReadOnlyMemory<byte> data, CancellationToken ct = default)
ValueTask<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CancellationToken ct = default)
JsonWebKey ExportPublicJsonWebKey()   // always throws NotSupportedException in this provider

NOT IMPLEMENTED — the example below does not work with this provider. It is retained to document the intended shape only. With OfflineVerificationCryptoProvider:

  • UpsertSigningKey(...) throws NotSupportedException (“The offline verification provider does not support key management.”), so the example fails on the first line that uses it.
  • Even calling GetSigner(...) directly and then SignAsync/VerifyAsync does not round-trip: each call instantiates a brand-new ephemeral key (ECDsa.Create(curve) / RSA.Create()) and ignores the CryptoKeyReference. A signature produced by SignAsync will not verify, and VerifyAsync on EcdsaSigner/RsaSigner checks against an unrelated random key.

For real verification use CreateEphemeralVerifier(...) (above). For real signing use a key-managing crypto provider.

Intended usage (does NOT work here):

// Signing — UpsertSigningKey THROWS on this provider; round-trip would fail even via GetSigner
var signingKey = new CryptoSigningKey(
    reference: new CryptoKeyReference("my-key"),
    algorithmId: "ES256",
    privateParameters: ecParameters,
    createdAt: DateTimeOffset.UtcNow);

cryptoProvider.UpsertSigningKey(signingKey);   // throws NotSupportedException
var signer = cryptoProvider.GetSigner("ES256", new CryptoKeyReference("my-key"));
var signature = await signer.SignAsync(data);

// Verification
var isValid = await signer.VerifyAsync(data, signature);

Trust Establishment

Offline Trust Bundle Structure

offline-kit.tar.gz
├── trust-anchors.json          # Public key fingerprints
├── public-keys/                # Public keys in SPKI format
│   ├── scanner-key-001.pub
│   ├── scanner-key-002.pub
│   └── attestor-key-001.pub
├── metadata/
│   ├── bundle-manifest.json    # Bundle metadata
│   └── bundle-signature.sig    # Bundle self-signature
└── crypto-plugins/
    └── StellaOps.Cryptography.Plugin.OfflineVerification.dll

trust-anchors.json Format

{
  "version": "1.0",
  "createdAt": "2025-12-23T00:00:00Z",
  "expiresAt": "2026-12-23T00:00:00Z",
  "trustAnchors": [
    {
      "keyId": "scanner-key-001",
      "algorithmId": "ES256",
      "fingerprint": "sha256:a1b2c3d4e5f6...",
      "purpose": "container-scanning",
      "notBefore": "2025-01-01T00:00:00Z",
      "notAfter": "2026-01-01T00:00:00Z"
    }
  ],
  "bundleSignature": {
    "keyId": "bundle-signing-key",
    "algorithmId": "ES256",
    "signature": "base64encodedSignature=="
  }
}

Fingerprint Computation

private string ComputeFingerprint(byte[] publicKeyBytes)
{
    var hasher = cryptoProvider.GetHasher("SHA-256");
    var hash = hasher.ComputeHash(publicKeyBytes);
    return "sha256:" + Convert.ToHexString(hash).ToLowerInvariant();
}

Out-of-Band Verification Process

  1. Bundle Reception: Operator receives offline-kit.tar.gz via physical media
  2. Checksum Verification: Compare SHA-256 hash against value published via secure channel
    sha256sum offline-kit.tar.gz
    # Compare with published value: a1b2c3d4e5f6...
    
  3. Bundle Signature Verification: Extract bundle, verify self-signature using bootstrap public key
  4. Trust Anchor Review: Manual review of trust-anchors.json entries
  5. Deployment: Extract crypto plugin and trust anchors to deployment directory

Threat Model

Attack Surface Analysis

Attack VectorLikelihoodImpactMitigation
Memory DumpMediumHighUse ephemeral keys, minimize key lifetime
Side-Channel (Timing)LowMedium.NET BCL uses constant-time primitives
Algorithm SubstitutionVery LowCriticalRuntime algorithm allowlist (switch throws NotSupportedException for unlisted IDs)
Public Key SubstitutionMediumCriticalFingerprint verification, out-of-band trust (caller-side; provider does not validate keys)
Replay AttackMediumMediumInclude timestamps in signed payloads (caller-side)
Man-in-the-MiddleLow (offline)N/APhysical media transport

Mitigations by Threat

T1: Private Key Extraction

T2: Public Key Substitution

T3: Signature Replay

T4: Algorithm Downgrade


Compliance

NIST Standards

StandardRequirementCompliance
FIPS 186-4Digital Signature Standard✅ ECDSA with P-256/384/521, RSA-PSS
FIPS 180-4Secure Hash Standard✅ SHA-256/384/512
FIPS 140-2Cryptographic Module Validation⚠️ .NET BCL (software-only, not validated)

Notes:

RFC Standards

RFCTitleCompliance
RFC 8017PKCS #1: RSA Cryptography v2.2✅ RSASSA-PKCS1-v1_5, RSASSA-PSS (verification)
RFC 6979Deterministic DSA/ECDSANOT IMPLEMENTED. No BouncyCastle dependency exists (csproj references only StellaOps.Cryptography and Microsoft.IdentityModel.Tokens); .NET ECDsa signing is non-deterministic.
RFC 5280X.509 Public Key Infrastructure✅ Consumes SubjectPublicKeyInfo (SPKI) DER public keys
RFC 7515JSON Web Signature (JWS)⚠️ Algorithm IDs match JWS (ES/RS/PS 256/384/512) and are verifiable here; this provider does not produce JWS signatures. ExportPublicJsonWebKey() throws.

Regional Standards

RegionStandardCompliance
European UnioneIDAS Regulation (EU) 910/2014❌ Use eIDAS plugin
RussiaGOST R 34.10-2012❌ Use CryptoPro plugin
ChinaSM2/SM3/SM4 (GM/T 0003-2012)❌ Use SM crypto plugin

Best Practices

Key Management

This provider does not manage keys. The DO/DON’T list below is general guidance for the signing provider you pair with it; for OfflineVerificationCryptoProvider itself the only relevant item is “use ephemeral verifiers for public-key-only scenarios.”

✅ DO:

❌ DON’T:

Algorithm Selection

Recommended:

  1. ES256 (ECDSA P-256/SHA-256) - Best balance
  2. PS256 (RSA-PSS 2048-bit/SHA-256) - For RSA-required scenarios
  3. SHA-256 - Default hashing algorithm

Avoid:

Performance Optimization

Caching:

// Cache hashers (thread-safe, reusable)
private readonly ICryptoHasher _sha256Hasher;

public MyService(ICryptoProviderRegistry registry)
{
    _sha256Hasher = registry.ResolveHasher("SHA-256").Hasher;
}

Troubleshooting

Common Issues

Issue: NotSupportedException: Algorithm 'RS256' is not supported

Resolution:


Issue: CryptographicException: Public key parsing failed

Resolution:


Issue: Signature verification always returns false

Resolution:

  1. Verify algorithm matches
  2. Ensure message is identical (byte-for-byte)
  3. Check public key matches private key
  4. Enable debug logging

References

External Standards


Document Control

VersionDateAuthorChanges
1.02025-12-23StellaOps Platform TeamInitial release with CreateEphemeralVerifier API
1.12026-05-30Doc/code reconciliationCorrected to match OfflineVerificationCryptoProvider.cs: provider is verification-only; GetSigner signing path is a non-functional stub (generates ephemeral keys, ignores CryptoKeyReference); UpsertSigningKey/RemoveSigningKey throw, GetSigningKeys is empty; ExportPublicJsonWebKey throws; password-hashing Supports()/GetPasswordHasher inconsistency flagged; RFC 6979/BouncyCastle/deterministic-signing removed (no such dependency); enforcement script and CI workflow marked missing/archived; fixed crypto-architecture doc link; documented manifest PS-verification gap.

License: BUSL-1.1