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,4 +1,9 @@
using System.Text;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Options;
using StellaOps.Telemetry.Federation;
using StellaOps.Telemetry.Federation.Consent;
using StellaOps.Telemetry.Federation.Security;
namespace StellaOps.Telemetry.Federation.Tests;
@@ -7,7 +12,7 @@ public sealed class ConsentManagerTests
[Fact]
public async Task Default_consent_state_is_not_granted()
{
var manager = new ConsentManager();
var manager = CreateManager();
var state = await manager.GetConsentStateAsync("tenant-1");
@@ -16,30 +21,115 @@ public sealed class ConsentManagerTests
Assert.Null(state.GrantedAt);
Assert.Null(state.ExpiresAt);
Assert.Null(state.DsseDigest);
Assert.Null(state.SignerKeyId);
}
[Fact]
public async Task Grant_consent_sets_granted_state()
public async Task Grant_consent_sets_granted_state_and_signer_metadata()
{
var manager = new ConsentManager();
var manager = CreateManager();
var proof = await manager.GrantConsentAsync("tenant-1", "admin@example.com");
Assert.Equal("tenant-1", proof.TenantId);
Assert.Equal("admin@example.com", proof.GrantedBy);
Assert.NotNull(proof.DsseDigest);
Assert.StartsWith("sha256:", proof.DsseDigest);
Assert.StartsWith("sha256:", proof.DsseDigest, StringComparison.Ordinal);
Assert.Equal("consent-key", proof.SignerKeyId);
Assert.NotEmpty(proof.Envelope);
var state = await manager.GetConsentStateAsync("tenant-1");
Assert.True(state.Granted);
Assert.Equal("admin@example.com", state.GrantedBy);
Assert.Equal("consent-key", state.SignerKeyId);
}
[Fact]
public async Task VerifyProofAsync_succeeds_for_valid_proof()
{
var manager = CreateManager();
var proof = await manager.GrantConsentAsync("tenant-1", "admin@example.com");
var valid = await manager.VerifyProofAsync(proof);
Assert.True(valid);
}
[Fact]
public async Task VerifyProofAsync_fails_for_payload_tampering()
{
var manager = CreateManager();
var proof = await manager.GrantConsentAsync("tenant-1", "admin@example.com");
var tamperedEnvelope = TamperEnvelopePayload(proof.Envelope);
var valid = await manager.VerifyProofAsync(proof with { Envelope = tamperedEnvelope });
Assert.False(valid);
}
[Fact]
public async Task VerifyProofAsync_fails_for_signature_tampering()
{
var manager = CreateManager();
var proof = await manager.GrantConsentAsync("tenant-1", "admin@example.com");
var tamperedEnvelope = TamperEnvelopeSignature(proof.Envelope);
var valid = await manager.VerifyProofAsync(proof with { Envelope = tamperedEnvelope });
Assert.False(valid);
}
[Fact]
public async Task VerifyProofAsync_fails_for_wrong_key_verifier()
{
var signerOptions = CreateOptions("consent-key", "sign-secret", ("consent-key", "sign-secret"));
var verifierOptions = CreateOptions("consent-key", "verify-secret", ("consent-key", "wrong-secret"));
var manager = new ConsentManager(
signerOptions,
new HmacFederationDsseEnvelopeService(signerOptions),
new HmacFederationDsseEnvelopeService(verifierOptions));
var proof = await manager.GrantConsentAsync("tenant-1", "admin@example.com");
var valid = await manager.VerifyProofAsync(proof);
Assert.False(valid);
}
[Fact]
public async Task VerifyProofAsync_fails_when_signature_valid_but_consent_expired()
{
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2026, 03, 04, 12, 0, 0, TimeSpan.Zero));
var manager = CreateManager(timeProvider: fakeTime);
var proof = await manager.GrantConsentAsync("tenant-1", "admin@example.com", ttl: TimeSpan.FromMinutes(30));
Assert.True(await manager.VerifyProofAsync(proof));
fakeTime.Advance(TimeSpan.FromHours(1));
var afterExpiry = await manager.VerifyProofAsync(proof);
Assert.False(afterExpiry);
}
[Fact]
public async Task Grant_with_fixed_clock_and_key_material_produces_deterministic_digest()
{
var fixedNow = new DateTimeOffset(2026, 03, 04, 15, 0, 0, TimeSpan.Zero);
var fakeTime = new FakeTimeProvider(fixedNow);
var manager = CreateManager(timeProvider: fakeTime);
var proof1 = await manager.GrantConsentAsync("tenant-1", "admin@example.com");
var proof2 = await manager.GrantConsentAsync("tenant-1", "admin@example.com");
Assert.Equal(proof1.DsseDigest, proof2.DsseDigest);
Assert.Equal(proof1.Envelope, proof2.Envelope);
Assert.Equal(proof1.SignerKeyId, proof2.SignerKeyId);
}
[Fact]
public async Task Revoke_consent_clears_state()
{
var manager = new ConsentManager();
var manager = CreateManager();
await manager.GrantConsentAsync("tenant-1", "admin@example.com");
await manager.RevokeConsentAsync("tenant-1", "admin@example.com");
@@ -48,42 +138,10 @@ public sealed class ConsentManagerTests
Assert.False(state.Granted);
}
[Fact]
public async Task TTL_expiry_revokes_consent()
{
var fakeTime = new FakeTimeProvider(DateTimeOffset.UtcNow);
var manager = new ConsentManager(fakeTime);
await manager.GrantConsentAsync("tenant-1", "admin@example.com", ttl: TimeSpan.FromHours(1));
var stateBefore = await manager.GetConsentStateAsync("tenant-1");
Assert.True(stateBefore.Granted);
// Advance time past TTL
fakeTime.Advance(TimeSpan.FromHours(2));
var stateAfter = await manager.GetConsentStateAsync("tenant-1");
Assert.False(stateAfter.Granted);
}
[Fact]
public async Task Grant_without_TTL_has_no_expiry()
{
var manager = new ConsentManager();
var proof = await manager.GrantConsentAsync("tenant-1", "admin@example.com");
Assert.Null(proof.ExpiresAt);
var state = await manager.GetConsentStateAsync("tenant-1");
Assert.True(state.Granted);
Assert.Null(state.ExpiresAt);
}
[Fact]
public async Task Multiple_tenants_independent()
{
var manager = new ConsentManager();
var manager = CreateManager();
await manager.GrantConsentAsync("tenant-1", "admin1@example.com");
await manager.GrantConsentAsync("tenant-2", "admin2@example.com");
@@ -100,9 +158,11 @@ public sealed class ConsentManagerTests
[Fact]
public async Task Grant_overwrites_previous_consent()
{
var manager = new ConsentManager();
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2026, 03, 04, 9, 0, 0, TimeSpan.Zero));
var manager = CreateManager(timeProvider: fakeTime);
var proof1 = await manager.GrantConsentAsync("tenant-1", "admin@example.com");
fakeTime.Advance(TimeSpan.FromSeconds(5));
var proof2 = await manager.GrantConsentAsync("tenant-1", "newadmin@example.com");
Assert.NotEqual(proof1.DsseDigest, proof2.DsseDigest);
@@ -110,6 +170,59 @@ public sealed class ConsentManagerTests
var state = await manager.GetConsentStateAsync("tenant-1");
Assert.Equal("newadmin@example.com", state.GrantedBy);
}
private static ConsentManager CreateManager(
IOptions<FederatedTelemetryOptions>? options = null,
TimeProvider? timeProvider = null,
IFederationDsseEnvelopeSigner? signer = null,
IFederationDsseEnvelopeVerifier? verifier = null)
{
options ??= CreateOptions("consent-key", "consent-secret", ("consent-key", "consent-secret"));
signer ??= new HmacFederationDsseEnvelopeService(options);
verifier ??= new HmacFederationDsseEnvelopeService(options);
return new ConsentManager(options, signer, verifier, timeProvider);
}
private static IOptions<FederatedTelemetryOptions> CreateOptions(
string signerKeyId,
string signerSecret,
params (string KeyId, string Secret)[] trustedSecrets)
{
var options = new FederatedTelemetryOptions
{
ConsentPredicateType = "stella.ops/federatedConsent@v1",
DsseSignerKeyId = signerKeyId,
DsseSignerSecret = signerSecret
};
options.DsseTrustedKeys.Clear();
foreach (var trusted in trustedSecrets)
{
options.DsseTrustedKeys[trusted.KeyId] = trusted.Secret;
}
return Options.Create(options);
}
private static byte[] TamperEnvelopePayload(byte[] envelope)
{
var document = JsonNode.Parse(envelope)!.AsObject();
var payload = Convert.FromBase64String(document["payload"]!.GetValue<string>());
payload[0] ^= 0x01;
document["payload"] = Convert.ToBase64String(payload);
return Encoding.UTF8.GetBytes(document.ToJsonString());
}
private static byte[] TamperEnvelopeSignature(byte[] envelope)
{
var document = JsonNode.Parse(envelope)!.AsObject();
var signatureNode = document["signatures"]!.AsArray()[0]!.AsObject();
var signature = signatureNode["sig"]!.GetValue<string>();
signatureNode["sig"] = signature[^1] == 'A'
? $"{signature[..^1]}B"
: $"{signature[..^1]}A";
return Encoding.UTF8.GetBytes(document.ToJsonString());
}
}
/// <summary>

