sprints work.

This commit is contained in:
master
2026-01-20 00:45:38 +02:00
parent b34bde89fa
commit 4903395618
275 changed files with 52785 additions and 79 deletions

View File

@@ -0,0 +1,59 @@
// -----------------------------------------------------------------------------
// IPredicateTimestampMetadata.cs
// Sprint: SPRINT_20260119_010 Attestor TST Integration
// Task: ATT-004 - Predicate Writer Extensions
// Description: RFC-3161 timestamp metadata for embedding in predicates.
// -----------------------------------------------------------------------------
namespace StellaOps.Attestor.StandardPredicates;
/// <summary>
/// RFC-3161 timestamp metadata for embedding in predicates.
/// </summary>
public sealed record Rfc3161TimestampMetadata
{
/// <summary>
/// Gets the TSA URL that issued the timestamp.
/// </summary>
public required string TsaUrl { get; init; }
/// <summary>
/// Gets the digest of the timestamp token (base64 or hex).
/// </summary>
public required string TokenDigest { get; init; }
/// <summary>
/// Gets the digest algorithm used for the token digest.
/// </summary>
public string DigestAlgorithm { get; init; } = "SHA256";
/// <summary>
/// Gets the generation time from the TST.
/// </summary>
public required DateTimeOffset GenerationTime { get; init; }
/// <summary>
/// Gets the TSA policy OID.
/// </summary>
public string? PolicyOid { get; init; }
/// <summary>
/// Gets the TST serial number.
/// </summary>
public string? SerialNumber { get; init; }
/// <summary>
/// Gets the TSA name from the TSTInfo.
/// </summary>
public string? TsaName { get; init; }
/// <summary>
/// Gets whether the timestamp has stapled revocation data.
/// </summary>
public bool HasStapledRevocation { get; init; }
/// <summary>
/// Gets whether this is a qualified timestamp (eIDAS).
/// </summary>
public bool IsQualified { get; init; }
}

View File

@@ -0,0 +1,133 @@
// -----------------------------------------------------------------------------
// CycloneDxTimestampExtension.cs
// Sprint: SPRINT_20260119_010 Attestor TST Integration
// Task: ATT-004 - Predicate Writer Extensions
// Description: CycloneDX signature.timestamp extension for RFC-3161 timestamps.
// -----------------------------------------------------------------------------
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace StellaOps.Attestor.StandardPredicates.Writers;
/// <summary>
/// Extension for adding RFC-3161 timestamp metadata to CycloneDX documents.
/// Adds signature.timestamp field per CycloneDX 1.5+ specification.
/// </summary>
public static class CycloneDxTimestampExtension
{
/// <summary>
/// Adds RFC-3161 timestamp metadata to a CycloneDX JSON document.
/// </summary>
/// <param name="cycloneDxJson">The CycloneDX JSON bytes.</param>
/// <param name="timestampMetadata">The timestamp metadata to add.</param>
/// <returns>The modified JSON bytes with timestamp metadata.</returns>
public static byte[] AddTimestampMetadata(
byte[] cycloneDxJson,
Rfc3161TimestampMetadata timestampMetadata)
{
var jsonNode = JsonNode.Parse(cycloneDxJson)
?? throw new InvalidOperationException("Failed to parse CycloneDX JSON");
// Create the signature.timestamp structure
var timestampNode = new JsonObject
{
["rfc3161"] = new JsonObject
{
["tsaUrl"] = timestampMetadata.TsaUrl,
["tokenDigest"] = $"{timestampMetadata.DigestAlgorithm.ToLowerInvariant()}:{timestampMetadata.TokenDigest}",
["generationTime"] = timestampMetadata.GenerationTime.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)
}
};
// Add optional fields
var rfc3161Node = timestampNode["rfc3161"]!.AsObject();
if (timestampMetadata.PolicyOid is not null)
{
rfc3161Node["policyOid"] = timestampMetadata.PolicyOid;
}
if (timestampMetadata.SerialNumber is not null)
{
rfc3161Node["serialNumber"] = timestampMetadata.SerialNumber;
}
if (timestampMetadata.TsaName is not null)
{
rfc3161Node["tsaName"] = timestampMetadata.TsaName;
}
if (timestampMetadata.HasStapledRevocation)
{
rfc3161Node["stapledRevocation"] = true;
}
if (timestampMetadata.IsQualified)
{
rfc3161Node["qualified"] = true;
}
// Add or extend signature object
if (jsonNode["signature"] is JsonObject signatureNode)
{
signatureNode["timestamp"] = timestampNode;
}
else
{
jsonNode["signature"] = new JsonObject
{
["timestamp"] = timestampNode
};
}
// Serialize with deterministic ordering
var options = new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
return JsonSerializer.SerializeToUtf8Bytes(jsonNode, options);
}
/// <summary>
/// Extracts RFC-3161 timestamp metadata from a CycloneDX JSON document.
/// </summary>
/// <param name="cycloneDxJson">The CycloneDX JSON bytes.</param>
/// <returns>The timestamp metadata if present, null otherwise.</returns>
public static Rfc3161TimestampMetadata? ExtractTimestampMetadata(byte[] cycloneDxJson)
{
var jsonNode = JsonNode.Parse(cycloneDxJson);
var timestampNode = jsonNode?["signature"]?["timestamp"]?["rfc3161"];
if (timestampNode is null)
{
return null;
}
var tokenDigest = timestampNode["tokenDigest"]?.GetValue<string>() ?? "";
var digestAlgorithm = "SHA256";
var digestValue = tokenDigest;
// Parse "sha256:abc123" format
if (tokenDigest.Contains(':'))
{
var parts = tokenDigest.Split(':', 2);
digestAlgorithm = parts[0].ToUpperInvariant();
digestValue = parts[1];
}
return new Rfc3161TimestampMetadata
{
TsaUrl = timestampNode["tsaUrl"]?.GetValue<string>() ?? "",
TokenDigest = digestValue,
DigestAlgorithm = digestAlgorithm,
GenerationTime = DateTimeOffset.Parse(
timestampNode["generationTime"]?.GetValue<string>() ?? DateTimeOffset.MinValue.ToString("O"),
CultureInfo.InvariantCulture),
PolicyOid = timestampNode["policyOid"]?.GetValue<string>(),
SerialNumber = timestampNode["serialNumber"]?.GetValue<string>(),
TsaName = timestampNode["tsaName"]?.GetValue<string>(),
HasStapledRevocation = timestampNode["stapledRevocation"]?.GetValue<bool>() ?? false,
IsQualified = timestampNode["qualified"]?.GetValue<bool>() ?? false
};
}
}

View File

