Add post-quantum cryptography support with PqSoftCryptoProvider
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
wine-csp-build / Build Wine CSP Image (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
wine-csp-build / Build Wine CSP Image (push) Has been cancelled
- Implemented PqSoftCryptoProvider for software-only post-quantum algorithms (Dilithium3, Falcon512) using BouncyCastle. - Added PqSoftProviderOptions and PqSoftKeyOptions for configuration. - Created unit tests for Dilithium3 and Falcon512 signing and verification. - Introduced EcdsaPolicyCryptoProvider for compliance profiles (FIPS/eIDAS) with explicit allow-lists. - Added KcmvpHashOnlyProvider for KCMVP baseline compliance. - Updated project files and dependencies for new libraries and testing frameworks.
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Notifications;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for publishing policy profile notification events.
|
||||
/// </summary>
|
||||
public interface IPolicyProfileNotificationPublisher
|
||||
{
|
||||
/// <summary>
|
||||
/// Publishes a notification event to the configured transport.
|
||||
/// </summary>
|
||||
Task PublishAsync(PolicyProfileNotificationEvent notification, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Delivers a notification via webhook with HMAC signature.
|
||||
/// </summary>
|
||||
Task<bool> DeliverWebhookAsync(WebhookDeliveryRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logging-based notification publisher for policy profile events.
|
||||
/// Logs notifications as structured events for downstream consumption.
|
||||
/// </summary>
|
||||
internal sealed class LoggingPolicyProfileNotificationPublisher : IPolicyProfileNotificationPublisher
|
||||
{
|
||||
private readonly ILogger<LoggingPolicyProfileNotificationPublisher> _logger;
|
||||
private readonly PolicyProfileNotificationOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
public LoggingPolicyProfileNotificationPublisher(
|
||||
ILogger<LoggingPolicyProfileNotificationPublisher> logger,
|
||||
IOptions<PolicyProfileNotificationOptions> options,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options?.Value ?? new PolicyProfileNotificationOptions();
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task PublishAsync(PolicyProfileNotificationEvent notification, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(notification);
|
||||
|
||||
if (!_options.Enabled)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Policy profile notifications disabled; skipping event {EventId} type {EventType}",
|
||||
notification.EventId,
|
||||
notification.EventType);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var payload = JsonSerializer.Serialize(notification, JsonOptions);
|
||||
|
||||
_logger.LogInformation(
|
||||
"PolicyProfileNotification topic={Topic} event_id={EventId} event_type={EventType} tenant={TenantId} profile={ProfileId}@{ProfileVersion} payload={Payload}",
|
||||
_options.TopicName,
|
||||
notification.EventId,
|
||||
notification.EventType,
|
||||
notification.TenantId,
|
||||
notification.ProfileId,
|
||||
notification.ProfileVersion,
|
||||
payload);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<bool> DeliverWebhookAsync(WebhookDeliveryRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var payload = JsonSerializer.Serialize(request.Event, JsonOptions);
|
||||
var signature = ComputeHmacSignature(payload, request.SharedSecret);
|
||||
|
||||
_logger.LogInformation(
|
||||
"PolicyProfileWebhook url={Url} event_id={EventId} event_type={EventType} signature={Signature}",
|
||||
request.Url,
|
||||
request.Event.EventId,
|
||||
request.Event.EventType,
|
||||
signature ?? "(no secret)");
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
private static string? ComputeHmacSignature(string payload, string? sharedSecret)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sharedSecret))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var keyBytes = Encoding.UTF8.GetBytes(sharedSecret);
|
||||
var payloadBytes = Encoding.UTF8.GetBytes(payload);
|
||||
|
||||
using var hmac = new HMACSHA256(keyBytes);
|
||||
var hashBytes = hmac.ComputeHash(payloadBytes);
|
||||
return Convert.ToHexStringLower(hashBytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating policy profile notification events.
|
||||
/// </summary>
|
||||
public sealed class PolicyProfileNotificationFactory
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly PolicyProfileNotificationOptions _options;
|
||||
|
||||
public PolicyProfileNotificationFactory(
|
||||
TimeProvider? timeProvider = null,
|
||||
PolicyProfileNotificationOptions? options = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_options = options ?? new PolicyProfileNotificationOptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a profile created notification event.
|
||||
/// </summary>
|
||||
public PolicyProfileNotificationEvent CreateProfileCreatedEvent(
|
||||
string tenantId,
|
||||
string profileId,
|
||||
string profileVersion,
|
||||
string? actorId,
|
||||
string? hash,
|
||||
NotificationEffectiveScope? scope = null)
|
||||
{
|
||||
return CreateEvent(
|
||||
PolicyProfileNotificationEventTypes.ProfileCreated,
|
||||
tenantId,
|
||||
profileId,
|
||||
profileVersion,
|
||||
"New profile draft created",
|
||||
actorId,
|
||||
hash,
|
||||
scope: scope);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a profile activated notification event.
|
||||
/// </summary>
|
||||
public PolicyProfileNotificationEvent CreateProfileActivatedEvent(
|
||||
string tenantId,
|
||||
string profileId,
|
||||
string profileVersion,
|
||||
string? actorId,
|
||||
string? hash,
|
||||
NotificationEffectiveScope? scope = null)
|
||||
{
|
||||
return CreateEvent(
|
||||
PolicyProfileNotificationEventTypes.ProfileActivated,
|
||||
tenantId,
|
||||
profileId,
|
||||
profileVersion,
|
||||
"Profile version activated",
|
||||
actorId,
|
||||
hash,
|
||||
scope: scope);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a profile deactivated notification event.
|
||||
/// </summary>
|
||||
public PolicyProfileNotificationEvent CreateProfileDeactivatedEvent(
|
||||
string tenantId,
|
||||
string profileId,
|
||||
string profileVersion,
|
||||
string? actorId,
|
||||
string? reason,
|
||||
string? hash)
|
||||
{
|
||||
return CreateEvent(
|
||||
PolicyProfileNotificationEventTypes.ProfileDeactivated,
|
||||
tenantId,
|
||||
profileId,
|
||||
profileVersion,
|
||||
reason ?? "Profile version deactivated",
|
||||
actorId,
|
||||
hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a threshold changed notification event.
|
||||
/// </summary>
|
||||
public PolicyProfileNotificationEvent CreateThresholdChangedEvent(
|
||||
string tenantId,
|
||||
string profileId,
|
||||
string profileVersion,
|
||||
string? actorId,
|
||||
string? reason,
|
||||
NotificationThresholds thresholds,
|
||||
string? hash,
|
||||
NotificationEffectiveScope? scope = null)
|
||||
{
|
||||
return CreateEvent(
|
||||
PolicyProfileNotificationEventTypes.ThresholdChanged,
|
||||
tenantId,
|
||||
profileId,
|
||||
profileVersion,
|
||||
reason ?? "Risk thresholds updated",
|
||||
actorId,
|
||||
hash,
|
||||
thresholds: thresholds,
|
||||
scope: scope);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an override added notification event.
|
||||
/// </summary>
|
||||
public PolicyProfileNotificationEvent CreateOverrideAddedEvent(
|
||||
string tenantId,
|
||||
string profileId,
|
||||
string profileVersion,
|
||||
string? actorId,
|
||||
NotificationOverrideDetails overrideDetails,
|
||||
string? hash)
|
||||
{
|
||||
return CreateEvent(
|
||||
PolicyProfileNotificationEventTypes.OverrideAdded,
|
||||
tenantId,
|
||||
profileId,
|
||||
profileVersion,
|
||||
$"Override added: {overrideDetails.OverrideType}",
|
||||
actorId,
|
||||
hash,
|
||||
overrideDetails: overrideDetails);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an override removed notification event.
|
||||
/// </summary>
|
||||
public PolicyProfileNotificationEvent CreateOverrideRemovedEvent(
|
||||
string tenantId,
|
||||
string profileId,
|
||||
string profileVersion,
|
||||
string? actorId,
|
||||
NotificationOverrideDetails overrideDetails,
|
||||
string? hash)
|
||||
{
|
||||
return CreateEvent(
|
||||
PolicyProfileNotificationEventTypes.OverrideRemoved,
|
||||
tenantId,
|
||||
profileId,
|
||||
profileVersion,
|
||||
$"Override removed: {overrideDetails.OverrideId}",
|
||||
actorId,
|
||||
hash,
|
||||
overrideDetails: overrideDetails);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a simulation ready notification event.
|
||||
/// </summary>
|
||||
public PolicyProfileNotificationEvent CreateSimulationReadyEvent(
|
||||
string tenantId,
|
||||
string profileId,
|
||||
string profileVersion,
|
||||
NotificationSimulationDetails simulationDetails,
|
||||
string? hash)
|
||||
{
|
||||
return CreateEvent(
|
||||
PolicyProfileNotificationEventTypes.SimulationReady,
|
||||
tenantId,
|
||||
profileId,
|
||||
profileVersion,
|
||||
"Simulation results available",
|
||||
actorId: null,
|
||||
hash,
|
||||
simulationDetails: simulationDetails);
|
||||
}
|
||||
|
||||
private PolicyProfileNotificationEvent CreateEvent(
|
||||
string eventType,
|
||||
string tenantId,
|
||||
string profileId,
|
||||
string profileVersion,
|
||||
string changeReason,
|
||||
string? actorId,
|
||||
string? hash,
|
||||
NotificationThresholds? thresholds = null,
|
||||
NotificationEffectiveScope? scope = null,
|
||||
NotificationOverrideDetails? overrideDetails = null,
|
||||
NotificationSimulationDetails? simulationDetails = null)
|
||||
{
|
||||
var eventId = GenerateUuidV7();
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
NotificationActor? actor = null;
|
||||
if (!string.IsNullOrWhiteSpace(actorId))
|
||||
{
|
||||
actor = new NotificationActor
|
||||
{
|
||||
Type = actorId.Contains('@') ? "user" : "system",
|
||||
Id = actorId
|
||||
};
|
||||
}
|
||||
|
||||
NotificationHash? hashInfo = null;
|
||||
if (!string.IsNullOrWhiteSpace(hash))
|
||||
{
|
||||
hashInfo = new NotificationHash
|
||||
{
|
||||
Algorithm = "sha256",
|
||||
Value = hash
|
||||
};
|
||||
}
|
||||
|
||||
NotificationLinks? links = null;
|
||||
if (!string.IsNullOrWhiteSpace(_options.BaseUrl))
|
||||
{
|
||||
links = new NotificationLinks
|
||||
{
|
||||
ProfileUrl = $"{_options.BaseUrl}/api/risk/profiles/{profileId}",
|
||||
DiffUrl = $"{_options.BaseUrl}/api/risk/profiles/{profileId}/diff",
|
||||
SimulationUrl = simulationDetails?.SimulationId is not null
|
||||
? $"{_options.BaseUrl}/api/risk/simulations/results/{simulationDetails.SimulationId}"
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
NotificationTraceContext? trace = null;
|
||||
if (_options.IncludeTraceContext)
|
||||
{
|
||||
var activity = Activity.Current;
|
||||
if (activity is not null)
|
||||
{
|
||||
trace = new NotificationTraceContext
|
||||
{
|
||||
TraceId = activity.TraceId.ToString(),
|
||||
SpanId = activity.SpanId.ToString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return new PolicyProfileNotificationEvent
|
||||
{
|
||||
EventId = eventId,
|
||||
EventType = eventType,
|
||||
EmittedAt = now,
|
||||
TenantId = tenantId,
|
||||
ProfileId = profileId,
|
||||
ProfileVersion = profileVersion,
|
||||
ChangeReason = changeReason,
|
||||
Actor = actor,
|
||||
Thresholds = thresholds,
|
||||
EffectiveScope = scope,
|
||||
Hash = hashInfo,
|
||||
Links = links,
|
||||
Trace = trace,
|
||||
OverrideDetails = overrideDetails,
|
||||
SimulationDetails = simulationDetails
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a UUIDv7 (time-ordered UUID) for event identification.
|
||||
/// </summary>
|
||||
private string GenerateUuidV7()
|
||||
{
|
||||
var timestamp = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds();
|
||||
var randomBytes = new byte[10];
|
||||
RandomNumberGenerator.Fill(randomBytes);
|
||||
|
||||
var bytes = new byte[16];
|
||||
|
||||
// First 6 bytes: timestamp (48 bits)
|
||||
bytes[0] = (byte)((timestamp >> 40) & 0xFF);
|
||||
bytes[1] = (byte)((timestamp >> 32) & 0xFF);
|
||||
bytes[2] = (byte)((timestamp >> 24) & 0xFF);
|
||||
bytes[3] = (byte)((timestamp >> 16) & 0xFF);
|
||||
bytes[4] = (byte)((timestamp >> 8) & 0xFF);
|
||||
bytes[5] = (byte)(timestamp & 0xFF);
|
||||
|
||||
// Version 7 (4 bits) + random (12 bits)
|
||||
bytes[6] = (byte)(0x70 | (randomBytes[0] & 0x0F));
|
||||
bytes[7] = randomBytes[1];
|
||||
|
||||
// Variant (2 bits) + random (62 bits)
|
||||
bytes[8] = (byte)(0x80 | (randomBytes[2] & 0x3F));
|
||||
Array.Copy(randomBytes, 3, bytes, 9, 7);
|
||||
|
||||
return new Guid(bytes).ToString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user