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;
}
}