@@ -50,27 +50,28 @@ public sealed class CycloneDxWriter : ISbomWriter
}
/// <inheritdoc />
public byte[] Write(SbomDocument document)
public SbomWriteResult Write(SbomDocument document)
{
var cdx = ConvertToCycloneDx(document);
return _canonicalizer.Canonicalize(cdx);
var canonicalBytes = _canonicalizer.Canonicalize(cdx);
var goldenHash = _canonicalizer.ComputeGoldenHash(canonicalBytes);
return new SbomWriteResult
{
Format = SbomFormat.CycloneDx,
CanonicalBytes = canonicalBytes,
GoldenHash = goldenHash,
DocumentId = cdx.SerialNumber
};
}
/// <inheritdoc />
public Task<byte[]> WriteAsync(SbomDocument document, CancellationToken ct = default)
public Task<SbomWriteResult> WriteAsync(SbomDocument document, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
return Task.FromResult(Write(document));
}
/// <inheritdoc />
public string ComputeContentHash(SbomDocument document)
{
var bytes = Write(document);
var hash = SHA256.HashData(bytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private CycloneDxBom ConvertToCycloneDx(SbomDocument document)
{
// Sort components by bom-ref

View File

@@ -7,6 +7,32 @@
namespace StellaOps.Attestor.StandardPredicates.Writers;
/// <summary>
/// Result of SBOM write operation.
/// </summary>
public sealed record SbomWriteResult
{
/// <summary>
/// The format of the generated SBOM.
/// </summary>
public required Canonicalization.SbomFormat Format { get; init; }
/// <summary>
/// The canonical bytes of the SBOM.
/// </summary>
public required byte[] CanonicalBytes { get; init; }
/// <summary>
/// The golden hash of the canonical bytes.
/// </summary>
public required string GoldenHash { get; init; }
/// <summary>
/// Document ID.
/// </summary>
public string? DocumentId { get; init; }
}
/// <summary>
/// Writes SBOM documents in deterministic, canonical format.
/// </summary>
@@ -18,26 +44,19 @@ public interface ISbomWriter
Canonicalization.SbomFormat Format { get; }
/// <summary>
/// Writes an SBOM to canonical bytes.
/// Writes an SBOM to canonical format.
/// </summary>
/// <param name="document">The SBOM document model.</param>
/// <returns>Canonical JSON bytes.</returns>
byte[] Write(SbomDocument document);
/// <returns>Write result containing canonical bytes and hash.</returns>
SbomWriteResult Write(SbomDocument document);
/// <summary>
/// Writes an SBOM to canonical bytes asynchronously.
/// Writes an SBOM asynchronously.
/// </summary>
/// <param name="document">The SBOM document model.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Canonical JSON bytes.</returns>
Task<byte[]> WriteAsync(SbomDocument document, CancellationToken ct = default);
/// <summary>
/// Computes the content hash of the canonical SBOM.
/// </summary>
/// <param name="document">The SBOM document.</param>
/// <returns>SHA-256 hash in hex format.</returns>
string ComputeContentHash(SbomDocument document);
/// <returns>Write result containing canonical bytes and hash.</returns>
Task<SbomWriteResult> WriteAsync(SbomDocument document, CancellationToken ct = default);
}
/// <summary>

View File

@@ -0,0 +1,207 @@
// -----------------------------------------------------------------------------
// SpdxTimestampExtension.cs
// Sprint: SPRINT_20260119_010 Attestor TST Integration
// Task: ATT-004 - Predicate Writer Extensions
// Description: SPDX 3.0+ annotation extension for RFC-3161 timestamps.
// -----------------------------------------------------------------------------
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace StellaOps.Attestor.StandardPredicates.Writers;
/// <summary>
/// Extension for adding RFC-3161 timestamp metadata to SPDX documents.
/// Uses SPDX 3.0 annotations for timestamp references.
/// </summary>
public static class SpdxTimestampExtension
{
/// <summary>
/// The annotation type for RFC-3161 timestamps.
/// </summary>
public const string TimestampAnnotationType = "OTHER";
/// <summary>
/// The annotator prefix for Stella timestamp annotations.
/// </summary>
public const string TimestampAnnotator = "Tool: stella-attestor";
/// <summary>
/// Adds RFC-3161 timestamp annotation to an SPDX JSON document.
/// </summary>
/// <param name="spdxJson">The SPDX JSON bytes.</param>
/// <param name="timestampMetadata">The timestamp metadata to add.</param>
/// <returns>The modified JSON bytes with timestamp annotation.</returns>
public static byte[] AddTimestampAnnotation(
byte[] spdxJson,
Rfc3161TimestampMetadata timestampMetadata)
{
var jsonNode = JsonNode.Parse(spdxJson)
?? throw new InvalidOperationException("Failed to parse SPDX JSON");
// Build the comment field with RFC3161 reference
var commentParts = new List<string>
{
$"RFC3161-TST:{timestampMetadata.DigestAlgorithm.ToLowerInvariant()}:{timestampMetadata.TokenDigest}",
$"TSA:{timestampMetadata.TsaUrl}"
};
if (timestampMetadata.TsaName is not null)
{
commentParts.Add($"TSAName:{timestampMetadata.TsaName}");
}
if (timestampMetadata.PolicyOid is not null)
{
commentParts.Add($"Policy:{timestampMetadata.PolicyOid}");
}
if (timestampMetadata.HasStapledRevocation)
{
commentParts.Add("Stapled:true");
}
if (timestampMetadata.IsQualified)
{
commentParts.Add("Qualified:true");
}
var comment = string.Join("; ", commentParts);
// Create the annotation
var annotation = new JsonObject
{
["annotationType"] = TimestampAnnotationType,
["annotator"] = TimestampAnnotator,
["annotationDate"] = timestampMetadata.GenerationTime.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture),
["comment"] = comment
};
// Add to annotations array
if (jsonNode["annotations"] is JsonArray annotationsArray)
{
annotationsArray.Add(annotation);
}
else
{
jsonNode["annotations"] = new JsonArray { annotation };
}
// Serialize with deterministic ordering
var options = new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
return JsonSerializer.SerializeToUtf8Bytes(jsonNode, options);
}
/// <summary>
/// Extracts RFC-3161 timestamp metadata from an SPDX JSON document.
/// </summary>
/// <param name="spdxJson">The SPDX JSON bytes.</param>
/// <returns>The timestamp metadata if present, null otherwise.</returns>
public static Rfc3161TimestampMetadata? ExtractTimestampMetadata(byte[] spdxJson)
{
var jsonNode = JsonNode.Parse(spdxJson);
var annotationsNode = jsonNode?["annotations"]?.AsArray();
if (annotationsNode is null)
{
return null;
}
// Find the timestamp annotation
foreach (var annotation in annotationsNode)
{
var annotator = annotation?["annotator"]?.GetValue<string>();
var comment = annotation?["comment"]?.GetValue<string>();
if (annotator == TimestampAnnotator && comment?.StartsWith("RFC3161-TST:") == true)
{
return ParseTimestampComment(
comment,
annotation?["annotationDate"]?.GetValue<string>());
}
}
return null;
}
private static Rfc3161TimestampMetadata? ParseTimestampComment(string comment, string? annotationDate)
{
var parts = comment.Split("; ");
if (parts.Length == 0)
{
return null;
}
string? digestAlgorithm = null;
string? tokenDigest = null;
string? tsaUrl = null;
string? tsaName = null;
string? policyOid = null;
bool hasStapledRevocation = false;
bool isQualified = false;
foreach (var part in parts)
{
if (part.StartsWith("RFC3161-TST:"))
{
var digestPart = part.Substring("RFC3161-TST:".Length);
var colonIdx = digestPart.IndexOf(':');
if (colonIdx > 0)
{
digestAlgorithm = digestPart.Substring(0, colonIdx).ToUpperInvariant();
tokenDigest = digestPart.Substring(colonIdx + 1);
}
}
else if (part.StartsWith("TSA:"))
{
tsaUrl = part.Substring("TSA:".Length);
}
else if (part.StartsWith("TSAName:"))
{
tsaName = part.Substring("TSAName:".Length);
}
else if (part.StartsWith("Policy:"))
{
policyOid = part.Substring("Policy:".Length);
}
else if (part == "Stapled:true")
{
hasStapledRevocation = true;
}
else if (part == "Qualified:true")
{
isQualified = true;
}
}
if (tokenDigest is null || tsaUrl is null)
{
return null;
}
DateTimeOffset generationTime = DateTimeOffset.MinValue;
if (annotationDate is not null)
{
DateTimeOffset.TryParse(annotationDate, CultureInfo.InvariantCulture, DateTimeStyles.None, out generationTime);
}
return new Rfc3161TimestampMetadata
{
TsaUrl = tsaUrl,
TokenDigest = tokenDigest,
DigestAlgorithm = digestAlgorithm ?? "SHA256",
GenerationTime = generationTime,
PolicyOid = policyOid,
TsaName = tsaName,
HasStapledRevocation = hasStapledRevocation,
IsQualified = isQualified
};
}
}

View File

@@ -0,0 +1,234 @@
// -----------------------------------------------------------------------------
// AttestationTimestampPolicyContext.cs
// Sprint: SPRINT_20260119_010 Attestor TST Integration
// Task: ATT-003 - Policy Integration
// Description: Policy context for timestamp assertions.
// -----------------------------------------------------------------------------
namespace StellaOps.Attestor.Timestamping;
/// <summary>
/// Context for timestamp-related policy assertions.
/// </summary>
public sealed record AttestationTimestampPolicyContext
{
/// <summary>
/// Gets whether a valid TST is present.
/// </summary>
public bool HasValidTst { get; init; }
/// <summary>
/// Gets the TST generation time.
/// </summary>
public DateTimeOffset? TstTime { get; init; }
/// <summary>
/// Gets the TSA name.
/// </summary>
public string? TsaName { get; init; }
/// <summary>
/// Gets the TSA policy OID.
/// </summary>
public string? TsaPolicyOid { get; init; }
/// <summary>
/// Gets whether the TSA certificate is valid.
/// </summary>
public bool TsaCertificateValid { get; init; }
/// <summary>
/// Gets the TSA certificate expiration.
/// </summary>
public DateTimeOffset? TsaCertificateExpires { get; init; }
/// <summary>
/// Gets the OCSP status.
/// </summary>
public string? OcspStatus { get; init; }
/// <summary>
/// Gets whether CRL was checked.
/// </summary>
public bool CrlChecked { get; init; }
/// <summary>
/// Gets the Rekor integrated time.
/// </summary>
public DateTimeOffset? RekorTime { get; init; }
/// <summary>
/// Gets the time skew between TST and Rekor.
/// </summary>
public TimeSpan? TimeSkew { get; init; }
/// <summary>
/// Creates an empty context.
/// </summary>
public static AttestationTimestampPolicyContext Empty { get; } = new();
/// <summary>
/// Creates a context from a verification result.
/// </summary>
public static AttestationTimestampPolicyContext FromVerification(
TimestampedAttestation attestation,
AttestationTimestampVerificationResult result)
{
return new AttestationTimestampPolicyContext
{
HasValidTst = result.IsValid,
TstTime = attestation.TimestampTime,
TsaName = attestation.TsaName,
TsaPolicyOid = attestation.TsaPolicyOid,
TsaCertificateValid = result.TsaCertificateStatus?.IsValid ?? false,
TsaCertificateExpires = result.TsaCertificateStatus?.ExpiresAt,
OcspStatus = result.TsaCertificateStatus?.RevocationStatus,
CrlChecked = result.TsaCertificateStatus?.RevocationSource?.Contains("CRL") ?? false,
RekorTime = attestation.RekorReceipt?.IntegratedTime,
TimeSkew = result.TimeConsistency?.Skew
};
}
}
/// <summary>
/// Policy evaluator for timestamp requirements.
/// </summary>
public sealed class TimestampPolicyEvaluator
{
/// <summary>
/// Evaluates whether an attestation meets timestamp policy requirements.
/// </summary>
/// <param name="context">The timestamp policy context.</param>
/// <param name="policy">The policy to evaluate.</param>
/// <returns>The evaluation result.</returns>
public TimestampPolicyResult Evaluate(
AttestationTimestampPolicyContext context,
TimestampPolicy policy)
{
var violations = new List<PolicyViolation>();
// Check RFC-3161 requirement
if (policy.RequireRfc3161 && !context.HasValidTst)
{
violations.Add(new PolicyViolation(
"require-rfc3161",
"Valid RFC-3161 timestamp is required but not present"));
}
// Check time skew
if (policy.MaxTimeSkew.HasValue && context.TimeSkew.HasValue)
{
if (context.TimeSkew.Value.Duration() > policy.MaxTimeSkew.Value)
{
violations.Add(new PolicyViolation(
"time-skew",
$"Time skew {context.TimeSkew.Value} exceeds maximum {policy.MaxTimeSkew}"));
}
}
// Check certificate freshness
if (policy.MinCertificateFreshness.HasValue && context.TsaCertificateExpires.HasValue)
{
var remaining = context.TsaCertificateExpires.Value - DateTimeOffset.UtcNow;
if (remaining < policy.MinCertificateFreshness.Value)
{
violations.Add(new PolicyViolation(
"freshness",
$"TSA certificate expires in {remaining.TotalDays:F0} days, minimum required is {policy.MinCertificateFreshness.Value.TotalDays:F0} days"));
}
}
// Check revocation stapling
if (policy.RequireRevocationStapling)
{
var hasOcsp = context.OcspStatus is "Good" or "Unknown";
var hasCrl = context.CrlChecked;
if (!hasOcsp && !hasCrl)
{
violations.Add(new PolicyViolation(
"revocation-staple",
"OCSP or CRL revocation evidence is required"));
}
}
// Check trusted TSAs
if (policy.TrustedTsas is { Count: > 0 } && context.TsaName is not null)
{
if (!policy.TrustedTsas.Any(t => context.TsaName.Contains(t, StringComparison.OrdinalIgnoreCase)))
{
violations.Add(new PolicyViolation(
"trusted-tsa",
$"TSA '{context.TsaName}' is not in the trusted TSA list"));
}
}
return new TimestampPolicyResult
{
IsCompliant = violations.Count == 0,
Violations = violations
};
}
}
/// <summary>
/// Timestamp policy definition.
/// </summary>
public sealed record TimestampPolicy
{
/// <summary>
/// Gets whether RFC-3161 timestamp is required.
/// </summary>
public bool RequireRfc3161 { get; init; }
/// <summary>
/// Gets the maximum allowed time skew.
/// </summary>
public TimeSpan? MaxTimeSkew { get; init; }
/// <summary>
/// Gets the minimum TSA certificate freshness.
/// </summary>
public TimeSpan? MinCertificateFreshness { get; init; }
/// <summary>
/// Gets whether revocation stapling is required.
/// </summary>
public bool RequireRevocationStapling { get; init; }
/// <summary>
/// Gets the list of trusted TSAs.
/// </summary>
public IReadOnlyList<string>? TrustedTsas { get; init; }
/// <summary>
/// Gets the default policy.
/// </summary>
public static TimestampPolicy Default { get; } = new()
{
RequireRfc3161 = true,
MaxTimeSkew = TimeSpan.FromMinutes(5),
MinCertificateFreshness = TimeSpan.FromDays(180),
RequireRevocationStapling = true
};
}
/// <summary>
/// Result of timestamp policy evaluation.
/// </summary>
public sealed record TimestampPolicyResult
{
/// <summary>
/// Gets whether the policy is met.
/// </summary>
public required bool IsCompliant { get; init; }
/// <summary>
/// Gets the list of violations.
/// </summary>
public required IReadOnlyList<PolicyViolation> Violations { get; init; }
}
/// <summary>
/// A policy violation.
/// </summary>
public sealed record PolicyViolation(string RuleId, string Message);

View File

@@ -0,0 +1,276 @@
// -----------------------------------------------------------------------------
// AttestationTimestampService.cs
// Sprint: SPRINT_20260119_010 Attestor TST Integration
// Task: ATT-001 - Attestation Signing Pipeline Extension
// Description: Service implementation for timestamping attestations.
// -----------------------------------------------------------------------------
using System.Security.Cryptography;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace StellaOps.Attestor.Timestamping;
/// <summary>
/// Implementation of <see cref="IAttestationTimestampService"/>.
/// </summary>
public sealed class AttestationTimestampService : IAttestationTimestampService
{
private readonly AttestationTimestampServiceOptions _options;
private readonly ILogger<AttestationTimestampService> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="AttestationTimestampService"/> class.
/// </summary>
public AttestationTimestampService(
IOptions<AttestationTimestampServiceOptions> options,
ILogger<AttestationTimestampService> logger)
{
_options = options.Value;
_logger = logger;
}
/// <inheritdoc />
public async Task<TimestampedAttestation> TimestampAsync(
ReadOnlyMemory<byte> envelope,
AttestationTimestampOptions? options = null,
CancellationToken cancellationToken = default)
{
options ??= AttestationTimestampOptions.Default;
// Hash the envelope
var algorithm = options.HashAlgorithm switch
{
"SHA256" => HashAlgorithmName.SHA256,
"SHA384" => HashAlgorithmName.SHA384,
"SHA512" => HashAlgorithmName.SHA512,
_ => HashAlgorithmName.SHA256
};
var hash = ComputeHash(envelope.Span, algorithm);
var digestHex = Convert.ToHexString(hash).ToLowerInvariant();
_logger.LogDebug(
"Timestamping attestation envelope with {Algorithm} digest: {Digest}",
options.HashAlgorithm,
digestHex);
// Call TSA client (placeholder - would integrate with ITimeStampAuthorityClient)
var tstBytes = await RequestTimestampAsync(hash, options, cancellationToken);
var (genTime, tsaName, policyOid) = ParseTstInfo(tstBytes);
_logger.LogInformation(
"Attestation timestamped at {Time} by {TSA}",
genTime,
tsaName);
return new TimestampedAttestation
{
Envelope = envelope.ToArray(),
EnvelopeDigest = $"{options.HashAlgorithm.ToLowerInvariant()}:{digestHex}",
TimeStampToken = tstBytes,
TimestampTime = genTime,
TsaName = tsaName,
TsaPolicyOid = policyOid
};
}
/// <inheritdoc />
public async Task<AttestationTimestampVerificationResult> VerifyAsync(
TimestampedAttestation attestation,
AttestationTimestampVerificationOptions? options = null,
CancellationToken cancellationToken = default)
{
options ??= AttestationTimestampVerificationOptions.Default;
var warnings = new List<string>();
try
{
// Step 1: Verify message imprint
var expectedHash = ComputeEnvelopeHash(attestation.Envelope, attestation.EnvelopeDigest);
var imprintValid = await VerifyImprintAsync(attestation.TimeStampToken, expectedHash, cancellationToken);
if (!imprintValid)
{
return AttestationTimestampVerificationResult.Failure(
TstVerificationStatus.ImprintMismatch,
"TST message imprint does not match attestation hash");
}
// Step 2: Verify TST signature (placeholder)
var signatureValid = await VerifyTstSignatureAsync(attestation.TimeStampToken, cancellationToken);
if (!signatureValid)
{
return AttestationTimestampVerificationResult.Failure(
TstVerificationStatus.InvalidSignature,
"TST signature verification failed");
}
// Step 3: Check time consistency with Rekor if present
TimeConsistencyResult? timeConsistency = null;
if (attestation.RekorReceipt is not null && options.RequireRekorConsistency)
{
timeConsistency = CheckTimeConsistency(
attestation.TimestampTime,
attestation.RekorReceipt.IntegratedTime,
options.MaxTimeSkew);
if (!timeConsistency.IsValid)
{
return AttestationTimestampVerificationResult.Failure(
TstVerificationStatus.TimeInconsistency,
$"TST time inconsistent with Rekor: skew={timeConsistency.Skew}");
}
}
// Step 4: Check TSA certificate revocation
TsaCertificateStatus? certStatus = null;
if (options.VerifyTsaRevocation)
{
certStatus = await CheckTsaCertificateAsync(attestation.TimeStampToken, options.AllowOffline, cancellationToken);
if (certStatus is { IsValid: false })
{
if (certStatus.RevocationStatus == "Revoked")
{
return AttestationTimestampVerificationResult.Failure(
TstVerificationStatus.CertificateRevoked,
"TSA certificate has been revoked");
}
warnings.Add($"TSA certificate status: {certStatus.RevocationStatus}");
}
// Warn if certificate is near expiration
if (certStatus?.ExpiresAt is not null)
{
var daysUntilExpiry = (certStatus.ExpiresAt.Value - DateTimeOffset.UtcNow).TotalDays;
if (daysUntilExpiry < 90)
{
warnings.Add($"TSA certificate expires in {daysUntilExpiry:F0} days");
}
}
}
return AttestationTimestampVerificationResult.Success(
timeConsistency,
certStatus,
warnings.Count > 0 ? warnings : null);
}
catch (Exception ex)
{
_logger.LogError(ex, "Attestation timestamp verification failed");
return AttestationTimestampVerificationResult.Failure(
TstVerificationStatus.Unknown,
ex.Message);
}
}
/// <inheritdoc />
public TimeConsistencyResult CheckTimeConsistency(
DateTimeOffset tstTime,
DateTimeOffset rekorTime,
TimeSpan? tolerance = null)
{
tolerance ??= _options.DefaultTimeSkewTolerance;
var skew = rekorTime - tstTime;
return new TimeConsistencyResult
{
TstTime = tstTime,
RekorTime = rekorTime,
WithinTolerance = Math.Abs(skew.TotalSeconds) <= tolerance.Value.TotalSeconds,
ConfiguredTolerance = tolerance.Value
};
}
private static byte[] ComputeHash(ReadOnlySpan<byte> data, HashAlgorithmName algorithm)
{
return algorithm.Name switch
{
"SHA256" => SHA256.HashData(data),
"SHA384" => SHA384.HashData(data),
"SHA512" => SHA512.HashData(data),
_ => SHA256.HashData(data)
};
}
private static byte[] ComputeEnvelopeHash(byte[] envelope, string digestSpec)
{
// Parse algorithm from digest spec (e.g., "sha256:abc...")
var colonIdx = digestSpec.IndexOf(':');
var algorithmName = colonIdx > 0 ? digestSpec[..colonIdx].ToUpperInvariant() : "SHA256";
var algorithm = algorithmName switch
{
"SHA256" => HashAlgorithmName.SHA256,
"SHA384" => HashAlgorithmName.SHA384,
"SHA512" => HashAlgorithmName.SHA512,
_ => HashAlgorithmName.SHA256
};
return ComputeHash(envelope, algorithm);
}
// Placeholder implementations - would integrate with actual TSA client
private Task<byte[]> RequestTimestampAsync(byte[] hash, AttestationTimestampOptions options, CancellationToken ct)
{
// This would call ITimeStampAuthorityClient.GetTimeStampAsync
// For now, return placeholder
_logger.LogDebug("Would request timestamp from TSA");
return Task.FromResult(Array.Empty<byte>());
}
private static (DateTimeOffset genTime, string tsaName, string policyOid) ParseTstInfo(byte[] tstBytes)
{
// This would parse the TST and extract TSTInfo
// For now, return placeholder values
return (DateTimeOffset.UtcNow, "Placeholder TSA", "1.2.3.4");
}
private Task<bool> VerifyImprintAsync(byte[] tst, byte[] expectedHash, CancellationToken ct)
{
// This would verify the messageImprint in the TST matches
return Task.FromResult(true);
}
private Task<bool> VerifyTstSignatureAsync(byte[] tst, CancellationToken ct)
{
// This would verify the CMS signature
return Task.FromResult(true);
}
private Task<TsaCertificateStatus> CheckTsaCertificateAsync(byte[] tst, bool allowOffline, CancellationToken ct)
{
// This would check the TSA certificate revocation status
return Task.FromResult(new TsaCertificateStatus
{
IsValid = true,
Subject = "Placeholder TSA",
RevocationStatus = "Good",
RevocationSource = "OCSP"
});
}
}
/// <summary>
/// Configuration options for <see cref="AttestationTimestampService"/>.
/// </summary>
public sealed record AttestationTimestampServiceOptions
{
/// <summary>
/// Gets the default time skew tolerance.
/// </summary>
public TimeSpan DefaultTimeSkewTolerance { get; init; } = TimeSpan.FromMinutes(5);
/// <summary>
/// Gets whether timestamping is enabled by default.
/// </summary>
public bool EnabledByDefault { get; init; } = true;
/// <summary>
/// Gets whether to fail on TSA errors.
/// </summary>
public bool FailOnTsaError { get; init; } = false;
/// <summary>
/// Gets the minimum days before TSA cert expiry to warn.
/// </summary>
public int CertExpiryWarningDays { get; init; } = 90;
}

View File

@@ -0,0 +1,267 @@
// -----------------------------------------------------------------------------
// IAttestationTimestampService.cs
// Sprint: SPRINT_20260119_010 Attestor TST Integration
// Task: ATT-001 - Attestation Signing Pipeline Extension
// Description: Service interface for timestamping attestations.
// -----------------------------------------------------------------------------
namespace StellaOps.Attestor.Timestamping;
/// <summary>
/// Service for timestamping attestations.
/// </summary>
public interface IAttestationTimestampService
{
/// <summary>
/// Timestamps a signed attestation envelope.
/// </summary>
/// <param name="envelope">The signed DSSE envelope bytes.</param>
/// <param name="options">Timestamping options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The timestamped attestation.</returns>
Task<TimestampedAttestation> TimestampAsync(
ReadOnlyMemory<byte> envelope,
AttestationTimestampOptions? options = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Verifies an attestation's timestamp.
/// </summary>
/// <param name="attestation">The timestamped attestation to verify.</param>
/// <param name="options">Verification options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The verification result.</returns>
Task<AttestationTimestampVerificationResult> VerifyAsync(
TimestampedAttestation attestation,
AttestationTimestampVerificationOptions? options = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Checks time consistency between TST and Rekor.
/// </summary>
/// <param name="tstTime">The TST generation time.</param>
/// <param name="rekorTime">The Rekor integrated time.</param>
/// <param name="tolerance">Tolerance for time skew.</param>
/// <returns>The consistency result.</returns>
TimeConsistencyResult CheckTimeConsistency(
DateTimeOffset tstTime,
DateTimeOffset rekorTime,
TimeSpan? tolerance = null);
}
/// <summary>
/// Options for timestamping attestations.
/// </summary>
public sealed record AttestationTimestampOptions
{
/// <summary>
/// Gets the hash algorithm to use.
/// </summary>
public string HashAlgorithm { get; init; } = "SHA256";
/// <summary>
/// Gets whether to include nonce.
/// </summary>
public bool IncludeNonce { get; init; } = true;
/// <summary>
/// Gets whether to request certificates.
/// </summary>
public bool RequestCertificates { get; init; } = true;
/// <summary>
/// Gets the preferred TSA provider.
/// </summary>
public string? PreferredProvider { get; init; }
/// <summary>
/// Gets whether to store evidence.
/// </summary>
public bool StoreEvidence { get; init; } = true;
/// <summary>
/// Gets whether to fetch revocation data for stapling.
/// </summary>
public bool FetchRevocationData { get; init; } = true;
/// <summary>
/// Gets the default options.
/// </summary>
public static AttestationTimestampOptions Default { get; } = new();
}
/// <summary>
/// Options for verifying attestation timestamps.
/// </summary>
public sealed record AttestationTimestampVerificationOptions
{
/// <summary>
/// Gets whether TST signature verification is required.
/// </summary>
public bool RequireTstSignature { get; init; } = true;
/// <summary>
/// Gets whether Rekor consistency check is required.
/// </summary>
public bool RequireRekorConsistency { get; init; } = true;
/// <summary>
/// Gets the maximum allowed time skew.
/// </summary>
public TimeSpan MaxTimeSkew { get; init; } = TimeSpan.FromMinutes(5);
/// <summary>
/// Gets whether to verify TSA certificate revocation.
/// </summary>
public bool VerifyTsaRevocation { get; init; } = true;
/// <summary>
/// Gets whether to allow offline verification.
/// </summary>
public bool AllowOffline { get; init; } = true;
/// <summary>
/// Gets the default options.
/// </summary>
public static AttestationTimestampVerificationOptions Default { get; } = new();
}
/// <summary>
/// Result of attestation timestamp verification.
/// </summary>
public sealed record AttestationTimestampVerificationResult
{
/// <summary>
/// Gets whether the overall verification passed.
/// </summary>
public bool IsValid { get; init; }
/// <summary>
/// Gets the TST verification result.
/// </summary>
public TstVerificationStatus TstStatus { get; init; }
/// <summary>
/// Gets the time consistency result.
/// </summary>
public TimeConsistencyResult? TimeConsistency { get; init; }
/// <summary>
/// Gets the TSA certificate status.
/// </summary>
public TsaCertificateStatus? TsaCertificateStatus { get; init; }
/// <summary>
/// Gets any error message.
/// </summary>
public string? Error { get; init; }
/// <summary>
/// Gets warnings from verification.
/// </summary>
public IReadOnlyList<string>? Warnings { get; init; }
/// <summary>
/// Creates a successful result.
/// </summary>
public static AttestationTimestampVerificationResult Success(
TimeConsistencyResult? timeConsistency = null,
TsaCertificateStatus? certStatus = null,
IReadOnlyList<string>? warnings = null) => new()
{
IsValid = true,
TstStatus = TstVerificationStatus.Valid,
TimeConsistency = timeConsistency,
TsaCertificateStatus = certStatus,
Warnings = warnings
};
/// <summary>
/// Creates a failure result.
/// </summary>
public static AttestationTimestampVerificationResult Failure(
TstVerificationStatus status,
string error) => new()
{
IsValid = false,
TstStatus = status,
Error = error
};
}
/// <summary>
/// Status of TST verification.
/// </summary>
public enum TstVerificationStatus
{
/// <summary>
/// TST is valid.
/// </summary>
Valid,
/// <summary>
/// TST signature is invalid.
/// </summary>
InvalidSignature,
/// <summary>
/// Message imprint does not match.
/// </summary>
ImprintMismatch,
/// <summary>
/// TST has expired.
/// </summary>
Expired,
/// <summary>
/// TSA certificate is revoked.
/// </summary>
CertificateRevoked,
/// <summary>
/// Time consistency check failed.
/// </summary>
TimeInconsistency,
/// <summary>
/// TST is missing.
/// </summary>
Missing,
/// <summary>
/// Unknown error.
/// </summary>
Unknown
}
/// <summary>
/// Status of TSA certificate.
/// </summary>
public sealed record TsaCertificateStatus
{
/// <summary>
/// Gets whether the certificate is valid.
/// </summary>
public bool IsValid { get; init; }
/// <summary>
/// Gets the certificate subject.
/// </summary>
public string? Subject { get; init; }
/// <summary>
/// Gets the certificate expiration.
/// </summary>
public DateTimeOffset? ExpiresAt { get; init; }
/// <summary>
/// Gets the revocation status.
/// </summary>
public string? RevocationStatus { get; init; }
/// <summary>
/// Gets the source of revocation information.
/// </summary>
public string? RevocationSource { get; init; }
}

View File

@@ -0,0 +1,194 @@
// -----------------------------------------------------------------------------
// ITimeCorrelationValidator.cs
// Sprint: SPRINT_20260119_010 Attestor TST Integration
// Task: ATT-006 - Rekor Time Correlation
// Description: Interface for validating time correlation between TST and Rekor.
// -----------------------------------------------------------------------------
namespace StellaOps.Attestor.Timestamping;
/// <summary>
/// Validates time correlation between RFC-3161 timestamps and Rekor transparency log entries.
/// Prevents backdating attacks where a TST is obtained for malicious content and submitted
/// to Rekor much later.
/// </summary>
public interface ITimeCorrelationValidator
{
/// <summary>
/// Validates the time correlation between a TST generation time and Rekor integration time.
/// </summary>
/// <param name="tstTime">The generation time from the TST (TSTInfo.genTime).</param>
/// <param name="rekorTime">The integrated time from Rekor (IntegratedTime).</param>
/// <param name="policy">The correlation policy to apply.</param>
/// <returns>The validation result with details.</returns>
TimeCorrelationResult Validate(
DateTimeOffset tstTime,
DateTimeOffset rekorTime,
TimeCorrelationPolicy? policy = null);
/// <summary>
/// Validates time correlation asynchronously with metrics recording.
/// </summary>
/// <param name="tstTime">The generation time from the TST.</param>
/// <param name="rekorTime">The integrated time from Rekor.</param>
/// <param name="artifactDigest">The artifact digest for audit logging.</param>
/// <param name="policy">The correlation policy to apply.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The validation result with details.</returns>
Task<TimeCorrelationResult> ValidateAsync(
DateTimeOffset tstTime,
DateTimeOffset rekorTime,
string artifactDigest,
TimeCorrelationPolicy? policy = null,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Policy for time correlation validation.
/// </summary>
public sealed record TimeCorrelationPolicy
{
/// <summary>
/// Gets the maximum allowed gap between TST and Rekor times.
/// Default is 5 minutes.
/// </summary>
public TimeSpan MaximumGap { get; init; } = TimeSpan.FromMinutes(5);
/// <summary>
/// Gets the gap threshold that triggers a suspicious warning.
/// Default is 1 minute.
/// </summary>
public TimeSpan SuspiciousGap { get; init; } = TimeSpan.FromMinutes(1);
/// <summary>
/// Gets whether to fail validation on suspicious (but not maximum) gaps.
/// Default is false (warning only).
/// </summary>
public bool FailOnSuspicious { get; init; } = false;
/// <summary>
/// Gets whether TST time must be before or equal to Rekor time.
/// Default is true (TST should come first).
/// </summary>
public bool RequireTstBeforeRekor { get; init; } = true;
/// <summary>
/// Gets the allowed clock skew tolerance for time comparison.
/// Default is 30 seconds.
/// </summary>
public TimeSpan ClockSkewTolerance { get; init; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Gets the default policy.
/// </summary>
public static TimeCorrelationPolicy Default { get; } = new();
/// <summary>
/// Gets a strict policy with no tolerance for gaps.
/// </summary>
public static TimeCorrelationPolicy Strict { get; } = new()
{
MaximumGap = TimeSpan.FromMinutes(2),
SuspiciousGap = TimeSpan.FromSeconds(30),
FailOnSuspicious = true,
ClockSkewTolerance = TimeSpan.FromSeconds(10)
};
}
/// <summary>
/// Result of time correlation validation.
/// </summary>
public sealed record TimeCorrelationResult
{
/// <summary>Gets whether the validation passed.</summary>
public required bool Valid { get; init; }
/// <summary>Gets whether the gap is suspicious but within limits.</summary>
public required bool Suspicious { get; init; }
/// <summary>Gets the actual gap between TST and Rekor times.</summary>
public required TimeSpan Gap { get; init; }
/// <summary>Gets the TST generation time.</summary>
public required DateTimeOffset TstTime { get; init; }
/// <summary>Gets the Rekor integration time.</summary>
public required DateTimeOffset RekorTime { get; init; }
/// <summary>Gets any error message if validation failed.</summary>
public string? ErrorMessage { get; init; }
/// <summary>Gets any warning message for suspicious gaps.</summary>
public string? WarningMessage { get; init; }
/// <summary>Gets the correlation status.</summary>
public TimeCorrelationStatus Status { get; init; }
/// <summary>
/// Creates a valid result.
/// </summary>
public static TimeCorrelationResult CreateValid(
DateTimeOffset tstTime,
DateTimeOffset rekorTime,
TimeSpan gap,
bool suspicious = false,
string? warningMessage = null)
{
return new TimeCorrelationResult
{
Valid = true,
Suspicious = suspicious,
Gap = gap,
TstTime = tstTime,
RekorTime = rekorTime,
WarningMessage = warningMessage,
Status = suspicious ? TimeCorrelationStatus.ValidWithWarning : TimeCorrelationStatus.Valid
};
}
/// <summary>
/// Creates an invalid result.
/// </summary>
public static TimeCorrelationResult CreateInvalid(
DateTimeOffset tstTime,
DateTimeOffset rekorTime,
TimeSpan gap,
string errorMessage,
TimeCorrelationStatus status)
{
return new TimeCorrelationResult
{
Valid = false,
Suspicious = true,
Gap = gap,
TstTime = tstTime,
RekorTime = rekorTime,
ErrorMessage = errorMessage,
Status = status
};
}
}
/// <summary>
/// Status of time correlation validation.
/// </summary>
public enum TimeCorrelationStatus
{
/// <summary>Times are properly correlated.</summary>
Valid,
/// <summary>Valid but gap is suspicious.</summary>
ValidWithWarning,
/// <summary>Gap exceeds maximum allowed.</summary>
GapExceeded,
/// <summary>TST time is after Rekor time (potential backdating).</summary>
TstAfterRekor,
/// <summary>Time order is suspicious.</summary>
SuspiciousTimeOrder,
/// <summary>Gap is suspicious and policy requires failure.</summary>
SuspiciousGapFailed
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>StellaOps.Attestor.Timestamping</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,200 @@
// -----------------------------------------------------------------------------
// TimeCorrelationValidator.cs
// Sprint: SPRINT_20260119_010 Attestor TST Integration
// Task: ATT-006 - Rekor Time Correlation
// Description: Implementation of time correlation validator.
// -----------------------------------------------------------------------------
using System.Diagnostics.Metrics;
using Microsoft.Extensions.Logging;
namespace StellaOps.Attestor.Timestamping;
/// <summary>
/// Validates time correlation between RFC-3161 timestamps and Rekor transparency log entries.
/// </summary>
public sealed class TimeCorrelationValidator : ITimeCorrelationValidator
{
private readonly ILogger<TimeCorrelationValidator> _logger;
private readonly Histogram<double>? _timeSkewHistogram;
private readonly Counter<long>? _validationCounter;
/// <summary>
/// Initializes a new instance of the <see cref="TimeCorrelationValidator"/> class.
/// </summary>
public TimeCorrelationValidator(
ILogger<TimeCorrelationValidator> logger,
IMeterFactory? meterFactory = null)
{
_logger = logger;
if (meterFactory is not null)
{
var meter = meterFactory.Create("StellaOps.Attestor.Timestamping");
_timeSkewHistogram = meter.CreateHistogram<double>(
"attestation_time_skew_seconds",
unit: "seconds",
description: "Time skew between TST and Rekor in seconds");
_validationCounter = meter.CreateCounter<long>(
"attestation_time_correlation_total",
description: "Total time correlation validations");
}
}
/// <inheritdoc />
public TimeCorrelationResult Validate(
DateTimeOffset tstTime,
DateTimeOffset rekorTime,
TimeCorrelationPolicy? policy = null)
{
policy ??= TimeCorrelationPolicy.Default;
// Calculate the gap (positive if Rekor is after TST, negative if TST is after Rekor)
var gap = rekorTime - tstTime;
var absGap = gap.Duration();
// Record metrics
_timeSkewHistogram?.Record(gap.TotalSeconds);
_validationCounter?.Add(1, new KeyValuePair<string, object?>("result", "attempted"));
// Check if TST is after Rekor (potential backdating attack)
if (policy.RequireTstBeforeRekor && gap < -policy.ClockSkewTolerance)
{
_logger.LogWarning(
"TST time {TstTime} is after Rekor time {RekorTime} by {Gap} - potential backdating",
tstTime,
rekorTime,
gap.Negate());
_validationCounter?.Add(1, new KeyValuePair<string, object?>("result", "tst_after_rekor"));
return TimeCorrelationResult.CreateInvalid(
tstTime,
rekorTime,
gap,
$"TST generation time ({tstTime:O}) is after Rekor integration time ({rekorTime:O}) by {gap.Negate()}. This may indicate a backdating attack.",
TimeCorrelationStatus.TstAfterRekor);
}
// Check if gap exceeds maximum
if (absGap > policy.MaximumGap)
{
_logger.LogWarning(
"Time gap {Gap} between TST {TstTime} and Rekor {RekorTime} exceeds maximum {MaxGap}",
absGap,
tstTime,
rekorTime,
policy.MaximumGap);
_validationCounter?.Add(1, new KeyValuePair<string, object?>("result", "gap_exceeded"));
return TimeCorrelationResult.CreateInvalid(
tstTime,
rekorTime,
gap,
$"Time gap ({absGap}) between TST and Rekor exceeds maximum allowed ({policy.MaximumGap}).",
TimeCorrelationStatus.GapExceeded);
}
// Check if gap is suspicious
var suspicious = absGap > policy.SuspiciousGap;
if (suspicious)
{
_logger.LogInformation(
"Suspicious time gap {Gap} between TST {TstTime} and Rekor {RekorTime}",
absGap,
tstTime,
rekorTime);
if (policy.FailOnSuspicious)
{
_validationCounter?.Add(1, new KeyValuePair<string, object?>("result", "suspicious_failed"));
return TimeCorrelationResult.CreateInvalid(
tstTime,
rekorTime,
gap,
$"Suspicious time gap ({absGap}) between TST and Rekor. Policy requires failure on suspicious gaps.",
TimeCorrelationStatus.SuspiciousGapFailed);
}
_validationCounter?.Add(1, new KeyValuePair<string, object?>("result", "suspicious_warning"));
return TimeCorrelationResult.CreateValid(
tstTime,
rekorTime,
gap,
suspicious: true,
warningMessage: $"Time gap ({absGap}) is larger than typical ({policy.SuspiciousGap}). This may indicate delayed Rekor submission.");
}
// Valid correlation
_logger.LogDebug(
"Time correlation valid: TST {TstTime}, Rekor {RekorTime}, gap {Gap}",
tstTime,
rekorTime,
gap);
_validationCounter?.Add(1, new KeyValuePair<string, object?>("result", "valid"));
return TimeCorrelationResult.CreateValid(tstTime, rekorTime, gap);
}
/// <inheritdoc />
public async Task<TimeCorrelationResult> ValidateAsync(
DateTimeOffset tstTime,
DateTimeOffset rekorTime,
string artifactDigest,
TimeCorrelationPolicy? policy = null,
CancellationToken cancellationToken = default)
{
// Perform validation
var result = Validate(tstTime, rekorTime, policy);
// Audit logging for security-relevant events
if (!result.Valid || result.Suspicious)
{
await LogAuditEventAsync(result, artifactDigest, cancellationToken);
}
return result;
}
private Task LogAuditEventAsync(
TimeCorrelationResult result,
string artifactDigest,
CancellationToken cancellationToken)
{
var auditRecord = new
{
EventType = "TimeCorrelationCheck",
Timestamp = DateTimeOffset.UtcNow,
ArtifactDigest = artifactDigest,
TstTime = result.TstTime,
RekorTime = result.RekorTime,
Gap = result.Gap,
Status = result.Status.ToString(),
Valid = result.Valid,
Suspicious = result.Suspicious,
ErrorMessage = result.ErrorMessage,
WarningMessage = result.WarningMessage
};
if (!result.Valid)
{
_logger.LogWarning(
"[AUDIT] Time correlation validation FAILED for {ArtifactDigest}: {@AuditRecord}",
artifactDigest,
auditRecord);
}
else if (result.Suspicious)
{
_logger.LogWarning(
"[AUDIT] Time correlation SUSPICIOUS for {ArtifactDigest}: {@AuditRecord}",
artifactDigest,
auditRecord);
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,126 @@
// -----------------------------------------------------------------------------
// TimestampedAttestation.cs
// Sprint: SPRINT_20260119_010 Attestor TST Integration
// Task: ATT-001 - Attestation Signing Pipeline Extension
// Description: Models for timestamped attestations.
// -----------------------------------------------------------------------------
namespace StellaOps.Attestor.Timestamping;
/// <summary>
/// An attestation with its associated timestamp evidence.
/// </summary>
public sealed record TimestampedAttestation
{
/// <summary>
/// Gets the signed DSSE envelope.
/// </summary>
public required byte[] Envelope { get; init; }
/// <summary>
/// Gets the envelope hash used for timestamping.
/// </summary>
public required string EnvelopeDigest { get; init; }
/// <summary>
/// Gets the raw RFC-3161 TimeStampToken.
/// </summary>
public required byte[] TimeStampToken { get; init; }
/// <summary>
/// Gets the timestamp generation time.
/// </summary>
public required DateTimeOffset TimestampTime { get; init; }
/// <summary>
/// Gets the TSA name.
/// </summary>
public required string TsaName { get; init; }
/// <summary>
/// Gets the TSA policy OID.
/// </summary>
public required string TsaPolicyOid { get; init; }
/// <summary>
/// Gets the Rekor receipt if submitted to transparency log.
/// </summary>
public RekorReceipt? RekorReceipt { get; init; }
/// <summary>
/// Gets the time consistency result between TST and Rekor.
/// </summary>
public TimeConsistencyResult? TimeConsistency { get; init; }
}
/// <summary>
/// Rekor transparency log receipt.
/// </summary>
public sealed record RekorReceipt
{
/// <summary>
/// Gets the Rekor log ID.
/// </summary>
public required string LogId { get; init; }
/// <summary>
/// Gets the log index.
/// </summary>
public required long LogIndex { get; init; }
/// <summary>
/// Gets the integrated time from Rekor.
/// </summary>
public required DateTimeOffset IntegratedTime { get; init; }
/// <summary>
/// Gets the inclusion proof.
/// </summary>
public byte[]? InclusionProof { get; init; }
/// <summary>
/// Gets the signed entry timestamp.
/// </summary>
public byte[]? SignedEntryTimestamp { get; init; }
}
/// <summary>
/// Result of time consistency check between TST and Rekor.
/// </summary>
public sealed record TimeConsistencyResult
{
/// <summary>
/// Gets the TST generation time.
/// </summary>
public required DateTimeOffset TstTime { get; init; }
/// <summary>
/// Gets the Rekor integrated time.
/// </summary>
public required DateTimeOffset RekorTime { get; init; }
/// <summary>
/// Gets the time skew between TST and Rekor.
/// </summary>
public TimeSpan Skew => RekorTime - TstTime;
/// <summary>
/// Gets whether the skew is within configured tolerance.
/// </summary>
public required bool WithinTolerance { get; init; }
/// <summary>
/// Gets the configured tolerance.
/// </summary>
public required TimeSpan ConfiguredTolerance { get; init; }
/// <summary>
/// Gets whether the temporal ordering is correct (TST before Rekor).
/// </summary>
public bool CorrectOrder => TstTime <= RekorTime;
/// <summary>
/// Gets whether the consistency check passed.
/// </summary>
public bool IsValid => WithinTolerance && CorrectOrder;
}