search and ai stabilization work, localization stablized.

This commit is contained in:
master
2026-02-24 23:29:36 +02:00
parent 4f947a8b61
commit b07d27772e
766 changed files with 55299 additions and 3221 deletions

View File

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

View File

@@ -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);

View File

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

View File

@@ -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>

View File

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

View File

@@ -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();

View File

@@ -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));

View File

@@ -16,5 +16,6 @@
<ItemGroup>
<ProjectReference Include="..\\StellaOps.Cryptography\\StellaOps.Cryptography.csproj" />
<ProjectReference Include="..\\StellaOps.Localization\\StellaOps.Localization.csproj" />
</ItemGroup>
</Project>

View File

@@ -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..]);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
using static StellaOps.Localization.T;
namespace StellaOps.Auth.Security.Dpop;
public sealed partial class DpopProofValidator

View File

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

View File

@@ -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"));
}
}
}

View File

@@ -42,5 +42,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Router\__Libraries\StellaOps.Messaging\StellaOps.Messaging.csproj" />
<ProjectReference Include="..\StellaOps.Localization\StellaOps.Localization.csproj" />
</ItemGroup>
</Project>

View File

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

View File

@@ -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);

View File

@@ -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

View File

@@ -7,4 +7,8 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Localization\StellaOps.Localization.csproj" />
</ItemGroup>
</Project>

View File

@@ -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

View File

@@ -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"));
}
}
}

View File

@@ -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"));
}
}
}

View File

@@ -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"));
}
}
}

View File

@@ -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"));
}
}
}

View File

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

View File

@@ -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)}"));
}
}
}

View File

@@ -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();

View File

@@ -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();

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using static StellaOps.Localization.T;
namespace StellaOps.Configuration;

View File

@@ -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);

View File

@@ -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"));
}
}
}

View File

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

View File

@@ -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))

View File

@@ -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"));
}
}
}

View File

@@ -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"));
}
}
}

View File

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

View File

@@ -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);

View File

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

View File

@@ -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"));
}
}
}

View File

@@ -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"));
}
}
}

View File

@@ -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);

View File

@@ -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'">

View File

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

View File

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

View File

@@ -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"));
}
}

View File

@@ -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"));
}
}
}

View File

@@ -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" />

View File

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

View File

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

View File

@@ -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()
{

View File

@@ -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(

View File

@@ -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());
}
}

View File

@@ -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)),
};
}
}

View File

@@ -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()
{

View File

@@ -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);

View File

@@ -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)),
};
}

View File

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

View File

@@ -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);

View File

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

View File

@@ -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
{

View File

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

View File

@@ -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);

View File

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

View File

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

View File

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

View File

@@ -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()
{

View File

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

View File

@@ -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,

View File

@@ -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"));
}

View File

@@ -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);

View File

@@ -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();

View File

@@ -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());

View File

@@ -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()

View File

@@ -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()
{

View File

@@ -15,5 +15,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
<ProjectReference Include="../StellaOps.Localization/StellaOps.Localization.csproj" />
</ItemGroup>
</Project>

View File

@@ -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();

View File

@@ -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);

View File

@@ -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>

View File

@@ -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"));
}
}

View File

@@ -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>

View File

@@ -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];

View File

@@ -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);

View File

@@ -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>

View File

@@ -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.

View File

@@ -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);

View File

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

View File

@@ -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();

View File

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

View File

@@ -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);

View File

@@ -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"));
}
}
}

View File

@@ -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();

View File

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

View File

@@ -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