Add post-quantum cryptography support with PqSoftCryptoProvider
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
wine-csp-build / Build Wine CSP Image (push) Has been cancelled

- Implemented PqSoftCryptoProvider for software-only post-quantum algorithms (Dilithium3, Falcon512) using BouncyCastle.
- Added PqSoftProviderOptions and PqSoftKeyOptions for configuration.
- Created unit tests for Dilithium3 and Falcon512 signing and verification.
- Introduced EcdsaPolicyCryptoProvider for compliance profiles (FIPS/eIDAS) with explicit allow-lists.
- Added KcmvpHashOnlyProvider for KCMVP baseline compliance.
- Updated project files and dependencies for new libraries and testing frameworks.
This commit is contained in:
StellaOps Bot
2025-12-07 15:04:19 +02:00
parent 862bb6ed80
commit 98e6b76584
119 changed files with 11436 additions and 1732 deletions

View File

@@ -0,0 +1,436 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Pqc.Crypto.Crystals.Dilithium;
using Org.BouncyCastle.Pqc.Crypto.Falcon;
using Org.BouncyCastle.Crypto.Prng;
using Org.BouncyCastle.Crypto.Digests;
using StellaOps.Cryptography;
namespace StellaOps.Cryptography.Plugin.PqSoft;
/// <summary>
/// Software-only post-quantum provider (Dilithium3, Falcon512) using BouncyCastle PQC primitives.
/// Guarded by the <c>PQ_SOFT_ALLOWED</c> environment variable by default.
/// </summary>
public sealed class PqSoftCryptoProvider : ICryptoProvider, ICryptoProviderDiagnostics
{
private const string EnvGate = "PQ_SOFT_ALLOWED";
private static readonly HashSet<string> SupportedAlgorithms = new(StringComparer.OrdinalIgnoreCase)
{
SignatureAlgorithms.Dilithium3,
SignatureAlgorithms.Falcon512
};
private readonly ConcurrentDictionary<string, PqKeyEntry> entries = new(StringComparer.OrdinalIgnoreCase);
private readonly ILogger<PqSoftCryptoProvider> logger;
private readonly PqSoftProviderOptions options;
public PqSoftCryptoProvider(
IOptions<PqSoftProviderOptions>? optionsAccessor = null,
ILogger<PqSoftCryptoProvider>? logger = null)
{
options = optionsAccessor?.Value ?? new PqSoftProviderOptions();
this.logger = logger ?? NullLogger<PqSoftCryptoProvider>.Instance;
foreach (var key in options.Keys)
{
TryLoadKeyFromFile(key);
}
}
public string Name => "pq.soft";
public bool Supports(CryptoCapability capability, string algorithmId)
{
if (!GateEnabled() || string.IsNullOrWhiteSpace(algorithmId))
{
return false;
}
return capability switch
{
CryptoCapability.Signing or CryptoCapability.Verification => SupportedAlgorithms.Contains(algorithmId),
_ => false
};
}
public IPasswordHasher GetPasswordHasher(string algorithmId)
=> throw new NotSupportedException("PQ provider does not expose password hashing.");
public ICryptoHasher GetHasher(string algorithmId)
=> throw new NotSupportedException("PQ provider does not expose hashing.");
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
{
EnsureAllowed();
ArgumentNullException.ThrowIfNull(keyReference);
if (!SupportedAlgorithms.Contains(algorithmId))
{
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider '{Name}'.");
}
if (!entries.TryGetValue(keyReference.KeyId, out var entry))
{
throw new KeyNotFoundException($"Signing key '{keyReference.KeyId}' is not registered with provider '{Name}'.");
}
if (!string.Equals(entry.AlgorithmId, algorithmId, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Signing key '{keyReference.KeyId}' is registered for algorithm '{entry.AlgorithmId}', not '{algorithmId}'.");
}
return entry.CreateSigner();
}
public void UpsertSigningKey(CryptoSigningKey signingKey)
{
EnsureAllowed();
ArgumentNullException.ThrowIfNull(signingKey);
var normalizedAlg = Normalize(signingKey.AlgorithmId);
if (!SupportedAlgorithms.Contains(normalizedAlg))
{
throw new InvalidOperationException($"Signing algorithm '{normalizedAlg}' is not supported by provider '{Name}'.");
}
if (signingKey.PrivateKey.IsEmpty)
{
throw new InvalidOperationException("PQ provider requires raw private key bytes.");
}
var entry = normalizedAlg switch
{
SignatureAlgorithms.Dilithium3 => CreateDilithiumEntry(signingKey),
SignatureAlgorithms.Falcon512 => CreateFalconEntry(signingKey),
_ => throw new InvalidOperationException($"Unsupported PQ algorithm '{normalizedAlg}'.")
};
entries.AddOrUpdate(signingKey.Reference.KeyId, entry, (_, _) => entry);
}
public bool RemoveSigningKey(string keyId)
{
if (string.IsNullOrWhiteSpace(keyId))
{
return false;
}
return entries.TryRemove(keyId, out _);
}
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys()
=> entries.Values.Select(static e => e.Descriptor).ToArray();
public IEnumerable<CryptoProviderKeyDescriptor> DescribeKeys()
{
foreach (var entry in entries.Values)
{
yield return new CryptoProviderKeyDescriptor(
Name,
entry.Descriptor.Reference.KeyId,
entry.AlgorithmId,
new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
["provider"] = Name,
["algorithm"] = entry.AlgorithmId,
["certified"] = "false",
["simulation"] = "software"
});
}
}
private bool GateEnabled()
{
if (!options.RequireEnvironmentGate)
{
return true;
}
var value = Environment.GetEnvironmentVariable(EnvGate);
return string.Equals(value, "1", StringComparison.OrdinalIgnoreCase) ||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
}
private void EnsureAllowed()
{
if (!GateEnabled())
{
throw new InvalidOperationException($"Provider '{Name}' is disabled. Set {EnvGate}=1 or disable RequireEnvironmentGate to enable.");
}
}
private void TryLoadKeyFromFile(PqSoftKeyOptions key)
{
if (string.IsNullOrWhiteSpace(key.KeyId) || string.IsNullOrWhiteSpace(key.PrivateKeyPath))
{
return;
}
try
{
var priv = File.ReadAllBytes(key.PrivateKeyPath);
var pub = string.IsNullOrWhiteSpace(key.PublicKeyPath) ? Array.Empty<byte>() : File.ReadAllBytes(key.PublicKeyPath);
var signingKey = new CryptoSigningKey(
new CryptoKeyReference(key.KeyId, Name),
key.Algorithm,
priv,
DateTimeOffset.UtcNow,
metadata: new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
["source"] = "file",
["path"] = key.PrivateKeyPath
},
publicKey: pub);
UpsertSigningKey(signingKey);
logger.LogInformation("Loaded PQ key {KeyId} for algorithm {Algorithm}", key.KeyId, key.Algorithm);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to load PQ key {KeyId} from {Path}", key.KeyId, key.PrivateKeyPath);
}
}
private static string Normalize(string algorithmId) => algorithmId.ToUpperInvariant();
private static PqKeyEntry CreateDilithiumEntry(CryptoSigningKey signingKey)
{
var parameters = DilithiumParameters.Dilithium3;
if (!signingKey.PublicKey.IsEmpty)
{
var pubFromBytes = new DilithiumPublicKeyParameters(parameters, signingKey.PublicKey.ToArray());
var privFromBytes = new DilithiumPrivateKeyParameters(parameters, signingKey.PrivateKey.ToArray(), pubFromBytes);
var descriptorFromBytes = new CryptoSigningKey(
signingKey.Reference,
SignatureAlgorithms.Dilithium3,
privFromBytes.GetEncoded(),
signingKey.CreatedAt,
signingKey.ExpiresAt,
pubFromBytes.GetEncoded(),
signingKey.Metadata);
return new DilithiumKeyEntry(descriptorFromBytes, privFromBytes, pubFromBytes);
}
var random = CreateSeededRandom(signingKey.PrivateKey);
var generator = new DilithiumKeyPairGenerator();
generator.Init(new DilithiumKeyGenerationParameters(random, parameters));
var pair = generator.GenerateKeyPair();
var priv = (DilithiumPrivateKeyParameters)pair.Private;
var pub = (DilithiumPublicKeyParameters)pair.Public;
var descriptor = new CryptoSigningKey(
signingKey.Reference,
SignatureAlgorithms.Dilithium3,
priv.GetEncoded(),
signingKey.CreatedAt,
signingKey.ExpiresAt,
pub.GetEncoded(),
signingKey.Metadata);
return new DilithiumKeyEntry(descriptor, priv, pub);
}
private static PqKeyEntry CreateFalconEntry(CryptoSigningKey signingKey)
{
var parameters = FalconParameters.falcon_512;
var random = CreateSeededRandom(signingKey.PrivateKey);
var generator = new FalconKeyPairGenerator();
generator.Init(new FalconKeyGenerationParameters(random, parameters));
var pair = generator.GenerateKeyPair();
var priv = (FalconPrivateKeyParameters)pair.Private;
var pub = (FalconPublicKeyParameters)pair.Public;
var descriptor = new CryptoSigningKey(
signingKey.Reference,
SignatureAlgorithms.Falcon512,
priv.GetEncoded(),
signingKey.CreatedAt,
signingKey.ExpiresAt,
pub.GetEncoded(),
signingKey.Metadata);
return new FalconKeyEntry(descriptor, priv, pub);
}
private static SecureRandom CreateSeededRandom(ReadOnlyMemory<byte> seed)
{
var generator = new DigestRandomGenerator(new Sha512Digest());
generator.AddSeedMaterial(seed.ToArray());
return new SecureRandom(generator);
}
}
/// <summary>
/// Options for the PQ soft provider.
/// </summary>
public sealed class PqSoftProviderOptions
{
public bool RequireEnvironmentGate { get; set; } = true;
public List<PqSoftKeyOptions> Keys { get; set; } = new();
}
/// <summary>
/// Key configuration for the PQ soft provider.
/// </summary>
public sealed class PqSoftKeyOptions
{
public required string KeyId { get; set; }
= string.Empty;
public required string Algorithm { get; set; }
= SignatureAlgorithms.Dilithium3;
public string? PrivateKeyPath { get; set; }
= string.Empty;
public string? PublicKeyPath { get; set; }
= string.Empty;
}
internal abstract record PqKeyEntry(CryptoSigningKey Descriptor, string AlgorithmId)
{
public abstract ICryptoSigner CreateSigner();
}
internal sealed record DilithiumKeyEntry(
CryptoSigningKey Descriptor,
DilithiumPrivateKeyParameters PrivateKey,
DilithiumPublicKeyParameters PublicKey)
: PqKeyEntry(Descriptor, SignatureAlgorithms.Dilithium3)
{
public override ICryptoSigner CreateSigner() => new DilithiumSignerWrapper(Descriptor.Reference.KeyId, PrivateKey, PublicKey);
}
internal sealed record FalconKeyEntry(
CryptoSigningKey Descriptor,
FalconPrivateKeyParameters PrivateKey,
FalconPublicKeyParameters PublicKey)
: PqKeyEntry(Descriptor, SignatureAlgorithms.Falcon512)
{
public override ICryptoSigner CreateSigner() => new FalconSignerWrapper(Descriptor.Reference.KeyId, PrivateKey, PublicKey);
}
internal sealed class DilithiumSignerWrapper : ICryptoSigner
{
private readonly string keyId;
private readonly DilithiumPrivateKeyParameters privateKey;
private readonly DilithiumPublicKeyParameters publicKey;
public DilithiumSignerWrapper(string keyId, DilithiumPrivateKeyParameters privateKey, DilithiumPublicKeyParameters publicKey)
{
this.keyId = keyId;
this.privateKey = privateKey;
this.publicKey = publicKey;
}
public string KeyId => keyId;
public string AlgorithmId => SignatureAlgorithms.Dilithium3;
public ValueTask<byte[]> SignAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var signer = new DilithiumSigner();
signer.Init(true, privateKey);
return ValueTask.FromResult(signer.GenerateSignature(data.ToArray()));
}
public ValueTask<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var verifier = new DilithiumSigner();
verifier.Init(false, publicKey);
var ok = verifier.VerifySignature(data.ToArray(), signature.ToArray());
return ValueTask.FromResult(ok);
}
public JsonWebKey ExportPublicJsonWebKey()
{
var jwk = new JsonWebKey
{
Kid = keyId,
Alg = AlgorithmId,
Kty = JsonWebAlgorithmsKeyTypes.Octet, // PQ JWK mapping not standard; encode as opaque octet key
Use = JsonWebKeyUseNames.Sig,
Crv = "Dilithium3"
};
jwk.KeyOps.Add("sign");
jwk.KeyOps.Add("verify");
jwk.X = Base64UrlEncoder.Encode(publicKey.GetEncoded());
return jwk;
}
}
internal sealed class FalconSignerWrapper : ICryptoSigner
{
private readonly string keyId;
private readonly FalconPrivateKeyParameters privateKey;
private readonly FalconPublicKeyParameters publicKey;
public FalconSignerWrapper(string keyId, FalconPrivateKeyParameters privateKey, FalconPublicKeyParameters publicKey)
{
this.keyId = keyId;
this.privateKey = privateKey;
this.publicKey = publicKey;
}
public string KeyId => keyId;
public string AlgorithmId => SignatureAlgorithms.Falcon512;
public ValueTask<byte[]> SignAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var signer = new FalconSigner();
signer.Init(true, privateKey);
return ValueTask.FromResult(signer.GenerateSignature(data.ToArray()));
}
public ValueTask<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var verifier = new FalconSigner();
verifier.Init(false, publicKey);
var ok = verifier.VerifySignature(data.ToArray(), signature.ToArray());
return ValueTask.FromResult(ok);
}
public JsonWebKey ExportPublicJsonWebKey()
{
var jwk = new JsonWebKey
{
Kid = keyId,
Alg = AlgorithmId,
Kty = JsonWebAlgorithmsKeyTypes.Octet,
Use = JsonWebKeyUseNames.Sig,
Crv = "Falcon512"
};
jwk.KeyOps.Add("sign");
jwk.KeyOps.Add("verify");
jwk.X = Base64UrlEncoder.Encode(publicKey.GetEncoded());
return jwk;
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
</ItemGroup>
</Project>