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,95 +1,95 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Cryptography.Audit;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a structured security event emitted by the Authority host and plugins.
|
||||
/// </summary>
|
||||
public sealed record AuthEventRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// Canonical event identifier (e.g. <c>authority.password.grant</c>).
|
||||
/// </summary>
|
||||
public required string EventType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp captured when the event occurred.
|
||||
/// </summary>
|
||||
public DateTimeOffset OccurredAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Stable correlation identifier that links the event across logs, traces, and persistence.
|
||||
/// </summary>
|
||||
public string? CorrelationId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Outcome classification for the audited operation.
|
||||
/// </summary>
|
||||
public AuthEventOutcome Outcome { get; init; } = AuthEventOutcome.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// Optional human-readable reason or failure descriptor.
|
||||
/// </summary>
|
||||
public string? Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Identity of the end-user (subject) involved in the event, when applicable.
|
||||
/// </summary>
|
||||
public AuthEventSubject? Subject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// OAuth/OIDC client metadata associated with the event, when applicable.
|
||||
/// </summary>
|
||||
public AuthEventClient? Client { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant identifier associated with the authenticated principal or client.
|
||||
/// </summary>
|
||||
public ClassifiedString Tenant { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Project identifier associated with the authenticated principal or client (optional).
|
||||
/// </summary>
|
||||
public ClassifiedString Project { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Granted or requested scopes tied to the event.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Scopes { get; init; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Network attributes (remote IP, forwarded headers, user agent) captured for the request.
|
||||
/// </summary>
|
||||
public AuthEventNetwork? Network { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional classified properties carried with the event.
|
||||
/// </summary>
|
||||
public IReadOnlyList<AuthEventProperty> Properties { get; init; } = Array.Empty<AuthEventProperty>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes the outcome of an audited flow.
|
||||
/// </summary>
|
||||
public enum AuthEventOutcome
|
||||
{
|
||||
/// <summary>
|
||||
/// Outcome has not been set.
|
||||
/// </summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Operation succeeded.
|
||||
/// </summary>
|
||||
Success,
|
||||
|
||||
/// <summary>
|
||||
/// Operation failed (invalid credentials, configuration issues, etc.).
|
||||
/// </summary>
|
||||
Failure,
|
||||
|
||||
/// <summary>
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Cryptography.Audit;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a structured security event emitted by the Authority host and plugins.
|
||||
/// </summary>
|
||||
public sealed record AuthEventRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// Canonical event identifier (e.g. <c>authority.password.grant</c>).
|
||||
/// </summary>
|
||||
public required string EventType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp captured when the event occurred.
|
||||
/// </summary>
|
||||
public DateTimeOffset OccurredAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Stable correlation identifier that links the event across logs, traces, and persistence.
|
||||
/// </summary>
|
||||
public string? CorrelationId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Outcome classification for the audited operation.
|
||||
/// </summary>
|
||||
public AuthEventOutcome Outcome { get; init; } = AuthEventOutcome.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// Optional human-readable reason or failure descriptor.
|
||||
/// </summary>
|
||||
public string? Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Identity of the end-user (subject) involved in the event, when applicable.
|
||||
/// </summary>
|
||||
public AuthEventSubject? Subject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// OAuth/OIDC client metadata associated with the event, when applicable.
|
||||
/// </summary>
|
||||
public AuthEventClient? Client { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant identifier associated with the authenticated principal or client.
|
||||
/// </summary>
|
||||
public ClassifiedString Tenant { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Project identifier associated with the authenticated principal or client (optional).
|
||||
/// </summary>
|
||||
public ClassifiedString Project { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Granted or requested scopes tied to the event.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Scopes { get; init; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Network attributes (remote IP, forwarded headers, user agent) captured for the request.
|
||||
/// </summary>
|
||||
public AuthEventNetwork? Network { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional classified properties carried with the event.
|
||||
/// </summary>
|
||||
public IReadOnlyList<AuthEventProperty> Properties { get; init; } = Array.Empty<AuthEventProperty>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes the outcome of an audited flow.
|
||||
/// </summary>
|
||||
public enum AuthEventOutcome
|
||||
{
|
||||
/// <summary>
|
||||
/// Outcome has not been set.
|
||||
/// </summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Operation succeeded.
|
||||
/// </summary>
|
||||
Success,
|
||||
|
||||
/// <summary>
|
||||
/// Operation failed (invalid credentials, configuration issues, etc.).
|
||||
/// </summary>
|
||||
Failure,
|
||||
|
||||
/// <summary>
|
||||
/// Operation failed due to a lockout policy.
|
||||
/// </summary>
|
||||
LockedOut,
|
||||
@@ -108,171 +108,171 @@ public enum AuthEventOutcome
|
||||
/// Operation was rejected due to rate limiting or throttling.
|
||||
/// </summary>
|
||||
RateLimited,
|
||||
|
||||
/// <summary>
|
||||
/// Operation encountered an unexpected error.
|
||||
/// </summary>
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a string value enriched with a data classification tag.
|
||||
/// </summary>
|
||||
public readonly record struct ClassifiedString(string? Value, AuthEventDataClassification Classification)
|
||||
{
|
||||
/// <summary>
|
||||
/// An empty classified string.
|
||||
/// </summary>
|
||||
public static ClassifiedString Empty => new(null, AuthEventDataClassification.None);
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the classified string carries a non-empty value.
|
||||
/// </summary>
|
||||
public bool HasValue => !string.IsNullOrWhiteSpace(Value);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a classified string for public/non-sensitive data.
|
||||
/// </summary>
|
||||
public static ClassifiedString Public(string? value) => Create(value, AuthEventDataClassification.None);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a classified string tagged as personally identifiable information (PII).
|
||||
/// </summary>
|
||||
public static ClassifiedString Personal(string? value) => Create(value, AuthEventDataClassification.Personal);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a classified string tagged as sensitive (e.g. credentials, secrets).
|
||||
/// </summary>
|
||||
public static ClassifiedString Sensitive(string? value) => Create(value, AuthEventDataClassification.Sensitive);
|
||||
|
||||
private static ClassifiedString Create(string? value, AuthEventDataClassification classification)
|
||||
{
|
||||
return new ClassifiedString(Normalize(value), classification);
|
||||
}
|
||||
|
||||
private static string? Normalize(string? value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supported classifications for audit data values.
|
||||
/// </summary>
|
||||
public enum AuthEventDataClassification
|
||||
{
|
||||
/// <summary>
|
||||
/// Data is not considered sensitive.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Personally identifiable information (PII) that warrants redaction in certain sinks.
|
||||
/// </summary>
|
||||
Personal,
|
||||
|
||||
/// <summary>
|
||||
/// Highly sensitive information (credentials, secrets, tokens).
|
||||
/// </summary>
|
||||
Sensitive
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures subject metadata for an audit event.
|
||||
/// </summary>
|
||||
public sealed record AuthEventSubject
|
||||
{
|
||||
/// <summary>
|
||||
/// Stable subject identifier (PII).
|
||||
/// </summary>
|
||||
public ClassifiedString SubjectId { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Username or login name (PII).
|
||||
/// </summary>
|
||||
public ClassifiedString Username { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Optional display name (PII).
|
||||
/// </summary>
|
||||
public ClassifiedString DisplayName { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Optional plugin or tenant realm controlling the subject namespace.
|
||||
/// </summary>
|
||||
public ClassifiedString Realm { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Additional classified attributes (e.g. email, phone).
|
||||
/// </summary>
|
||||
public IReadOnlyList<AuthEventProperty> Attributes { get; init; } = Array.Empty<AuthEventProperty>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures OAuth/OIDC client metadata for an audit event.
|
||||
/// </summary>
|
||||
public sealed record AuthEventClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Client identifier (PII for confidential clients).
|
||||
/// </summary>
|
||||
public ClassifiedString ClientId { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Friendly client name (may be public).
|
||||
/// </summary>
|
||||
public ClassifiedString Name { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Identity provider/plugin originating the client.
|
||||
/// </summary>
|
||||
public ClassifiedString Provider { get; init; } = ClassifiedString.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures network metadata for an audit event.
|
||||
/// </summary>
|
||||
public sealed record AuthEventNetwork
|
||||
{
|
||||
/// <summary>
|
||||
/// Remote address observed for the request (PII).
|
||||
/// </summary>
|
||||
public ClassifiedString RemoteAddress { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Forwarded address supplied by proxies (PII).
|
||||
/// </summary>
|
||||
public ClassifiedString ForwardedFor { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User agent string associated with the request.
|
||||
/// </summary>
|
||||
public ClassifiedString UserAgent { get; init; } = ClassifiedString.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an additional classified property associated with the audit event.
|
||||
/// </summary>
|
||||
public sealed record AuthEventProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// Property name (canonical snake-case identifier).
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Classified value assigned to the property.
|
||||
/// </summary>
|
||||
public ClassifiedString Value { get; init; } = ClassifiedString.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sink that receives completed audit event records.
|
||||
/// </summary>
|
||||
public interface IAuthEventSink
|
||||
{
|
||||
/// <summary>
|
||||
/// Persists the supplied audit event.
|
||||
/// </summary>
|
||||
ValueTask WriteAsync(AuthEventRecord record, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Operation encountered an unexpected error.
|
||||
/// </summary>
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a string value enriched with a data classification tag.
|
||||
/// </summary>
|
||||
public readonly record struct ClassifiedString(string? Value, AuthEventDataClassification Classification)
|
||||
{
|
||||
/// <summary>
|
||||
/// An empty classified string.
|
||||
/// </summary>
|
||||
public static ClassifiedString Empty => new(null, AuthEventDataClassification.None);
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the classified string carries a non-empty value.
|
||||
/// </summary>
|
||||
public bool HasValue => !string.IsNullOrWhiteSpace(Value);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a classified string for public/non-sensitive data.
|
||||
/// </summary>
|
||||
public static ClassifiedString Public(string? value) => Create(value, AuthEventDataClassification.None);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a classified string tagged as personally identifiable information (PII).
|
||||
/// </summary>
|
||||
public static ClassifiedString Personal(string? value) => Create(value, AuthEventDataClassification.Personal);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a classified string tagged as sensitive (e.g. credentials, secrets).
|
||||
/// </summary>
|
||||
public static ClassifiedString Sensitive(string? value) => Create(value, AuthEventDataClassification.Sensitive);
|
||||
|
||||
private static ClassifiedString Create(string? value, AuthEventDataClassification classification)
|
||||
{
|
||||
return new ClassifiedString(Normalize(value), classification);
|
||||
}
|
||||
|
||||
private static string? Normalize(string? value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supported classifications for audit data values.
|
||||
/// </summary>
|
||||
public enum AuthEventDataClassification
|
||||
{
|
||||
/// <summary>
|
||||
/// Data is not considered sensitive.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Personally identifiable information (PII) that warrants redaction in certain sinks.
|
||||
/// </summary>
|
||||
Personal,
|
||||
|
||||
/// <summary>
|
||||
/// Highly sensitive information (credentials, secrets, tokens).
|
||||
/// </summary>
|
||||
Sensitive
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures subject metadata for an audit event.
|
||||
/// </summary>
|
||||
public sealed record AuthEventSubject
|
||||
{
|
||||
/// <summary>
|
||||
/// Stable subject identifier (PII).
|
||||
/// </summary>
|
||||
public ClassifiedString SubjectId { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Username or login name (PII).
|
||||
/// </summary>
|
||||
public ClassifiedString Username { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Optional display name (PII).
|
||||
/// </summary>
|
||||
public ClassifiedString DisplayName { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Optional plugin or tenant realm controlling the subject namespace.
|
||||
/// </summary>
|
||||
public ClassifiedString Realm { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Additional classified attributes (e.g. email, phone).
|
||||
/// </summary>
|
||||
public IReadOnlyList<AuthEventProperty> Attributes { get; init; } = Array.Empty<AuthEventProperty>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures OAuth/OIDC client metadata for an audit event.
|
||||
/// </summary>
|
||||
public sealed record AuthEventClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Client identifier (PII for confidential clients).
|
||||
/// </summary>
|
||||
public ClassifiedString ClientId { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Friendly client name (may be public).
|
||||
/// </summary>
|
||||
public ClassifiedString Name { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Identity provider/plugin originating the client.
|
||||
/// </summary>
|
||||
public ClassifiedString Provider { get; init; } = ClassifiedString.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures network metadata for an audit event.
|
||||
/// </summary>
|
||||
public sealed record AuthEventNetwork
|
||||
{
|
||||
/// <summary>
|
||||
/// Remote address observed for the request (PII).
|
||||
/// </summary>
|
||||
public ClassifiedString RemoteAddress { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Forwarded address supplied by proxies (PII).
|
||||
/// </summary>
|
||||
public ClassifiedString ForwardedFor { get; init; } = ClassifiedString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// User agent string associated with the request.
|
||||
/// </summary>
|
||||
public ClassifiedString UserAgent { get; init; } = ClassifiedString.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an additional classified property associated with the audit event.
|
||||
/// </summary>
|
||||
public sealed record AuthEventProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// Property name (canonical snake-case identifier).
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Classified value assigned to the property.
|
||||
/// </summary>
|
||||
public ClassifiedString Value { get; init; } = ClassifiedString.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sink that receives completed audit event records.
|
||||
/// </summary>
|
||||
public interface IAuthEventSink
|
||||
{
|
||||
/// <summary>
|
||||
/// Persists the supplied audit event.
|
||||
/// </summary>
|
||||
ValueTask WriteAsync(AuthEventRecord record, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,176 +1,176 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the underlying key material for a <see cref="CryptoSigningKey"/>.
|
||||
/// </summary>
|
||||
public enum CryptoSigningKeyKind
|
||||
{
|
||||
Ec,
|
||||
Raw
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents asymmetric signing key material managed by a crypto provider.
|
||||
/// </summary>
|
||||
public sealed class CryptoSigningKey
|
||||
{
|
||||
private static readonly ReadOnlyDictionary<string, string?> EmptyMetadata =
|
||||
new(new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase));
|
||||
private static readonly byte[] EmptyKey = Array.Empty<byte>();
|
||||
|
||||
private readonly byte[] privateKeyBytes;
|
||||
private readonly byte[] publicKeyBytes;
|
||||
|
||||
public CryptoSigningKey(
|
||||
CryptoKeyReference reference,
|
||||
string algorithmId,
|
||||
in ECParameters privateParameters,
|
||||
DateTimeOffset createdAt,
|
||||
DateTimeOffset? expiresAt = null,
|
||||
IReadOnlyDictionary<string, string?>? metadata = null)
|
||||
{
|
||||
Reference = reference ?? throw new ArgumentNullException(nameof(reference));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
throw new ArgumentException("Algorithm identifier is required.", nameof(algorithmId));
|
||||
}
|
||||
|
||||
if (privateParameters.D is null || privateParameters.D.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("Private key parameters must include the scalar component.", nameof(privateParameters));
|
||||
}
|
||||
|
||||
AlgorithmId = algorithmId;
|
||||
CreatedAt = createdAt;
|
||||
ExpiresAt = expiresAt;
|
||||
Kind = CryptoSigningKeyKind.Ec;
|
||||
|
||||
privateKeyBytes = EmptyKey;
|
||||
publicKeyBytes = EmptyKey;
|
||||
|
||||
PrivateParameters = CloneParameters(privateParameters, includePrivate: true);
|
||||
PublicParameters = CloneParameters(privateParameters, includePrivate: false);
|
||||
Metadata = metadata is null
|
||||
? EmptyMetadata
|
||||
: new ReadOnlyDictionary<string, string?>(metadata.ToDictionary(
|
||||
static pair => pair.Key,
|
||||
static pair => pair.Value,
|
||||
StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public CryptoSigningKey(
|
||||
CryptoKeyReference reference,
|
||||
string algorithmId,
|
||||
ReadOnlyMemory<byte> privateKey,
|
||||
DateTimeOffset createdAt,
|
||||
DateTimeOffset? expiresAt = null,
|
||||
ReadOnlyMemory<byte> publicKey = default,
|
||||
IReadOnlyDictionary<string, string?>? metadata = null)
|
||||
{
|
||||
Reference = reference ?? throw new ArgumentNullException(nameof(reference));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
throw new ArgumentException("Algorithm identifier is required.", nameof(algorithmId));
|
||||
}
|
||||
|
||||
if (privateKey.IsEmpty)
|
||||
{
|
||||
throw new ArgumentException("Private key material must be provided.", nameof(privateKey));
|
||||
}
|
||||
|
||||
AlgorithmId = algorithmId;
|
||||
CreatedAt = createdAt;
|
||||
ExpiresAt = expiresAt;
|
||||
Kind = CryptoSigningKeyKind.Raw;
|
||||
|
||||
privateKeyBytes = privateKey.ToArray();
|
||||
publicKeyBytes = publicKey.IsEmpty ? EmptyKey : publicKey.ToArray();
|
||||
|
||||
PrivateParameters = default;
|
||||
PublicParameters = default;
|
||||
Metadata = metadata is null
|
||||
? EmptyMetadata
|
||||
: new ReadOnlyDictionary<string, string?>(metadata.ToDictionary(
|
||||
static pair => pair.Key,
|
||||
static pair => pair.Value,
|
||||
StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key reference (id + provider hint).
|
||||
/// </summary>
|
||||
public CryptoKeyReference Reference { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the algorithm identifier (e.g., ES256).
|
||||
/// </summary>
|
||||
public string AlgorithmId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the private EC parameters (cloned).
|
||||
/// </summary>
|
||||
public ECParameters PrivateParameters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the public EC parameters (cloned, no private scalar).
|
||||
/// </summary>
|
||||
public ECParameters PublicParameters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the underlying key material representation.
|
||||
/// </summary>
|
||||
public CryptoSigningKeyKind Kind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw private key bytes when available (empty for EC-backed keys).
|
||||
/// </summary>
|
||||
public ReadOnlyMemory<byte> PrivateKey => privateKeyBytes;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw public key bytes when available (empty for EC-backed keys or when not supplied).
|
||||
/// </summary>
|
||||
public ReadOnlyMemory<byte> PublicKey => publicKeyBytes;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp when the key was created/imported.
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional expiry timestamp for the key.
|
||||
/// </summary>
|
||||
public DateTimeOffset? ExpiresAt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets arbitrary metadata entries associated with the key.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string?> Metadata { get; }
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the underlying key material for a <see cref="CryptoSigningKey"/>.
|
||||
/// </summary>
|
||||
public enum CryptoSigningKeyKind
|
||||
{
|
||||
Ec,
|
||||
Raw
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents asymmetric signing key material managed by a crypto provider.
|
||||
/// </summary>
|
||||
public sealed class CryptoSigningKey
|
||||
{
|
||||
private static readonly ReadOnlyDictionary<string, string?> EmptyMetadata =
|
||||
new(new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase));
|
||||
private static readonly byte[] EmptyKey = Array.Empty<byte>();
|
||||
|
||||
private readonly byte[] privateKeyBytes;
|
||||
private readonly byte[] publicKeyBytes;
|
||||
|
||||
public CryptoSigningKey(
|
||||
CryptoKeyReference reference,
|
||||
string algorithmId,
|
||||
in ECParameters privateParameters,
|
||||
DateTimeOffset createdAt,
|
||||
DateTimeOffset? expiresAt = null,
|
||||
IReadOnlyDictionary<string, string?>? metadata = null)
|
||||
{
|
||||
Reference = reference ?? throw new ArgumentNullException(nameof(reference));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
throw new ArgumentException("Algorithm identifier is required.", nameof(algorithmId));
|
||||
}
|
||||
|
||||
if (privateParameters.D is null || privateParameters.D.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("Private key parameters must include the scalar component.", nameof(privateParameters));
|
||||
}
|
||||
|
||||
AlgorithmId = algorithmId;
|
||||
CreatedAt = createdAt;
|
||||
ExpiresAt = expiresAt;
|
||||
Kind = CryptoSigningKeyKind.Ec;
|
||||
|
||||
privateKeyBytes = EmptyKey;
|
||||
publicKeyBytes = EmptyKey;
|
||||
|
||||
PrivateParameters = CloneParameters(privateParameters, includePrivate: true);
|
||||
PublicParameters = CloneParameters(privateParameters, includePrivate: false);
|
||||
Metadata = metadata is null
|
||||
? EmptyMetadata
|
||||
: new ReadOnlyDictionary<string, string?>(metadata.ToDictionary(
|
||||
static pair => pair.Key,
|
||||
static pair => pair.Value,
|
||||
StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public CryptoSigningKey(
|
||||
CryptoKeyReference reference,
|
||||
string algorithmId,
|
||||
ReadOnlyMemory<byte> privateKey,
|
||||
DateTimeOffset createdAt,
|
||||
DateTimeOffset? expiresAt = null,
|
||||
ReadOnlyMemory<byte> publicKey = default,
|
||||
IReadOnlyDictionary<string, string?>? metadata = null)
|
||||
{
|
||||
Reference = reference ?? throw new ArgumentNullException(nameof(reference));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
throw new ArgumentException("Algorithm identifier is required.", nameof(algorithmId));
|
||||
}
|
||||
|
||||
if (privateKey.IsEmpty)
|
||||
{
|
||||
throw new ArgumentException("Private key material must be provided.", nameof(privateKey));
|
||||
}
|
||||
|
||||
AlgorithmId = algorithmId;
|
||||
CreatedAt = createdAt;
|
||||
ExpiresAt = expiresAt;
|
||||
Kind = CryptoSigningKeyKind.Raw;
|
||||
|
||||
privateKeyBytes = privateKey.ToArray();
|
||||
publicKeyBytes = publicKey.IsEmpty ? EmptyKey : publicKey.ToArray();
|
||||
|
||||
PrivateParameters = default;
|
||||
PublicParameters = default;
|
||||
Metadata = metadata is null
|
||||
? EmptyMetadata
|
||||
: new ReadOnlyDictionary<string, string?>(metadata.ToDictionary(
|
||||
static pair => pair.Key,
|
||||
static pair => pair.Value,
|
||||
StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key reference (id + provider hint).
|
||||
/// </summary>
|
||||
public CryptoKeyReference Reference { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the algorithm identifier (e.g., ES256).
|
||||
/// </summary>
|
||||
public string AlgorithmId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the private EC parameters (cloned).
|
||||
/// </summary>
|
||||
public ECParameters PrivateParameters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the public EC parameters (cloned, no private scalar).
|
||||
/// </summary>
|
||||
public ECParameters PublicParameters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the underlying key material representation.
|
||||
/// </summary>
|
||||
public CryptoSigningKeyKind Kind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw private key bytes when available (empty for EC-backed keys).
|
||||
/// </summary>
|
||||
public ReadOnlyMemory<byte> PrivateKey => privateKeyBytes;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw public key bytes when available (empty for EC-backed keys or when not supplied).
|
||||
/// </summary>
|
||||
public ReadOnlyMemory<byte> PublicKey => publicKeyBytes;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp when the key was created/imported.
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional expiry timestamp for the key.
|
||||
/// </summary>
|
||||
public DateTimeOffset? ExpiresAt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets arbitrary metadata entries associated with the key.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string?> Metadata { get; }
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,181 +1,181 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
/// <summary>
|
||||
/// Default in-process crypto provider exposing password hashing capabilities.
|
||||
/// </summary>
|
||||
public sealed class DefaultCryptoProvider : ICryptoProvider, ICryptoProviderDiagnostics
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, IPasswordHasher> passwordHashers;
|
||||
private readonly ConcurrentDictionary<string, CryptoSigningKey> signingKeys;
|
||||
private static readonly HashSet<string> SupportedSigningAlgorithms = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
SignatureAlgorithms.Es256
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> SupportedHashAlgorithms = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
HashAlgorithms.Sha256,
|
||||
HashAlgorithms.Sha384,
|
||||
HashAlgorithms.Sha512
|
||||
};
|
||||
|
||||
public DefaultCryptoProvider()
|
||||
{
|
||||
passwordHashers = new ConcurrentDictionary<string, IPasswordHasher>(StringComparer.OrdinalIgnoreCase);
|
||||
signingKeys = new ConcurrentDictionary<string, CryptoSigningKey>(StringComparer.Ordinal);
|
||||
|
||||
var argon = new Argon2idPasswordHasher();
|
||||
var pbkdf2 = new Pbkdf2PasswordHasher();
|
||||
|
||||
passwordHashers.TryAdd(PasswordHashAlgorithm.Argon2id.ToString(), argon);
|
||||
passwordHashers.TryAdd(PasswordHashAlgorithms.Argon2id, argon);
|
||||
passwordHashers.TryAdd(PasswordHashAlgorithm.Pbkdf2.ToString(), pbkdf2);
|
||||
passwordHashers.TryAdd(PasswordHashAlgorithms.Pbkdf2Sha256, pbkdf2);
|
||||
}
|
||||
|
||||
public string Name => "default";
|
||||
|
||||
public bool Supports(CryptoCapability capability, string algorithmId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return capability switch
|
||||
{
|
||||
CryptoCapability.PasswordHashing => passwordHashers.ContainsKey(algorithmId),
|
||||
CryptoCapability.Signing or CryptoCapability.Verification => SupportedSigningAlgorithms.Contains(algorithmId),
|
||||
CryptoCapability.ContentHashing => SupportedHashAlgorithms.Contains(algorithmId),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public IPasswordHasher GetPasswordHasher(string algorithmId)
|
||||
{
|
||||
if (!Supports(CryptoCapability.PasswordHashing, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Password hashing algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
}
|
||||
|
||||
return passwordHashers[algorithmId];
|
||||
}
|
||||
|
||||
public ICryptoHasher GetHasher(string algorithmId)
|
||||
{
|
||||
if (!Supports(CryptoCapability.ContentHashing, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Hash algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
}
|
||||
|
||||
return new DefaultCryptoHasher(algorithmId);
|
||||
}
|
||||
|
||||
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(keyReference);
|
||||
|
||||
if (!Supports(CryptoCapability.Signing, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
}
|
||||
|
||||
if (!signingKeys.TryGetValue(keyReference.KeyId, out var signingKey))
|
||||
{
|
||||
throw new KeyNotFoundException($"Signing key '{keyReference.KeyId}' is not registered with provider '{Name}'.");
|
||||
}
|
||||
|
||||
if (!string.Equals(signingKey.AlgorithmId, algorithmId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Signing key '{keyReference.KeyId}' is registered for algorithm '{signingKey.AlgorithmId}', not '{algorithmId}'.");
|
||||
}
|
||||
|
||||
return EcdsaSigner.Create(signingKey);
|
||||
}
|
||||
|
||||
public void UpsertSigningKey(CryptoSigningKey signingKey)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(signingKey);
|
||||
EnsureSigningSupported(signingKey.AlgorithmId);
|
||||
if (signingKey.Kind != CryptoSigningKeyKind.Ec)
|
||||
{
|
||||
throw new InvalidOperationException($"Provider '{Name}' only accepts EC signing keys.");
|
||||
}
|
||||
ValidateSigningKey(signingKey);
|
||||
|
||||
signingKeys.AddOrUpdate(signingKey.Reference.KeyId, signingKey, (_, _) => signingKey);
|
||||
}
|
||||
|
||||
public bool RemoveSigningKey(string keyId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(keyId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return signingKeys.TryRemove(keyId, out _);
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys()
|
||||
=> signingKeys.Values.ToArray();
|
||||
|
||||
public IEnumerable<CryptoProviderKeyDescriptor> DescribeKeys()
|
||||
{
|
||||
foreach (var key in signingKeys.Values)
|
||||
{
|
||||
var metadata = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["kind"] = key.Kind.ToString(),
|
||||
["createdAt"] = key.CreatedAt.UtcDateTime.ToString("O"),
|
||||
["providerHint"] = key.Reference.ProviderHint,
|
||||
["provider"] = Name
|
||||
};
|
||||
|
||||
if (key.ExpiresAt.HasValue)
|
||||
{
|
||||
metadata["expiresAt"] = key.ExpiresAt.Value.UtcDateTime.ToString("O");
|
||||
}
|
||||
|
||||
foreach (var pair in key.Metadata)
|
||||
{
|
||||
metadata[$"meta.{pair.Key}"] = pair.Value;
|
||||
}
|
||||
|
||||
yield return new CryptoProviderKeyDescriptor(
|
||||
Name,
|
||||
key.Reference.KeyId,
|
||||
key.AlgorithmId,
|
||||
metadata);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureSigningSupported(string algorithmId)
|
||||
{
|
||||
if (!SupportedSigningAlgorithms.Contains(algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider 'default'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSigningKey(CryptoSigningKey signingKey)
|
||||
{
|
||||
if (!string.Equals(signingKey.AlgorithmId, SignatureAlgorithms.Es256, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"Only ES256 signing keys are currently supported by provider 'default'.");
|
||||
}
|
||||
|
||||
var expected = ECCurve.NamedCurves.nistP256;
|
||||
var curve = signingKey.PrivateParameters.Curve;
|
||||
if (!curve.IsNamed || !string.Equals(curve.Oid.Value, expected.Oid.Value, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException("ES256 signing keys must use the NIST P-256 curve.");
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
/// <summary>
|
||||
/// Default in-process crypto provider exposing password hashing capabilities.
|
||||
/// </summary>
|
||||
public sealed class DefaultCryptoProvider : ICryptoProvider, ICryptoProviderDiagnostics
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, IPasswordHasher> passwordHashers;
|
||||
private readonly ConcurrentDictionary<string, CryptoSigningKey> signingKeys;
|
||||
private static readonly HashSet<string> SupportedSigningAlgorithms = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
SignatureAlgorithms.Es256
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> SupportedHashAlgorithms = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
HashAlgorithms.Sha256,
|
||||
HashAlgorithms.Sha384,
|
||||
HashAlgorithms.Sha512
|
||||
};
|
||||
|
||||
public DefaultCryptoProvider()
|
||||
{
|
||||
passwordHashers = new ConcurrentDictionary<string, IPasswordHasher>(StringComparer.OrdinalIgnoreCase);
|
||||
signingKeys = new ConcurrentDictionary<string, CryptoSigningKey>(StringComparer.Ordinal);
|
||||
|
||||
var argon = new Argon2idPasswordHasher();
|
||||
var pbkdf2 = new Pbkdf2PasswordHasher();
|
||||
|
||||
passwordHashers.TryAdd(PasswordHashAlgorithm.Argon2id.ToString(), argon);
|
||||
passwordHashers.TryAdd(PasswordHashAlgorithms.Argon2id, argon);
|
||||
passwordHashers.TryAdd(PasswordHashAlgorithm.Pbkdf2.ToString(), pbkdf2);
|
||||
passwordHashers.TryAdd(PasswordHashAlgorithms.Pbkdf2Sha256, pbkdf2);
|
||||
}
|
||||
|
||||
public string Name => "default";
|
||||
|
||||
public bool Supports(CryptoCapability capability, string algorithmId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return capability switch
|
||||
{
|
||||
CryptoCapability.PasswordHashing => passwordHashers.ContainsKey(algorithmId),
|
||||
CryptoCapability.Signing or CryptoCapability.Verification => SupportedSigningAlgorithms.Contains(algorithmId),
|
||||
CryptoCapability.ContentHashing => SupportedHashAlgorithms.Contains(algorithmId),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public IPasswordHasher GetPasswordHasher(string algorithmId)
|
||||
{
|
||||
if (!Supports(CryptoCapability.PasswordHashing, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Password hashing algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
}
|
||||
|
||||
return passwordHashers[algorithmId];
|
||||
}
|
||||
|
||||
public ICryptoHasher GetHasher(string algorithmId)
|
||||
{
|
||||
if (!Supports(CryptoCapability.ContentHashing, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Hash algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
}
|
||||
|
||||
return new DefaultCryptoHasher(algorithmId);
|
||||
}
|
||||
|
||||
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(keyReference);
|
||||
|
||||
if (!Supports(CryptoCapability.Signing, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
}
|
||||
|
||||
if (!signingKeys.TryGetValue(keyReference.KeyId, out var signingKey))
|
||||
{
|
||||
throw new KeyNotFoundException($"Signing key '{keyReference.KeyId}' is not registered with provider '{Name}'.");
|
||||
}
|
||||
|
||||
if (!string.Equals(signingKey.AlgorithmId, algorithmId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Signing key '{keyReference.KeyId}' is registered for algorithm '{signingKey.AlgorithmId}', not '{algorithmId}'.");
|
||||
}
|
||||
|
||||
return EcdsaSigner.Create(signingKey);
|
||||
}
|
||||
|
||||
public void UpsertSigningKey(CryptoSigningKey signingKey)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(signingKey);
|
||||
EnsureSigningSupported(signingKey.AlgorithmId);
|
||||
if (signingKey.Kind != CryptoSigningKeyKind.Ec)
|
||||
{
|
||||
throw new InvalidOperationException($"Provider '{Name}' only accepts EC signing keys.");
|
||||
}
|
||||
ValidateSigningKey(signingKey);
|
||||
|
||||
signingKeys.AddOrUpdate(signingKey.Reference.KeyId, signingKey, (_, _) => signingKey);
|
||||
}
|
||||
|
||||
public bool RemoveSigningKey(string keyId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(keyId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return signingKeys.TryRemove(keyId, out _);
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys()
|
||||
=> signingKeys.Values.ToArray();
|
||||
|
||||
public IEnumerable<CryptoProviderKeyDescriptor> DescribeKeys()
|
||||
{
|
||||
foreach (var key in signingKeys.Values)
|
||||
{
|
||||
var metadata = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["kind"] = key.Kind.ToString(),
|
||||
["createdAt"] = key.CreatedAt.UtcDateTime.ToString("O"),
|
||||
["providerHint"] = key.Reference.ProviderHint,
|
||||
["provider"] = Name
|
||||
};
|
||||
|
||||
if (key.ExpiresAt.HasValue)
|
||||
{
|
||||
metadata["expiresAt"] = key.ExpiresAt.Value.UtcDateTime.ToString("O");
|
||||
}
|
||||
|
||||
foreach (var pair in key.Metadata)
|
||||
{
|
||||
metadata[$"meta.{pair.Key}"] = pair.Value;
|
||||
}
|
||||
|
||||
yield return new CryptoProviderKeyDescriptor(
|
||||
Name,
|
||||
key.Reference.KeyId,
|
||||
key.AlgorithmId,
|
||||
metadata);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureSigningSupported(string algorithmId)
|
||||
{
|
||||
if (!SupportedSigningAlgorithms.Contains(algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider 'default'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSigningKey(CryptoSigningKey signingKey)
|
||||
{
|
||||
if (!string.Equals(signingKey.AlgorithmId, SignatureAlgorithms.Es256, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"Only ES256 signing keys are currently supported by provider 'default'.");
|
||||
}
|
||||
|
||||
var expected = ECCurve.NamedCurves.nistP256;
|
||||
var curve = signingKey.PrivateParameters.Curve;
|
||||
if (!curve.IsNamed || !string.Equals(curve.Oid.Value, expected.Oid.Value, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException("ES256 signing keys must use the NIST P-256 curve.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user