using System; using System.Security.Cryptography; using StellaOps.Cryptography; namespace StellaOps.Attestor.Envelope; /// /// Describes the underlying key algorithm for DSSE envelope signing. /// public enum EnvelopeKeyKind { Ed25519, Ecdsa } /// /// Represents signing or verification key material for DSSE envelope operations. /// public sealed class EnvelopeKey { private const int Ed25519PublicKeyLength = 32; private const int Ed25519PrivateKeySeedLength = 32; private const int Ed25519PrivateKeyExpandedLength = 64; private readonly byte[]? ed25519PublicKey; private readonly byte[]? ed25519PrivateKey; private readonly ECParameters? ecdsaPublicParameters; private readonly ECParameters? ecdsaPrivateParameters; private EnvelopeKey( EnvelopeKeyKind kind, string algorithmId, string keyId, byte[]? ed25519PublicKey, byte[]? ed25519PrivateKey, ECParameters? ecdsaPublicParameters, ECParameters? ecdsaPrivateParameters) { Kind = kind; AlgorithmId = algorithmId; KeyId = keyId; this.ed25519PublicKey = ed25519PublicKey; this.ed25519PrivateKey = ed25519PrivateKey; this.ecdsaPublicParameters = ecdsaPublicParameters; this.ecdsaPrivateParameters = ecdsaPrivateParameters; } /// /// Gets the key classification. /// public EnvelopeKeyKind Kind { get; } /// /// Gets the signing algorithm identifier (e.g., ED25519, ES256). /// public string AlgorithmId { get; } /// /// Gets the deterministic key identifier (RFC7638 JWK thumbprint based). /// public string KeyId { get; } /// /// Indicates whether the key has private material available. /// public bool HasPrivateMaterial => Kind switch { EnvelopeKeyKind.Ed25519 => ed25519PrivateKey is not null, EnvelopeKeyKind.Ecdsa => ecdsaPrivateParameters.HasValue, _ => false }; /// /// Indicates whether the key has public material available. /// public bool HasPublicMaterial => Kind switch { EnvelopeKeyKind.Ed25519 => ed25519PublicKey is not null, EnvelopeKeyKind.Ecdsa => ecdsaPublicParameters.HasValue, _ => false }; internal ReadOnlySpan GetEd25519PublicKey() { if (Kind != EnvelopeKeyKind.Ed25519 || ed25519PublicKey is null) { throw new InvalidOperationException("Key does not provide Ed25519 public material."); } return ed25519PublicKey; } internal ReadOnlySpan GetEd25519PrivateKey() { if (Kind != EnvelopeKeyKind.Ed25519 || ed25519PrivateKey is null) { throw new InvalidOperationException("Key does not provide Ed25519 private material."); } return ed25519PrivateKey; } internal ECParameters GetEcdsaPublicParameters() { if (Kind != EnvelopeKeyKind.Ecdsa || !ecdsaPublicParameters.HasValue) { throw new InvalidOperationException("Key does not provide ECDSA public parameters."); } return CloneParameters(ecdsaPublicParameters.Value, includePrivate: false); } internal ECParameters GetEcdsaPrivateParameters() { if (Kind != EnvelopeKeyKind.Ecdsa || !ecdsaPrivateParameters.HasValue) { throw new InvalidOperationException("Key does not provide ECDSA private parameters."); } return CloneParameters(ecdsaPrivateParameters.Value, includePrivate: true); } /// /// Creates an Ed25519 signing key (requires private + public material). /// /// 64-byte Ed25519 private key (seed || public key). /// 32-byte Ed25519 public key. /// Optional external key identifier override. /// Envelope key instance. public static EnvelopeKey CreateEd25519Signer(ReadOnlySpan privateKey, ReadOnlySpan publicKey, string? keyId = null) { var normalizedPrivate = NormalizeEd25519PrivateKey(privateKey); ValidateEd25519PublicLength(publicKey); var publicCopy = publicKey.ToArray(); var resolvedKeyId = string.IsNullOrWhiteSpace(keyId) ? EnvelopeKeyIdCalculator.FromEd25519(publicCopy) : keyId; return new EnvelopeKey( EnvelopeKeyKind.Ed25519, SignatureAlgorithms.Ed25519, resolvedKeyId, publicCopy, normalizedPrivate, ecdsaPublicParameters: null, ecdsaPrivateParameters: null); } /// /// Creates an Ed25519 verification key (public material only). /// /// 32-byte Ed25519 public key. /// Optional external key identifier override. /// Envelope key instance. public static EnvelopeKey CreateEd25519Verifier(ReadOnlySpan publicKey, string? keyId = null) { ValidateEd25519PublicLength(publicKey); var publicCopy = publicKey.ToArray(); var resolvedKeyId = string.IsNullOrWhiteSpace(keyId) ? EnvelopeKeyIdCalculator.FromEd25519(publicCopy) : keyId; return new EnvelopeKey( EnvelopeKeyKind.Ed25519, SignatureAlgorithms.Ed25519, resolvedKeyId, publicCopy, ed25519PrivateKey: null, ecdsaPublicParameters: null, ecdsaPrivateParameters: null); } /// /// Creates an ECDSA signing key (private + public EC parameters). /// /// ECDSA algorithm identifier (ES256, ES384, ES512). /// EC parameters including private scalar. /// Optional external key identifier override. /// Envelope key instance. public static EnvelopeKey CreateEcdsaSigner(string algorithmId, in ECParameters privateParameters, string? keyId = null) { ValidateEcdsaAlgorithm(algorithmId); if (privateParameters.D is null || privateParameters.D.Length == 0) { throw new ArgumentException("ECDSA private parameters must include the scalar component (D).", nameof(privateParameters)); } if (privateParameters.Q.X is null || privateParameters.Q.Y is null) { throw new ArgumentException("ECDSA private parameters must include public coordinates.", nameof(privateParameters)); } var publicClone = CloneParameters(privateParameters, includePrivate: false); var privateClone = CloneParameters(privateParameters, includePrivate: true); var resolvedKeyId = string.IsNullOrWhiteSpace(keyId) ? EnvelopeKeyIdCalculator.FromEcdsa(algorithmId, publicClone) : keyId; return new EnvelopeKey( EnvelopeKeyKind.Ecdsa, algorithmId, resolvedKeyId, ed25519PublicKey: null, ed25519PrivateKey: null, ecdsaPublicParameters: publicClone, ecdsaPrivateParameters: privateClone); } /// /// Creates an ECDSA verification key (public EC parameters). /// /// ECDSA algorithm identifier (ES256, ES384, ES512). /// EC parameters containing only public coordinates. /// Optional external key identifier override. /// Envelope key instance. public static EnvelopeKey CreateEcdsaVerifier(string algorithmId, in ECParameters publicParameters, string? keyId = null) { ValidateEcdsaAlgorithm(algorithmId); if (publicParameters.Q.X is null || publicParameters.Q.Y is null) { throw new ArgumentException("ECDSA public parameters must include X and Y coordinates.", nameof(publicParameters)); } if (publicParameters.D is not null) { throw new ArgumentException("ECDSA verification parameters must not include private scalar data.", nameof(publicParameters)); } var publicClone = CloneParameters(publicParameters, includePrivate: false); var resolvedKeyId = string.IsNullOrWhiteSpace(keyId) ? EnvelopeKeyIdCalculator.FromEcdsa(algorithmId, publicClone) : keyId; return new EnvelopeKey( EnvelopeKeyKind.Ecdsa, algorithmId, resolvedKeyId, ed25519PublicKey: null, ed25519PrivateKey: null, ecdsaPublicParameters: publicClone, ecdsaPrivateParameters: null); } private static byte[] NormalizeEd25519PrivateKey(ReadOnlySpan privateKey) { return privateKey.Length switch { Ed25519PrivateKeySeedLength => privateKey.ToArray(), Ed25519PrivateKeyExpandedLength => privateKey[..Ed25519PrivateKeySeedLength].ToArray(), _ => throw new ArgumentException($"Ed25519 private key must be {Ed25519PrivateKeySeedLength} or {Ed25519PrivateKeyExpandedLength} bytes.", nameof(privateKey)) }; } private static void ValidateEd25519PublicLength(ReadOnlySpan publicKey) { if (publicKey.Length != Ed25519PublicKeyLength) { throw new ArgumentException($"Ed25519 public key must be {Ed25519PublicKeyLength} bytes.", nameof(publicKey)); } } private static void ValidateEcdsaAlgorithm(string algorithmId) { if (string.IsNullOrWhiteSpace(algorithmId)) { throw new ArgumentException("Algorithm identifier is required.", nameof(algorithmId)); } var supported = string.Equals(algorithmId, SignatureAlgorithms.Es256, StringComparison.OrdinalIgnoreCase) || string.Equals(algorithmId, SignatureAlgorithms.Es384, StringComparison.OrdinalIgnoreCase) || string.Equals(algorithmId, SignatureAlgorithms.Es512, StringComparison.OrdinalIgnoreCase); if (!supported) { throw new ArgumentException($"Unsupported ECDSA algorithm '{algorithmId}'.", nameof(algorithmId)); } } private static ECParameters CloneParameters(ECParameters source, bool includePrivate) { var clone = new ECParameters { Curve = source.Curve, Q = new ECPoint { X = source.Q.X is null ? null : (byte[])source.Q.X.Clone(), Y = source.Q.Y is null ? null : (byte[])source.Q.Y.Clone() } }; if (includePrivate && source.D is not null) { clone.D = (byte[])source.D.Clone(); } return clone; } }