license switch agpl -> busl1, sprints work, new product advisories
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user