license switch agpl -> busl1, sprints work, new product advisories

This commit is contained in:
master
2026-01-20 15:32:20 +02:00
parent 4903395618
commit c32fff8f86
1835 changed files with 38630 additions and 4359 deletions

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using FluentAssertions;
using StellaOps.Attestor.Core.InToto;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using System.Text.Json;
using FluentAssertions;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using System.Text.Json;
using FluentAssertions;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using System.Collections.Immutable;
using FluentAssertions;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using FluentAssertions;
using StellaOps.Attestor.Core.InToto;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;

View File

@@ -0,0 +1,113 @@
using System.Security.Cryptography;
using System.Text.Json;
using StellaOps.Attestor.Core.Predicates;
using StellaOps.Attestor.Core.Signing;
using StellaOps.Attestor.Envelope;
using StellaOps.Attestor.Serialization;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Attestor.Core.Tests.Signing;
public sealed class VerificationReportSignerTests
{
[Fact]
public async Task SignAsync_ProducesDsseEnvelopeWithVerifiableSignature()
{
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var parameters = ecdsa.ExportParameters(true);
var signingKey = EnvelopeKey.CreateEcdsaSigner(SignatureAlgorithms.Es256, parameters, "test-key");
var verificationKey = EnvelopeKey.CreateEcdsaVerifier(
SignatureAlgorithms.Es256,
new ECParameters { Curve = parameters.Curve, Q = parameters.Q },
"test-key");
var signedAt = new DateTimeOffset(2026, 1, 20, 12, 0, 0, TimeSpan.Zero);
var certificatePem = "-----BEGIN CERTIFICATE-----\nTESTCERT\n-----END CERTIFICATE-----";
var report = BuildReport(new DateTimeOffset(2026, 1, 20, 11, 59, 0, TimeSpan.Zero));
var signer = new DsseVerificationReportSigner(new EnvelopeSignatureService());
var result = await signer.SignAsync(new VerificationReportSigningRequest(
report,
signingKey,
VerifierCertificatePem: certificatePem,
SignedAt: signedAt));
Assert.Equal(VerificationReportPredicate.PredicateType, result.PayloadType);
Assert.NotNull(result.Report.Verifier);
Assert.Equal(signingKey.AlgorithmId, result.Report.Verifier!.Algo);
Assert.Equal(certificatePem, result.Report.Verifier.Cert);
Assert.Equal(signedAt, result.Report.Verifier.SignedAt);
using var document = JsonDocument.Parse(result.EnvelopeJson);
var payloadBase64 = document.RootElement.GetProperty("payload").GetString();
Assert.False(string.IsNullOrWhiteSpace(payloadBase64));
var payloadBytes = Convert.FromBase64String(payloadBase64!);
var expectedPayload = CanonicalJsonSerializer.SerializeToBytes(result.Report, prettify: false);
Assert.Equal(expectedPayload, payloadBytes);
var signatureElement = document.RootElement.GetProperty("signatures")[0];
var signatureBase64 = signatureElement.GetProperty("sig").GetString();
Assert.False(string.IsNullOrWhiteSpace(signatureBase64));
var signatureBytes = Convert.FromBase64String(signatureBase64!);
var envelopeSignature = new EnvelopeSignature(
signatureElement.GetProperty("keyid").GetString() ?? "test-key",
signingKey.AlgorithmId,
signatureBytes);
var verifier = new EnvelopeSignatureService();
var verifyResult = verifier.VerifyDsse(
VerificationReportPredicate.PredicateType,
payloadBytes,
envelopeSignature,
verificationKey);
Assert.True(verifyResult.IsSuccess);
Assert.True(verifyResult.Value);
}
private static VerificationReportPredicate BuildReport(DateTimeOffset generatedAt)
{
return new VerificationReportPredicate
{
ReportId = "report-001",
GeneratedAt = generatedAt,
Generator = new GeneratorInfo
{
Tool = "stella bundle verify",
Version = "test"
},
Subject = new VerificationSubject
{
BundleId = "bundle-001",
BundleDigest = "sha256:bundle",
ArtifactDigest = "sha256:artifact",
ArtifactName = "registry.example.com/app@sha256:deadbeef"
},
VerificationSteps =
[
new VerificationStep
{
Step = 1,
Name = "manifest",
Status = VerificationStepStatus.Passed,
DurationMs = 1,
Details = "manifest parsed",
Issues = Array.Empty<VerificationIssue>()
}
],
OverallResult = new OverallVerificationResult
{
Status = VerificationStepStatus.Passed,
Summary = "PASSED",
TotalDurationMs = 1,
PassedSteps = 1,
FailedSteps = 0,
WarningSteps = 0,
SkippedSteps = 0
}
};
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
// Models are now in the same namespace

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using System.Collections.Frozen;
using System.Collections.Immutable;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
namespace StellaOps.Attestor.Core.InToto;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
namespace StellaOps.Attestor.Core.InToto;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
namespace StellaOps.Attestor.Core.InToto;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using System.Collections.Immutable;
using System.Text.Json;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using System.Collections.Frozen;
using System.Collections.Immutable;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using System.Collections.Immutable;
using StellaOps.Attestor.Envelope;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using System.Collections.Immutable;
using System.Text.Json;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using System.Collections.Immutable;
using System.Text.RegularExpressions;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using System.Collections.Immutable;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
namespace StellaOps.Attestor.Core.InToto;

View File

@@ -123,9 +123,9 @@ public sealed class AttestorOptions
public int MaxAttempts { get; set; } = 60;
/// <summary>
/// Log version to use: Auto, V1, or V2.
/// Log version to use: Auto or V2.
/// V2 uses tile-based (Sunlight) log structure.
/// Default: Auto (backward compatible).
/// Default: Auto (v2 tiles).
/// </summary>
public string Version { get; set; } = "Auto";
@@ -141,10 +141,6 @@ public sealed class AttestorOptions
/// </summary>
public string? LogId { get; set; }
/// <summary>
/// When true and Version is Auto, prefer tile-based proofs over v1 proofs.
/// </summary>
public bool PreferTileProofs { get; set; } = false;
}
public sealed class RekorMirrorOptions : RekorBackendOptions

View File

@@ -1,5 +1,5 @@
// <copyright file="PathWitnessPredicateTypes.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_20260112_006_ATTESTOR_path_witness_predicate (PW-ATT-003)
// </copyright>

View File

@@ -1,4 +1,4 @@
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
using System.Text;
using Microsoft.Extensions.Logging;

View File

@@ -1,4 +1,4 @@
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
using System.Text.Json.Serialization;

View File

@@ -8,15 +8,10 @@ namespace StellaOps.Attestor.Core.Rekor;
public enum RekorLogVersion
{
/// <summary>
/// Automatically detect log version from server capabilities.
/// Automatically select the supported log format (defaults to v2 tiles).
/// </summary>
Auto = 0,
/// <summary>
/// Rekor v1 with Trillian-backed Merkle tree.
/// </summary>
V1 = 1,
/// <summary>
/// Rekor v2 with tile-based (Sunlight) log structure.
/// Provides cheaper operation and simpler verification.
@@ -31,7 +26,7 @@ public sealed class RekorBackend
public required Uri Url { get; init; }
/// <summary>
/// Log version to use. Default is Auto for backward compatibility.
/// Log version to use. Default is Auto (v2 tiles).
/// Set to V2 to explicitly opt into tile-based verification.
/// </summary>
public RekorLogVersion Version { get; init; } = RekorLogVersion.Auto;
@@ -50,12 +45,6 @@ public sealed class RekorBackend
/// </summary>
public string? LogId { get; init; }
/// <summary>
/// Whether to prefer tile-based proofs when available.
/// When true and Version is Auto, will attempt tile fetching first.
/// </summary>
public bool PreferTileProofs { get; init; } = false;
public TimeSpan ProofTimeout { get; init; } = TimeSpan.FromSeconds(15);
public TimeSpan PollInterval { get; init; } = TimeSpan.FromMilliseconds(250);

View File

@@ -1,5 +1,5 @@
// <copyright file="RekorEntryEvent.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-License-Identifier: BUSL-1.1
// Sprint: SPRINT_20260112_007_ATTESTOR_rekor_entry_events (ATT-REKOR-001, ATT-REKOR-002)
// </copyright>

View File

@@ -1,4 +1,4 @@
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
using System.Collections;
using System.Text;

View File

@@ -1,4 +1,4 @@
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
using System.Security.Cryptography;
using System.Text;

View File

@@ -0,0 +1,81 @@
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
using System.Text;
using StellaOps.Attestor.Core.Predicates;
using StellaOps.Attestor.Envelope;
using StellaOps.Attestor.Serialization;
namespace StellaOps.Attestor.Core.Signing;
public sealed class DsseVerificationReportSigner : IVerificationReportSigner
{
private readonly EnvelopeSignatureService _signatureService;
public DsseVerificationReportSigner(EnvelopeSignatureService signatureService)
{
_signatureService = signatureService ?? throw new ArgumentNullException(nameof(signatureService));
}
public Task<VerificationReportSigningResult> SignAsync(
VerificationReportSigningRequest request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(request.Report);
ArgumentNullException.ThrowIfNull(request.SigningKey);
cancellationToken.ThrowIfCancellationRequested();
var signedAt = request.SignedAt ?? DateTimeOffset.UtcNow;
var report = request.Report with
{
Verifier = new VerifierInfo
{
Algo = request.SigningKey.AlgorithmId,
Cert = NormalizePem(request.VerifierCertificatePem),
SignedAt = signedAt
}
};
var payloadBytes = CanonicalJsonSerializer.SerializeToBytes(report, prettify: false);
var signResult = _signatureService.SignDsse(
VerificationReportPredicate.PredicateType,
payloadBytes,
request.SigningKey,
cancellationToken);
if (!signResult.IsSuccess)
{
throw new InvalidOperationException($"Verification report DSSE signing failed: {signResult.Error.Message}");
}
var signature = DsseSignature.FromBytes(signResult.Value.Value.Span, signResult.Value.KeyId);
var envelope = new DsseEnvelope(
VerificationReportPredicate.PredicateType,
payloadBytes,
new[] { signature },
payloadContentType: "application/json");
var serialization = DsseEnvelopeSerializer.Serialize(envelope);
var envelopeJson = serialization.CompactJson is null
? string.Empty
: Encoding.UTF8.GetString(serialization.CompactJson);
return Task.FromResult(new VerificationReportSigningResult
{
PayloadType = envelope.PayloadType,
Payload = payloadBytes,
Signatures = envelope.Signatures,
EnvelopeJson = envelopeJson,
Report = report
});
}
private static string? NormalizePem(string? pem)
{
if (string.IsNullOrWhiteSpace(pem))
{
return null;
}
return pem.Replace("\r\n", "\n").Trim();
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
using System.Security.Cryptography;
using System.Text.Json;

View File

@@ -0,0 +1,27 @@
// Copyright (c) StellaOps. Licensed under BUSL-1.1.
using StellaOps.Attestor.Core.Predicates;
using StellaOps.Attestor.Envelope;
namespace StellaOps.Attestor.Core.Signing;
public interface IVerificationReportSigner
{
Task<VerificationReportSigningResult> SignAsync(
VerificationReportSigningRequest request,
CancellationToken cancellationToken = default);
}
public sealed record VerificationReportSigningRequest(
VerificationReportPredicate Report,
EnvelopeKey SigningKey,
string? VerifierCertificatePem = null,
DateTimeOffset? SignedAt = null);
public sealed record VerificationReportSigningResult
{
public required string PayloadType { get; init; }
public required byte[] Payload { get; init; }
public required IReadOnlyList<DsseSignature> Signatures { get; init; }
public required string EnvelopeJson { get; init; }
public required VerificationReportPredicate Report { get; init; }
}

View File

@@ -18,6 +18,10 @@
<ItemGroup>
<EmbeddedResource Include="Schemas\*.json" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\__Libraries\StellaOps.Attestor.Core\Predicates\VerificationReportPredicate.cs"
Link="Predicates\VerificationReportPredicate.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography.Kms\StellaOps.Cryptography.Kms.csproj" />

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using System.Text;
using Microsoft.Extensions.Logging;

View File

@@ -52,7 +52,6 @@ internal static class RekorBackendResolver
? null
: new Uri(options.TileBaseUrl, UriKind.Absolute),
LogId = options.LogId,
PreferTileProofs = options.PreferTileProofs,
ProofTimeout = TimeSpan.FromMilliseconds(options.ProofTimeoutMs),
PollInterval = TimeSpan.FromMilliseconds(options.PollIntervalMs),
MaxAttempts = options.MaxAttempts
@@ -72,9 +71,11 @@ internal static class RekorBackendResolver
return version.Trim().ToUpperInvariant() switch
{
"AUTO" => RekorLogVersion.Auto,
"V1" or "1" => RekorLogVersion.V1,
"V2" or "2" => RekorLogVersion.V2,
_ => RekorLogVersion.Auto
"V1" or "1" => throw new InvalidOperationException(
"Rekor v1 is no longer supported. Use Auto or V2."),
_ => throw new InvalidOperationException(
$"Unsupported Rekor version '{version}'. Use Auto or V2.")
};
}
@@ -84,6 +85,6 @@ internal static class RekorBackendResolver
public static bool ShouldUseTileProofs(RekorBackend backend)
{
return backend.Version == RekorLogVersion.V2 ||
(backend.Version == RekorLogVersion.Auto && backend.PreferTileProofs);
backend.Version == RekorLogVersion.Auto;
}
}

View File

@@ -26,7 +26,8 @@
<ProjectReference Include="../../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
<ProjectReference Include="..\..\StellaOps.Attestor.Verify\StellaOps.Attestor.Verify.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.Timestamping\StellaOps.Attestor.Timestamping.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography.Plugin.SmSoft\StellaOps.Cryptography.Plugin.SmSoft.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -8,3 +8,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0066-M | DONE | Revalidated 2026-01-06 (maintainability audit). |
| AUDIT-0066-T | DONE | Revalidated 2026-01-06 (test coverage audit). |
| AUDIT-0066-A | DONE | Waived (test project; revalidated 2026-01-06). |
| ATT-001 | DONE | Timestamping service unit tests for envelope digest handling. |
| ATT-002 | DONE | Timestamp verification scenarios covered. |
| ATT-003 | DONE | Timestamp policy evaluator scenarios covered. |
| ATT-006 | DONE | Time correlation validator unit tests added. |

View File

@@ -0,0 +1,122 @@
using FluentAssertions;
using StellaOps.Attestor.Timestamping;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Attestor.Tests.Timestamping;
public sealed class AttestationTimestampPolicyTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Evaluate_WhenRfc3161RequiredAndMissing_Fails()
{
var evaluator = new TimestampPolicyEvaluator();
var context = new AttestationTimestampPolicyContext { HasValidTst = false };
var policy = new TimestampPolicy { RequireRfc3161 = true };
var result = evaluator.Evaluate(context, policy);
result.IsCompliant.Should().BeFalse();
result.Violations.Should().Contain(v => v.RuleId == "require-rfc3161");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Evaluate_WhenSkewExceedsThreshold_Fails()
{
var evaluator = new TimestampPolicyEvaluator();
var context = new AttestationTimestampPolicyContext
{
HasValidTst = true,
TimeSkew = TimeSpan.FromMinutes(10)
};
var policy = new TimestampPolicy
{
RequireRfc3161 = true,
MaxTimeSkew = TimeSpan.FromMinutes(5)
};
var result = evaluator.Evaluate(context, policy);
result.IsCompliant.Should().BeFalse();
result.Violations.Should().Contain(v => v.RuleId == "time-skew");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Evaluate_WhenRevocationStapleMissing_Fails()
{
var evaluator = new TimestampPolicyEvaluator();
var context = new AttestationTimestampPolicyContext
{
HasValidTst = true,
OcspStatus = null,
CrlChecked = false
};
var policy = new TimestampPolicy
{
RequireRfc3161 = true,
RequireRevocationStapling = true
};
var result = evaluator.Evaluate(context, policy);
result.IsCompliant.Should().BeFalse();
result.Violations.Should().Contain(v => v.RuleId == "revocation-staple");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Evaluate_WhenTsaNotTrusted_Fails()
{
var evaluator = new TimestampPolicyEvaluator();
var context = new AttestationTimestampPolicyContext
{
HasValidTst = true,
TsaName = "Untrusted TSA",
OcspStatus = "Good",
CrlChecked = true
};
var policy = new TimestampPolicy
{
RequireRfc3161 = true,
RequireRevocationStapling = true,
TrustedTsas = new[] { "Trusted TSA" }
};
var result = evaluator.Evaluate(context, policy);
result.IsCompliant.Should().BeFalse();
result.Violations.Should().Contain(v => v.RuleId == "trusted-tsa");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Evaluate_WhenAllRequirementsMet_Passes()
{
var evaluator = new TimestampPolicyEvaluator();
var context = new AttestationTimestampPolicyContext
{
HasValidTst = true,
TimeSkew = TimeSpan.FromMinutes(1),
TsaName = "Trusted TSA",
OcspStatus = "Good",
CrlChecked = true,
TsaCertificateExpires = DateTimeOffset.UtcNow.AddDays(365)
};
var policy = new TimestampPolicy
{
RequireRfc3161 = true,
RequireRevocationStapling = true,
MaxTimeSkew = TimeSpan.FromMinutes(5),
MinCertificateFreshness = TimeSpan.FromDays(180),
TrustedTsas = new[] { "Trusted TSA" }
};
var result = evaluator.Evaluate(context, policy);
result.IsCompliant.Should().BeTrue();
result.Violations.Should().BeEmpty();
}
}

View File

@@ -0,0 +1,115 @@
using System.Security.Cryptography;
using System.Text;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Attestor.Timestamping;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Attestor.Tests.Timestamping;
public sealed class AttestationTimestampServiceTests
{
private static AttestationTimestampService CreateService()
{
var options = Options.Create(new AttestationTimestampServiceOptions());
return new AttestationTimestampService(options, NullLogger<AttestationTimestampService>.Instance);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TimestampAsync_ComputesEnvelopeDigest()
{
var service = CreateService();
var envelope = Encoding.UTF8.GetBytes("{\"payload\":\"test\"}");
var result = await service.TimestampAsync(
envelope,
new AttestationTimestampOptions { HashAlgorithm = "SHA256" });
var expectedHash = Convert.ToHexString(SHA256.HashData(envelope)).ToLowerInvariant();
result.EnvelopeDigest.Should().Be($"sha256:{expectedHash}");
result.Envelope.Should().Equal(envelope);
result.TsaName.Should().NotBeNullOrWhiteSpace();
result.TsaPolicyOid.Should().NotBeNullOrWhiteSpace();
result.TimestampTime.Should().NotBe(default);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task VerifyAsync_WithConsistentRekorTime_ReturnsSuccess()
{
var service = CreateService();
var envelope = Encoding.UTF8.GetBytes("{\"payload\":\"test\"}");
var digest = "sha256:" + Convert.ToHexString(SHA256.HashData(envelope)).ToLowerInvariant();
var tstTime = new DateTimeOffset(2026, 1, 19, 12, 0, 0, TimeSpan.Zero);
var attestation = new TimestampedAttestation
{
Envelope = envelope,
EnvelopeDigest = digest,
TimeStampToken = Array.Empty<byte>(),
TimestampTime = tstTime,
TsaName = "Test TSA",
TsaPolicyOid = "1.2.3.4",
RekorReceipt = new RekorReceipt
{
LogId = "rekor",
LogIndex = 42,
IntegratedTime = tstTime.AddMinutes(1)
}
};
var options = new AttestationTimestampVerificationOptions
{
RequireRekorConsistency = true,
MaxTimeSkew = TimeSpan.FromMinutes(5),
VerifyTsaRevocation = false
};
var result = await service.VerifyAsync(attestation, options);
result.IsValid.Should().BeTrue();
result.TimeConsistency.Should().NotBeNull();
result.TimeConsistency!.WithinTolerance.Should().BeTrue();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task VerifyAsync_WithRekorInconsistency_ReturnsFailure()
{
var service = CreateService();
var envelope = Encoding.UTF8.GetBytes("{\"payload\":\"test\"}");
var digest = "sha256:" + Convert.ToHexString(SHA256.HashData(envelope)).ToLowerInvariant();
var tstTime = new DateTimeOffset(2026, 1, 19, 12, 0, 0, TimeSpan.Zero);
var attestation = new TimestampedAttestation
{
Envelope = envelope,
EnvelopeDigest = digest,
TimeStampToken = Array.Empty<byte>(),
TimestampTime = tstTime,
TsaName = "Test TSA",
TsaPolicyOid = "1.2.3.4",
RekorReceipt = new RekorReceipt
{
LogId = "rekor",
LogIndex = 42,
IntegratedTime = tstTime.AddMinutes(-10)
}
};
var options = new AttestationTimestampVerificationOptions
{
RequireRekorConsistency = true,
MaxTimeSkew = TimeSpan.FromMinutes(5),
VerifyTsaRevocation = false
};
var result = await service.VerifyAsync(attestation, options);
result.IsValid.Should().BeFalse();
result.TstStatus.Should().Be(TstVerificationStatus.TimeInconsistency);
}
}

View File

@@ -0,0 +1,85 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Attestor.Timestamping;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Attestor.Tests.Timestamping;
public sealed class TimeCorrelationValidatorTests
{
private static TimeCorrelationValidator CreateValidator()
=> new(NullLogger<TimeCorrelationValidator>.Instance);
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_WhenGapWithinLimits_ReturnsValid()
{
var validator = CreateValidator();
var tstTime = new DateTimeOffset(2026, 1, 19, 12, 0, 0, TimeSpan.Zero);
var rekorTime = tstTime.AddSeconds(30);
var result = validator.Validate(tstTime, rekorTime);
result.Valid.Should().BeTrue();
result.Status.Should().Be(TimeCorrelationStatus.Valid);
result.Suspicious.Should().BeFalse();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_WhenGapExceedsMaximum_ReturnsGapExceeded()
{
var validator = CreateValidator();
var policy = new TimeCorrelationPolicy
{
MaximumGap = TimeSpan.FromMinutes(2),
SuspiciousGap = TimeSpan.FromMinutes(1)
};
var tstTime = new DateTimeOffset(2026, 1, 19, 12, 0, 0, TimeSpan.Zero);
var rekorTime = tstTime.AddMinutes(5);
var result = validator.Validate(tstTime, rekorTime, policy);
result.Valid.Should().BeFalse();
result.Status.Should().Be(TimeCorrelationStatus.GapExceeded);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_WhenTstAfterRekor_ReturnsInvalid()
{
var validator = CreateValidator();
var policy = new TimeCorrelationPolicy
{
ClockSkewTolerance = TimeSpan.FromSeconds(5)
};
var tstTime = new DateTimeOffset(2026, 1, 19, 12, 0, 10, TimeSpan.Zero);
var rekorTime = tstTime.AddSeconds(-30);
var result = validator.Validate(tstTime, rekorTime, policy);
result.Valid.Should().BeFalse();
result.Status.Should().Be(TimeCorrelationStatus.TstAfterRekor);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_WhenSuspiciousAndFailOnSuspicious_ReturnsFailure()
{
var validator = CreateValidator();
var policy = new TimeCorrelationPolicy
{
MaximumGap = TimeSpan.FromMinutes(5),
SuspiciousGap = TimeSpan.FromSeconds(10),
FailOnSuspicious = true
};
var tstTime = new DateTimeOffset(2026, 1, 19, 12, 0, 0, TimeSpan.Zero);
var rekorTime = tstTime.AddSeconds(30);
var result = validator.Validate(tstTime, rekorTime, policy);
result.Valid.Should().BeFalse();
result.Status.Should().Be(TimeCorrelationStatus.SuspiciousGapFailed);
}
}

View File

@@ -1,4 +1,4 @@
// Licensed under AGPL-3.0-or-later. Copyright (C) 2024-2026 StellaOps Contributors.
// Licensed under BUSL-1.1. Copyright (C) 2024-2026 StellaOps Contributors.
using System.Text.Json.Serialization;
using StellaOps.Attestor.Core.InToto;