- Introduced AuthorityAdvisoryAiOptions and related classes for managing advisory AI configurations, including remote inference options and tenant-specific settings. - Added AuthorityApiLifecycleOptions to control API lifecycle settings, including legacy OAuth endpoint configurations. - Implemented validation and normalization methods for both advisory AI and API lifecycle options to ensure proper configuration. - Created AuthorityNotificationsOptions and its related classes for managing notification settings, including ack tokens, webhooks, and escalation options. - Developed IssuerDirectoryClient and related models for interacting with the issuer directory service, including caching mechanisms and HTTP client configurations. - Added support for dependency injection through ServiceCollectionExtensions for the Issuer Directory Client. - Updated project file to include necessary package references for the new Issuer Directory Client library.
302 lines
11 KiB
C#
302 lines
11 KiB
C#
using System;
|
|
using System.Security.Cryptography;
|
|
using StellaOps.Cryptography;
|
|
|
|
namespace StellaOps.Attestor.Envelope;
|
|
|
|
/// <summary>
|
|
/// Describes the underlying key algorithm for DSSE envelope signing.
|
|
/// </summary>
|
|
public enum EnvelopeKeyKind
|
|
{
|
|
Ed25519,
|
|
Ecdsa
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents signing or verification key material for DSSE envelope operations.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the key classification.
|
|
/// </summary>
|
|
public EnvelopeKeyKind Kind { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the signing algorithm identifier (e.g., ED25519, ES256).
|
|
/// </summary>
|
|
public string AlgorithmId { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the deterministic key identifier (RFC7638 JWK thumbprint based).
|
|
/// </summary>
|
|
public string KeyId { get; }
|
|
|
|
/// <summary>
|
|
/// Indicates whether the key has private material available.
|
|
/// </summary>
|
|
public bool HasPrivateMaterial => Kind switch
|
|
{
|
|
EnvelopeKeyKind.Ed25519 => ed25519PrivateKey is not null,
|
|
EnvelopeKeyKind.Ecdsa => ecdsaPrivateParameters.HasValue,
|
|
_ => false
|
|
};
|
|
|
|
/// <summary>
|
|
/// Indicates whether the key has public material available.
|
|
/// </summary>
|
|
public bool HasPublicMaterial => Kind switch
|
|
{
|
|
EnvelopeKeyKind.Ed25519 => ed25519PublicKey is not null,
|
|
EnvelopeKeyKind.Ecdsa => ecdsaPublicParameters.HasValue,
|
|
_ => false
|
|
};
|
|
|
|
internal ReadOnlySpan<byte> GetEd25519PublicKey()
|
|
{
|
|
if (Kind != EnvelopeKeyKind.Ed25519 || ed25519PublicKey is null)
|
|
{
|
|
throw new InvalidOperationException("Key does not provide Ed25519 public material.");
|
|
}
|
|
|
|
return ed25519PublicKey;
|
|
}
|
|
|
|
internal ReadOnlySpan<byte> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an Ed25519 signing key (requires private + public material).
|
|
/// </summary>
|
|
/// <param name="privateKey">64-byte Ed25519 private key (seed || public key).</param>
|
|
/// <param name="publicKey">32-byte Ed25519 public key.</param>
|
|
/// <param name="keyId">Optional external key identifier override.</param>
|
|
/// <returns>Envelope key instance.</returns>
|
|
public static EnvelopeKey CreateEd25519Signer(ReadOnlySpan<byte> privateKey, ReadOnlySpan<byte> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an Ed25519 verification key (public material only).
|
|
/// </summary>
|
|
/// <param name="publicKey">32-byte Ed25519 public key.</param>
|
|
/// <param name="keyId">Optional external key identifier override.</param>
|
|
/// <returns>Envelope key instance.</returns>
|
|
public static EnvelopeKey CreateEd25519Verifier(ReadOnlySpan<byte> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an ECDSA signing key (private + public EC parameters).
|
|
/// </summary>
|
|
/// <param name="algorithmId">ECDSA algorithm identifier (ES256, ES384, ES512).</param>
|
|
/// <param name="privateParameters">EC parameters including private scalar.</param>
|
|
/// <param name="keyId">Optional external key identifier override.</param>
|
|
/// <returns>Envelope key instance.</returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an ECDSA verification key (public EC parameters).
|
|
/// </summary>
|
|
/// <param name="algorithmId">ECDSA algorithm identifier (ES256, ES384, ES512).</param>
|
|
/// <param name="publicParameters">EC parameters containing only public coordinates.</param>
|
|
/// <param name="keyId">Optional external key identifier override.</param>
|
|
/// <returns>Envelope key instance.</returns>
|
|
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<byte> 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<byte> 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;
|
|
}
|
|
}
|