Add unit tests for RabbitMq and Udp transport servers and clients
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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:
96
src/__Libraries/StellaOps.Cryptography/ComplianceProfile.cs
Normal file
96
src/__Libraries/StellaOps.Cryptography/ComplianceProfile.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
252
src/__Libraries/StellaOps.Cryptography/ComplianceProfiles.cs
Normal file
252
src/__Libraries/StellaOps.Cryptography/ComplianceProfiles.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
74
src/__Libraries/StellaOps.Cryptography/HashPurpose.cs
Normal file
74
src/__Libraries/StellaOps.Cryptography/HashPurpose.cs
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user