Add unit tests for RabbitMq and Udp transport servers and clients
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Implemented comprehensive unit tests for RabbitMqTransportServer, covering constructor, disposal, connection management, event handlers, and exception handling.
- Added configuration tests for RabbitMqTransportServer to validate SSL, durable queues, auto-recovery, and custom virtual host options.
- Created unit tests for UdpFrameProtocol, including frame parsing and serialization, header size validation, and round-trip data preservation.
- Developed tests for UdpTransportClient, focusing on connection handling, event subscriptions, and exception scenarios.
- Established tests for UdpTransportServer, ensuring proper start/stop behavior, connection state management, and event handling.
- Included tests for UdpTransportOptions to verify default values and modification capabilities.
- Enhanced service registration tests for Udp transport services in the dependency injection container.
This commit is contained in:
master
2025-12-05 19:01:12 +02:00
parent 53508ceccb
commit cc69d332e3
245 changed files with 22440 additions and 27719 deletions

View File

@@ -0,0 +1,96 @@
namespace StellaOps.Cryptography;
/// <summary>
/// Represents a cryptographic compliance profile that maps hash purposes to algorithms
/// according to a specific compliance standard (e.g., FIPS 140-3, GOST, SM).
/// </summary>
public sealed class ComplianceProfile
{
/// <summary>
/// Unique identifier for this profile (e.g., "world", "fips", "gost", "sm", "kcmvp", "eidas").
/// </summary>
public required string ProfileId { get; init; }
/// <summary>
/// Human-readable name of the compliance standard.
/// </summary>
public required string StandardName { get; init; }
/// <summary>
/// Description of the compliance standard and its requirements.
/// </summary>
public string? Description { get; init; }
/// <summary>
/// Mapping of hash purposes to algorithm identifiers.
/// Keys are from <see cref="HashPurpose"/>, values are from <see cref="HashAlgorithms"/>.
/// </summary>
public required IReadOnlyDictionary<string, string> PurposeAlgorithms { get; init; }
/// <summary>
/// Mapping of hash purposes to hash prefixes (e.g., "blake3:", "sha256:", "gost3411:").
/// </summary>
public required IReadOnlyDictionary<string, string> HashPrefixes { get; init; }
/// <summary>
/// When true, the Interop purpose may use SHA-256 even if not the profile default.
/// Default: true.
/// </summary>
public bool AllowInteropOverride { get; init; } = true;
/// <summary>
/// Gets the algorithm for a given purpose.
/// </summary>
/// <param name="purpose">The hash purpose.</param>
/// <returns>The algorithm identifier.</returns>
/// <exception cref="ArgumentException">Thrown when the purpose is unknown.</exception>
public string GetAlgorithmForPurpose(string purpose)
{
if (PurposeAlgorithms.TryGetValue(purpose, out var algorithm))
{
return algorithm;
}
throw new ArgumentException($"Unknown hash purpose '{purpose}' in profile '{ProfileId}'.", nameof(purpose));
}
/// <summary>
/// Gets the hash prefix for a given purpose (e.g., "blake3:", "sha256:").
/// </summary>
/// <param name="purpose">The hash purpose.</param>
/// <returns>The hash prefix string.</returns>
public string GetHashPrefix(string purpose)
{
if (HashPrefixes.TryGetValue(purpose, out var prefix))
{
return prefix;
}
// Fallback to algorithm-based prefix
var algorithm = GetAlgorithmForPurpose(purpose);
return algorithm.ToLowerInvariant() + ":";
}
/// <summary>
/// Checks if the given algorithm is compliant for the specified purpose.
/// </summary>
/// <param name="purpose">The hash purpose.</param>
/// <param name="algorithmId">The algorithm to check.</param>
/// <returns>True if compliant; otherwise, false.</returns>
public bool IsCompliant(string purpose, string algorithmId)
{
if (!PurposeAlgorithms.TryGetValue(purpose, out var expectedAlgorithm))
{
return false;
}
// Interop always allows SHA-256 if AllowInteropOverride is true
if (purpose == HashPurpose.Interop && AllowInteropOverride &&
string.Equals(algorithmId, HashAlgorithms.Sha256, StringComparison.OrdinalIgnoreCase))
{
return true;
}
return string.Equals(expectedAlgorithm, algorithmId, StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -0,0 +1,252 @@
namespace StellaOps.Cryptography;
/// <summary>
/// Built-in compliance profiles for different jurisdictional crypto requirements.
/// </summary>
public static class ComplianceProfiles
{
/// <summary>
/// Default/World profile using BLAKE3 for graph hashing, SHA-256 for everything else.
/// Suitable for international deployments without specific compliance requirements.
/// </summary>
public static readonly ComplianceProfile World = new()
{
ProfileId = "world",
StandardName = "ISO/Default",
Description = "Default profile using BLAKE3 for graph content-addressing, SHA-256 for symbol/content hashing.",
PurposeAlgorithms = new Dictionary<string, string>
{
[HashPurpose.Graph] = HashAlgorithms.Blake3_256,
[HashPurpose.Symbol] = HashAlgorithms.Sha256,
[HashPurpose.Content] = HashAlgorithms.Sha256,
[HashPurpose.Merkle] = HashAlgorithms.Sha256,
[HashPurpose.Attestation] = HashAlgorithms.Sha256,
[HashPurpose.Interop] = HashAlgorithms.Sha256,
[HashPurpose.Secret] = PasswordHashAlgorithms.Argon2id,
},
HashPrefixes = new Dictionary<string, string>
{
[HashPurpose.Graph] = "blake3:",
[HashPurpose.Symbol] = "sha256:",
[HashPurpose.Content] = "sha256:",
[HashPurpose.Merkle] = "sha256:",
[HashPurpose.Attestation] = "sha256:",
[HashPurpose.Interop] = "sha256:",
[HashPurpose.Secret] = "argon2id:",
},
AllowInteropOverride = true,
};
/// <summary>
/// FIPS 140-3 (US Federal) compliance profile.
/// Uses only FIPS-approved algorithms: SHA-256, SHA-384, SHA-512, PBKDF2.
/// Note: BLAKE3 is not FIPS-approved, so SHA-256 is used for graph hashing.
/// </summary>
public static readonly ComplianceProfile Fips = new()
{
ProfileId = "fips",
StandardName = "FIPS 140-3",
Description = "US Federal Information Processing Standard 140-3. Uses only FIPS-approved algorithms.",
PurposeAlgorithms = new Dictionary<string, string>
{
[HashPurpose.Graph] = HashAlgorithms.Sha256, // BLAKE3 not FIPS-approved
[HashPurpose.Symbol] = HashAlgorithms.Sha256,
[HashPurpose.Content] = HashAlgorithms.Sha256,
[HashPurpose.Merkle] = HashAlgorithms.Sha256,
[HashPurpose.Attestation] = HashAlgorithms.Sha256,
[HashPurpose.Interop] = HashAlgorithms.Sha256,
[HashPurpose.Secret] = PasswordHashAlgorithms.Pbkdf2Sha256,
},
HashPrefixes = new Dictionary<string, string>
{
[HashPurpose.Graph] = "sha256:",
[HashPurpose.Symbol] = "sha256:",
[HashPurpose.Content] = "sha256:",
[HashPurpose.Merkle] = "sha256:",
[HashPurpose.Attestation] = "sha256:",
[HashPurpose.Interop] = "sha256:",
[HashPurpose.Secret] = "pbkdf2:",
},
AllowInteropOverride = true,
};
/// <summary>
/// GOST R 34.11-2012 (Russian) compliance profile.
/// Uses GOST Stribog hash for all purposes except Interop (which remains SHA-256 for external compatibility).
/// </summary>
public static readonly ComplianceProfile Gost = new()
{
ProfileId = "gost",
StandardName = "GOST R 34.11-2012",
Description = "Russian GOST R 34.11-2012 (Stribog) hash standard. Interop uses SHA-256 for external tool compatibility.",
PurposeAlgorithms = new Dictionary<string, string>
{
[HashPurpose.Graph] = HashAlgorithms.Gost3411_2012_256,
[HashPurpose.Symbol] = HashAlgorithms.Gost3411_2012_256,
[HashPurpose.Content] = HashAlgorithms.Gost3411_2012_256,
[HashPurpose.Merkle] = HashAlgorithms.Gost3411_2012_256,
[HashPurpose.Attestation] = HashAlgorithms.Gost3411_2012_256,
[HashPurpose.Interop] = HashAlgorithms.Sha256, // Override for external compatibility
[HashPurpose.Secret] = PasswordHashAlgorithms.Argon2id,
},
HashPrefixes = new Dictionary<string, string>
{
[HashPurpose.Graph] = "gost3411:",
[HashPurpose.Symbol] = "gost3411:",
[HashPurpose.Content] = "gost3411:",
[HashPurpose.Merkle] = "gost3411:",
[HashPurpose.Attestation] = "gost3411:",
[HashPurpose.Interop] = "sha256:",
[HashPurpose.Secret] = "argon2id:",
},
AllowInteropOverride = true,
};
/// <summary>
/// GB/T SM3 (Chinese) compliance profile.
/// Uses SM3 hash for all purposes except Interop (which remains SHA-256 for external compatibility).
/// </summary>
public static readonly ComplianceProfile Sm = new()
{
ProfileId = "sm",
StandardName = "GB/T (SM3)",
Description = "Chinese GB/T 32905-2016 SM3 cryptographic hash standard. Interop uses SHA-256 for external tool compatibility.",
PurposeAlgorithms = new Dictionary<string, string>
{
[HashPurpose.Graph] = HashAlgorithms.Sm3,
[HashPurpose.Symbol] = HashAlgorithms.Sm3,
[HashPurpose.Content] = HashAlgorithms.Sm3,
[HashPurpose.Merkle] = HashAlgorithms.Sm3,
[HashPurpose.Attestation] = HashAlgorithms.Sm3,
[HashPurpose.Interop] = HashAlgorithms.Sha256, // Override for external compatibility
[HashPurpose.Secret] = PasswordHashAlgorithms.Argon2id,
},
HashPrefixes = new Dictionary<string, string>
{
[HashPurpose.Graph] = "sm3:",
[HashPurpose.Symbol] = "sm3:",
[HashPurpose.Content] = "sm3:",
[HashPurpose.Merkle] = "sm3:",
[HashPurpose.Attestation] = "sm3:",
[HashPurpose.Interop] = "sha256:",
[HashPurpose.Secret] = "argon2id:",
},
AllowInteropOverride = true,
};
/// <summary>
/// KCMVP (Korea Cryptographic Module Validation Program) compliance profile.
/// Uses SHA-256 for hashing. Note: ARIA/SEED/LEA are for encryption, KCDSA for signatures.
/// </summary>
public static readonly ComplianceProfile Kcmvp = new()
{
ProfileId = "kcmvp",
StandardName = "KCMVP (Korea)",
Description = "Korea Cryptographic Module Validation Program. Uses SHA-256 for hashing.",
PurposeAlgorithms = new Dictionary<string, string>
{
[HashPurpose.Graph] = HashAlgorithms.Sha256,
[HashPurpose.Symbol] = HashAlgorithms.Sha256,
[HashPurpose.Content] = HashAlgorithms.Sha256,
[HashPurpose.Merkle] = HashAlgorithms.Sha256,
[HashPurpose.Attestation] = HashAlgorithms.Sha256,
[HashPurpose.Interop] = HashAlgorithms.Sha256,
[HashPurpose.Secret] = PasswordHashAlgorithms.Argon2id,
},
HashPrefixes = new Dictionary<string, string>
{
[HashPurpose.Graph] = "sha256:",
[HashPurpose.Symbol] = "sha256:",
[HashPurpose.Content] = "sha256:",
[HashPurpose.Merkle] = "sha256:",
[HashPurpose.Attestation] = "sha256:",
[HashPurpose.Interop] = "sha256:",
[HashPurpose.Secret] = "argon2id:",
},
AllowInteropOverride = true,
};
/// <summary>
/// eIDAS/ETSI TS 119 312 (European) compliance profile.
/// Uses SHA-256 for hashing per ETSI cryptographic suites specification.
/// </summary>
public static readonly ComplianceProfile Eidas = new()
{
ProfileId = "eidas",
StandardName = "eIDAS/ETSI TS 119 312",
Description = "European eIDAS regulation with ETSI TS 119 312 cryptographic suites. Uses SHA-256/384 for hashing.",
PurposeAlgorithms = new Dictionary<string, string>
{
[HashPurpose.Graph] = HashAlgorithms.Sha256,
[HashPurpose.Symbol] = HashAlgorithms.Sha256,
[HashPurpose.Content] = HashAlgorithms.Sha256,
[HashPurpose.Merkle] = HashAlgorithms.Sha256,
[HashPurpose.Attestation] = HashAlgorithms.Sha256,
[HashPurpose.Interop] = HashAlgorithms.Sha256,
[HashPurpose.Secret] = PasswordHashAlgorithms.Argon2id,
},
HashPrefixes = new Dictionary<string, string>
{
[HashPurpose.Graph] = "sha256:",
[HashPurpose.Symbol] = "sha256:",
[HashPurpose.Content] = "sha256:",
[HashPurpose.Merkle] = "sha256:",
[HashPurpose.Attestation] = "sha256:",
[HashPurpose.Interop] = "sha256:",
[HashPurpose.Secret] = "argon2id:",
},
AllowInteropOverride = true,
};
/// <summary>
/// All built-in profiles indexed by profile ID.
/// </summary>
public static readonly IReadOnlyDictionary<string, ComplianceProfile> All =
new Dictionary<string, ComplianceProfile>(StringComparer.OrdinalIgnoreCase)
{
[World.ProfileId] = World,
[Fips.ProfileId] = Fips,
[Gost.ProfileId] = Gost,
[Sm.ProfileId] = Sm,
[Kcmvp.ProfileId] = Kcmvp,
[Eidas.ProfileId] = Eidas,
};
/// <summary>
/// Gets a profile by ID, returning the World profile if not found.
/// </summary>
/// <param name="profileId">The profile ID to look up.</param>
/// <returns>The matching profile, or World if not found.</returns>
public static ComplianceProfile GetProfile(string? profileId)
{
if (string.IsNullOrWhiteSpace(profileId))
{
return World;
}
return All.TryGetValue(profileId, out var profile) ? profile : World;
}
/// <summary>
/// Gets a profile by ID, throwing if not found.
/// </summary>
/// <param name="profileId">The profile ID to look up.</param>
/// <returns>The matching profile.</returns>
/// <exception cref="ArgumentException">Thrown when the profile ID is not found.</exception>
public static ComplianceProfile GetProfileOrThrow(string profileId)
{
if (string.IsNullOrWhiteSpace(profileId))
{
throw new ArgumentException("Profile ID cannot be null or empty.", nameof(profileId));
}
if (!All.TryGetValue(profileId, out var profile))
{
throw new ArgumentException(
$"Unknown compliance profile '{profileId}'. Valid profiles: {string.Join(", ", All.Keys)}",
nameof(profileId));
}
return profile;
}
}

View File

@@ -0,0 +1,117 @@
using System.Diagnostics;
using System.Diagnostics.Metrics;
namespace StellaOps.Cryptography;
/// <summary>
/// Telemetry diagnostics for crypto compliance operations.
/// </summary>
public sealed class CryptoComplianceDiagnostics : IDisposable
{
/// <summary>
/// Activity source name for distributed tracing.
/// </summary>
public const string ActivitySourceName = "StellaOps.Crypto.Compliance";
/// <summary>
/// Meter name for metrics.
/// </summary>
public const string MeterName = "StellaOps.Crypto.Compliance";
private readonly ActivitySource _activitySource;
private readonly Meter _meter;
// Counters
private readonly Counter<long> _hashOperations;
private readonly Counter<long> _complianceViolations;
private readonly Histogram<double> _hashDurationMs;
public CryptoComplianceDiagnostics()
{
_activitySource = new ActivitySource(ActivitySourceName, "1.0.0");
_meter = new Meter(MeterName, "1.0.0");
_hashOperations = _meter.CreateCounter<long>(
name: "crypto.hash.operations",
unit: "{operation}",
description: "Total number of hash operations performed.");
_complianceViolations = _meter.CreateCounter<long>(
name: "crypto.compliance.violations",
unit: "{violation}",
description: "Number of compliance violations detected.");
_hashDurationMs = _meter.CreateHistogram<double>(
name: "crypto.hash.duration",
unit: "ms",
description: "Duration of hash operations in milliseconds.");
}
/// <summary>
/// Starts an activity for a hash operation.
/// </summary>
public Activity? StartHashOperation(string purpose, string algorithm, string profile)
{
var activity = _activitySource.StartActivity("crypto.hash", ActivityKind.Internal);
if (activity is not null)
{
activity.SetTag("crypto.purpose", purpose);
activity.SetTag("crypto.algorithm", algorithm);
activity.SetTag("crypto.profile", profile);
}
return activity;
}
/// <summary>
/// Records a completed hash operation.
/// </summary>
public void RecordHashOperation(
string profile,
string purpose,
string algorithm,
TimeSpan duration,
bool success = true)
{
var tags = new TagList
{
{ "profile", profile },
{ "purpose", purpose },
{ "algorithm", algorithm },
{ "success", success.ToString().ToLowerInvariant() }
};
_hashOperations.Add(1, tags);
_hashDurationMs.Record(duration.TotalMilliseconds, tags);
}
/// <summary>
/// Records a compliance violation.
/// </summary>
public void RecordComplianceViolation(
string profile,
string purpose,
string requestedAlgorithm,
string expectedAlgorithm,
bool wasBlocked)
{
var tags = new TagList
{
{ "profile", profile },
{ "purpose", purpose },
{ "requested_algorithm", requestedAlgorithm },
{ "expected_algorithm", expectedAlgorithm },
{ "blocked", wasBlocked.ToString().ToLowerInvariant() }
};
_complianceViolations.Add(1, tags);
}
/// <summary>
/// Disposes of the diagnostics resources.
/// </summary>
public void Dispose()
{
_activitySource.Dispose();
_meter.Dispose();
}
}

View File

@@ -0,0 +1,56 @@
namespace StellaOps.Cryptography;
/// <summary>
/// Exception thrown when a cryptographic operation violates compliance requirements.
/// </summary>
public sealed class CryptoComplianceException : Exception
{
/// <summary>
/// The compliance profile that was violated.
/// </summary>
public string ProfileId { get; }
/// <summary>
/// The hash purpose that was being processed.
/// </summary>
public string Purpose { get; }
/// <summary>
/// The algorithm that was requested.
/// </summary>
public string RequestedAlgorithm { get; }
/// <summary>
/// The algorithm that is expected for the profile and purpose.
/// </summary>
public string ExpectedAlgorithm { get; }
public CryptoComplianceException(
string message,
string profileId,
string purpose,
string requestedAlgorithm,
string expectedAlgorithm)
: base(message)
{
ProfileId = profileId;
Purpose = purpose;
RequestedAlgorithm = requestedAlgorithm;
ExpectedAlgorithm = expectedAlgorithm;
}
public CryptoComplianceException(
string message,
string profileId,
string purpose,
string requestedAlgorithm,
string expectedAlgorithm,
Exception innerException)
: base(message, innerException)
{
ProfileId = profileId;
Purpose = purpose;
RequestedAlgorithm = requestedAlgorithm;
ExpectedAlgorithm = expectedAlgorithm;
}
}

View File

@@ -0,0 +1,70 @@
namespace StellaOps.Cryptography;
/// <summary>
/// Configuration options for cryptographic compliance.
/// </summary>
public sealed class CryptoComplianceOptions
{
/// <summary>
/// The configuration section key for binding.
/// </summary>
public const string SectionKey = "Crypto:Compliance";
/// <summary>
/// Active compliance profile ID.
/// Valid values: "world", "fips", "gost", "sm", "kcmvp", "eidas".
/// Default: "world".
/// Can be overridden by STELLAOPS_CRYPTO_COMPLIANCE_PROFILE environment variable.
/// </summary>
public string ProfileId { get; set; } = "world";
/// <summary>
/// When true, fail on non-compliant algorithm usage.
/// Default: true.
/// Can be overridden by STELLAOPS_CRYPTO_STRICT_VALIDATION environment variable.
/// </summary>
public bool StrictValidation { get; set; } = true;
/// <summary>
/// When StrictValidation=false, emit warning instead of silently proceeding.
/// Default: true.
/// </summary>
public bool WarnOnNonCompliant { get; set; } = true;
/// <summary>
/// Allow Interop purpose to override profile algorithm with SHA-256.
/// Default: true.
/// </summary>
public bool AllowInteropOverride { get; set; } = true;
/// <summary>
/// Enable telemetry for all crypto operations.
/// Default: true.
/// </summary>
public bool EnableTelemetry { get; set; } = true;
/// <summary>
/// Custom purpose-to-algorithm overrides that take precedence over profile defaults.
/// Keys are from <see cref="HashPurpose"/>, values are from <see cref="HashAlgorithms"/>.
/// </summary>
public Dictionary<string, string>? PurposeOverrides { get; set; }
/// <summary>
/// Applies environment variable overrides.
/// </summary>
public void ApplyEnvironmentOverrides()
{
var profileEnv = Environment.GetEnvironmentVariable("STELLAOPS_CRYPTO_COMPLIANCE_PROFILE");
if (!string.IsNullOrWhiteSpace(profileEnv))
{
ProfileId = profileEnv.Trim().ToLowerInvariant();
}
var strictEnv = Environment.GetEnvironmentVariable("STELLAOPS_CRYPTO_STRICT_VALIDATION");
if (!string.IsNullOrWhiteSpace(strictEnv) &&
bool.TryParse(strictEnv, out var strict))
{
StrictValidation = strict;
}
}
}

View File

@@ -0,0 +1,165 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
namespace StellaOps.Cryptography;
/// <summary>
/// Service for resolving cryptographic algorithms based on the active compliance profile.
/// </summary>
public sealed class CryptoComplianceService : ICryptoComplianceService
{
private readonly IOptionsMonitor<CryptoComplianceOptions> _options;
private readonly ILogger<CryptoComplianceService> _logger;
public CryptoComplianceService(
IOptionsMonitor<CryptoComplianceOptions> options,
ILogger<CryptoComplianceService>? logger = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? NullLogger<CryptoComplianceService>.Instance;
}
/// <summary>
/// Gets the currently active compliance profile.
/// </summary>
public ComplianceProfile ActiveProfile
{
get
{
var opts = _options.CurrentValue;
opts.ApplyEnvironmentOverrides();
return ComplianceProfiles.GetProfile(opts.ProfileId);
}
}
/// <summary>
/// Gets the algorithm for a given purpose based on the active profile.
/// </summary>
/// <param name="purpose">The hash purpose.</param>
/// <returns>The algorithm identifier.</returns>
public string GetAlgorithmForPurpose(string purpose)
{
if (string.IsNullOrWhiteSpace(purpose))
{
throw new ArgumentException("Purpose cannot be null or empty.", nameof(purpose));
}
var opts = _options.CurrentValue;
opts.ApplyEnvironmentOverrides();
// Check for purpose overrides first
if (opts.PurposeOverrides?.TryGetValue(purpose, out var overrideAlgorithm) == true &&
!string.IsNullOrWhiteSpace(overrideAlgorithm))
{
_logger.LogDebug(
"Using purpose override for {Purpose}: {Algorithm} (profile: {Profile})",
purpose, overrideAlgorithm, opts.ProfileId);
return overrideAlgorithm;
}
// Get from active profile
var profile = ComplianceProfiles.GetProfile(opts.ProfileId);
return profile.GetAlgorithmForPurpose(purpose);
}
/// <summary>
/// Gets the hash prefix for a given purpose (e.g., "blake3:", "sha256:").
/// </summary>
/// <param name="purpose">The hash purpose.</param>
/// <returns>The hash prefix string.</returns>
public string GetHashPrefix(string purpose)
{
if (string.IsNullOrWhiteSpace(purpose))
{
throw new ArgumentException("Purpose cannot be null or empty.", nameof(purpose));
}
var profile = ActiveProfile;
return profile.GetHashPrefix(purpose);
}
/// <summary>
/// Validates an algorithm request against the active compliance profile.
/// </summary>
/// <param name="purpose">The hash purpose (or null if unknown).</param>
/// <param name="requestedAlgorithm">The requested algorithm.</param>
/// <exception cref="CryptoComplianceException">
/// Thrown when StrictValidation is enabled and the algorithm is not compliant.
/// </exception>
public void ValidateAlgorithm(string? purpose, string requestedAlgorithm)
{
var opts = _options.CurrentValue;
opts.ApplyEnvironmentOverrides();
var profile = ComplianceProfiles.GetProfile(opts.ProfileId);
// If purpose is specified, check compliance
if (!string.IsNullOrWhiteSpace(purpose))
{
if (!profile.IsCompliant(purpose, requestedAlgorithm))
{
var expectedAlgorithm = profile.GetAlgorithmForPurpose(purpose);
var message = $"Algorithm '{requestedAlgorithm}' is not compliant for purpose '{purpose}' " +
$"in profile '{profile.ProfileId}'. Expected: '{expectedAlgorithm}'.";
if (opts.StrictValidation)
{
_logger.LogError(
"Compliance violation: {Message}",
message);
throw new CryptoComplianceException(message, profile.ProfileId, purpose, requestedAlgorithm, expectedAlgorithm);
}
if (opts.WarnOnNonCompliant)
{
_logger.LogWarning(
"Compliance warning: {Message}",
message);
}
}
}
}
/// <summary>
/// Checks if the given algorithm is compliant for any purpose in the active profile.
/// </summary>
/// <param name="algorithmId">The algorithm to check.</param>
/// <returns>True if the algorithm is used by any purpose in the profile.</returns>
public bool IsAlgorithmCompliant(string algorithmId)
{
var profile = ActiveProfile;
return HashPurpose.All.Any(purpose => profile.IsCompliant(purpose, algorithmId));
}
}
/// <summary>
/// Interface for compliance-aware cryptographic algorithm resolution.
/// </summary>
public interface ICryptoComplianceService
{
/// <summary>
/// Gets the currently active compliance profile.
/// </summary>
ComplianceProfile ActiveProfile { get; }
/// <summary>
/// Gets the algorithm for a given purpose based on the active profile.
/// </summary>
string GetAlgorithmForPurpose(string purpose);
/// <summary>
/// Gets the hash prefix for a given purpose.
/// </summary>
string GetHashPrefix(string purpose);
/// <summary>
/// Validates an algorithm request against the active compliance profile.
/// </summary>
void ValidateAlgorithm(string? purpose, string requestedAlgorithm);
/// <summary>
/// Checks if the given algorithm is compliant for any purpose in the active profile.
/// </summary>
bool IsAlgorithmCompliant(string algorithmId);
}

View File

@@ -4,43 +4,65 @@ using System.IO;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Blake3;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Digests;
namespace StellaOps.Cryptography;
public sealed class DefaultCryptoHash : ICryptoHash
{
private readonly IOptionsMonitor<CryptoHashOptions> options;
private readonly ILogger<DefaultCryptoHash> logger;
private readonly IOptionsMonitor<CryptoHashOptions> _hashOptions;
private readonly IOptionsMonitor<CryptoComplianceOptions> _complianceOptions;
private readonly ILogger<DefaultCryptoHash> _logger;
[ActivatorUtilitiesConstructor]
public DefaultCryptoHash(
IOptionsMonitor<CryptoHashOptions> options,
IOptionsMonitor<CryptoHashOptions> hashOptions,
IOptionsMonitor<CryptoComplianceOptions>? complianceOptions = null,
ILogger<DefaultCryptoHash>? logger = null)
{
this.options = options ?? throw new ArgumentNullException(nameof(options));
this.logger = logger ?? NullLogger<DefaultCryptoHash>.Instance;
_hashOptions = hashOptions ?? throw new ArgumentNullException(nameof(hashOptions));
_complianceOptions = complianceOptions ?? new StaticComplianceOptionsMonitor(new CryptoComplianceOptions());
_logger = logger ?? NullLogger<DefaultCryptoHash>.Instance;
}
internal DefaultCryptoHash(CryptoHashOptions? options = null)
: this(new StaticOptionsMonitor(options ?? new CryptoHashOptions()), NullLogger<DefaultCryptoHash>.Instance)
internal DefaultCryptoHash(CryptoHashOptions? hashOptions = null, CryptoComplianceOptions? complianceOptions = null)
: this(
new StaticOptionsMonitor(hashOptions ?? new CryptoHashOptions()),
new StaticComplianceOptionsMonitor(complianceOptions ?? new CryptoComplianceOptions()),
NullLogger<DefaultCryptoHash>.Instance)
{
}
/// <summary>
/// Creates a new <see cref="DefaultCryptoHash"/> instance for use in tests.
/// Uses default options with no compliance profile.
/// </summary>
public static DefaultCryptoHash CreateForTests()
=> new(new CryptoHashOptions(), new CryptoComplianceOptions());
public byte[] ComputeHash(ReadOnlySpan<byte> data, string? algorithmId = null)
{
var algorithm = NormalizeAlgorithm(algorithmId);
return algorithm switch
return ComputeHashWithAlgorithm(data, algorithm);
}
private static byte[] ComputeHashWithAlgorithm(ReadOnlySpan<byte> data, string algorithm)
{
return algorithm.ToUpperInvariant() switch
{
HashAlgorithms.Sha256 => ComputeSha256(data),
HashAlgorithms.Sha512 => ComputeSha512(data),
HashAlgorithms.Gost3411_2012_256 => GostDigestUtilities.ComputeDigest(data, use256: true),
HashAlgorithms.Gost3411_2012_512 => GostDigestUtilities.ComputeDigest(data, use256: false),
_ => throw new InvalidOperationException($"Unsupported hash algorithm {algorithm}.")
"SHA256" => ComputeSha256(data),
"SHA384" => ComputeSha384(data),
"SHA512" => ComputeSha512(data),
"GOST3411-2012-256" => GostDigestUtilities.ComputeDigest(data, use256: true),
"GOST3411-2012-512" => GostDigestUtilities.ComputeDigest(data, use256: false),
"BLAKE3-256" => ComputeBlake3(data),
"SM3" => ComputeSm3(data),
_ => throw new InvalidOperationException($"Unsupported hash algorithm '{algorithm}'.")
};
}
@@ -56,13 +78,21 @@ public sealed class DefaultCryptoHash : ICryptoHash
cancellationToken.ThrowIfCancellationRequested();
var algorithm = NormalizeAlgorithm(algorithmId);
return algorithm switch
return await ComputeHashWithAlgorithmAsync(stream, algorithm, cancellationToken).ConfigureAwait(false);
}
private static async ValueTask<byte[]> ComputeHashWithAlgorithmAsync(Stream stream, string algorithm, CancellationToken cancellationToken)
{
return algorithm.ToUpperInvariant() switch
{
HashAlgorithms.Sha256 => await ComputeShaStreamAsync(HashAlgorithmName.SHA256, stream, cancellationToken).ConfigureAwait(false),
HashAlgorithms.Sha512 => await ComputeShaStreamAsync(HashAlgorithmName.SHA512, stream, cancellationToken).ConfigureAwait(false),
HashAlgorithms.Gost3411_2012_256 => await ComputeGostStreamAsync(use256: true, stream, cancellationToken).ConfigureAwait(false),
HashAlgorithms.Gost3411_2012_512 => await ComputeGostStreamAsync(use256: false, stream, cancellationToken).ConfigureAwait(false),
_ => throw new InvalidOperationException($"Unsupported hash algorithm {algorithm}.")
"SHA256" => await ComputeShaStreamAsync(HashAlgorithmName.SHA256, stream, cancellationToken).ConfigureAwait(false),
"SHA384" => await ComputeShaStreamAsync(HashAlgorithmName.SHA384, stream, cancellationToken).ConfigureAwait(false),
"SHA512" => await ComputeShaStreamAsync(HashAlgorithmName.SHA512, stream, cancellationToken).ConfigureAwait(false),
"GOST3411-2012-256" => await ComputeGostStreamAsync(use256: true, stream, cancellationToken).ConfigureAwait(false),
"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}'.")
};
}
@@ -130,7 +160,7 @@ public sealed class DefaultCryptoHash : ICryptoHash
private string NormalizeAlgorithm(string? algorithmId)
{
var defaultAlgorithm = options.CurrentValue?.DefaultAlgorithm;
var defaultAlgorithm = _hashOptions.CurrentValue?.DefaultAlgorithm;
if (!string.IsNullOrWhiteSpace(algorithmId))
{
return algorithmId.Trim().ToUpperInvariant();
@@ -144,26 +174,194 @@ public sealed class DefaultCryptoHash : ICryptoHash
return HashAlgorithms.Sha256;
}
#region Purpose-based methods
private ComplianceProfile GetActiveProfile()
{
var opts = _complianceOptions.CurrentValue;
opts.ApplyEnvironmentOverrides();
return ComplianceProfiles.GetProfile(opts.ProfileId);
}
public byte[] ComputeHashForPurpose(ReadOnlySpan<byte> data, string purpose)
{
var algorithm = GetAlgorithmForPurpose(purpose);
return ComputeHashWithAlgorithm(data, algorithm);
}
public string ComputeHashHexForPurpose(ReadOnlySpan<byte> data, string purpose)
=> Convert.ToHexString(ComputeHashForPurpose(data, purpose)).ToLowerInvariant();
public string ComputeHashBase64ForPurpose(ReadOnlySpan<byte> data, string purpose)
=> Convert.ToBase64String(ComputeHashForPurpose(data, purpose));
public async ValueTask<byte[]> ComputeHashForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(stream);
cancellationToken.ThrowIfCancellationRequested();
var algorithm = GetAlgorithmForPurpose(purpose);
return await ComputeHashWithAlgorithmAsync(stream, algorithm, cancellationToken).ConfigureAwait(false);
}
public async ValueTask<string> ComputeHashHexForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default)
{
var bytes = await ComputeHashForPurposeAsync(stream, purpose, cancellationToken).ConfigureAwait(false);
return Convert.ToHexString(bytes).ToLowerInvariant();
}
public string GetAlgorithmForPurpose(string purpose)
{
if (string.IsNullOrWhiteSpace(purpose))
{
throw new ArgumentException("Purpose cannot be null or empty.", nameof(purpose));
}
var opts = _complianceOptions.CurrentValue;
opts.ApplyEnvironmentOverrides();
// Check for purpose overrides first
if (opts.PurposeOverrides?.TryGetValue(purpose, out var overrideAlgorithm) == true &&
!string.IsNullOrWhiteSpace(overrideAlgorithm))
{
return overrideAlgorithm;
}
// Get from active profile
var profile = ComplianceProfiles.GetProfile(opts.ProfileId);
return profile.GetAlgorithmForPurpose(purpose);
}
public string GetHashPrefix(string purpose)
{
if (string.IsNullOrWhiteSpace(purpose))
{
throw new ArgumentException("Purpose cannot be null or empty.", nameof(purpose));
}
var profile = GetActiveProfile();
return profile.GetHashPrefix(purpose);
}
public string ComputePrefixedHashForPurpose(ReadOnlySpan<byte> data, string purpose)
{
var prefix = GetHashPrefix(purpose);
var hash = ComputeHashHexForPurpose(data, purpose);
return prefix + hash;
}
#endregion
#region Algorithm implementations
private static byte[] ComputeSha384(ReadOnlySpan<byte> data)
{
Span<byte> buffer = stackalloc byte[48];
SHA384.HashData(data, buffer);
return buffer.ToArray();
}
private static byte[] ComputeBlake3(ReadOnlySpan<byte> data)
{
using var hasher = Hasher.New();
hasher.Update(data);
var hash = hasher.Finalize();
return hash.AsSpan().ToArray();
}
private static byte[] ComputeSm3(ReadOnlySpan<byte> data)
{
var digest = new SM3Digest();
digest.BlockUpdate(data);
var output = new byte[digest.GetDigestSize()];
digest.DoFinal(output, 0);
return output;
}
private static async ValueTask<byte[]> ComputeBlake3StreamAsync(Stream stream, CancellationToken cancellationToken)
{
using var hasher = Hasher.New();
var buffer = ArrayPool<byte>.Shared.Rent(128 * 1024);
try
{
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
{
hasher.Update(buffer.AsSpan(0, bytesRead));
}
var hash = hasher.Finalize();
return hash.AsSpan().ToArray();
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
private static async ValueTask<byte[]> ComputeSm3StreamAsync(Stream stream, CancellationToken cancellationToken)
{
var digest = new SM3Digest();
var buffer = ArrayPool<byte>.Shared.Rent(128 * 1024);
try
{
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
{
digest.BlockUpdate(buffer, 0, bytesRead);
}
var output = new byte[digest.GetDigestSize()];
digest.DoFinal(output, 0);
return output;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
#endregion
#region Static options monitors
private sealed class StaticOptionsMonitor : IOptionsMonitor<CryptoHashOptions>
{
private readonly CryptoHashOptions options;
private readonly CryptoHashOptions _options;
public StaticOptionsMonitor(CryptoHashOptions options)
=> this.options = options;
=> _options = options;
public CryptoHashOptions CurrentValue => options;
public CryptoHashOptions CurrentValue => _options;
public CryptoHashOptions Get(string? name) => options;
public CryptoHashOptions Get(string? name) => _options;
public IDisposable OnChange(Action<CryptoHashOptions, string> listener)
=> NullDisposable.Instance;
}
private sealed class NullDisposable : IDisposable
private sealed class StaticComplianceOptionsMonitor : IOptionsMonitor<CryptoComplianceOptions>
{
private readonly CryptoComplianceOptions _options;
public StaticComplianceOptionsMonitor(CryptoComplianceOptions options)
=> _options = options;
public CryptoComplianceOptions CurrentValue => _options;
public CryptoComplianceOptions Get(string? name) => _options;
public IDisposable OnChange(Action<CryptoComplianceOptions, string> listener)
=> NullDisposable.Instance;
}
private sealed class NullDisposable : IDisposable
{
public static readonly NullDisposable Instance = new();
public void Dispose()
{
public static readonly NullDisposable Instance = new();
public void Dispose()
{
}
}
}
#endregion
}

View File

@@ -5,9 +5,24 @@ namespace StellaOps.Cryptography;
/// </summary>
public static class HashAlgorithms
{
/// <summary>SHA-256 (256-bit). FIPS/eIDAS/KCMVP compliant.</summary>
public const string Sha256 = "SHA256";
/// <summary>SHA-384 (384-bit). FIPS/eIDAS compliant.</summary>
public const string Sha384 = "SHA384";
/// <summary>SHA-512 (512-bit). FIPS/eIDAS compliant.</summary>
public const string Sha512 = "SHA512";
/// <summary>GOST R 34.11-2012 Stribog (256-bit). Russian compliance.</summary>
public const string Gost3411_2012_256 = "GOST3411-2012-256";
/// <summary>GOST R 34.11-2012 Stribog (512-bit). Russian compliance.</summary>
public const string Gost3411_2012_512 = "GOST3411-2012-512";
/// <summary>BLAKE3-256 (256-bit). Fast, parallelizable, used for graph content-addressing.</summary>
public const string Blake3_256 = "BLAKE3-256";
/// <summary>SM3 (256-bit). Chinese GB/T 32905-2016 compliance.</summary>
public const string Sm3 = "SM3";
}

View File

@@ -0,0 +1,74 @@
namespace StellaOps.Cryptography;
/// <summary>
/// Well-known hash purpose identifiers for compliance-aware cryptographic operations.
/// Components should request hashing by PURPOSE, not by algorithm.
/// The platform resolves the correct algorithm based on the active compliance profile.
/// </summary>
public static class HashPurpose
{
/// <summary>
/// Graph content-addressing (richgraph-v1).
/// Default: BLAKE3-256 (world), SHA-256 (fips), GOST3411-2012-256 (gost), SM3 (sm).
/// </summary>
public const string Graph = "graph";
/// <summary>
/// Symbol identification (SymbolID, CodeID).
/// Default: SHA-256 (world/fips/kcmvp/eidas), GOST3411-2012-256 (gost), SM3 (sm).
/// </summary>
public const string Symbol = "symbol";
/// <summary>
/// Content/file hashing for integrity verification.
/// Default: SHA-256 (world/fips/kcmvp/eidas), GOST3411-2012-256 (gost), SM3 (sm).
/// </summary>
public const string Content = "content";
/// <summary>
/// Merkle tree node hashing.
/// Default: SHA-256 (world/fips/kcmvp/eidas), GOST3411-2012-256 (gost), SM3 (sm).
/// </summary>
public const string Merkle = "merkle";
/// <summary>
/// DSSE payload digest for attestations.
/// Default: SHA-256 (world/fips/kcmvp/eidas), GOST3411-2012-256 (gost), SM3 (sm).
/// </summary>
public const string Attestation = "attestation";
/// <summary>
/// External interoperability (third-party tools like cosign, rekor).
/// Always SHA-256, regardless of compliance profile.
/// Every use of this purpose MUST be documented with justification.
/// </summary>
public const string Interop = "interop";
/// <summary>
/// Password/secret derivation.
/// Default: Argon2id (world/gost/sm/kcmvp/eidas), PBKDF2-SHA256 (fips).
/// </summary>
public const string Secret = "secret";
/// <summary>
/// All known hash purposes for validation.
/// </summary>
public static readonly IReadOnlyList<string> All = new[]
{
Graph,
Symbol,
Content,
Merkle,
Attestation,
Interop,
Secret
};
/// <summary>
/// Validates whether the given purpose is known.
/// </summary>
/// <param name="purpose">The purpose to validate.</param>
/// <returns>True if the purpose is known; otherwise, false.</returns>
public static bool IsKnown(string? purpose)
=> !string.IsNullOrWhiteSpace(purpose) && All.Contains(purpose);
}

View File

@@ -5,15 +5,112 @@ using System.Threading.Tasks;
namespace StellaOps.Cryptography;
/// <summary>
/// Interface for cryptographic hashing operations with compliance profile support.
/// </summary>
public interface ICryptoHash
{
#region Algorithm-based methods (backward compatible)
/// <summary>
/// Computes a hash using the specified or default algorithm.
/// </summary>
/// <param name="data">The data to hash.</param>
/// <param name="algorithmId">Optional algorithm identifier. If null, uses the default algorithm.</param>
/// <returns>The hash bytes.</returns>
byte[] ComputeHash(ReadOnlySpan<byte> data, string? algorithmId = null);
/// <summary>
/// Computes a hash and returns it as a lowercase hex string.
/// </summary>
string ComputeHashHex(ReadOnlySpan<byte> data, string? algorithmId = null);
/// <summary>
/// Computes a hash and returns it as a Base64 string.
/// </summary>
string ComputeHashBase64(ReadOnlySpan<byte> data, string? algorithmId = null);
/// <summary>
/// Computes a hash from a stream asynchronously.
/// </summary>
ValueTask<byte[]> ComputeHashAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default);
/// <summary>
/// Computes a hash from a stream and returns it as a lowercase hex string.
/// </summary>
ValueTask<string> ComputeHashHexAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default);
#endregion
#region Purpose-based methods (preferred for compliance)
/// <summary>
/// Computes a hash for the specified purpose using the active compliance profile's algorithm.
/// </summary>
/// <param name="data">The data to hash.</param>
/// <param name="purpose">The hash purpose from <see cref="HashPurpose"/>.</param>
/// <returns>The hash bytes.</returns>
byte[] ComputeHashForPurpose(ReadOnlySpan<byte> data, string purpose);
/// <summary>
/// Computes a hash for the specified purpose and returns it as a lowercase hex string.
/// </summary>
/// <param name="data">The data to hash.</param>
/// <param name="purpose">The hash purpose from <see cref="HashPurpose"/>.</param>
/// <returns>The hash as a lowercase hex string.</returns>
string ComputeHashHexForPurpose(ReadOnlySpan<byte> data, string purpose);
/// <summary>
/// Computes a hash for the specified purpose and returns it as a Base64 string.
/// </summary>
/// <param name="data">The data to hash.</param>
/// <param name="purpose">The hash purpose from <see cref="HashPurpose"/>.</param>
/// <returns>The hash as a Base64 string.</returns>
string ComputeHashBase64ForPurpose(ReadOnlySpan<byte> data, string purpose);
/// <summary>
/// Computes a hash for the specified purpose from a stream asynchronously.
/// </summary>
/// <param name="stream">The stream to hash.</param>
/// <param name="purpose">The hash purpose from <see cref="HashPurpose"/>.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The hash bytes.</returns>
ValueTask<byte[]> ComputeHashForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default);
/// <summary>
/// Computes a hash for the specified purpose from a stream and returns it as a lowercase hex string.
/// </summary>
/// <param name="stream">The stream to hash.</param>
/// <param name="purpose">The hash purpose from <see cref="HashPurpose"/>.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The hash as a lowercase hex string.</returns>
ValueTask<string> ComputeHashHexForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default);
#endregion
#region Metadata methods
/// <summary>
/// Gets the algorithm that will be used for the specified purpose based on the active compliance profile.
/// </summary>
/// <param name="purpose">The hash purpose from <see cref="HashPurpose"/>.</param>
/// <returns>The algorithm identifier.</returns>
string GetAlgorithmForPurpose(string purpose);
/// <summary>
/// Gets the hash prefix for the specified purpose (e.g., "blake3:", "sha256:", "gost3411:").
/// </summary>
/// <param name="purpose">The hash purpose from <see cref="HashPurpose"/>.</param>
/// <returns>The hash prefix string.</returns>
string GetHashPrefix(string purpose);
/// <summary>
/// Computes a hash for the specified purpose and returns it with the appropriate prefix.
/// </summary>
/// <param name="data">The data to hash.</param>
/// <param name="purpose">The hash purpose from <see cref="HashPurpose"/>.</param>
/// <returns>The prefixed hash string (e.g., "blake3:abc123...").</returns>
string ComputePrefixedHashForPurpose(ReadOnlySpan<byte> data, string purpose);
#endregion
}

View File

@@ -9,10 +9,12 @@
<PropertyGroup Condition="'$(StellaOpsCryptoSodium)' == 'true'">
<DefineConstants>$(DefineConstants);STELLAOPS_CRYPTO_SODIUM</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.14.0" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
</Project>
<ItemGroup>
<PackageReference Include="Blake3" Version="1.1.0" />
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.14.0" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
</Project>