up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (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
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (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
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
This commit is contained in:
@@ -1,21 +1,21 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.BouncyCastle;
|
||||
|
||||
/// <summary>
|
||||
/// Dependency injection helpers for registering the BouncyCastle Ed25519 crypto provider.
|
||||
/// </summary>
|
||||
public static class BouncyCastleCryptoServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddBouncyCastleEd25519Provider(this IServiceCollection services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
services.TryAddSingleton<BouncyCastleEd25519CryptoProvider>();
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<ICryptoProvider, BouncyCastleEd25519CryptoProvider>());
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.BouncyCastle;
|
||||
|
||||
/// <summary>
|
||||
/// Dependency injection helpers for registering the BouncyCastle Ed25519 crypto provider.
|
||||
/// </summary>
|
||||
public static class BouncyCastleCryptoServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddBouncyCastleEd25519Provider(this IServiceCollection services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
services.TryAddSingleton<BouncyCastleEd25519CryptoProvider>();
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<ICryptoProvider, BouncyCastleEd25519CryptoProvider>());
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,214 +1,214 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Crypto.Signers;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.BouncyCastle;
|
||||
|
||||
/// <summary>
|
||||
/// Ed25519 signing provider backed by BouncyCastle primitives.
|
||||
/// </summary>
|
||||
public sealed class BouncyCastleEd25519CryptoProvider : ICryptoProvider
|
||||
{
|
||||
private static readonly HashSet<string> SupportedAlgorithms = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
SignatureAlgorithms.Ed25519,
|
||||
SignatureAlgorithms.EdDsa
|
||||
};
|
||||
|
||||
private static readonly string[] DefaultKeyOps = { "sign", "verify" };
|
||||
|
||||
private readonly ConcurrentDictionary<string, KeyEntry> signingKeys = new(StringComparer.Ordinal);
|
||||
|
||||
public string Name => "bouncycastle.ed25519";
|
||||
|
||||
public bool Supports(CryptoCapability capability, string algorithmId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return capability switch
|
||||
{
|
||||
CryptoCapability.Signing or CryptoCapability.Verification => SupportedAlgorithms.Contains(algorithmId),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public ICryptoHasher GetHasher(string algorithmId)
|
||||
=> throw new NotSupportedException("BouncyCastle Ed25519 provider does not expose hashing capabilities.");
|
||||
|
||||
public IPasswordHasher GetPasswordHasher(string algorithmId)
|
||||
=> throw new NotSupportedException("BouncyCastle provider does not expose password hashing capabilities.");
|
||||
|
||||
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(algorithmId);
|
||||
ArgumentNullException.ThrowIfNull(keyReference);
|
||||
|
||||
if (!signingKeys.TryGetValue(keyReference.KeyId, out var entry))
|
||||
{
|
||||
throw new KeyNotFoundException($"Signing key '{keyReference.KeyId}' is not registered with provider '{Name}'.");
|
||||
}
|
||||
|
||||
EnsureAlgorithmSupported(algorithmId);
|
||||
var normalized = NormalizeAlgorithm(algorithmId);
|
||||
if (!string.Equals(entry.Descriptor.AlgorithmId, normalized, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Signing key '{keyReference.KeyId}' is registered for algorithm '{entry.Descriptor.AlgorithmId}', not '{algorithmId}'.");
|
||||
}
|
||||
|
||||
return new Ed25519SignerWrapper(entry);
|
||||
}
|
||||
|
||||
public void UpsertSigningKey(CryptoSigningKey signingKey)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(signingKey);
|
||||
EnsureAlgorithmSupported(signingKey.AlgorithmId);
|
||||
|
||||
if (signingKey.Kind != CryptoSigningKeyKind.Raw)
|
||||
{
|
||||
throw new InvalidOperationException($"Provider '{Name}' requires raw Ed25519 private key material.");
|
||||
}
|
||||
|
||||
var privateKey = NormalizePrivateKey(signingKey.PrivateKey);
|
||||
var publicKey = NormalizePublicKey(signingKey.PublicKey, privateKey);
|
||||
|
||||
var privateKeyParameters = new Ed25519PrivateKeyParameters(privateKey, 0);
|
||||
var publicKeyParameters = new Ed25519PublicKeyParameters(publicKey, 0);
|
||||
|
||||
var descriptor = new CryptoSigningKey(
|
||||
signingKey.Reference,
|
||||
NormalizeAlgorithm(signingKey.AlgorithmId),
|
||||
privateKey,
|
||||
signingKey.CreatedAt,
|
||||
signingKey.ExpiresAt,
|
||||
publicKey,
|
||||
signingKey.Metadata);
|
||||
|
||||
signingKeys.AddOrUpdate(
|
||||
signingKey.Reference.KeyId,
|
||||
_ => new KeyEntry(descriptor, privateKeyParameters, publicKeyParameters),
|
||||
(_, _) => new KeyEntry(descriptor, privateKeyParameters, publicKeyParameters));
|
||||
}
|
||||
|
||||
public bool RemoveSigningKey(string keyId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(keyId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return signingKeys.TryRemove(keyId, out _);
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys()
|
||||
=> signingKeys.Values.Select(static entry => entry.Descriptor).ToArray();
|
||||
|
||||
private static void EnsureAlgorithmSupported(string algorithmId)
|
||||
{
|
||||
if (!SupportedAlgorithms.Contains(algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider 'bouncycastle.ed25519'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeAlgorithm(string algorithmId)
|
||||
=> string.Equals(algorithmId, SignatureAlgorithms.EdDsa, StringComparison.OrdinalIgnoreCase)
|
||||
? SignatureAlgorithms.Ed25519
|
||||
: SignatureAlgorithms.Ed25519;
|
||||
|
||||
private static byte[] NormalizePrivateKey(ReadOnlyMemory<byte> privateKey)
|
||||
{
|
||||
var span = privateKey.Span;
|
||||
return span.Length switch
|
||||
{
|
||||
32 => span.ToArray(),
|
||||
64 => span[..32].ToArray(),
|
||||
_ => throw new InvalidOperationException("Ed25519 private key must be 32 or 64 bytes.")
|
||||
};
|
||||
}
|
||||
|
||||
private static byte[] NormalizePublicKey(ReadOnlyMemory<byte> publicKey, byte[] privateKey)
|
||||
{
|
||||
if (publicKey.IsEmpty)
|
||||
{
|
||||
var privateParams = new Ed25519PrivateKeyParameters(privateKey, 0);
|
||||
return privateParams.GeneratePublicKey().GetEncoded();
|
||||
}
|
||||
|
||||
if (publicKey.Span.Length != 32)
|
||||
{
|
||||
throw new InvalidOperationException("Ed25519 public key must be 32 bytes.");
|
||||
}
|
||||
|
||||
return publicKey.ToArray();
|
||||
}
|
||||
|
||||
private sealed record KeyEntry(
|
||||
CryptoSigningKey Descriptor,
|
||||
Ed25519PrivateKeyParameters PrivateKey,
|
||||
Ed25519PublicKeyParameters PublicKey);
|
||||
|
||||
private sealed class Ed25519SignerWrapper : ICryptoSigner
|
||||
{
|
||||
private readonly KeyEntry entry;
|
||||
|
||||
public Ed25519SignerWrapper(KeyEntry entry)
|
||||
{
|
||||
this.entry = entry ?? throw new ArgumentNullException(nameof(entry));
|
||||
}
|
||||
|
||||
public string KeyId => entry.Descriptor.Reference.KeyId;
|
||||
|
||||
public string AlgorithmId => entry.Descriptor.AlgorithmId;
|
||||
|
||||
public ValueTask<byte[]> SignAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var signer = new Ed25519Signer();
|
||||
var buffer = data.ToArray();
|
||||
signer.Init(true, entry.PrivateKey);
|
||||
signer.BlockUpdate(buffer, 0, buffer.Length);
|
||||
var signature = signer.GenerateSignature();
|
||||
return ValueTask.FromResult(signature);
|
||||
}
|
||||
|
||||
public ValueTask<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var verifier = new Ed25519Signer();
|
||||
var buffer = data.ToArray();
|
||||
verifier.Init(false, entry.PublicKey);
|
||||
verifier.BlockUpdate(buffer, 0, buffer.Length);
|
||||
var verified = verifier.VerifySignature(signature.ToArray());
|
||||
return ValueTask.FromResult(verified);
|
||||
}
|
||||
|
||||
public JsonWebKey ExportPublicJsonWebKey()
|
||||
{
|
||||
var jwk = new JsonWebKey
|
||||
{
|
||||
Kid = entry.Descriptor.Reference.KeyId,
|
||||
Alg = SignatureAlgorithms.EdDsa,
|
||||
Kty = "OKP",
|
||||
Use = JsonWebKeyUseNames.Sig,
|
||||
Crv = "Ed25519"
|
||||
};
|
||||
|
||||
foreach (var op in DefaultKeyOps)
|
||||
{
|
||||
jwk.KeyOps.Add(op);
|
||||
}
|
||||
|
||||
jwk.X = Base64UrlEncoder.Encode(entry.PublicKey.GetEncoded());
|
||||
|
||||
return jwk;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Crypto.Signers;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.BouncyCastle;
|
||||
|
||||
/// <summary>
|
||||
/// Ed25519 signing provider backed by BouncyCastle primitives.
|
||||
/// </summary>
|
||||
public sealed class BouncyCastleEd25519CryptoProvider : ICryptoProvider
|
||||
{
|
||||
private static readonly HashSet<string> SupportedAlgorithms = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
SignatureAlgorithms.Ed25519,
|
||||
SignatureAlgorithms.EdDsa
|
||||
};
|
||||
|
||||
private static readonly string[] DefaultKeyOps = { "sign", "verify" };
|
||||
|
||||
private readonly ConcurrentDictionary<string, KeyEntry> signingKeys = new(StringComparer.Ordinal);
|
||||
|
||||
public string Name => "bouncycastle.ed25519";
|
||||
|
||||
public bool Supports(CryptoCapability capability, string algorithmId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return capability switch
|
||||
{
|
||||
CryptoCapability.Signing or CryptoCapability.Verification => SupportedAlgorithms.Contains(algorithmId),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public ICryptoHasher GetHasher(string algorithmId)
|
||||
=> throw new NotSupportedException("BouncyCastle Ed25519 provider does not expose hashing capabilities.");
|
||||
|
||||
public IPasswordHasher GetPasswordHasher(string algorithmId)
|
||||
=> throw new NotSupportedException("BouncyCastle provider does not expose password hashing capabilities.");
|
||||
|
||||
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(algorithmId);
|
||||
ArgumentNullException.ThrowIfNull(keyReference);
|
||||
|
||||
if (!signingKeys.TryGetValue(keyReference.KeyId, out var entry))
|
||||
{
|
||||
throw new KeyNotFoundException($"Signing key '{keyReference.KeyId}' is not registered with provider '{Name}'.");
|
||||
}
|
||||
|
||||
EnsureAlgorithmSupported(algorithmId);
|
||||
var normalized = NormalizeAlgorithm(algorithmId);
|
||||
if (!string.Equals(entry.Descriptor.AlgorithmId, normalized, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Signing key '{keyReference.KeyId}' is registered for algorithm '{entry.Descriptor.AlgorithmId}', not '{algorithmId}'.");
|
||||
}
|
||||
|
||||
return new Ed25519SignerWrapper(entry);
|
||||
}
|
||||
|
||||
public void UpsertSigningKey(CryptoSigningKey signingKey)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(signingKey);
|
||||
EnsureAlgorithmSupported(signingKey.AlgorithmId);
|
||||
|
||||
if (signingKey.Kind != CryptoSigningKeyKind.Raw)
|
||||
{
|
||||
throw new InvalidOperationException($"Provider '{Name}' requires raw Ed25519 private key material.");
|
||||
}
|
||||
|
||||
var privateKey = NormalizePrivateKey(signingKey.PrivateKey);
|
||||
var publicKey = NormalizePublicKey(signingKey.PublicKey, privateKey);
|
||||
|
||||
var privateKeyParameters = new Ed25519PrivateKeyParameters(privateKey, 0);
|
||||
var publicKeyParameters = new Ed25519PublicKeyParameters(publicKey, 0);
|
||||
|
||||
var descriptor = new CryptoSigningKey(
|
||||
signingKey.Reference,
|
||||
NormalizeAlgorithm(signingKey.AlgorithmId),
|
||||
privateKey,
|
||||
signingKey.CreatedAt,
|
||||
signingKey.ExpiresAt,
|
||||
publicKey,
|
||||
signingKey.Metadata);
|
||||
|
||||
signingKeys.AddOrUpdate(
|
||||
signingKey.Reference.KeyId,
|
||||
_ => new KeyEntry(descriptor, privateKeyParameters, publicKeyParameters),
|
||||
(_, _) => new KeyEntry(descriptor, privateKeyParameters, publicKeyParameters));
|
||||
}
|
||||
|
||||
public bool RemoveSigningKey(string keyId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(keyId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return signingKeys.TryRemove(keyId, out _);
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys()
|
||||
=> signingKeys.Values.Select(static entry => entry.Descriptor).ToArray();
|
||||
|
||||
private static void EnsureAlgorithmSupported(string algorithmId)
|
||||
{
|
||||
if (!SupportedAlgorithms.Contains(algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider 'bouncycastle.ed25519'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeAlgorithm(string algorithmId)
|
||||
=> string.Equals(algorithmId, SignatureAlgorithms.EdDsa, StringComparison.OrdinalIgnoreCase)
|
||||
? SignatureAlgorithms.Ed25519
|
||||
: SignatureAlgorithms.Ed25519;
|
||||
|
||||
private static byte[] NormalizePrivateKey(ReadOnlyMemory<byte> privateKey)
|
||||
{
|
||||
var span = privateKey.Span;
|
||||
return span.Length switch
|
||||
{
|
||||
32 => span.ToArray(),
|
||||
64 => span[..32].ToArray(),
|
||||
_ => throw new InvalidOperationException("Ed25519 private key must be 32 or 64 bytes.")
|
||||
};
|
||||
}
|
||||
|
||||
private static byte[] NormalizePublicKey(ReadOnlyMemory<byte> publicKey, byte[] privateKey)
|
||||
{
|
||||
if (publicKey.IsEmpty)
|
||||
{
|
||||
var privateParams = new Ed25519PrivateKeyParameters(privateKey, 0);
|
||||
return privateParams.GeneratePublicKey().GetEncoded();
|
||||
}
|
||||
|
||||
if (publicKey.Span.Length != 32)
|
||||
{
|
||||
throw new InvalidOperationException("Ed25519 public key must be 32 bytes.");
|
||||
}
|
||||
|
||||
return publicKey.ToArray();
|
||||
}
|
||||
|
||||
private sealed record KeyEntry(
|
||||
CryptoSigningKey Descriptor,
|
||||
Ed25519PrivateKeyParameters PrivateKey,
|
||||
Ed25519PublicKeyParameters PublicKey);
|
||||
|
||||
private sealed class Ed25519SignerWrapper : ICryptoSigner
|
||||
{
|
||||
private readonly KeyEntry entry;
|
||||
|
||||
public Ed25519SignerWrapper(KeyEntry entry)
|
||||
{
|
||||
this.entry = entry ?? throw new ArgumentNullException(nameof(entry));
|
||||
}
|
||||
|
||||
public string KeyId => entry.Descriptor.Reference.KeyId;
|
||||
|
||||
public string AlgorithmId => entry.Descriptor.AlgorithmId;
|
||||
|
||||
public ValueTask<byte[]> SignAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var signer = new Ed25519Signer();
|
||||
var buffer = data.ToArray();
|
||||
signer.Init(true, entry.PrivateKey);
|
||||
signer.BlockUpdate(buffer, 0, buffer.Length);
|
||||
var signature = signer.GenerateSignature();
|
||||
return ValueTask.FromResult(signature);
|
||||
}
|
||||
|
||||
public ValueTask<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var verifier = new Ed25519Signer();
|
||||
var buffer = data.ToArray();
|
||||
verifier.Init(false, entry.PublicKey);
|
||||
verifier.BlockUpdate(buffer, 0, buffer.Length);
|
||||
var verified = verifier.VerifySignature(signature.ToArray());
|
||||
return ValueTask.FromResult(verified);
|
||||
}
|
||||
|
||||
public JsonWebKey ExportPublicJsonWebKey()
|
||||
{
|
||||
var jwk = new JsonWebKey
|
||||
{
|
||||
Kid = entry.Descriptor.Reference.KeyId,
|
||||
Alg = SignatureAlgorithms.EdDsa,
|
||||
Kty = "OKP",
|
||||
Use = JsonWebKeyUseNames.Sig,
|
||||
Crv = "Ed25519"
|
||||
};
|
||||
|
||||
foreach (var op in DefaultKeyOps)
|
||||
{
|
||||
jwk.KeyOps.Add(op);
|
||||
}
|
||||
|
||||
jwk.X = Base64UrlEncoder.Encode(entry.PublicKey.GetEncoded());
|
||||
|
||||
return jwk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user