View File

@@ -1,59 +1,26 @@
using System.Text;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Options;
using StellaOps.Telemetry.Federation;
using StellaOps.Telemetry.Federation.Aggregation;
using StellaOps.Telemetry.Federation.Bundles;
using StellaOps.Telemetry.Federation.Consent;
using StellaOps.Telemetry.Federation.Security;
namespace StellaOps.Telemetry.Federation.Tests;
public sealed class FederatedTelemetryBundleBuilderTests
{
private static FederatedTelemetryBundleBuilder CreateBuilder(string siteId = "test-site")
{
var options = Options.Create(new FederatedTelemetryOptions
{
SiteId = siteId,
BundlePredicateType = "stella.ops/federatedTelemetry@v1"
});
return new FederatedTelemetryBundleBuilder(options);
}
private static AggregationResult CreateAggregation()
{
return new AggregationResult(
Buckets:
[
new AggregationBucket("CVE-2024-0001", 10, 5, 10.3, false),
new AggregationBucket("CVE-2024-0002", 3, 2, 0, true),
],
TotalFacts: 13,
SuppressedBuckets: 1,
EpsilonSpent: 0.5,
AggregatedAt: DateTimeOffset.UtcNow);
}
private static ConsentProof CreateConsentProof()
{
return new ConsentProof(
TenantId: "tenant-1",
GrantedBy: "admin@example.com",
GrantedAt: DateTimeOffset.UtcNow,
ExpiresAt: null,
DsseDigest: "sha256:abc123",
Envelope: new byte[] { 1, 2, 3 });
}
[Fact]
public async Task Build_creates_bundle_with_correct_site_id()
{
var builder = CreateBuilder("my-site");
var aggregation = CreateAggregation();
var consent = CreateConsentProof();
var builder = CreateBuilder(siteId: "my-site");
var bundle = await builder.BuildAsync(aggregation, consent);
var bundle = await builder.BuildAsync(CreateAggregation(), CreateConsentProof());
Assert.Equal("my-site", bundle.SourceSiteId);
Assert.NotEqual(Guid.Empty, bundle.Id);
Assert.Equal("bundle-key", bundle.SignerKeyId);
}
[Fact]
@@ -68,13 +35,13 @@ public sealed class FederatedTelemetryBundleBuilderTests
}
[Fact]
public async Task Build_produces_valid_dsse_digest()
public async Task Build_produces_signed_envelope_digest()
{
var builder = CreateBuilder();
var bundle = await builder.BuildAsync(CreateAggregation(), CreateConsentProof());
Assert.StartsWith("sha256:", bundle.BundleDsseDigest);
Assert.StartsWith("sha256:", bundle.BundleDsseDigest, StringComparison.Ordinal);
Assert.NotEmpty(bundle.Envelope);
}
@@ -82,33 +49,132 @@ public sealed class FederatedTelemetryBundleBuilderTests
public async Task Verify_succeeds_for_unmodified_bundle()
{
var builder = CreateBuilder();
var bundle = await builder.BuildAsync(CreateAggregation(), CreateConsentProof());
var isValid = await builder.VerifyAsync(bundle);
Assert.True(isValid);
}
[Fact]
public async Task Verify_fails_for_tampered_bundle()
public async Task Verify_fails_for_payload_tampering_even_when_digest_is_recomputed()
{
var builder = CreateBuilder();
var bundle = await builder.BuildAsync(CreateAggregation(), CreateConsentProof());
// Tamper with the envelope
var tampered = bundle with
var tamperedEnvelope = TamperEnvelopePayload(bundle.Envelope);
var tamperedBundle = bundle with
{
Envelope = new byte[] { 0xFF, 0xFE, 0xFD }
Envelope = tamperedEnvelope,
BundleDsseDigest = HmacFederationDsseEnvelopeService.ComputeDigest(tamperedEnvelope)
};
var isValid = await builder.VerifyAsync(tampered);
var isValid = await builder.VerifyAsync(tamperedBundle);
Assert.False(isValid);
}
[Fact]
public async Task Build_includes_aggregation_data()
public async Task Verify_fails_for_signature_tampering_even_when_digest_is_recomputed()
{
var builder = CreateBuilder();
var bundle = await builder.BuildAsync(CreateAggregation(), CreateConsentProof());
var tamperedEnvelope = TamperEnvelopeSignature(bundle.Envelope);
var tamperedBundle = bundle with
{
Envelope = tamperedEnvelope,
BundleDsseDigest = HmacFederationDsseEnvelopeService.ComputeDigest(tamperedEnvelope)
};
var isValid = await builder.VerifyAsync(tamperedBundle);
Assert.False(isValid);
}
[Fact]
public async Task Verify_fails_for_wrong_key_verification()
{
var signerOptions = CreateOptions("bundle-key", "bundle-secret", ("bundle-key", "bundle-secret"));
var verifierOptions = CreateOptions("bundle-key", "bundle-secret", ("bundle-key", "different-secret"));
var builder = new FederatedTelemetryBundleBuilder(
signerOptions,
new HmacFederationDsseEnvelopeService(signerOptions),
new HmacFederationDsseEnvelopeService(verifierOptions));
var bundle = await builder.BuildAsync(CreateAggregation(), CreateConsentProof());
var isValid = await builder.VerifyAsync(bundle);
Assert.False(isValid);
}
[Fact]
public async Task Verify_fails_when_consent_digest_linkage_is_mismatched()
{
var builder = CreateBuilder();
var bundle = await builder.BuildAsync(CreateAggregation(), CreateConsentProof());
var mismatch = bundle with { ConsentDsseDigest = "sha256:not-the-original-consent" };
var isValid = await builder.VerifyAsync(mismatch);
Assert.False(isValid);
}
[Fact]
public async Task Build_replay_with_fixed_clock_and_key_is_digest_deterministic()
{
var fixedTime = new FakeTimeProvider(new DateTimeOffset(2026, 03, 04, 16, 0, 0, TimeSpan.Zero));
var builder = CreateBuilder(timeProvider: fixedTime);
var aggregation = CreateAggregation();
var consent = CreateConsentProof();
var bundle1 = await builder.BuildAsync(aggregation, consent);
var bundle2 = await builder.BuildAsync(aggregation, consent);
Assert.Equal(bundle1.Id, bundle2.Id);
Assert.Equal(bundle1.BundleDsseDigest, bundle2.BundleDsseDigest);
Assert.Equal(bundle1.Envelope, bundle2.Envelope);
}
[Fact]
public async Task Build_canonicalizes_bucket_order_for_identical_logical_inputs()
{
var fixedTime = new FakeTimeProvider(new DateTimeOffset(2026, 03, 04, 16, 30, 0, TimeSpan.Zero));
var builder = CreateBuilder(timeProvider: fixedTime);
var consent = CreateConsentProof();
var aggregationA = new AggregationResult(
Buckets:
[
new AggregationBucket("CVE-2024-7777", 9, 4, 9.1, false),
new AggregationBucket("CVE-2024-0001", 10, 5, 10.3, false),
new AggregationBucket("CVE-2024-0002", 3, 2, 0, true),
],
TotalFacts: 19,
SuppressedBuckets: 1,
EpsilonSpent: 0.7,
AggregatedAt: new DateTimeOffset(2026, 03, 04, 15, 59, 0, TimeSpan.Zero));
var aggregationB = aggregationA with
{
Buckets =
[
new AggregationBucket("CVE-2024-0001", 10, 5, 10.3, false),
new AggregationBucket("CVE-2024-0002", 3, 2, 0, true),
new AggregationBucket("CVE-2024-7777", 9, 4, 9.1, false),
]
};
var bundleA = await builder.BuildAsync(aggregationA, consent);
var bundleB = await builder.BuildAsync(aggregationB, consent);
Assert.Equal(bundleA.BundleDsseDigest, bundleB.BundleDsseDigest);
Assert.Equal(bundleA.Envelope, bundleB.Envelope);
}
[Fact]
public async Task Build_includes_aggregation_data_reference()
{
var builder = CreateBuilder();
var aggregation = CreateAggregation();
@@ -119,14 +185,89 @@ public sealed class FederatedTelemetryBundleBuilderTests
}
[Fact]
public async Task Build_sets_creation_timestamp()
public async Task Build_sets_creation_timestamp_from_time_provider()
{
var builder = CreateBuilder();
var now = new DateTimeOffset(2026, 03, 04, 18, 0, 0, TimeSpan.Zero);
var builder = CreateBuilder(timeProvider: new FakeTimeProvider(now));
var before = DateTimeOffset.UtcNow;
var bundle = await builder.BuildAsync(CreateAggregation(), CreateConsentProof());
var after = DateTimeOffset.UtcNow;
Assert.InRange(bundle.CreatedAt, before, after);
Assert.Equal(now, bundle.CreatedAt);
}
private static FederatedTelemetryBundleBuilder CreateBuilder(string siteId = "test-site", TimeProvider? timeProvider = null)
{
var options = CreateOptions("bundle-key", "bundle-secret", ("bundle-key", "bundle-secret"), siteId);
return new FederatedTelemetryBundleBuilder(
options,
new HmacFederationDsseEnvelopeService(options),
new HmacFederationDsseEnvelopeService(options),
timeProvider);
}
private static IOptions<FederatedTelemetryOptions> CreateOptions(
string signerKeyId,
string signerSecret,
(string KeyId, string Secret) trustedKey,
string siteId = "test-site")
{
var options = new FederatedTelemetryOptions
{
SiteId = siteId,
BundlePredicateType = "stella.ops/federatedTelemetry@v1",
DsseSignerKeyId = signerKeyId,
DsseSignerSecret = signerSecret
};
options.DsseTrustedKeys.Clear();
options.DsseTrustedKeys[trustedKey.KeyId] = trustedKey.Secret;
return Options.Create(options);
}
private static AggregationResult CreateAggregation()
{
return new AggregationResult(
Buckets:
[
new AggregationBucket("CVE-2024-0001", 10, 5, 10.3, false),
new AggregationBucket("CVE-2024-0002", 3, 2, 0, true),
new AggregationBucket("CVE-2024-7777", 9, 4, 9.1, false),
],
TotalFacts: 22,
SuppressedBuckets: 1,
EpsilonSpent: 0.8,
AggregatedAt: new DateTimeOffset(2026, 03, 04, 14, 30, 0, TimeSpan.Zero));
}
private static ConsentProof CreateConsentProof()
{
return new ConsentProof(
TenantId: "tenant-1",
GrantedBy: "admin@example.com",
GrantedAt: new DateTimeOffset(2026, 03, 04, 13, 0, 0, TimeSpan.Zero),
ExpiresAt: new DateTimeOffset(2026, 03, 05, 13, 0, 0, TimeSpan.Zero),
DsseDigest: "sha256:consent-proof",
Envelope: new byte[] { 1, 2, 3 },
SignerKeyId: "consent-key");
}
private static byte[] TamperEnvelopePayload(byte[] envelope)
{
var document = JsonNode.Parse(envelope)!.AsObject();
var payload = Convert.FromBase64String(document["payload"]!.GetValue<string>());
payload[0] ^= 0x01;
document["payload"] = Convert.ToBase64String(payload);
return Encoding.UTF8.GetBytes(document.ToJsonString());
}
private static byte[] TamperEnvelopeSignature(byte[] envelope)
{
var document = JsonNode.Parse(envelope)!.AsObject();
var signatureNode = document["signatures"]!.AsArray()[0]!.AsObject();
var signature = signatureNode["sig"]!.GetValue<string>();
signatureNode["sig"] = signature[^1] == 'A'
? $"{signature[..^1]}B"
: $"{signature[..^1]}A";
return Encoding.UTF8.GetBytes(document.ToJsonString());
}
}

View File

@@ -296,6 +296,12 @@ public sealed class FederationSyncAndIntelligenceTests
Envelope: [0x10, 0x11, 0x12]));
}
public Task<bool> VerifyProofAsync(ConsentProof proof, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
return Task.FromResult(true);
}
public Task RevokeConsentAsync(string tenantId, string revokedBy, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();