sprints work.
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user