consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -1,20 +1,46 @@
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options;
using StellaOps.Telemetry.Federation.Security;
namespace StellaOps.Telemetry.Federation.Consent;
public sealed class ConsentManager : IConsentManager
{
private readonly ConcurrentDictionary<string, ConsentEntry> _consents = new();
private readonly FederatedTelemetryOptions _options;
private readonly IFederationDsseEnvelopeSigner _envelopeSigner;
private readonly IFederationDsseEnvelopeVerifier _envelopeVerifier;
private readonly TimeProvider _timeProvider;
public ConsentManager(TimeProvider? timeProvider = null)
public ConsentManager(
IOptions<FederatedTelemetryOptions> options,
IFederationDsseEnvelopeSigner envelopeSigner,
IFederationDsseEnvelopeVerifier envelopeVerifier,
TimeProvider? timeProvider = null)
{
ArgumentNullException.ThrowIfNull(options);
_options = options.Value;
_envelopeSigner = envelopeSigner ?? throw new ArgumentNullException(nameof(envelopeSigner));
_envelopeVerifier = envelopeVerifier ?? throw new ArgumentNullException(nameof(envelopeVerifier));
_timeProvider = timeProvider ?? TimeProvider.System;
}
public ConsentManager(IOptions<FederatedTelemetryOptions> options, TimeProvider? timeProvider = null)
: this(
options,
new HmacFederationDsseEnvelopeService(options),
new HmacFederationDsseEnvelopeService(options),
timeProvider)
{
}
public ConsentManager(TimeProvider? timeProvider = null)
: this(Options.Create(new FederatedTelemetryOptions()), timeProvider)
{
}
public Task<ConsentState> GetConsentStateAsync(string tenantId, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
@@ -26,7 +52,8 @@ public sealed class ConsentManager : IConsentManager
GrantedBy: null,
GrantedAt: null,
ExpiresAt: null,
DsseDigest: null));
DsseDigest: null,
SignerKeyId: null));
}
var now = _timeProvider.GetUtcNow();
@@ -38,7 +65,8 @@ public sealed class ConsentManager : IConsentManager
GrantedBy: null,
GrantedAt: null,
ExpiresAt: null,
DsseDigest: null));
DsseDigest: null,
SignerKeyId: null));
}
return Task.FromResult(new ConsentState(
@@ -46,10 +74,11 @@ public sealed class ConsentManager : IConsentManager
GrantedBy: entry.GrantedBy,
GrantedAt: entry.GrantedAt,
ExpiresAt: entry.ExpiresAt,
DsseDigest: entry.DsseDigest));
DsseDigest: entry.DsseDigest,
SignerKeyId: entry.SignerKeyId));
}
public Task<ConsentProof> GrantConsentAsync(
public async Task<ConsentProof> GrantConsentAsync(
string tenantId,
string grantedBy,
TimeSpan? ttl = null,
@@ -60,28 +89,101 @@ public sealed class ConsentManager : IConsentManager
var now = _timeProvider.GetUtcNow();
var expiresAt = ttl.HasValue ? now + ttl.Value : (DateTimeOffset?)null;
var payload = JsonSerializer.SerializeToUtf8Bytes(new
{
tenantId,
grantedBy,
grantedAt = now,
expiresAt,
type = "stella.ops/federatedConsent@v1"
});
var digest = ComputeDigest(payload);
var envelope = payload; // Placeholder: real DSSE envelope wraps with signature
var entry = new ConsentEntry(tenantId, grantedBy, now, expiresAt, digest);
_consents[tenantId] = entry;
return Task.FromResult(new ConsentProof(
var payload = JsonSerializer.SerializeToUtf8Bytes(new ConsentPayloadDocument(
TenantId: tenantId,
GrantedBy: grantedBy,
GrantedAt: now,
ExpiresAt: expiresAt,
DsseDigest: digest,
Envelope: envelope));
Type: _options.ConsentPredicateType));
FederationDsseEnvelopeSignResult signResult;
try
{
signResult = await _envelopeSigner
.SignAsync(_options.ConsentPredicateType, payload, ct)
.ConfigureAwait(false);
}
catch (FederationSignatureException)
{
throw;
}
catch (Exception ex)
{
throw new FederationSignatureException(
"federation.dsse.sign_failed",
"Consent proof could not be DSSE-signed.",
ex);
}
var entry = new ConsentEntry(tenantId, grantedBy, now, expiresAt, signResult.EnvelopeDigest, signResult.SignerKeyId);
_consents[tenantId] = entry;
return new ConsentProof(
TenantId: tenantId,
GrantedBy: grantedBy,
GrantedAt: now,
ExpiresAt: expiresAt,
DsseDigest: signResult.EnvelopeDigest,
Envelope: signResult.Envelope,
SignerKeyId: signResult.SignerKeyId);
}
public async Task<bool> VerifyProofAsync(ConsentProof proof, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
ArgumentNullException.ThrowIfNull(proof);
var envelopeDigest = HmacFederationDsseEnvelopeService.ComputeDigest(proof.Envelope);
if (!string.Equals(envelopeDigest, proof.DsseDigest, StringComparison.Ordinal))
{
return false;
}
var verifyResult = await _envelopeVerifier
.VerifyAsync(proof.Envelope, _options.ConsentPredicateType, ct)
.ConfigureAwait(false);
if (!verifyResult.IsValid || verifyResult.Payload is null)
{
return false;
}
ConsentPayloadDocument? payload;
try
{
payload = JsonSerializer.Deserialize<ConsentPayloadDocument>(verifyResult.Payload);
}
catch (JsonException)
{
return false;
}
if (payload is null)
{
return false;
}
if (!string.Equals(payload.TenantId, proof.TenantId, StringComparison.Ordinal) ||
!string.Equals(payload.GrantedBy, proof.GrantedBy, StringComparison.Ordinal) ||
payload.GrantedAt != proof.GrantedAt ||
payload.ExpiresAt != proof.ExpiresAt ||
!string.Equals(payload.Type, _options.ConsentPredicateType, StringComparison.Ordinal))
{
return false;
}
if (!string.IsNullOrWhiteSpace(proof.SignerKeyId) &&
!string.Equals(proof.SignerKeyId, verifyResult.SignerKeyId, StringComparison.Ordinal))
{
return false;
}
var now = _timeProvider.GetUtcNow();
if (payload.ExpiresAt.HasValue && now >= payload.ExpiresAt.Value)
{
return false;
}
return true;
}
public Task RevokeConsentAsync(string tenantId, string revokedBy, CancellationToken ct = default)
@@ -91,16 +193,18 @@ public sealed class ConsentManager : IConsentManager
return Task.CompletedTask;
}
private static string ComputeDigest(byte[] payload)
{
var hash = SHA256.HashData(payload);
return $"sha256:{Convert.ToHexStringLower(hash)}";
}
private sealed record ConsentEntry(
string TenantId,
string GrantedBy,
DateTimeOffset GrantedAt,
DateTimeOffset? ExpiresAt,
string DsseDigest);
string DsseDigest,
string SignerKeyId);
private sealed record ConsentPayloadDocument(
[property: JsonPropertyName("tenantId")] string TenantId,
[property: JsonPropertyName("grantedBy")] string GrantedBy,
[property: JsonPropertyName("grantedAt")] DateTimeOffset GrantedAt,
[property: JsonPropertyName("expiresAt")] DateTimeOffset? ExpiresAt,
[property: JsonPropertyName("type")] string Type);
}

View File

@@ -4,6 +4,7 @@ public interface IConsentManager
{
Task<ConsentState> GetConsentStateAsync(string tenantId, CancellationToken ct = default);
Task<ConsentProof> GrantConsentAsync(string tenantId, string grantedBy, TimeSpan? ttl = null, CancellationToken ct = default);
Task<bool> VerifyProofAsync(ConsentProof proof, CancellationToken ct = default);
Task RevokeConsentAsync(string tenantId, string revokedBy, CancellationToken ct = default);
}
@@ -12,7 +13,8 @@ public sealed record ConsentState(
string? GrantedBy,
DateTimeOffset? GrantedAt,
DateTimeOffset? ExpiresAt,
string? DsseDigest);
string? DsseDigest,
string? SignerKeyId = null);
public sealed record ConsentProof(
string TenantId,
@@ -20,4 +22,5 @@ public sealed record ConsentProof(
DateTimeOffset GrantedAt,
DateTimeOffset? ExpiresAt,
string DsseDigest,
byte[] Envelope);
byte[] Envelope,
string? SignerKeyId = null);