search and ai stabilization work, localization stablized.
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
// Task: AS-005 - Create artifact submission endpoint
|
||||
// Description: Content fetch helpers for artifact submissions
|
||||
// -----------------------------------------------------------------------------
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Artifact.Api;
|
||||
|
||||
public sealed partial class ArtifactController
|
||||
@@ -18,7 +20,7 @@ public sealed partial class ArtifactController
|
||||
|
||||
if (!Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri))
|
||||
{
|
||||
throw new ArgumentException($"Invalid URI format: {uri}");
|
||||
throw new ArgumentException(_t("common.artifact.uri_format_invalid", uri));
|
||||
}
|
||||
|
||||
return parsedUri.Scheme.ToLowerInvariant() switch
|
||||
@@ -26,7 +28,7 @@ public sealed partial class ArtifactController
|
||||
"s3" => await FetchFromS3Async(parsedUri, ct).ConfigureAwait(false),
|
||||
"http" or "https" => await FetchFromHttpAsync(parsedUri, ct).ConfigureAwait(false),
|
||||
"file" => await FetchFromFileAsync(parsedUri, ct).ConfigureAwait(false),
|
||||
_ => throw new NotSupportedException($"URI scheme not supported: {parsedUri.Scheme}")
|
||||
_ => throw new NotSupportedException(_t("common.artifact.uri_scheme_not_supported", parsedUri.Scheme))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Description: File fetch for artifact submissions
|
||||
// -----------------------------------------------------------------------------
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Artifact.Api;
|
||||
|
||||
@@ -18,14 +19,14 @@ public sealed partial class ArtifactController
|
||||
|
||||
if (!System.IO.File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException($"File not accessible: {filePath}");
|
||||
throw new FileNotFoundException(_t("common.artifact.file_not_accessible", filePath));
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
if (fileInfo.Length > 100 * 1024 * 1024)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"File too large: {fileInfo.Length} bytes exceeds 100MB limit");
|
||||
_t("common.artifact.file_too_large", fileInfo.Length));
|
||||
}
|
||||
|
||||
return await System.IO.File.ReadAllBytesAsync(filePath, ct).ConfigureAwait(false);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Artifact.Api;
|
||||
|
||||
@@ -26,21 +27,21 @@ public sealed partial class ArtifactController
|
||||
if (!headResponse.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"URI not accessible: {uri} returned {headResponse.StatusCode}");
|
||||
_t("common.artifact.uri_not_accessible", uri, headResponse.StatusCode));
|
||||
}
|
||||
|
||||
var contentLength = headResponse.Content.Headers.ContentLength;
|
||||
if (contentLength > 100 * 1024 * 1024)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Content too large: {contentLength} bytes exceeds 100MB limit");
|
||||
_t("common.artifact.content_too_large", contentLength));
|
||||
}
|
||||
|
||||
return await httpClient.GetByteArrayAsync(uri, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to fetch from {uri}: {ex.Message}", ex);
|
||||
throw new InvalidOperationException(_t("common.artifact.fetch_failed", uri, ex.Message), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,5 +17,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Concelier\__Libraries\StellaOps.Concelier.SbomIntegration\StellaOps.Concelier.SbomIntegration.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Localization\StellaOps.Localization.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
public sealed partial class ReplayToken
|
||||
@@ -11,20 +13,20 @@ public sealed partial class ReplayToken
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(canonical))
|
||||
{
|
||||
throw new ArgumentException("Token cannot be empty.", nameof(canonical));
|
||||
throw new ArgumentException(_t("common.audit.replay_token_empty"), nameof(canonical));
|
||||
}
|
||||
|
||||
var parts = canonical.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (parts.Length < 4 || parts.Length > 5 || !string.Equals(parts[0], Scheme, StringComparison.Ordinal))
|
||||
{
|
||||
throw new FormatException($"Invalid replay token format: {canonical}");
|
||||
throw new FormatException(_t("common.audit.replay_token_format_invalid", canonical));
|
||||
}
|
||||
|
||||
var versionPart = parts[1];
|
||||
if (!versionPart.StartsWith("v", StringComparison.Ordinal) || versionPart.Length <= 1)
|
||||
{
|
||||
throw new FormatException($"Invalid replay token version: {canonical}");
|
||||
throw new FormatException(_t("common.audit.replay_token_version_invalid", canonical));
|
||||
}
|
||||
|
||||
var version = versionPart[1..];
|
||||
@@ -36,7 +38,7 @@ public sealed partial class ReplayToken
|
||||
{
|
||||
if (!long.TryParse(parts[4], out var unixSeconds))
|
||||
{
|
||||
throw new FormatException($"Invalid expiration timestamp in replay token: {canonical}");
|
||||
throw new FormatException(_t("common.audit.replay_token_expiration_invalid", canonical));
|
||||
}
|
||||
expiresAt = DateTimeOffset.FromUnixTimeSeconds(unixSeconds);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
/// <summary>
|
||||
@@ -67,7 +69,7 @@ public sealed partial class ReplayToken : IEquatable<ReplayToken>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new ArgumentException("Token value cannot be empty.", nameof(value));
|
||||
throw new ArgumentException(_t("common.audit.replay_token_value_empty"), nameof(value));
|
||||
}
|
||||
|
||||
Value = value.Trim();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Audit.ReplayToken;
|
||||
|
||||
@@ -51,7 +52,7 @@ public sealed partial class Sha256ReplayTokenGenerator
|
||||
var key = kvp.Key.Trim();
|
||||
if (!seen.Add(key))
|
||||
{
|
||||
throw new ArgumentException($"AdditionalContext contains duplicate key after normalization: '{key}'.", nameof(values));
|
||||
throw new ArgumentException(_t("common.audit.replay_token_duplicate_context_key", key), nameof(values));
|
||||
}
|
||||
|
||||
normalized.Add(new KeyValuePair<string, string>(key, kvp.Value?.Trim() ?? string.Empty));
|
||||
|
||||
@@ -16,5 +16,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\StellaOps.Cryptography\\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\\StellaOps.Localization\\StellaOps.Localization.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Auth.Security.Dpop;
|
||||
|
||||
@@ -60,7 +61,7 @@ internal static class DpopNonceUtilities
|
||||
{
|
||||
if (value.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("Value must not be empty after trimming.");
|
||||
throw new ArgumentException(_t("common.validation.empty_after_trim", "Value"));
|
||||
}
|
||||
|
||||
value.AsSpan().CopyTo(span[index..]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text.Json;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Auth.Security.Dpop;
|
||||
|
||||
@@ -19,7 +20,7 @@ public sealed partial class DpopProofValidator
|
||||
_logger?.LogWarning("DPoP header decode failure: {Error}", headerError);
|
||||
failure = DpopValidationResult.Failure(
|
||||
"invalid_header",
|
||||
headerError ?? "Unable to decode header.");
|
||||
headerError ?? _t("auth.dpop.header_decode_failed"));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -27,27 +28,27 @@ public sealed partial class DpopProofValidator
|
||||
typElement.ValueKind != JsonValueKind.String ||
|
||||
!string.Equals(typElement.GetString(), ProofType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
failure = DpopValidationResult.Failure("invalid_header", "DPoP proof missing typ=dpop+jwt header.");
|
||||
failure = DpopValidationResult.Failure("invalid_header", _t("auth.dpop.header_missing_typ"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!headerElement.TryGetProperty("alg", out var algElement) ||
|
||||
algElement.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
failure = DpopValidationResult.Failure("invalid_header", "DPoP proof missing alg header.");
|
||||
failure = DpopValidationResult.Failure("invalid_header", _t("auth.dpop.header_missing_alg"));
|
||||
return false;
|
||||
}
|
||||
|
||||
var algorithm = algElement.GetString()?.Trim().ToUpperInvariant();
|
||||
if (string.IsNullOrEmpty(algorithm) || !_options.NormalizedAlgorithms.Contains(algorithm))
|
||||
{
|
||||
failure = DpopValidationResult.Failure("invalid_header", "Unsupported DPoP algorithm.");
|
||||
failure = DpopValidationResult.Failure("invalid_header", _t("auth.dpop.header_unsupported_alg"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!headerElement.TryGetProperty("jwk", out var jwkElement))
|
||||
{
|
||||
failure = DpopValidationResult.Failure("invalid_header", "DPoP proof missing jwk header.");
|
||||
failure = DpopValidationResult.Failure("invalid_header", _t("auth.dpop.header_missing_jwk"));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -59,7 +60,7 @@ public sealed partial class DpopProofValidator
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to parse DPoP jwk header.");
|
||||
failure = DpopValidationResult.Failure("invalid_header", "DPoP proof jwk header is invalid.");
|
||||
failure = DpopValidationResult.Failure("invalid_header", _t("auth.dpop.header_invalid_jwk"));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text.Json;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Auth.Security.Dpop;
|
||||
|
||||
@@ -27,13 +28,13 @@ public sealed partial class DpopProofValidator
|
||||
var segments = token.Split('.');
|
||||
if (segments.Length != 3)
|
||||
{
|
||||
error = "Token must contain three segments.";
|
||||
error = _t("auth.dpop.token_three_segments");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (segmentIndex < 0 || segmentIndex > 2)
|
||||
{
|
||||
error = "Segment index out of range.";
|
||||
error = _t("auth.dpop.segment_out_of_range");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Auth.Security.Dpop;
|
||||
|
||||
@@ -18,14 +19,14 @@ public sealed partial class DpopProofValidator
|
||||
if (!payloadElement.TryGetProperty("nonce", out var nonceElement) ||
|
||||
nonceElement.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
failure = DpopValidationResult.Failure("invalid_token", "DPoP proof missing nonce claim.");
|
||||
failure = DpopValidationResult.Failure("invalid_token", _t("auth.dpop.nonce_missing"));
|
||||
return false;
|
||||
}
|
||||
|
||||
actualNonce = nonceElement.GetString();
|
||||
if (!string.Equals(actualNonce, expectedNonce, StringComparison.Ordinal))
|
||||
{
|
||||
failure = DpopValidationResult.Failure("invalid_token", "DPoP nonce mismatch.");
|
||||
failure = DpopValidationResult.Failure("invalid_token", _t("auth.dpop.nonce_mismatch"));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Auth.Security.Dpop;
|
||||
|
||||
@@ -21,49 +22,49 @@ public sealed partial class DpopProofValidator
|
||||
_logger?.LogWarning("DPoP payload decode failure: {Error}", payloadError);
|
||||
failure = DpopValidationResult.Failure(
|
||||
"invalid_payload",
|
||||
payloadError ?? "Unable to decode payload.");
|
||||
payloadError ?? _t("auth.dpop.payload_decode_failed"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!payloadElement.TryGetProperty("htm", out var htmElement) ||
|
||||
htmElement.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
failure = DpopValidationResult.Failure("invalid_payload", "DPoP proof missing htm claim.");
|
||||
failure = DpopValidationResult.Failure("invalid_payload", _t("auth.dpop.payload_missing_htm"));
|
||||
return false;
|
||||
}
|
||||
|
||||
var method = httpMethod.Trim().ToUpperInvariant();
|
||||
if (!string.Equals(htmElement.GetString(), method, StringComparison.Ordinal))
|
||||
{
|
||||
failure = DpopValidationResult.Failure("invalid_payload", "DPoP htm does not match request method.");
|
||||
failure = DpopValidationResult.Failure("invalid_payload", _t("auth.dpop.payload_htm_mismatch"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!payloadElement.TryGetProperty("htu", out var htuElement) ||
|
||||
htuElement.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
failure = DpopValidationResult.Failure("invalid_payload", "DPoP proof missing htu claim.");
|
||||
failure = DpopValidationResult.Failure("invalid_payload", _t("auth.dpop.payload_missing_htu"));
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalizedHtu = NormalizeHtu(httpUri);
|
||||
if (!string.Equals(htuElement.GetString(), normalizedHtu, StringComparison.Ordinal))
|
||||
{
|
||||
failure = DpopValidationResult.Failure("invalid_payload", "DPoP htu does not match request URI.");
|
||||
failure = DpopValidationResult.Failure("invalid_payload", _t("auth.dpop.payload_htu_mismatch"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!payloadElement.TryGetProperty("iat", out var iatElement) ||
|
||||
iatElement.ValueKind is not JsonValueKind.Number)
|
||||
{
|
||||
failure = DpopValidationResult.Failure("invalid_payload", "DPoP proof missing iat claim.");
|
||||
failure = DpopValidationResult.Failure("invalid_payload", _t("auth.dpop.payload_missing_iat"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!payloadElement.TryGetProperty("jti", out var jtiElement) ||
|
||||
jtiElement.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
failure = DpopValidationResult.Failure("invalid_payload", "DPoP proof missing jti claim.");
|
||||
failure = DpopValidationResult.Failure("invalid_payload", _t("auth.dpop.payload_missing_jti"));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -74,7 +75,7 @@ public sealed partial class DpopProofValidator
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
failure = DpopValidationResult.Failure("invalid_payload", "DPoP proof iat claim is not a valid number.");
|
||||
failure = DpopValidationResult.Failure("invalid_payload", _t("auth.dpop.payload_iat_invalid"));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Auth.Security.Dpop;
|
||||
|
||||
public sealed partial class DpopProofValidator
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Auth.Security.Dpop;
|
||||
|
||||
@@ -24,7 +25,7 @@ public sealed partial class DpopProofValidator : IDpopProofValidator
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
var snapshot = options.Value ?? throw new InvalidOperationException("DPoP options must be provided.");
|
||||
var snapshot = options.Value ?? throw new InvalidOperationException(_t("auth.dpop.options_required"));
|
||||
_options = snapshot.Snapshot();
|
||||
_replayCache = replayCache ?? NullReplayCache.Instance;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Auth.Security.Dpop;
|
||||
|
||||
@@ -66,22 +67,22 @@ public sealed class DpopValidationOptions
|
||||
{
|
||||
if (ProofLifetime <= TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("DPoP proof lifetime must be greater than zero.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.proof_lifetime_invalid"));
|
||||
}
|
||||
|
||||
if (AllowedClockSkew < TimeSpan.Zero || AllowedClockSkew > TimeSpan.FromMinutes(5))
|
||||
{
|
||||
throw new InvalidOperationException("DPoP allowed clock skew must be between 0 seconds and 5 minutes.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.clock_skew_invalid"));
|
||||
}
|
||||
|
||||
if (ReplayWindow < TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("DPoP replay window must be greater than or equal to zero.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.replay_window_invalid"));
|
||||
}
|
||||
|
||||
if (_allowedAlgorithms.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("At least one allowed DPoP algorithm must be configured.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.algorithm_required"));
|
||||
}
|
||||
|
||||
NormalizedAlgorithms = _allowedAlgorithms
|
||||
@@ -91,7 +92,7 @@ public sealed class DpopValidationOptions
|
||||
|
||||
if (NormalizedAlgorithms.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Allowed DPoP algorithms cannot be empty after normalization.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.algorithm_empty_after_normalization"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,5 +42,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Router\__Libraries\StellaOps.Messaging\StellaOps.Messaging.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Localization\StellaOps.Localization.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Canonicalization.Json;
|
||||
|
||||
@@ -41,6 +42,6 @@ public static class CanonicalJsonSerializer
|
||||
public static T Deserialize<T>(string json)
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(json, _options)
|
||||
?? throw new InvalidOperationException($"Failed to deserialize {typeof(T).Name}");
|
||||
?? throw new InvalidOperationException(_t("common.canonicalization.deserialize_failed", typeof(T).Name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Canonicalization.Json;
|
||||
|
||||
@@ -11,7 +12,7 @@ public sealed class Iso8601DateTimeConverter : JsonConverter<DateTimeOffset>
|
||||
{
|
||||
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> DateTimeOffset.Parse(
|
||||
reader.GetString() ?? throw new JsonException("DateTimeOffset value is null."),
|
||||
reader.GetString() ?? throw new JsonException(_t("common.canonicalization.datetime_value_null")),
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Canonicalization.Json;
|
||||
|
||||
@@ -48,7 +49,7 @@ public sealed class StableDictionaryConverter<TKey, TValue> : JsonConverter<IDic
|
||||
{
|
||||
if (key is null)
|
||||
{
|
||||
throw new ArgumentException("Dictionary key cannot be null.", nameof(key));
|
||||
throw new ArgumentException(_t("common.canonicalization.dict_key_null"), nameof(key));
|
||||
}
|
||||
|
||||
return key switch
|
||||
|
||||
@@ -7,4 +7,8 @@
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Localization\StellaOps.Localization.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -60,7 +61,7 @@ public sealed class AdvisoryAiRemoteInferenceOptions
|
||||
|
||||
if (_allowedProfiles.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Authority configuration requires at least one advisory AI remote inference profile when remote inference is enabled.");
|
||||
throw new InvalidOperationException(_t("config.authority.remote_inference_required"));
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -38,29 +39,29 @@ public sealed class AdvisoryAiTenantRemoteInferenceOptions
|
||||
|
||||
if (remoteOptions is null || !remoteOptions.Enabled)
|
||||
{
|
||||
throw new InvalidOperationException("Tenant remote inference consent cannot be granted when remote inference is disabled.");
|
||||
throw new InvalidOperationException(_t("config.tenant.remote_inference_disabled"));
|
||||
}
|
||||
|
||||
if (ConsentVersion is { Length: > MaxConsentVersionLength })
|
||||
{
|
||||
throw new InvalidOperationException($"Tenant remote inference consentVersion must be {MaxConsentVersionLength} characters or fewer.");
|
||||
throw new InvalidOperationException(_t("config.tenant.remote_inference_consent_version_length", MaxConsentVersionLength));
|
||||
}
|
||||
|
||||
if (ConsentedBy is { Length: > MaxConsentedByLength })
|
||||
{
|
||||
throw new InvalidOperationException($"Tenant remote inference consentedBy must be {MaxConsentedByLength} characters or fewer.");
|
||||
throw new InvalidOperationException(_t("config.tenant.remote_inference_consented_by_length", MaxConsentedByLength));
|
||||
}
|
||||
|
||||
if (remoteOptions.RequireTenantConsent)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ConsentVersion))
|
||||
{
|
||||
throw new InvalidOperationException("Tenant remote inference consent requires consentVersion when consentGranted is true.");
|
||||
throw new InvalidOperationException(_t("config.tenant.remote_inference_consent_version_required"));
|
||||
}
|
||||
|
||||
if (!ConsentedAt.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException("Tenant remote inference consent requires consentedAt when consentGranted is true.");
|
||||
throw new InvalidOperationException(_t("config.tenant.remote_inference_consented_at_required"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using StellaOps.Cryptography;
|
||||
using System;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -14,27 +15,27 @@ public sealed partial class AuthorityAckTokenOptions
|
||||
|
||||
if (string.IsNullOrWhiteSpace(PayloadType))
|
||||
{
|
||||
throw new InvalidOperationException("notifications.ackTokens.payloadType must be specified when ack tokens are enabled.");
|
||||
throw new InvalidOperationException(_t("config.ack_token.payload_type_required"));
|
||||
}
|
||||
|
||||
if (DefaultLifetime <= TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("notifications.ackTokens.defaultLifetime must be greater than zero.");
|
||||
throw new InvalidOperationException(_t("config.ack_token.default_lifetime_invalid"));
|
||||
}
|
||||
|
||||
if (MaxLifetime <= TimeSpan.Zero || MaxLifetime < DefaultLifetime)
|
||||
{
|
||||
throw new InvalidOperationException("notifications.ackTokens.maxLifetime must be greater than zero and greater than or equal to defaultLifetime.");
|
||||
throw new InvalidOperationException(_t("config.ack_token.max_lifetime_invalid"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ActiveKeyId))
|
||||
{
|
||||
throw new InvalidOperationException("notifications.ackTokens.activeKeyId must be provided when ack tokens are enabled.");
|
||||
throw new InvalidOperationException(_t("config.ack_token.key_id_required"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(KeyPath))
|
||||
{
|
||||
throw new InvalidOperationException("notifications.ackTokens.keyPath must be provided when ack tokens are enabled.");
|
||||
throw new InvalidOperationException(_t("config.ack_token.key_path_required"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(KeySource))
|
||||
@@ -59,7 +60,7 @@ public sealed partial class AuthorityAckTokenOptions
|
||||
|
||||
if (JwksCacheLifetime <= TimeSpan.Zero || JwksCacheLifetime > TimeSpan.FromHours(1))
|
||||
{
|
||||
throw new InvalidOperationException("notifications.ackTokens.jwksCacheLifetime must be between 00:00:01 and 01:00:00.");
|
||||
throw new InvalidOperationException(_t("config.ack_token.jwks_cache_range"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -58,7 +59,7 @@ public sealed class AuthorityLegacyAuthEndpointOptions
|
||||
|
||||
if (normalizedSunset <= normalizedDeprecation)
|
||||
{
|
||||
throw new InvalidOperationException("Legacy auth sunset date must be after the deprecation date.");
|
||||
throw new InvalidOperationException(_t("config.api_lifecycle.sunset_after_deprecation"));
|
||||
}
|
||||
|
||||
DeprecationDate = normalizedDeprecation;
|
||||
@@ -69,7 +70,7 @@ public sealed class AuthorityLegacyAuthEndpointOptions
|
||||
if (!Uri.TryCreate(DocumentationUrl, UriKind.Absolute, out var uri) ||
|
||||
(uri.Scheme != Uri.UriSchemeHttps && uri.Scheme != Uri.UriSchemeHttp))
|
||||
{
|
||||
throw new InvalidOperationException("Legacy auth documentation URL must be an absolute HTTP or HTTPS URL.");
|
||||
throw new InvalidOperationException(_t("config.api_lifecycle.docs_url_format"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -28,12 +29,12 @@ public sealed class AuthorityBootstrapOptions
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ApiKey))
|
||||
{
|
||||
throw new InvalidOperationException("Authority bootstrap configuration requires an API key when enabled.");
|
||||
throw new InvalidOperationException(_t("config.bootstrap.api_key_required"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(DefaultIdentityProvider))
|
||||
{
|
||||
throw new InvalidOperationException("Authority bootstrap configuration requires a default identity provider name when enabled.");
|
||||
throw new InvalidOperationException(_t("config.bootstrap.idp_required"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -44,7 +45,7 @@ public sealed class AuthorityDelegationOptions
|
||||
|
||||
if (!seenAccounts.Add(account.AccountId))
|
||||
{
|
||||
throw new InvalidOperationException($"Delegation configuration contains duplicate service account id '{account.AccountId}'.");
|
||||
throw new InvalidOperationException(_t("config.delegation.duplicate_account", account.AccountId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -10,7 +11,7 @@ public sealed class AuthorityDelegationQuotaOptions
|
||||
{
|
||||
if (MaxActiveTokens <= 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Authority delegation configuration requires {propertyName}.{nameof(MaxActiveTokens)} to be greater than zero.");
|
||||
throw new InvalidOperationException(_t("config.delegation.quota_max_tokens", $"{propertyName}.{nameof(MaxActiveTokens)}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -30,29 +31,29 @@ public sealed class AuthorityDpopNonceOptions
|
||||
{
|
||||
if (Ttl <= TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("Dpop.Nonce.Ttl must be greater than zero.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.nonce_ttl_invalid"));
|
||||
}
|
||||
|
||||
if (MaxIssuancePerMinute < 1)
|
||||
{
|
||||
throw new InvalidOperationException("Dpop.Nonce.MaxIssuancePerMinute must be at least 1.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.nonce_max_issuance_invalid"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Store))
|
||||
{
|
||||
throw new InvalidOperationException("Dpop.Nonce.Store must be specified.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.nonce_store_required"));
|
||||
}
|
||||
|
||||
Store = Store.Trim().ToLowerInvariant();
|
||||
|
||||
if (Store is not ("memory" or "redis"))
|
||||
{
|
||||
throw new InvalidOperationException("Dpop.Nonce.Store must be either 'memory' or 'redis'.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.nonce_store_invalid"));
|
||||
}
|
||||
|
||||
if (Store == "redis" && string.IsNullOrWhiteSpace(RedisConnectionString))
|
||||
{
|
||||
throw new InvalidOperationException("Dpop.Nonce.RedisConnectionString must be provided when using the 'redis' store.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.nonce_redis_required"));
|
||||
}
|
||||
|
||||
var normalizedAudiences = _requiredAudiences
|
||||
@@ -62,7 +63,7 @@ public sealed class AuthorityDpopNonceOptions
|
||||
|
||||
if (normalizedAudiences.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Dpop.Nonce.RequiredAudiences must include at least one audience.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.nonce_audiences_required"));
|
||||
}
|
||||
|
||||
_requiredAudiences.Clear();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -35,22 +36,22 @@ public sealed class AuthorityDpopOptions
|
||||
{
|
||||
if (ProofLifetime <= TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("Dpop.ProofLifetime must be greater than zero.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.proof_lifetime_invalid"));
|
||||
}
|
||||
|
||||
if (AllowedClockSkew < TimeSpan.Zero || AllowedClockSkew > TimeSpan.FromMinutes(5))
|
||||
{
|
||||
throw new InvalidOperationException("Dpop.AllowedClockSkew must be between 0 and 5 minutes.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.clock_skew_invalid"));
|
||||
}
|
||||
|
||||
if (ReplayWindow < TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("Dpop.ReplayWindow must be greater than or equal to zero.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.replay_window_invalid"));
|
||||
}
|
||||
|
||||
if (_allowedAlgorithms.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("At least one DPoP algorithm must be configured.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.algorithm_required"));
|
||||
}
|
||||
|
||||
NormalizedAlgorithms = _allowedAlgorithms
|
||||
@@ -60,7 +61,7 @@ public sealed class AuthorityDpopOptions
|
||||
|
||||
if (NormalizedAlgorithms.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Allowed DPoP algorithms cannot be empty after normalization.");
|
||||
throw new InvalidOperationException(_t("auth.dpop.algorithm_empty_after_normalization"));
|
||||
}
|
||||
|
||||
Nonce.Validate();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Threading.RateLimiting;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -39,17 +40,17 @@ public sealed class AuthorityEndpointRateLimitOptions
|
||||
|
||||
if (PermitLimit <= 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Authority rate limiting '{name}' requires permitLimit to be greater than zero.");
|
||||
throw new InvalidOperationException(_t("config.rate_limit.permit_limit", name));
|
||||
}
|
||||
|
||||
if (QueueLimit < 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Authority rate limiting '{name}' queueLimit cannot be negative.");
|
||||
throw new InvalidOperationException(_t("config.rate_limit.queue_limit", name));
|
||||
}
|
||||
|
||||
if (Window <= TimeSpan.Zero || Window > TimeSpan.FromHours(1))
|
||||
{
|
||||
throw new InvalidOperationException($"Authority rate limiting '{name}' window must be greater than zero and no more than one hour.");
|
||||
throw new InvalidOperationException(_t("config.rate_limit.window", name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -21,7 +22,7 @@ public sealed class AuthorityEscalationOptions
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Scope))
|
||||
{
|
||||
throw new InvalidOperationException("notifications.escalation.scope must be specified.");
|
||||
throw new InvalidOperationException(_t("config.escalation.scope_required"));
|
||||
}
|
||||
|
||||
Scope = Scope.Trim().ToLowerInvariant();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -35,12 +36,12 @@ public sealed class AuthorityExceptionRoutingTemplateOptions
|
||||
{
|
||||
if (string.IsNullOrEmpty(Id))
|
||||
{
|
||||
throw new InvalidOperationException("Authority exception routing templates require an id.");
|
||||
throw new InvalidOperationException(_t("config.exceptions.template_id_required"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(AuthorityRouteId))
|
||||
{
|
||||
throw new InvalidOperationException($"Authority exception routing template '{Id}' requires authorityRouteId.");
|
||||
throw new InvalidOperationException(_t("config.exceptions.template_route_required", Id));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -39,7 +40,7 @@ public sealed class AuthorityExceptionsOptions
|
||||
{
|
||||
if (templateOptions is null)
|
||||
{
|
||||
throw new InvalidOperationException("Authority exception routing template entries must not be null.");
|
||||
throw new InvalidOperationException(_t("config.exceptions.null_template"));
|
||||
}
|
||||
|
||||
templateOptions.Normalize();
|
||||
@@ -48,7 +49,7 @@ public sealed class AuthorityExceptionsOptions
|
||||
var template = templateOptions.ToImmutable();
|
||||
if (!normalized.TryAdd(template.Id, template))
|
||||
{
|
||||
throw new InvalidOperationException($"Authority exception routing template '{template.Id}' is configured more than once.");
|
||||
throw new InvalidOperationException(_t("config.exceptions.duplicate_template", template.Id));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -42,7 +43,7 @@ public sealed class AuthorityMtlsOptions
|
||||
{
|
||||
if (RotationGrace < TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("Mtls.RotationGrace must be non-negative.");
|
||||
throw new InvalidOperationException(_t("config.mtls.rotation_grace_negative"));
|
||||
}
|
||||
|
||||
NormalizedAudiences = _enforceForAudiences
|
||||
@@ -52,12 +53,12 @@ public sealed class AuthorityMtlsOptions
|
||||
|
||||
if (Enabled && NormalizedAudiences.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Mtls.EnforceForAudiences must include at least one audience when enabled.");
|
||||
throw new InvalidOperationException(_t("config.mtls.audiences_required"));
|
||||
}
|
||||
|
||||
if (AllowedCertificateAuthorities.Any(static path => string.IsNullOrWhiteSpace(path)))
|
||||
{
|
||||
throw new InvalidOperationException("Mtls.AllowedCertificateAuthorities entries must not be empty.");
|
||||
throw new InvalidOperationException(_t("config.mtls.ca_empty"));
|
||||
}
|
||||
|
||||
NormalizedSanTypes = _allowedSanTypes
|
||||
@@ -68,7 +69,7 @@ public sealed class AuthorityMtlsOptions
|
||||
|
||||
if (Enabled && NormalizedSanTypes.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Mtls.AllowedSanTypes must include at least one entry when enabled.");
|
||||
throw new InvalidOperationException(_t("config.mtls.san_types_required"));
|
||||
}
|
||||
|
||||
var compiledPatterns = new List<Regex>(AllowedSubjectPatterns.Count);
|
||||
@@ -77,7 +78,7 @@ public sealed class AuthorityMtlsOptions
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pattern))
|
||||
{
|
||||
throw new InvalidOperationException("Mtls.AllowedSubjectPatterns entries must not be empty.");
|
||||
throw new InvalidOperationException(_t("config.mtls.subject_patterns_empty"));
|
||||
}
|
||||
|
||||
try
|
||||
@@ -86,7 +87,7 @@ public sealed class AuthorityMtlsOptions
|
||||
}
|
||||
catch (RegexParseException ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Mtls.AllowedSubjectPatterns entry '{pattern}' is not a valid regular expression.", ex);
|
||||
throw new InvalidOperationException(_t("config.mtls.subject_pattern_invalid", pattern), ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -30,7 +31,7 @@ public sealed class AuthorityPluginSettings
|
||||
{
|
||||
if (descriptor is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Authority plugin descriptor '{name}' is null.");
|
||||
throw new InvalidOperationException(_t("config.plugin.descriptor_null", name));
|
||||
}
|
||||
|
||||
descriptor.Normalize(name);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -41,17 +42,17 @@ public sealed class AuthoritySealedModeOptions
|
||||
|
||||
if (string.IsNullOrWhiteSpace(EvidencePath))
|
||||
{
|
||||
throw new InvalidOperationException("AirGap.SealedMode.EvidencePath must be provided when enforcement is enabled.");
|
||||
throw new InvalidOperationException(_t("config.sealed_mode.evidence_path_required"));
|
||||
}
|
||||
|
||||
if (MaxEvidenceAge <= TimeSpan.Zero || MaxEvidenceAge > TimeSpan.FromDays(7))
|
||||
{
|
||||
throw new InvalidOperationException("AirGap.SealedMode.MaxEvidenceAge must be between 00:00:01 and 7.00:00:00.");
|
||||
throw new InvalidOperationException(_t("config.sealed_mode.max_age_range"));
|
||||
}
|
||||
|
||||
if (CacheLifetime <= TimeSpan.Zero || CacheLifetime > MaxEvidenceAge)
|
||||
{
|
||||
throw new InvalidOperationException("AirGap.SealedMode.CacheLifetime must be greater than zero and less than or equal to AirGap.SealedMode.MaxEvidenceAge.");
|
||||
throw new InvalidOperationException(_t("config.sealed_mode.cache_lifetime_range"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -18,27 +19,27 @@ public sealed partial class AuthorityServiceAccountSeedOptions
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(AccountId))
|
||||
{
|
||||
throw new InvalidOperationException("Delegation service account seeds require an accountId.");
|
||||
throw new InvalidOperationException(_t("config.service_account.id_required"));
|
||||
}
|
||||
|
||||
if (!_accountIdRegex.IsMatch(AccountId))
|
||||
{
|
||||
throw new InvalidOperationException($"Service account id '{AccountId}' must contain lowercase letters, digits, colon, underscore, or hyphen.");
|
||||
throw new InvalidOperationException(_t("config.service_account.id_format", AccountId));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Tenant))
|
||||
{
|
||||
throw new InvalidOperationException($"Service account '{AccountId}' requires a tenant assignment.");
|
||||
throw new InvalidOperationException(_t("config.service_account.tenant_required", AccountId));
|
||||
}
|
||||
|
||||
if (tenantIds.Count > 0 && !tenantIds.Contains(Tenant))
|
||||
{
|
||||
throw new InvalidOperationException($"Service account '{AccountId}' references unknown tenant '{Tenant}'.");
|
||||
throw new InvalidOperationException(_t("config.service_account.tenant_unknown", AccountId, Tenant));
|
||||
}
|
||||
|
||||
if (AllowedScopes.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Service account '{AccountId}' must specify at least one allowed scope.");
|
||||
throw new InvalidOperationException(_t("config.service_account.scope_required", AccountId));
|
||||
}
|
||||
|
||||
if (Attributes.Count > 0)
|
||||
@@ -47,7 +48,7 @@ public sealed partial class AuthorityServiceAccountSeedOptions
|
||||
{
|
||||
if (!_allowedAttributeKeys.Contains(attributeName))
|
||||
{
|
||||
throw new InvalidOperationException($"Service account '{AccountId}' defines unsupported attribute '{attributeName}'. Allowed attributes: env, owner, business_tier.");
|
||||
throw new InvalidOperationException(_t("config.service_account.unsupported_attribute", AccountId, attributeName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -14,12 +15,12 @@ public sealed class AuthoritySigningAdditionalKeyOptions
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(KeyId))
|
||||
{
|
||||
throw new InvalidOperationException("Additional signing keys require a keyId.");
|
||||
throw new InvalidOperationException(_t("config.signing.additional_key_id_required"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Path))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing key '{KeyId}' requires a path.");
|
||||
throw new InvalidOperationException(_t("config.signing.additional_key_path_required", KeyId));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Source))
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using StellaOps.Cryptography;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -62,12 +63,12 @@ public sealed class AuthoritySigningOptions
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ActiveKeyId))
|
||||
{
|
||||
throw new InvalidOperationException("Authority signing configuration requires signing.activeKeyId.");
|
||||
throw new InvalidOperationException(_t("config.signing.key_id_required"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(KeyPath))
|
||||
{
|
||||
throw new InvalidOperationException("Authority signing configuration requires signing.keyPath.");
|
||||
throw new InvalidOperationException(_t("config.signing.key_path_required"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Algorithm))
|
||||
@@ -87,7 +88,7 @@ public sealed class AuthoritySigningOptions
|
||||
|
||||
if (JwksCacheLifetime <= TimeSpan.Zero || JwksCacheLifetime > TimeSpan.FromHours(1))
|
||||
{
|
||||
throw new InvalidOperationException("Authority signing configuration requires signing.jwksCacheLifetime to be between 00:00:01 and 01:00:00.");
|
||||
throw new InvalidOperationException(_t("config.signing.jwks_cache_range"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -23,12 +24,12 @@ public sealed class AuthorityStorageOptions
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ConnectionString))
|
||||
{
|
||||
throw new InvalidOperationException("Authority storage requires a connection string.");
|
||||
throw new InvalidOperationException(_t("config.storage.connection_required"));
|
||||
}
|
||||
|
||||
if (CommandTimeout <= TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("Authority storage command timeout must be greater than zero.");
|
||||
throw new InvalidOperationException(_t("config.storage.timeout_invalid"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -17,7 +18,7 @@ public sealed class AuthorityTenantDelegationOptions
|
||||
|
||||
if (MaxActiveTokens is { } value && value <= 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Tenant '{tenantId}' delegation maxActiveTokens must be greater than zero when specified.");
|
||||
throw new InvalidOperationException(_t("config.tenant.delegation_max_tokens", tenantId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -12,12 +13,12 @@ public sealed partial class AuthorityTenantOptions
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Id))
|
||||
{
|
||||
throw new InvalidOperationException("Each tenant requires an id (slug).");
|
||||
throw new InvalidOperationException(_t("config.tenant.id_required"));
|
||||
}
|
||||
|
||||
if (!_tenantSlugRegex.IsMatch(Id))
|
||||
{
|
||||
throw new InvalidOperationException($"Tenant id '{Id}' must contain only lowercase letters, digits, and hyphen.");
|
||||
throw new InvalidOperationException(_t("config.tenant.id_format", Id));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(DisplayName))
|
||||
@@ -31,7 +32,7 @@ public sealed partial class AuthorityTenantOptions
|
||||
{
|
||||
if (!_projectSlugRegex.IsMatch(project))
|
||||
{
|
||||
throw new InvalidOperationException($"Tenant '{Id}' defines project '{project}' which must contain only lowercase letters, digits, and hyphen.");
|
||||
throw new InvalidOperationException(_t("config.tenant.project_format", Id, project));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +46,7 @@ public sealed partial class AuthorityTenantOptions
|
||||
{
|
||||
if (roleOptions is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Tenant '{Id}' defines role '{roleName}' without configuration.");
|
||||
throw new InvalidOperationException(_t("config.tenant.role_missing_config", Id, roleName));
|
||||
}
|
||||
|
||||
roleOptions.Validate(Id, roleName);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -17,14 +18,14 @@ public sealed partial class AuthorityTenantRoleOptions
|
||||
{
|
||||
if (Scopes.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Tenant '{tenantId}' role '{roleName}' must specify at least one scope.");
|
||||
throw new InvalidOperationException(_t("config.tenant.role_scope_required", tenantId, roleName));
|
||||
}
|
||||
|
||||
foreach (var scope in Scopes)
|
||||
{
|
||||
if (!StellaOpsScopes.IsKnown(scope))
|
||||
{
|
||||
throw new InvalidOperationException($"Tenant '{tenantId}' role '{roleName}' references unknown scope '{scope}'.");
|
||||
throw new InvalidOperationException(_t("config.tenant.role_unknown_scope", tenantId, roleName, scope));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +35,7 @@ public sealed partial class AuthorityTenantRoleOptions
|
||||
{
|
||||
if (!_allowedAttributeKeys.Contains(attributeName))
|
||||
{
|
||||
throw new InvalidOperationException($"Tenant '{tenantId}' role '{roleName}' defines unsupported attribute '{attributeName}'. Allowed attributes: env, owner, business_tier.");
|
||||
throw new InvalidOperationException(_t("config.tenant.role_unsupported_attribute", tenantId, roleName, attributeName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -46,27 +47,27 @@ public sealed class AuthorityVulnAntiForgeryOptions
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Audience))
|
||||
{
|
||||
throw new InvalidOperationException("vulnerabilityExplorer.workflow.antiForgery.audience must be specified when anti-forgery tokens are enabled.");
|
||||
throw new InvalidOperationException(_t("config.anti_forgery.audience_required"));
|
||||
}
|
||||
|
||||
if (DefaultLifetime <= TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("vulnerabilityExplorer.workflow.antiForgery.defaultLifetime must be greater than zero.");
|
||||
throw new InvalidOperationException(_t("config.anti_forgery.default_lifetime_invalid"));
|
||||
}
|
||||
|
||||
if (MaxLifetime <= TimeSpan.Zero || MaxLifetime < DefaultLifetime)
|
||||
{
|
||||
throw new InvalidOperationException("vulnerabilityExplorer.workflow.antiForgery.maxLifetime must be greater than zero and greater than or equal to defaultLifetime.");
|
||||
throw new InvalidOperationException(_t("config.anti_forgery.max_lifetime_invalid"));
|
||||
}
|
||||
|
||||
if (MaxContextEntries < 0)
|
||||
{
|
||||
throw new InvalidOperationException("vulnerabilityExplorer.workflow.antiForgery.maxContextEntries must be non-negative.");
|
||||
throw new InvalidOperationException(_t("config.anti_forgery.max_context_entries"));
|
||||
}
|
||||
|
||||
if (MaxContextValueLength <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("vulnerabilityExplorer.workflow.antiForgery.maxContextValueLength must be greater than zero.");
|
||||
throw new InvalidOperationException(_t("config.anti_forgery.max_context_value_length"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -46,27 +47,27 @@ public sealed class AuthorityVulnAttachmentOptions
|
||||
|
||||
if (DefaultLifetime <= TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("vulnerabilityExplorer.attachments.defaultLifetime must be greater than zero when attachment tokens are enabled.");
|
||||
throw new InvalidOperationException(_t("config.attachment.default_lifetime_invalid"));
|
||||
}
|
||||
|
||||
if (MaxLifetime <= TimeSpan.Zero || MaxLifetime < DefaultLifetime)
|
||||
{
|
||||
throw new InvalidOperationException("vulnerabilityExplorer.attachments.maxLifetime must be greater than zero and greater than or equal to defaultLifetime.");
|
||||
throw new InvalidOperationException(_t("config.attachment.max_lifetime_invalid"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(PayloadType))
|
||||
{
|
||||
throw new InvalidOperationException("vulnerabilityExplorer.attachments.payloadType must be specified when attachment tokens are enabled.");
|
||||
throw new InvalidOperationException(_t("config.attachment.payload_type_required"));
|
||||
}
|
||||
|
||||
if (MaxMetadataEntries < 0)
|
||||
{
|
||||
throw new InvalidOperationException("vulnerabilityExplorer.attachments.maxMetadataEntries must be non-negative.");
|
||||
throw new InvalidOperationException(_t("config.attachment.max_metadata_entries"));
|
||||
}
|
||||
|
||||
if (MaxMetadataValueLength <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("vulnerabilityExplorer.attachments.maxMetadataValueLength must be greater than zero.");
|
||||
throw new InvalidOperationException(_t("config.attachment.max_metadata_value_length"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -35,7 +36,7 @@ public sealed class AuthorityWebhookAllowlistOptions
|
||||
|
||||
if (_allowedHosts.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("notifications.webhooks.allowedHosts must include at least one host when enabled.");
|
||||
throw new InvalidOperationException(_t("config.webhook.hosts_required"));
|
||||
}
|
||||
|
||||
NormalizeList(_allowedHosts);
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.Pkcs11Gost\StellaOps.Cryptography.Plugin.Pkcs11Gost.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.DependencyInjection\StellaOps.Cryptography.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Localization\StellaOps.Localization.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(StellaOpsEnableCryptoPro)' == 'true'">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -9,12 +10,12 @@ public sealed partial class StellaOpsAuthorityOptions
|
||||
{
|
||||
if (value <= TimeSpan.Zero)
|
||||
{
|
||||
throw new InvalidOperationException($"Authority configuration requires {propertyName} to be greater than zero.");
|
||||
throw new InvalidOperationException(_t("config.authority.property_greater_than_zero", propertyName));
|
||||
}
|
||||
|
||||
if (value > maximum)
|
||||
{
|
||||
throw new InvalidOperationException($"Authority configuration requires {propertyName} to be less than or equal to {maximum}.");
|
||||
throw new InvalidOperationException(_t("config.authority.property_max", propertyName, maximum));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -13,22 +14,22 @@ public sealed partial class StellaOpsAuthorityOptions
|
||||
{
|
||||
if (SchemaVersion <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("Authority configuration requires a positive schemaVersion.");
|
||||
throw new InvalidOperationException(_t("config.authority.schema_version_required"));
|
||||
}
|
||||
|
||||
if (Issuer is null)
|
||||
{
|
||||
throw new InvalidOperationException("Authority configuration requires an issuer URL.");
|
||||
throw new InvalidOperationException(_t("config.authority.issuer_required"));
|
||||
}
|
||||
|
||||
if (!Issuer.IsAbsoluteUri)
|
||||
{
|
||||
throw new InvalidOperationException("Authority issuer must be an absolute URI.");
|
||||
throw new InvalidOperationException(_t("config.authority.issuer_absolute"));
|
||||
}
|
||||
|
||||
if (string.Equals(Issuer.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && !Issuer.IsLoopback)
|
||||
{
|
||||
throw new InvalidOperationException("Authority issuer must use HTTPS unless running on a loopback interface.");
|
||||
throw new InvalidOperationException(_t("config.authority.issuer_https"));
|
||||
}
|
||||
|
||||
ValidateLifetime(AccessTokenLifetime, nameof(AccessTokenLifetime), TimeSpan.FromHours(24));
|
||||
@@ -63,7 +64,7 @@ public sealed partial class StellaOpsAuthorityOptions
|
||||
tenant.Validate(AdvisoryAi, Delegation);
|
||||
if (!identifiers.Add(tenant.Id))
|
||||
{
|
||||
throw new InvalidOperationException($"Authority configuration contains duplicate tenant identifier '{tenant.Id}'.");
|
||||
throw new InvalidOperationException(_t("config.authority.duplicate_tenant", tenant.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.PluginLoader;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.DependencyInjection;
|
||||
|
||||
@@ -32,8 +33,7 @@ internal sealed class CryptoPluginProviderList : IReadOnlyList<ICryptoProvider>
|
||||
|
||||
if (_providers.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"No crypto providers were loaded. Check plugin configuration and manifest.");
|
||||
throw new InvalidOperationException(_t("crypto.di.no_plugins_loaded"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.DependencyInjection;
|
||||
|
||||
@@ -46,18 +47,18 @@ public static partial class CryptoProviderRegistryValidator
|
||||
var resolved = options.ResolvePreferredProviders();
|
||||
if (resolved.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Crypto provider registry cannot be empty. Configure at least one provider for RU deployments.");
|
||||
throw new InvalidOperationException(_t("crypto.di.registry_empty"));
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux() && enableOpenSsl &&
|
||||
!resolved.Contains("ru.openssl.gost", _ordinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException("Linux RU baseline requires provider 'ru.openssl.gost' (set STELLAOPS_CRYPTO_ENABLE_RU_OPENSSL=0 to override explicitly).");
|
||||
throw new InvalidOperationException(_t("crypto.di.ru_openssl_required"));
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux() && !enableOpenSsl && !enablePkcs11)
|
||||
{
|
||||
throw new InvalidOperationException("RU Linux baseline is misconfigured: both ru.openssl.gost and ru.pkcs11 are disabled via environment. Enable at least one provider.");
|
||||
throw new InvalidOperationException(_t("crypto.di.ru_provider_required"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Localization\StellaOps.Localization.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.PluginLoader\StellaOps.Cryptography.PluginLoader.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.Pkcs11Gost\StellaOps.Cryptography.Plugin.Pkcs11Gost.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.OpenSslGost\StellaOps.Cryptography.Plugin.OpenSslGost.csproj" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -11,7 +12,7 @@ public sealed partial class AwsKmsClient
|
||||
var digest = new byte[32];
|
||||
if (!SHA256.TryHashData(data.Span, digest, out _))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to hash payload with SHA-256.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.hash_failed"));
|
||||
}
|
||||
|
||||
return digest;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -56,8 +57,8 @@ public sealed partial class AwsKmsClient
|
||||
KmsAlgorithms.Es256,
|
||||
ResolveCurveName(publicKey.Curve),
|
||||
Array.Empty<byte>(),
|
||||
parameters.Q.X ?? throw new InvalidOperationException("Public key missing X coordinate."),
|
||||
parameters.Q.Y ?? throw new InvalidOperationException("Public key missing Y coordinate."),
|
||||
parameters.Q.X ?? throw new InvalidOperationException(_t("crypto.kms.public_key_missing_x")),
|
||||
parameters.Q.Y ?? throw new InvalidOperationException(_t("crypto.kms.public_key_missing_y")),
|
||||
metadata.CreatedAt);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -34,10 +35,10 @@ public sealed partial class AwsKmsClient : IKmsClient, IDisposable
|
||||
}
|
||||
|
||||
public Task<KmsKeyMetadata> RotateAsync(string keyId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("AWS KMS rotation must be orchestrated via AWS KMS policies or schedules.");
|
||||
=> throw new NotSupportedException(_t("crypto.kms.rotation_via_policy", "AWS KMS", "AWS KMS"));
|
||||
|
||||
public Task RevokeAsync(string keyId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("AWS KMS key revocation must be managed through AWS KMS APIs or console.");
|
||||
=> throw new NotSupportedException(_t("crypto.kms.revocation_via_policy", "AWS KMS key", "AWS KMS"));
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using Amazon.KeyManagementService.Model;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -16,7 +17,7 @@ internal sealed partial class AwsKmsFacade
|
||||
KeyId = keyId,
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var metadata = response.KeyMetadata ?? throw new InvalidOperationException($"Key '{keyId}' was not found.");
|
||||
var metadata = response.KeyMetadata ?? throw new InvalidOperationException(_t("crypto.kms.key_not_found", keyId));
|
||||
var createdAt = metadata.CreationDate?.ToUniversalTime() ?? _timeProvider.GetUtcNow();
|
||||
|
||||
return new AwsKeyMetadata(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -18,8 +19,8 @@ public sealed partial class Fido2KmsClient
|
||||
metadata.Algorithm,
|
||||
_curveName,
|
||||
Array.Empty<byte>(),
|
||||
_publicParameters.Q.X ?? throw new InvalidOperationException("FIDO2 public key missing X coordinate."),
|
||||
_publicParameters.Q.Y ?? throw new InvalidOperationException("FIDO2 public key missing Y coordinate."),
|
||||
_publicParameters.Q.X ?? throw new InvalidOperationException(_t("crypto.fido2.missing_x")),
|
||||
_publicParameters.Q.Y ?? throw new InvalidOperationException(_t("crypto.fido2.missing_y")),
|
||||
_options.CreatedAt ?? _timeProvider.GetUtcNow());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -11,7 +12,7 @@ public sealed partial class Fido2KmsClient
|
||||
var digest = new byte[32];
|
||||
if (!SHA256.TryHashData(data.Span, digest, out _))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to hash payload with SHA-256.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.hash_failed"));
|
||||
}
|
||||
|
||||
return digest;
|
||||
@@ -33,7 +34,7 @@ public sealed partial class Fido2KmsClient
|
||||
"1.2.840.10045.3.1.7" => JsonWebKeyECTypes.P256,
|
||||
"1.3.132.0.34" => JsonWebKeyECTypes.P384,
|
||||
"1.3.132.0.35" => JsonWebKeyECTypes.P521,
|
||||
_ => throw new InvalidOperationException($"Unsupported FIDO2 curve OID '{oid}'."),
|
||||
_ => throw new InvalidOperationException(_t("crypto.fido2.curve_unsupported", oid ?? string.Empty)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
public sealed partial class Fido2KmsClient
|
||||
{
|
||||
public Task<KmsKeyMetadata> RotateAsync(string keyId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("FIDO2 credential rotation requires new enrolment.");
|
||||
=> throw new NotSupportedException(_t("crypto.fido2.rotation_required"));
|
||||
|
||||
public Task RevokeAsync(string keyId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("FIDO2 credential revocation must be managed in the relying party.");
|
||||
=> throw new NotSupportedException(_t("crypto.fido2.revocation_relying_party"));
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -10,7 +11,7 @@ public sealed partial class FileKmsClient
|
||||
{
|
||||
if (!string.Equals(algorithm, KmsAlgorithms.Es256, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new NotSupportedException($"Algorithm '{algorithm}' is not supported by the file KMS driver.");
|
||||
throw new NotSupportedException(_t("crypto.kms.algorithm_unsupported", algorithm));
|
||||
}
|
||||
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -53,6 +54,6 @@ public sealed partial class FileKmsClient
|
||||
private static ECCurve ResolveCurve(string curveName) => curveName switch
|
||||
{
|
||||
"nistP256" or "P-256" or "ES256" => ECCurve.NamedCurves.nistP256,
|
||||
_ => throw new NotSupportedException($"Curve '{curveName}' is not supported."),
|
||||
_ => throw new NotSupportedException(_t("crypto.kms.curve_unsupported", curveName)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -32,11 +33,11 @@ public sealed partial class FileKmsClient
|
||||
try
|
||||
{
|
||||
var record = await LoadOrCreateMetadataAsync(keyId, cancellationToken, createIfMissing: true).ConfigureAwait(false)
|
||||
?? throw new InvalidOperationException("Failed to create or load key metadata.");
|
||||
?? throw new InvalidOperationException(_t("crypto.kms.metadata_failed"));
|
||||
|
||||
if (!string.Equals(record.Algorithm, material.Algorithm, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"Algorithm mismatch. Expected '{record.Algorithm}', received '{material.Algorithm}'.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.algorithm_mismatch", record.Algorithm, material.Algorithm));
|
||||
}
|
||||
|
||||
var versionId = string.IsNullOrWhiteSpace(material.VersionId)
|
||||
@@ -45,7 +46,7 @@ public sealed partial class FileKmsClient
|
||||
|
||||
if (record.Versions.Any(v => string.Equals(v.VersionId, versionId, StringComparison.Ordinal)))
|
||||
{
|
||||
throw new InvalidOperationException($"Key version '{versionId}' already exists for key '{record.KeyId}'.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.version_exists", versionId, record.KeyId));
|
||||
}
|
||||
|
||||
var curveName = string.IsNullOrWhiteSpace(material.Curve) ? "nistP256" : material.Curve;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -14,7 +15,7 @@ public sealed partial class FileKmsClient
|
||||
try
|
||||
{
|
||||
var record = await LoadOrCreateMetadataAsync(keyId, cancellationToken, createIfMissing: false).ConfigureAwait(false)
|
||||
?? throw new InvalidOperationException($"Key '{keyId}' does not exist.");
|
||||
?? throw new InvalidOperationException(_t("crypto.kms.key_not_found", keyId));
|
||||
return ToMetadata(record);
|
||||
}
|
||||
finally
|
||||
@@ -31,12 +32,12 @@ public sealed partial class FileKmsClient
|
||||
try
|
||||
{
|
||||
var record = await LoadOrCreateMetadataAsync(keyId, cancellationToken, createIfMissing: false).ConfigureAwait(false)
|
||||
?? throw new InvalidOperationException($"Key '{keyId}' does not exist.");
|
||||
?? throw new InvalidOperationException(_t("crypto.kms.key_not_found", keyId));
|
||||
|
||||
var version = ResolveVersion(record, keyVersion);
|
||||
if (string.IsNullOrWhiteSpace(version.PublicKey))
|
||||
{
|
||||
throw new InvalidOperationException($"Key '{keyId}' version '{version.VersionId}' does not have public key material.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.key_no_public_material", keyId, version.VersionId));
|
||||
}
|
||||
|
||||
var privateKey = await LoadPrivateKeyAsync(record, version, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -24,7 +25,7 @@ public sealed partial class FileKmsClient
|
||||
version = record.Versions.SingleOrDefault(v => string.Equals(v.VersionId, keyVersion, StringComparison.Ordinal));
|
||||
if (version is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Key version '{keyVersion}' does not exist for key '{record.KeyId}'.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.key_version_not_found", keyVersion, record.KeyId));
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(record.ActiveVersion))
|
||||
@@ -39,7 +40,7 @@ public sealed partial class FileKmsClient
|
||||
|
||||
if (version is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Key '{record.KeyId}' does not have an active version.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.key_no_active_version", record.KeyId));
|
||||
}
|
||||
|
||||
return version;
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -70,18 +71,18 @@ public sealed partial class FileKmsClient
|
||||
var keyPath = Path.Combine(GetKeyDirectory(record.KeyId), version.FileName);
|
||||
if (!File.Exists(keyPath))
|
||||
{
|
||||
throw new InvalidOperationException($"Key material for version '{version.VersionId}' was not found.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.material_not_found", version.VersionId));
|
||||
}
|
||||
|
||||
await using var stream = File.Open(keyPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var envelope = await JsonSerializer.DeserializeAsync<KeyEnvelope>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false)
|
||||
?? throw new InvalidOperationException("Key envelope could not be deserialized.");
|
||||
?? throw new InvalidOperationException(_t("crypto.kms.envelope_deserialize_failed"));
|
||||
|
||||
var payload = DecryptPrivateKey(envelope);
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<EcdsaPrivateKeyRecord>(payload, _jsonOptions)
|
||||
?? throw new InvalidOperationException("Key payload could not be deserialized.");
|
||||
?? throw new InvalidOperationException(_t("crypto.kms.payload_deserialize_failed"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -17,11 +18,11 @@ public sealed partial class FileKmsClient
|
||||
try
|
||||
{
|
||||
var record = await LoadOrCreateMetadataAsync(keyId, cancellationToken, createIfMissing: true).ConfigureAwait(false)
|
||||
?? throw new InvalidOperationException("Failed to create or load key metadata.");
|
||||
?? throw new InvalidOperationException(_t("crypto.kms.metadata_failed"));
|
||||
|
||||
if (record.State == KmsKeyState.Revoked)
|
||||
{
|
||||
throw new InvalidOperationException($"Key '{keyId}' has been revoked and cannot be rotated.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.key_revoked", keyId));
|
||||
}
|
||||
|
||||
var timestamp = _timeProvider.GetUtcNow();
|
||||
@@ -76,7 +77,7 @@ public sealed partial class FileKmsClient
|
||||
try
|
||||
{
|
||||
var record = await LoadOrCreateMetadataAsync(keyId, cancellationToken, createIfMissing: false).ConfigureAwait(false)
|
||||
?? throw new InvalidOperationException($"Key '{keyId}' does not exist.");
|
||||
?? throw new InvalidOperationException(_t("crypto.kms.key_not_found", keyId));
|
||||
|
||||
var timestamp = _timeProvider.GetUtcNow();
|
||||
record.State = KmsKeyState.Revoked;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -22,17 +23,17 @@ public sealed partial class FileKmsClient
|
||||
try
|
||||
{
|
||||
var record = await LoadOrCreateMetadataAsync(keyId, cancellationToken, createIfMissing: false).ConfigureAwait(false)
|
||||
?? throw new InvalidOperationException($"Key '{keyId}' does not exist.");
|
||||
?? throw new InvalidOperationException(_t("crypto.kms.key_not_found", keyId));
|
||||
|
||||
if (record.State == KmsKeyState.Revoked)
|
||||
{
|
||||
throw new InvalidOperationException($"Key '{keyId}' is revoked and cannot be used for signing.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.key_revoked_signing", keyId));
|
||||
}
|
||||
|
||||
var version = ResolveVersion(record, keyVersion);
|
||||
if (version.State != KmsKeyState.Active)
|
||||
{
|
||||
throw new InvalidOperationException($"Key version '{version.VersionId}' is not active. Current state: {version.State}");
|
||||
throw new InvalidOperationException(_t("crypto.kms.key_version_inactive", version.VersionId, version.State));
|
||||
}
|
||||
|
||||
var privateKey = await LoadPrivateKeyAsync(record, version, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -3,6 +3,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -30,7 +31,7 @@ public sealed partial class GcpKmsClient
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pem))
|
||||
{
|
||||
throw new InvalidOperationException("Public key PEM cannot be empty.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.pem_empty"));
|
||||
}
|
||||
|
||||
var builder = new StringBuilder(pem.Length);
|
||||
@@ -54,7 +55,7 @@ public sealed partial class GcpKmsClient
|
||||
var digest = new byte[32];
|
||||
if (!SHA256.TryHashData(data.Span, digest, out _))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to hash payload with SHA-256.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.hash_failed"));
|
||||
}
|
||||
|
||||
return digest;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -64,8 +65,8 @@ public sealed partial class GcpKmsClient
|
||||
KmsAlgorithms.Es256,
|
||||
ResolveCurve(publicMaterial.Algorithm),
|
||||
Array.Empty<byte>(),
|
||||
parameters.Q.X ?? throw new InvalidOperationException("Public key missing X coordinate."),
|
||||
parameters.Q.Y ?? throw new InvalidOperationException("Public key missing Y coordinate."),
|
||||
parameters.Q.X ?? throw new InvalidOperationException(_t("crypto.kms.public_key_missing_x")),
|
||||
parameters.Q.Y ?? throw new InvalidOperationException(_t("crypto.kms.public_key_missing_y")),
|
||||
snapshot.Metadata.CreateTime);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -25,6 +26,6 @@ public sealed partial class GcpKmsClient
|
||||
return firstActive.VersionName;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Crypto key '{keyId}' does not have an active primary version.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.no_primary_version", keyId));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -35,10 +36,10 @@ public sealed partial class GcpKmsClient : IKmsClient, IDisposable
|
||||
}
|
||||
|
||||
public Task<KmsKeyMetadata> RotateAsync(string keyId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("Google Cloud KMS rotation must be managed via Cloud KMS rotation schedules.");
|
||||
=> throw new NotSupportedException(_t("crypto.kms.rotation_via_policy", "Google Cloud KMS", "Cloud KMS rotation schedules"));
|
||||
|
||||
public Task RevokeAsync(string keyId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("Google Cloud KMS key revocation must be managed via Cloud KMS destroy/disable operations.");
|
||||
=> throw new NotSupportedException(_t("crypto.kms.revocation_via_policy", "Google Cloud KMS key", "Cloud KMS destroy/disable"));
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using StellaOps.Cryptography;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -29,10 +30,10 @@ public sealed partial class KmsCryptoProvider : ICryptoProvider
|
||||
}
|
||||
|
||||
public IPasswordHasher GetPasswordHasher(string algorithmId)
|
||||
=> throw new InvalidOperationException($"Provider '{Name}' does not support password hashing.");
|
||||
=> throw new InvalidOperationException(_t("crypto.provider.no_password_hashing", Name));
|
||||
|
||||
public ICryptoHasher GetHasher(string algorithmId)
|
||||
=> throw new InvalidOperationException($"Provider '{Name}' does not support content hashing.");
|
||||
=> throw new InvalidOperationException(_t("crypto.provider.no_content_hashing", Name));
|
||||
|
||||
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
|
||||
{
|
||||
@@ -40,12 +41,12 @@ public sealed partial class KmsCryptoProvider : ICryptoProvider
|
||||
|
||||
if (!Supports(CryptoCapability.Signing, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.algorithm_not_supported", algorithmId, Name));
|
||||
}
|
||||
|
||||
if (!_registrations.TryGetValue(keyReference.KeyId, out var registration))
|
||||
{
|
||||
throw new KeyNotFoundException($"Signing key '{keyReference.KeyId}' is not registered with provider '{Name}'.");
|
||||
throw new KeyNotFoundException(_t("crypto.provider.key_not_registered", keyReference.KeyId, Name));
|
||||
}
|
||||
|
||||
return new KmsSigner(_kmsClient, registration);
|
||||
@@ -57,14 +58,14 @@ public sealed partial class KmsCryptoProvider : ICryptoProvider
|
||||
|
||||
if (!string.Equals(signingKey.AlgorithmId, KmsAlgorithms.Es256, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"Provider '{Name}' only supports {KmsAlgorithms.Es256} signing keys.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.es256_only", Name));
|
||||
}
|
||||
|
||||
if (signingKey.Metadata is null ||
|
||||
!signingKey.Metadata.TryGetValue(KmsMetadataKeys.Version, out var versionId) ||
|
||||
string.IsNullOrWhiteSpace(versionId))
|
||||
{
|
||||
throw new InvalidOperationException("KMS signing keys must include metadata entry 'kms.version'.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.version_metadata_required"));
|
||||
}
|
||||
|
||||
KmsPublicKey? publicKey = null;
|
||||
|
||||
@@ -3,6 +3,7 @@ using StellaOps.Cryptography;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -42,7 +43,7 @@ internal sealed class KmsSigner : ICryptoSigner
|
||||
public JsonWebKey ExportPublicJsonWebKey()
|
||||
{
|
||||
var publicKey = _registration.PublicKey
|
||||
?? throw new InvalidOperationException("KMS signing key is missing public key material.");
|
||||
?? throw new InvalidOperationException(_t("crypto.kms.missing_public_key"));
|
||||
var jwk = new JsonWebKey
|
||||
{
|
||||
Kid = _registration.Reference.KeyId,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
internal sealed class MissingFido2Authenticator : IFido2Authenticator
|
||||
{
|
||||
public Task<byte[]> SignAsync(string credentialId, ReadOnlyMemory<byte> digest, CancellationToken cancellationToken = default)
|
||||
=> throw new InvalidOperationException("IFido2Authenticator must be registered to use FIDO2 KMS.");
|
||||
=> throw new InvalidOperationException(_t("crypto.fido2.authenticator_required"));
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Formats.Asn1;
|
||||
using System.Linq;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -90,7 +91,7 @@ internal sealed partial class Pkcs11InteropFacade
|
||||
"1.2.840.10045.3.1.7" => JsonWebKeyECTypes.P256,
|
||||
"1.3.132.0.34" => JsonWebKeyECTypes.P384,
|
||||
"1.3.132.0.35" => JsonWebKeyECTypes.P521,
|
||||
_ => throw new InvalidOperationException($"Unsupported EC curve OID '{oid}'."),
|
||||
_ => throw new InvalidOperationException(_t("crypto.pkcs11.curve_oid_unsupported", oid)),
|
||||
};
|
||||
|
||||
var coordinateSize = curve switch
|
||||
@@ -98,7 +99,7 @@ internal sealed partial class Pkcs11InteropFacade
|
||||
JsonWebKeyECTypes.P256 => 32,
|
||||
JsonWebKeyECTypes.P384 => 48,
|
||||
JsonWebKeyECTypes.P521 => 66,
|
||||
_ => throw new InvalidOperationException($"Unsupported EC curve '{curve}'."),
|
||||
_ => throw new InvalidOperationException(_t("crypto.pkcs11.curve_unsupported", curve)),
|
||||
};
|
||||
|
||||
return (curve, coordinateSize);
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
internal sealed partial class Pkcs11InteropFacade
|
||||
{
|
||||
#pragma warning disable CS1998
|
||||
private async Task<SessionContext> OpenSessionAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -30,7 +31,7 @@ internal sealed partial class Pkcs11InteropFacade : IPkcs11Facade
|
||||
_factories = new Pkcs11InteropFactories();
|
||||
_library = _factories.Pkcs11LibraryFactory.LoadPkcs11Library(_factories, _options.LibraryPath, AppType.MultiThreaded);
|
||||
_slot = ResolveSlot(_library, _options)
|
||||
?? throw new InvalidOperationException("Could not resolve PKCS#11 slot.");
|
||||
?? throw new InvalidOperationException(_t("crypto.pkcs11.slot_not_found"));
|
||||
}
|
||||
|
||||
public Pkcs11InteropFacade(IOptions<Pkcs11Options> options, TimeProvider timeProvider)
|
||||
@@ -45,7 +46,7 @@ internal sealed partial class Pkcs11InteropFacade : IPkcs11Facade
|
||||
var privateHandle = FindKey(session, CKO.CKO_PRIVATE_KEY, _options.PrivateKeyLabel);
|
||||
if (privateHandle is null)
|
||||
{
|
||||
throw new InvalidOperationException("PKCS#11 private key not found.");
|
||||
throw new InvalidOperationException(_t("crypto.pkcs11.private_key_not_found"));
|
||||
}
|
||||
|
||||
var labelAttr = GetAttribute(session, privateHandle, CKA.CKA_LABEL);
|
||||
@@ -64,20 +65,20 @@ internal sealed partial class Pkcs11InteropFacade : IPkcs11Facade
|
||||
var publicHandle = FindKey(session, CKO.CKO_PUBLIC_KEY, _options.PublicKeyLabel ?? _options.PrivateKeyLabel);
|
||||
if (publicHandle is null)
|
||||
{
|
||||
throw new InvalidOperationException("PKCS#11 public key not found.");
|
||||
throw new InvalidOperationException(_t("crypto.pkcs11.public_key_not_found"));
|
||||
}
|
||||
|
||||
var pointAttr = GetAttribute(session, publicHandle, CKA.CKA_EC_POINT)
|
||||
?? throw new InvalidOperationException("Public key missing EC point.");
|
||||
?? throw new InvalidOperationException(_t("crypto.pkcs11.missing_ec_point"));
|
||||
var paramsAttr = GetAttribute(session, publicHandle, CKA.CKA_EC_PARAMS)
|
||||
?? throw new InvalidOperationException("Public key missing EC parameters.");
|
||||
?? throw new InvalidOperationException(_t("crypto.pkcs11.missing_ec_params"));
|
||||
|
||||
var ecPoint = ExtractEcPoint(pointAttr.GetValueAsByteArray());
|
||||
var (curve, coordinateSize) = DecodeCurve(paramsAttr.GetValueAsByteArray());
|
||||
|
||||
if (ecPoint.Length != 1 + (coordinateSize * 2) || ecPoint[0] != 0x04)
|
||||
{
|
||||
throw new InvalidOperationException("Unsupported EC point format.");
|
||||
throw new InvalidOperationException(_t("crypto.pkcs11.unsupported_point_format"));
|
||||
}
|
||||
|
||||
var qx = ecPoint.AsSpan(1, coordinateSize).ToArray();
|
||||
@@ -98,7 +99,7 @@ internal sealed partial class Pkcs11InteropFacade : IPkcs11Facade
|
||||
using var context = await OpenSessionAsync(cancellationToken).ConfigureAwait(false);
|
||||
var session = context.Session;
|
||||
var privateHandle = FindKey(session, CKO.CKO_PRIVATE_KEY, _options.PrivateKeyLabel)
|
||||
?? throw new InvalidOperationException("PKCS#11 private key not found.");
|
||||
?? throw new InvalidOperationException(_t("crypto.pkcs11.private_key_not_found"));
|
||||
|
||||
var mechanism = _factories.MechanismFactory.Create(_options.MechanismId);
|
||||
return session.Sign(mechanism, privateHandle, digest.ToArray());
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -11,7 +12,7 @@ public sealed partial class Pkcs11KmsClient
|
||||
var digest = new byte[32];
|
||||
if (!SHA256.TryHashData(data.Span, digest, out _))
|
||||
{
|
||||
throw new InvalidOperationException("Failed to hash payload with SHA-256.");
|
||||
throw new InvalidOperationException(_t("crypto.kms.hash_failed"));
|
||||
}
|
||||
|
||||
return digest;
|
||||
@@ -23,7 +24,7 @@ public sealed partial class Pkcs11KmsClient
|
||||
JsonWebKeyECTypes.P256 => ECCurve.NamedCurves.nistP256,
|
||||
JsonWebKeyECTypes.P384 => ECCurve.NamedCurves.nistP384,
|
||||
JsonWebKeyECTypes.P521 => ECCurve.NamedCurves.nistP521,
|
||||
_ => throw new InvalidOperationException($"Unsupported EC curve '{curve}'."),
|
||||
_ => throw new InvalidOperationException(_t("crypto.pkcs11.curve_unsupported", curve)),
|
||||
};
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Kms;
|
||||
|
||||
public sealed partial class Pkcs11KmsClient
|
||||
{
|
||||
public Task<KmsKeyMetadata> RotateAsync(string keyId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("PKCS#11 rotation requires HSM administrative tooling.");
|
||||
=> throw new NotSupportedException(_t("crypto.pkcs11.rotation_hsm"));
|
||||
|
||||
public Task RevokeAsync(string keyId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("PKCS#11 revocation must be handled by HSM policies.");
|
||||
=> throw new NotSupportedException(_t("crypto.pkcs11.revocation_hsm"));
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Localization/StellaOps.Localization.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using StellaOps.Cryptography;
|
||||
using System;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.BouncyCastle;
|
||||
|
||||
@@ -10,7 +11,7 @@ public sealed partial class BouncyCastleEd25519CryptoProvider
|
||||
{
|
||||
if (!_supportedAlgorithms.Contains(algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider 'bouncycastle.ed25519'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.algorithm_not_supported", algorithmId, "bouncycastle.ed25519"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +27,7 @@ public sealed partial class BouncyCastleEd25519CryptoProvider
|
||||
{
|
||||
32 => span.ToArray(),
|
||||
64 => span[..32].ToArray(),
|
||||
_ => throw new InvalidOperationException("Ed25519 private key must be 32 or 64 bytes.")
|
||||
_ => throw new InvalidOperationException(_t("crypto.ed25519.private_key_size"))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,7 +41,7 @@ public sealed partial class BouncyCastleEd25519CryptoProvider
|
||||
|
||||
if (publicKey.Span.Length != 32)
|
||||
{
|
||||
throw new InvalidOperationException("Ed25519 public key must be 32 bytes.");
|
||||
throw new InvalidOperationException(_t("crypto.ed25519.public_key_size"));
|
||||
}
|
||||
|
||||
return publicKey.ToArray();
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.BouncyCastle;
|
||||
|
||||
@@ -38,10 +39,10 @@ public sealed partial class BouncyCastleEd25519CryptoProvider : ICryptoProvider
|
||||
}
|
||||
|
||||
public ICryptoHasher GetHasher(string algorithmId)
|
||||
=> throw new NotSupportedException("BouncyCastle Ed25519 provider does not expose hashing capabilities.");
|
||||
=> throw new NotSupportedException(_t("crypto.ed25519.no_hashing"));
|
||||
|
||||
public IPasswordHasher GetPasswordHasher(string algorithmId)
|
||||
=> throw new NotSupportedException("BouncyCastle provider does not expose password hashing capabilities.");
|
||||
=> throw new NotSupportedException(_t("crypto.provider.no_password_hashing", Name));
|
||||
|
||||
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
|
||||
{
|
||||
@@ -50,7 +51,7 @@ public sealed partial class BouncyCastleEd25519CryptoProvider : ICryptoProvider
|
||||
|
||||
if (!_signingKeys.TryGetValue(keyReference.KeyId, out var entry))
|
||||
{
|
||||
throw new KeyNotFoundException($"Signing key '{keyReference.KeyId}' is not registered with provider '{Name}'.");
|
||||
throw new KeyNotFoundException(_t("crypto.provider.key_not_registered", keyReference.KeyId, Name));
|
||||
}
|
||||
|
||||
EnsureAlgorithmSupported(algorithmId);
|
||||
@@ -58,7 +59,7 @@ public sealed partial class BouncyCastleEd25519CryptoProvider : ICryptoProvider
|
||||
if (!string.Equals(entry.Descriptor.AlgorithmId, normalized, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Signing key '{keyReference.KeyId}' is registered for algorithm '{entry.Descriptor.AlgorithmId}', not '{algorithmId}'.");
|
||||
_t("crypto.provider.key_algorithm_mismatch", keyReference.KeyId, entry.Descriptor.AlgorithmId, algorithmId));
|
||||
}
|
||||
|
||||
return new Ed25519SignerWrapper(entry);
|
||||
@@ -71,7 +72,7 @@ public sealed partial class BouncyCastleEd25519CryptoProvider : ICryptoProvider
|
||||
|
||||
if (signingKey.Kind != CryptoSigningKeyKind.Raw)
|
||||
{
|
||||
throw new InvalidOperationException($"Provider '{Name}' requires raw Ed25519 private key material.");
|
||||
throw new InvalidOperationException(_t("crypto.ed25519.raw_key_required", Name));
|
||||
}
|
||||
|
||||
var privateKey = NormalizePrivateKey(signingKey.PrivateKey);
|
||||
|
||||
@@ -13,5 +13,6 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Localization\StellaOps.Localization.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -15,6 +14,7 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.SmSoft;
|
||||
|
||||
@@ -61,14 +61,14 @@ public sealed class SmSoftCryptoProvider : ICryptoProvider, ICryptoProviderDiagn
|
||||
}
|
||||
|
||||
public IPasswordHasher GetPasswordHasher(string algorithmId)
|
||||
=> throw new NotSupportedException("SM provider does not expose password hashing.");
|
||||
=> throw new NotSupportedException(_t("crypto.sm.no_password_hashing"));
|
||||
|
||||
public ICryptoHasher GetHasher(string algorithmId)
|
||||
{
|
||||
EnsureAllowed();
|
||||
if (!string.Equals(algorithmId, HashAlgorithms.Sm3, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"Hash algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.hash_not_supported", algorithmId, Name));
|
||||
}
|
||||
|
||||
return new Sm3CryptoHasher();
|
||||
@@ -81,12 +81,12 @@ public sealed class SmSoftCryptoProvider : ICryptoProvider, ICryptoProviderDiagn
|
||||
|
||||
if (!string.Equals(algorithmId, SignatureAlgorithms.Sm2, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.algorithm_not_supported", algorithmId, Name));
|
||||
}
|
||||
|
||||
if (!keys.TryGetValue(keyReference.KeyId, out var entry))
|
||||
{
|
||||
throw new KeyNotFoundException($"Signing key '{keyReference.KeyId}' is not registered with provider '{Name}'.");
|
||||
throw new KeyNotFoundException(_t("crypto.provider.key_not_registered", keyReference.KeyId, Name));
|
||||
}
|
||||
|
||||
return new Sm2SoftSigner(entry);
|
||||
@@ -99,13 +99,13 @@ public sealed class SmSoftCryptoProvider : ICryptoProvider, ICryptoProviderDiagn
|
||||
|
||||
if (!string.Equals(signingKey.AlgorithmId, SignatureAlgorithms.Sm2, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing algorithm '{signingKey.AlgorithmId}' is not supported by provider '{Name}'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.algorithm_not_supported", signingKey.AlgorithmId, Name));
|
||||
}
|
||||
|
||||
// Accept raw key bytes (PKCS#8 DER) or ECParameters are not SM2-compatible in BCL.
|
||||
if (signingKey.PrivateKey.IsEmpty)
|
||||
{
|
||||
throw new InvalidOperationException("SM2 provider requires raw private key bytes (PKCS#8 DER).");
|
||||
throw new InvalidOperationException(_t("crypto.sm.raw_key_required"));
|
||||
}
|
||||
|
||||
var keyPair = LoadKeyPair(signingKey.PrivateKey.ToArray());
|
||||
@@ -158,8 +158,7 @@ public sealed class SmSoftCryptoProvider : ICryptoProvider, ICryptoProviderDiagn
|
||||
{
|
||||
if (!GateEnabled())
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Provider '{Name}' is disabled. Set {EnvGate}=1 (or disable RequireEnvironmentGate) to enable software SM2/SM3.");
|
||||
throw new InvalidOperationException(_t("crypto.sm.disabled", Name, EnvGate));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +214,7 @@ public sealed class SmSoftCryptoProvider : ICryptoProvider, ICryptoProviderDiagn
|
||||
return new AsymmetricCipherKeyPair(pub, ecPriv);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Unsupported SM2 key format. Expect PEM or PKCS#8 DER.");
|
||||
throw new InvalidOperationException(_t("crypto.sm.unsupported_format"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Localization\StellaOps.Localization.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
@@ -21,7 +22,7 @@ public sealed partial class Argon2idPasswordHasher : IPasswordHasher
|
||||
|
||||
if (options.Algorithm != PasswordHashAlgorithm.Argon2id)
|
||||
{
|
||||
throw new InvalidOperationException("Argon2idPasswordHasher only supports the Argon2id algorithm.");
|
||||
throw new InvalidOperationException(_t("crypto.password.algorithm_mismatch", "Argon2idPasswordHasher", "Argon2id"));
|
||||
}
|
||||
|
||||
Span<byte> salt = stackalloc byte[SaltLengthBytes];
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
@@ -32,12 +33,12 @@ public class EcdsaPolicyCryptoProvider : ICryptoProvider, ICryptoProviderDiagnos
|
||||
|
||||
if (this.signingAlgorithms.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("At least one signing algorithm must be supplied.", nameof(signingAlgorithms));
|
||||
throw new ArgumentException(_t("crypto.compliance.at_least_one_signing"), nameof(signingAlgorithms));
|
||||
}
|
||||
|
||||
if (this.hashAlgorithms.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("At least one hash algorithm must be supplied.", nameof(hashAlgorithms));
|
||||
throw new ArgumentException(_t("crypto.compliance.at_least_one_hash"), nameof(hashAlgorithms));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +60,7 @@ public class EcdsaPolicyCryptoProvider : ICryptoProvider, ICryptoProviderDiagnos
|
||||
}
|
||||
|
||||
public IPasswordHasher GetPasswordHasher(string algorithmId)
|
||||
=> throw new NotSupportedException($"Provider '{Name}' does not expose password hashing.");
|
||||
=> throw new NotSupportedException(_t("crypto.provider.no_password_hashing", Name));
|
||||
|
||||
public ICryptoHasher GetHasher(string algorithmId)
|
||||
{
|
||||
@@ -74,12 +75,12 @@ public class EcdsaPolicyCryptoProvider : ICryptoProvider, ICryptoProviderDiagnos
|
||||
|
||||
if (!signingKeys.TryGetValue(keyReference.KeyId, out var signingKey))
|
||||
{
|
||||
throw new KeyNotFoundException($"Signing key '{keyReference.KeyId}' is not registered with provider '{Name}'.");
|
||||
throw new KeyNotFoundException(_t("crypto.provider.key_not_registered", keyReference.KeyId, Name));
|
||||
}
|
||||
|
||||
if (!string.Equals(signingKey.AlgorithmId, NormalizeAlg(algorithmId), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing key '{keyReference.KeyId}' is registered for algorithm '{signingKey.AlgorithmId}', not '{algorithmId}'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.key_algorithm_mismatch", keyReference.KeyId, signingKey.AlgorithmId, algorithmId));
|
||||
}
|
||||
|
||||
return EcdsaSigner.Create(signingKey);
|
||||
@@ -89,7 +90,7 @@ public class EcdsaPolicyCryptoProvider : ICryptoProvider, ICryptoProviderDiagnos
|
||||
{
|
||||
if (!Supports(CryptoCapability.Verification, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Verification algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.verify_not_supported", algorithmId, Name));
|
||||
}
|
||||
|
||||
return EcdsaSigner.CreateVerifierFromPublicKey(algorithmId, publicKeyBytes);
|
||||
@@ -102,7 +103,7 @@ public class EcdsaPolicyCryptoProvider : ICryptoProvider, ICryptoProviderDiagnos
|
||||
|
||||
if (signingKey.Kind != CryptoSigningKeyKind.Ec)
|
||||
{
|
||||
throw new InvalidOperationException($"Provider '{Name}' only accepts EC signing keys.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.ec_keys_only", Name));
|
||||
}
|
||||
|
||||
ValidateCurve(signingKey.AlgorithmId, signingKey.PrivateParameters);
|
||||
@@ -155,7 +156,7 @@ public class EcdsaPolicyCryptoProvider : ICryptoProvider, ICryptoProviderDiagnos
|
||||
{
|
||||
if (!Supports(CryptoCapability.Signing, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.algorithm_not_supported", algorithmId, Name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +164,7 @@ public class EcdsaPolicyCryptoProvider : ICryptoProvider, ICryptoProviderDiagnos
|
||||
{
|
||||
if (!Supports(CryptoCapability.ContentHashing, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Hash algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.hash_not_supported", algorithmId, Name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +187,7 @@ public class EcdsaPolicyCryptoProvider : ICryptoProvider, ICryptoProviderDiagnos
|
||||
|
||||
if (!matches)
|
||||
{
|
||||
throw new InvalidOperationException($"Signing key curve mismatch. Expected curve '{expectedCurve}' for algorithm '{algorithmId}'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.curve_mismatch", expectedCurve, algorithmId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +197,7 @@ public class EcdsaPolicyCryptoProvider : ICryptoProvider, ICryptoProviderDiagnos
|
||||
SignatureAlgorithms.Es256 => JsonWebKeyECTypes.P256,
|
||||
SignatureAlgorithms.Es384 => JsonWebKeyECTypes.P384,
|
||||
SignatureAlgorithms.Es512 => JsonWebKeyECTypes.P521,
|
||||
_ => throw new InvalidOperationException($"Unsupported ECDSA curve mapping for algorithm '{algorithmId}'.")
|
||||
_ => throw new InvalidOperationException(_t("crypto.ecdsa.curve_unsupported", algorithmId))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -251,13 +252,13 @@ public sealed class KcmvpHashOnlyProvider : ICryptoProvider
|
||||
}
|
||||
|
||||
public IPasswordHasher GetPasswordHasher(string algorithmId)
|
||||
=> throw new NotSupportedException("KCMVP hash provider does not expose password hashing.");
|
||||
=> throw new NotSupportedException(_t("crypto.provider.no_password_hashing", Name));
|
||||
|
||||
public ICryptoHasher GetHasher(string algorithmId)
|
||||
{
|
||||
if (!Supports(CryptoCapability.ContentHashing, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Hash algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.hash_not_supported", algorithmId, Name));
|
||||
}
|
||||
|
||||
return new DefaultCryptoHasher(HashAlgorithms.Sha256);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
/// <summary>
|
||||
@@ -57,7 +59,7 @@ public sealed class ComplianceProfile
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unknown hash purpose '{purpose}' in profile '{ProfileId}'.", nameof(purpose));
|
||||
throw new ArgumentException(_t("crypto.profile.unknown_purpose", purpose, ProfileId), nameof(purpose));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
@@ -54,7 +55,7 @@ public interface ICryptoProvider
|
||||
/// <param name="publicKeyBytes">Public key in SubjectPublicKeyInfo format (DER-encoded).</param>
|
||||
/// <returns>Ephemeral signer instance (supports VerifyAsync only).</returns>
|
||||
ICryptoSigner CreateEphemeralVerifier(string algorithmId, ReadOnlySpan<byte> publicKeyBytes)
|
||||
=> throw new NotSupportedException($"Provider '{Name}' does not support ephemeral verification.");
|
||||
=> throw new NotSupportedException(_t("crypto.provider.no_ephemeral_verification", Name));
|
||||
|
||||
/// <summary>
|
||||
/// Adds or replaces signing key material managed by this provider.
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
@@ -29,7 +30,7 @@ public sealed class CryptoProviderRegistry : ICryptoProviderRegistry
|
||||
var providerList = providers.ToList();
|
||||
if (providerList.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("At least one crypto provider must be registered.", nameof(providers));
|
||||
throw new ArgumentException(_t("crypto.registry.empty"), nameof(providers));
|
||||
}
|
||||
|
||||
providersByName = providerList.ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase);
|
||||
@@ -66,7 +67,7 @@ public sealed class CryptoProviderRegistry : ICryptoProviderRegistry
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
throw new ArgumentException("Algorithm identifier is required.", nameof(algorithmId));
|
||||
throw new ArgumentException(_t("crypto.registry.algorithm_required"), nameof(algorithmId));
|
||||
}
|
||||
|
||||
foreach (var provider in EnumerateCandidates())
|
||||
@@ -79,8 +80,14 @@ public sealed class CryptoProviderRegistry : ICryptoProviderRegistry
|
||||
}
|
||||
|
||||
CryptoProviderMetrics.RecordProviderResolutionFailure(capability, algorithmId);
|
||||
throw new InvalidOperationException(
|
||||
$"No crypto provider is registered for capability '{capability}' and algorithm '{algorithmId}'.");
|
||||
var notSupportedMessage = capability switch
|
||||
{
|
||||
CryptoCapability.Signing => _t("crypto.registry.signing_not_supported", algorithmId),
|
||||
CryptoCapability.ContentHashing => _t("crypto.registry.hash_not_supported", algorithmId),
|
||||
CryptoCapability.Verification => _t("crypto.registry.verify_not_supported", algorithmId),
|
||||
_ => _t("crypto.registry.signing_not_supported", algorithmId)
|
||||
};
|
||||
throw new InvalidOperationException(notSupportedMessage);
|
||||
}
|
||||
|
||||
public CryptoSignerResolution ResolveSigner(
|
||||
@@ -113,7 +120,7 @@ public sealed class CryptoProviderRegistry : ICryptoProviderRegistry
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
throw new ArgumentException("Algorithm identifier is required.", nameof(algorithmId));
|
||||
throw new ArgumentException(_t("crypto.registry.algorithm_required"), nameof(algorithmId));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(preferredProvider) &&
|
||||
@@ -122,7 +129,7 @@ public sealed class CryptoProviderRegistry : ICryptoProviderRegistry
|
||||
if (!hinted.Supports(CryptoCapability.ContentHashing, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Provider '{preferredProvider}' does not support content hashing with algorithm '{algorithmId}'.");
|
||||
_t("crypto.provider.no_content_hashing", preferredProvider));
|
||||
}
|
||||
|
||||
var hasher = hinted.GetHasher(algorithmId);
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
@@ -39,12 +40,12 @@ public sealed class CryptoSigningKey
|
||||
|
||||
if (string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
throw new ArgumentException("Algorithm identifier is required.", nameof(algorithmId));
|
||||
throw new ArgumentException(_t("crypto.key.algorithm_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));
|
||||
throw new ArgumentException(_t("crypto.key.private_scalar_required"), nameof(privateParameters));
|
||||
}
|
||||
|
||||
AlgorithmId = algorithmId;
|
||||
@@ -79,19 +80,19 @@ public sealed class CryptoSigningKey
|
||||
{
|
||||
if (!verificationOnly)
|
||||
{
|
||||
throw new ArgumentException("This constructor is only for verification-only keys. Set verificationOnly to true.", nameof(verificationOnly));
|
||||
throw new ArgumentException(_t("crypto.key.verification_only"), nameof(verificationOnly));
|
||||
}
|
||||
|
||||
Reference = reference ?? throw new ArgumentNullException(nameof(reference));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
throw new ArgumentException("Algorithm identifier is required.", nameof(algorithmId));
|
||||
throw new ArgumentException(_t("crypto.key.algorithm_required"), nameof(algorithmId));
|
||||
}
|
||||
|
||||
if (publicParameters.Q.X is null || publicParameters.Q.Y is null)
|
||||
{
|
||||
throw new ArgumentException("Public key parameters must include X and Y coordinates.", nameof(publicParameters));
|
||||
throw new ArgumentException(_t("crypto.key.public_xy_required"), nameof(publicParameters));
|
||||
}
|
||||
|
||||
AlgorithmId = algorithmId;
|
||||
@@ -125,12 +126,12 @@ public sealed class CryptoSigningKey
|
||||
|
||||
if (string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
throw new ArgumentException("Algorithm identifier is required.", nameof(algorithmId));
|
||||
throw new ArgumentException(_t("crypto.key.algorithm_required"), nameof(algorithmId));
|
||||
}
|
||||
|
||||
if (privateKey.IsEmpty)
|
||||
{
|
||||
throw new ArgumentException("Private key material must be provided.", nameof(privateKey));
|
||||
throw new ArgumentException(_t("crypto.key.private_material_required"), nameof(privateKey));
|
||||
}
|
||||
|
||||
AlgorithmId = algorithmId;
|
||||
|
||||
@@ -11,6 +11,7 @@ using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
@@ -63,7 +64,7 @@ public sealed class DefaultCryptoHash : ICryptoHash
|
||||
"GOST3411-2012-512" => GostDigestUtilities.ComputeDigest(data, use256: false),
|
||||
"BLAKE3-256" => ComputeBlake3(data),
|
||||
"SM3" => ComputeSm3(data),
|
||||
_ => throw new InvalidOperationException($"Unsupported hash algorithm '{algorithm}'.")
|
||||
_ => throw new InvalidOperationException(_t("crypto.hash.algorithm_unsupported", algorithm))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -93,7 +94,7 @@ public sealed class DefaultCryptoHash : ICryptoHash
|
||||
"GOST3411-2012-512" => await ComputeGostStreamAsync(use256: false, stream, cancellationToken).ConfigureAwait(false),
|
||||
"BLAKE3-256" => await ComputeBlake3StreamAsync(stream, cancellationToken).ConfigureAwait(false),
|
||||
"SM3" => await ComputeSm3StreamAsync(stream, cancellationToken).ConfigureAwait(false),
|
||||
_ => throw new InvalidOperationException($"Unsupported hash algorithm '{algorithm}'.")
|
||||
_ => throw new InvalidOperationException(_t("crypto.hash.algorithm_unsupported", algorithm))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -215,7 +216,7 @@ public sealed class DefaultCryptoHash : ICryptoHash
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(purpose))
|
||||
{
|
||||
throw new ArgumentException("Purpose cannot be null or empty.", nameof(purpose));
|
||||
throw new ArgumentException(_t("crypto.hash.purpose_required"), nameof(purpose));
|
||||
}
|
||||
|
||||
var opts = _complianceOptions.CurrentValue;
|
||||
@@ -237,7 +238,7 @@ public sealed class DefaultCryptoHash : ICryptoHash
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(purpose))
|
||||
{
|
||||
throw new ArgumentException("Purpose cannot be null or empty.", nameof(purpose));
|
||||
throw new ArgumentException(_t("crypto.hash.purpose_required"), nameof(purpose));
|
||||
}
|
||||
|
||||
var profile = GetActiveProfile();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
@@ -12,7 +13,7 @@ public sealed class DefaultCryptoHasher : ICryptoHasher
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(algorithmId))
|
||||
{
|
||||
throw new ArgumentException("Algorithm identifier is required.", nameof(algorithmId));
|
||||
throw new ArgumentException(_t("crypto.registry.algorithm_required"), nameof(algorithmId));
|
||||
}
|
||||
|
||||
AlgorithmId = algorithmId.ToUpperInvariant();
|
||||
@@ -27,7 +28,7 @@ public sealed class DefaultCryptoHasher : ICryptoHasher
|
||||
HashAlgorithms.Sha256 => SHA256.HashData(data),
|
||||
HashAlgorithms.Sha384 => SHA384.HashData(data),
|
||||
HashAlgorithms.Sha512 => SHA512.HashData(data),
|
||||
_ => throw new InvalidOperationException($"Unsupported hash algorithm '{AlgorithmId}'.")
|
||||
_ => throw new InvalidOperationException(_t("crypto.hash.algorithm_unsupported", AlgorithmId))
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
@@ -136,7 +137,7 @@ public sealed class DefaultCryptoHmac : ICryptoHmac
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(purpose))
|
||||
{
|
||||
throw new ArgumentException("Purpose cannot be null or empty.", nameof(purpose));
|
||||
throw new ArgumentException(_t("crypto.hash.purpose_required"), nameof(purpose));
|
||||
}
|
||||
|
||||
var profile = GetActiveProfile();
|
||||
@@ -153,7 +154,7 @@ public sealed class DefaultCryptoHmac : ICryptoHmac
|
||||
"HMAC-SHA512" => 64,
|
||||
"HMAC-GOST3411" => 32, // GOST R 34.11-2012 Stribog-256
|
||||
"HMAC-SM3" => 32,
|
||||
_ => throw new InvalidOperationException($"Unknown HMAC algorithm '{algorithm}'.")
|
||||
_ => throw new InvalidOperationException(_t("crypto.hmac.algorithm_unknown", algorithm))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -170,7 +171,7 @@ public sealed class DefaultCryptoHmac : ICryptoHmac
|
||||
"HMAC-SHA512" => ComputeHmacSha512(key, data),
|
||||
"HMAC-GOST3411" => ComputeHmacGost3411(key, data),
|
||||
"HMAC-SM3" => ComputeHmacSm3(key, data),
|
||||
_ => throw new InvalidOperationException($"Unsupported HMAC algorithm '{algorithm}'.")
|
||||
_ => throw new InvalidOperationException(_t("crypto.hmac.algorithm_unsupported", algorithm))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -183,7 +184,7 @@ public sealed class DefaultCryptoHmac : ICryptoHmac
|
||||
"HMAC-SHA512" => await ComputeHmacShaStreamAsync(HashAlgorithmName.SHA512, key, stream, cancellationToken).ConfigureAwait(false),
|
||||
"HMAC-GOST3411" => await ComputeHmacGost3411StreamAsync(key, stream, cancellationToken).ConfigureAwait(false),
|
||||
"HMAC-SM3" => await ComputeHmacSm3StreamAsync(key, stream, cancellationToken).ConfigureAwait(false),
|
||||
_ => throw new InvalidOperationException($"Unsupported HMAC algorithm '{algorithm}'.")
|
||||
_ => throw new InvalidOperationException(_t("crypto.hmac.algorithm_unsupported", algorithm))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -237,7 +238,7 @@ public sealed class DefaultCryptoHmac : ICryptoHmac
|
||||
"SHA256" => (HMAC)new HMACSHA256(key.ToArray()),
|
||||
"SHA384" => new HMACSHA384(key.ToArray()),
|
||||
"SHA512" => new HMACSHA512(key.ToArray()),
|
||||
_ => throw new InvalidOperationException($"Unsupported hash algorithm '{name}'.")
|
||||
_ => throw new InvalidOperationException(_t("crypto.hash.algorithm_unsupported", name))
|
||||
};
|
||||
|
||||
return await hmac.ComputeHashAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
@@ -62,7 +63,7 @@ public sealed class DefaultCryptoProvider : ICryptoProvider, ICryptoProviderDiag
|
||||
{
|
||||
if (!Supports(CryptoCapability.PasswordHashing, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Password hashing algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.algorithm_not_supported", algorithmId, Name));
|
||||
}
|
||||
|
||||
return passwordHashers[algorithmId];
|
||||
@@ -72,7 +73,7 @@ public sealed class DefaultCryptoProvider : ICryptoProvider, ICryptoProviderDiag
|
||||
{
|
||||
if (!Supports(CryptoCapability.ContentHashing, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Hash algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.hash_not_supported", algorithmId, Name));
|
||||
}
|
||||
|
||||
return new DefaultCryptoHasher(algorithmId);
|
||||
@@ -84,18 +85,18 @@ public sealed class DefaultCryptoProvider : ICryptoProvider, ICryptoProviderDiag
|
||||
|
||||
if (!Supports(CryptoCapability.Signing, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.algorithm_not_supported", algorithmId, Name));
|
||||
}
|
||||
|
||||
if (!signingKeys.TryGetValue(keyReference.KeyId, out var signingKey))
|
||||
{
|
||||
throw new KeyNotFoundException($"Signing key '{keyReference.KeyId}' is not registered with provider '{Name}'.");
|
||||
throw new KeyNotFoundException(_t("crypto.provider.key_not_registered", keyReference.KeyId, Name));
|
||||
}
|
||||
|
||||
if (!string.Equals(signingKey.AlgorithmId, algorithmId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Signing key '{keyReference.KeyId}' is registered for algorithm '{signingKey.AlgorithmId}', not '{algorithmId}'.");
|
||||
_t("crypto.provider.key_algorithm_mismatch", keyReference.KeyId, signingKey.AlgorithmId, algorithmId));
|
||||
}
|
||||
|
||||
return EcdsaSigner.Create(signingKey);
|
||||
@@ -105,7 +106,7 @@ public sealed class DefaultCryptoProvider : ICryptoProvider, ICryptoProviderDiag
|
||||
{
|
||||
if (!Supports(CryptoCapability.Verification, algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Verification algorithm '{algorithmId}' is not supported by provider '{Name}'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.verify_not_supported", algorithmId, Name));
|
||||
}
|
||||
|
||||
return EcdsaSigner.CreateVerifierFromPublicKey(algorithmId, publicKeyBytes);
|
||||
@@ -117,7 +118,7 @@ public sealed class DefaultCryptoProvider : ICryptoProvider, ICryptoProviderDiag
|
||||
EnsureSigningSupported(signingKey.AlgorithmId);
|
||||
if (signingKey.Kind != CryptoSigningKeyKind.Ec)
|
||||
{
|
||||
throw new InvalidOperationException($"Provider '{Name}' only accepts EC signing keys.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.ec_keys_only", Name));
|
||||
}
|
||||
ValidateSigningKey(signingKey);
|
||||
|
||||
@@ -171,7 +172,7 @@ public sealed class DefaultCryptoProvider : ICryptoProvider, ICryptoProviderDiag
|
||||
{
|
||||
if (!SupportedSigningAlgorithms.Contains(algorithmId))
|
||||
{
|
||||
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider 'default'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.algorithm_not_supported", algorithmId, "default"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,14 +180,14 @@ public sealed class DefaultCryptoProvider : ICryptoProvider, ICryptoProviderDiag
|
||||
{
|
||||
if (!string.Equals(signingKey.AlgorithmId, SignatureAlgorithms.Es256, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"Only ES256 signing keys are currently supported by provider 'default'.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.es256_only", "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.");
|
||||
throw new InvalidOperationException(_t("crypto.provider.p256_required"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography.Digests;
|
||||
|
||||
/// <summary>
|
||||
@@ -20,7 +22,7 @@ public static class Sha256Digest
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(digest))
|
||||
{
|
||||
throw new ArgumentException("Digest is required.", parameterName ?? nameof(digest));
|
||||
throw new ArgumentException(_t("crypto.digest.required"), parameterName ?? nameof(digest));
|
||||
}
|
||||
|
||||
var trimmed = digest.Trim();
|
||||
@@ -33,11 +35,11 @@ public static class Sha256Digest
|
||||
else if (requirePrefix)
|
||||
{
|
||||
var name = string.IsNullOrWhiteSpace(parameterName) ? "Digest" : parameterName;
|
||||
throw new FormatException($"{name} must start with '{Prefix}'.");
|
||||
throw new FormatException(_t("crypto.digest.prefix_required", name, Prefix));
|
||||
}
|
||||
else if (trimmed.Contains(':', StringComparison.Ordinal))
|
||||
{
|
||||
throw new FormatException($"Unsupported digest algorithm in '{digest}'. Only sha256 is supported.");
|
||||
throw new FormatException(_t("crypto.digest.algorithm_unsupported", digest));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -48,7 +50,7 @@ public static class Sha256Digest
|
||||
if (hex.Length != HexLength || !IsHex(hex.AsSpan()))
|
||||
{
|
||||
var name = string.IsNullOrWhiteSpace(parameterName) ? "Digest" : parameterName;
|
||||
throw new FormatException($"{name} must contain {HexLength} hexadecimal characters.");
|
||||
throw new FormatException(_t("crypto.digest.hex_length", name, HexLength));
|
||||
}
|
||||
|
||||
return Prefix + hex.ToLowerInvariant();
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
@@ -87,7 +88,7 @@ public sealed class EcdsaSigner : ICryptoSigner
|
||||
{ } alg when string.Equals(alg, SignatureAlgorithms.Es256, StringComparison.OrdinalIgnoreCase) => HashAlgorithmName.SHA256,
|
||||
{ } alg when string.Equals(alg, SignatureAlgorithms.Es384, StringComparison.OrdinalIgnoreCase) => HashAlgorithmName.SHA384,
|
||||
{ } alg when string.Equals(alg, SignatureAlgorithms.Es512, StringComparison.OrdinalIgnoreCase) => HashAlgorithmName.SHA512,
|
||||
_ => throw new InvalidOperationException($"Unsupported ECDSA signing algorithm '{algorithmId}'.")
|
||||
_ => throw new InvalidOperationException(_t("crypto.ecdsa.algorithm_unsupported", algorithmId))
|
||||
};
|
||||
|
||||
private static string ResolveCurve(string algorithmId)
|
||||
@@ -96,6 +97,6 @@ public sealed class EcdsaSigner : ICryptoSigner
|
||||
{ } alg when string.Equals(alg, SignatureAlgorithms.Es256, StringComparison.OrdinalIgnoreCase) => JsonWebKeyECTypes.P256,
|
||||
{ } alg when string.Equals(alg, SignatureAlgorithms.Es384, StringComparison.OrdinalIgnoreCase) => JsonWebKeyECTypes.P384,
|
||||
{ } alg when string.Equals(alg, SignatureAlgorithms.Es512, StringComparison.OrdinalIgnoreCase) => JsonWebKeyECTypes.P521,
|
||||
_ => throw new InvalidOperationException($"Unsupported ECDSA curve mapping for algorithm '{algorithmId}'.")
|
||||
_ => throw new InvalidOperationException(_t("crypto.ecdsa.curve_unsupported", algorithmId))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Org.BouncyCastle.Asn1;
|
||||
using Org.BouncyCastle.Math;
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
@@ -41,13 +42,13 @@ public static class GostSignatureEncoding
|
||||
{
|
||||
if (!IsDer(der))
|
||||
{
|
||||
throw new CryptographicException("Signature is not DER encoded.");
|
||||
throw new CryptographicException(_t("crypto.gost.not_der"));
|
||||
}
|
||||
|
||||
var sequence = Asn1Sequence.GetInstance(Asn1Object.FromByteArray(der.ToArray()));
|
||||
if (sequence.Count != 2)
|
||||
{
|
||||
throw new CryptographicException("Invalid DER structure for GOST signature.");
|
||||
throw new CryptographicException(_t("crypto.gost.invalid_der"));
|
||||
}
|
||||
|
||||
var r = NormalizeCoordinate(((DerInteger)sequence[0]).PositiveValue.ToByteArrayUnsigned(), coordinateLength);
|
||||
@@ -63,7 +64,7 @@ public static class GostSignatureEncoding
|
||||
{
|
||||
if (raw.Length != coordinateLength * 2)
|
||||
{
|
||||
throw new CryptographicException($"Raw GOST signature must be {coordinateLength * 2} bytes.");
|
||||
throw new CryptographicException(_t("crypto.gost.raw_length", coordinateLength * 2));
|
||||
}
|
||||
|
||||
var s = raw[..coordinateLength].ToArray();
|
||||
@@ -83,7 +84,7 @@ public static class GostSignatureEncoding
|
||||
var sequence = Asn1Sequence.GetInstance(Asn1Object.FromByteArray(signature.ToArray()));
|
||||
if (sequence.Count != 2)
|
||||
{
|
||||
throw new CryptographicException("Invalid DER structure for GOST signature.");
|
||||
throw new CryptographicException(_t("crypto.gost.invalid_der"));
|
||||
}
|
||||
|
||||
return (((DerInteger)sequence[0]).PositiveValue, ((DerInteger)sequence[1]).PositiveValue);
|
||||
@@ -98,7 +99,7 @@ public static class GostSignatureEncoding
|
||||
return (new BigInteger(1, r), new BigInteger(1, s));
|
||||
}
|
||||
|
||||
throw new CryptographicException("Signature payload is neither DER nor raw GOST format.");
|
||||
throw new CryptographicException(_t("crypto.gost.neither_format"));
|
||||
}
|
||||
|
||||
private static byte[] NormalizeCoordinate(ReadOnlySpan<byte> value, int coordinateLength)
|
||||
@@ -106,7 +107,7 @@ public static class GostSignatureEncoding
|
||||
var trimmed = TrimLeadingZeros(value);
|
||||
if (trimmed.Length > coordinateLength)
|
||||
{
|
||||
throw new CryptographicException("Coordinate exceeds expected length.");
|
||||
throw new CryptographicException(_t("crypto.gost.coordinate_overflow"));
|
||||
}
|
||||
|
||||
var output = new byte[coordinateLength];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user