Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism
- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace StellaOps.Signer.Keyless;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an ephemeral keypair that exists only in memory.
|
||||
/// Private key material is securely erased on disposal.
|
||||
/// </summary>
|
||||
public sealed class EphemeralKeyPair : IDisposable
|
||||
{
|
||||
private byte[] _privateKey;
|
||||
private readonly byte[] _publicKey;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// The public key bytes.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<byte> PublicKey => _publicKey;
|
||||
|
||||
/// <summary>
|
||||
/// The private key bytes. Only accessible while not disposed.
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException">Thrown if accessed after disposal.</exception>
|
||||
public ReadOnlySpan<byte> PrivateKey
|
||||
{
|
||||
get
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
return _privateKey;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The cryptographic algorithm used for this keypair.
|
||||
/// </summary>
|
||||
public string Algorithm { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The UTC timestamp when this keypair was created.
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ephemeral keypair.
|
||||
/// </summary>
|
||||
/// <param name="publicKey">The public key bytes.</param>
|
||||
/// <param name="privateKey">The private key bytes (will be copied).</param>
|
||||
/// <param name="algorithm">The algorithm identifier.</param>
|
||||
public EphemeralKeyPair(byte[] publicKey, byte[] privateKey, string algorithm)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(publicKey);
|
||||
ArgumentNullException.ThrowIfNull(privateKey);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(algorithm);
|
||||
|
||||
_publicKey = (byte[])publicKey.Clone();
|
||||
_privateKey = (byte[])privateKey.Clone();
|
||||
Algorithm = algorithm;
|
||||
CreatedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signs the specified data using the ephemeral private key.
|
||||
/// </summary>
|
||||
/// <param name="data">The data to sign.</param>
|
||||
/// <returns>The signature bytes.</returns>
|
||||
/// <exception cref="ObjectDisposedException">Thrown if called after disposal.</exception>
|
||||
public byte[] Sign(ReadOnlySpan<byte> data)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
return Algorithm switch
|
||||
{
|
||||
KeylessAlgorithms.EcdsaP256 => SignWithEcdsaP256(data),
|
||||
KeylessAlgorithms.Ed25519 => SignWithEd25519(data),
|
||||
_ => throw new NotSupportedException($"Unsupported algorithm: {Algorithm}")
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] SignWithEcdsaP256(ReadOnlySpan<byte> data)
|
||||
{
|
||||
using var ecdsa = ECDsa.Create();
|
||||
ecdsa.ImportECPrivateKey(_privateKey, out _);
|
||||
return ecdsa.SignData(data.ToArray(), HashAlgorithmName.SHA256);
|
||||
}
|
||||
|
||||
private byte[] SignWithEd25519(ReadOnlySpan<byte> data)
|
||||
{
|
||||
// Ed25519 signing implementation
|
||||
// Note: .NET 9+ has native Ed25519 support via EdDSA
|
||||
throw new NotImplementedException("Ed25519 signing requires additional implementation");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Securely disposes the keypair, zeroing all private key material.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
// Zero out the private key memory
|
||||
if (_privateKey != null)
|
||||
{
|
||||
CryptographicOperations.ZeroMemory(_privateKey);
|
||||
_privateKey = [];
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizer ensures private key is zeroed if Dispose is not called.
|
||||
/// </summary>
|
||||
~EphemeralKeyPair()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Well-known algorithm identifiers for keyless signing.
|
||||
/// </summary>
|
||||
public static class KeylessAlgorithms
|
||||
{
|
||||
/// <summary>
|
||||
/// ECDSA with P-256 curve (NIST P-256, secp256r1).
|
||||
/// </summary>
|
||||
public const string EcdsaP256 = "ECDSA_P256";
|
||||
|
||||
/// <summary>
|
||||
/// Edwards-curve Digital Signature Algorithm with Curve25519.
|
||||
/// </summary>
|
||||
public const string Ed25519 = "Ed25519";
|
||||
|
||||
/// <summary>
|
||||
/// All supported algorithms.
|
||||
/// </summary>
|
||||
public static readonly IReadOnlySet<string> Supported = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
EcdsaP256,
|
||||
Ed25519
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the specified algorithm is supported.
|
||||
/// </summary>
|
||||
public static bool IsSupported(string algorithm) =>
|
||||
Supported.Contains(algorithm);
|
||||
}
|
||||
Reference in New Issue
Block a user