176 lines
5.7 KiB
C#
176 lines
5.7 KiB
C#
// -----------------------------------------------------------------------------
|
|
// CertificateStatusResult.cs
|
|
// Sprint: SPRINT_20260119_008 Certificate Status Provider
|
|
// Task: CSP-001 - Core Abstractions
|
|
// Description: Result model for certificate status checking.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
using System.Security.Cryptography.X509Certificates;
|
|
|
|
namespace StellaOps.Cryptography.CertificateStatus.Abstractions;
|
|
|
|
/// <summary>
|
|
/// Result of a certificate revocation status check.
|
|
/// </summary>
|
|
public sealed record CertificateStatusResult
|
|
{
|
|
/// <summary>
|
|
/// Gets the revocation status.
|
|
/// </summary>
|
|
public required RevocationStatus Status { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the source of the revocation information.
|
|
/// </summary>
|
|
public required RevocationSource Source { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the time the status was produced.
|
|
/// </summary>
|
|
public DateTimeOffset? ProducedAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets when this status information was last updated.
|
|
/// </summary>
|
|
public DateTimeOffset? ThisUpdate { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets when this status information will next be updated.
|
|
/// </summary>
|
|
public DateTimeOffset? NextUpdate { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the revocation time if the certificate is revoked.
|
|
/// </summary>
|
|
public DateTimeOffset? RevocationTime { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the revocation reason if the certificate is revoked.
|
|
/// </summary>
|
|
public RevocationReason? RevocationReason { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the raw OCSP response if available.
|
|
/// </summary>
|
|
public ReadOnlyMemory<byte>? RawOcspResponse { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the raw CRL if available.
|
|
/// </summary>
|
|
public ReadOnlyMemory<byte>? RawCrl { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets error details if the check failed.
|
|
/// </summary>
|
|
public string? Error { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the responder URL that was used.
|
|
/// </summary>
|
|
public string? ResponderUrl { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets whether the status is considered valid.
|
|
/// </summary>
|
|
public bool IsValid => Status == RevocationStatus.Good;
|
|
|
|
/// <summary>
|
|
/// Gets whether the response is still fresh.
|
|
/// </summary>
|
|
public bool IsFresh => NextUpdate.HasValue && NextUpdate.Value > DateTimeOffset.UtcNow;
|
|
|
|
/// <summary>
|
|
/// Creates a result indicating the certificate is good.
|
|
/// </summary>
|
|
public static CertificateStatusResult Good(
|
|
RevocationSource source,
|
|
DateTimeOffset? thisUpdate = null,
|
|
DateTimeOffset? nextUpdate = null,
|
|
ReadOnlyMemory<byte>? rawResponse = null) => new()
|
|
{
|
|
Status = RevocationStatus.Good,
|
|
Source = source,
|
|
ProducedAt = DateTimeOffset.UtcNow,
|
|
ThisUpdate = thisUpdate,
|
|
NextUpdate = nextUpdate,
|
|
RawOcspResponse = source is RevocationSource.Ocsp or RevocationSource.OcspStapled ? rawResponse : null,
|
|
RawCrl = source is RevocationSource.Crl or RevocationSource.CrlCached ? rawResponse : null
|
|
};
|
|
|
|
/// <summary>
|
|
/// Creates a result indicating the certificate is revoked.
|
|
/// </summary>
|
|
public static CertificateStatusResult Revoked(
|
|
RevocationSource source,
|
|
DateTimeOffset revocationTime,
|
|
RevocationReason reason = Abstractions.RevocationReason.Unspecified) => new()
|
|
{
|
|
Status = RevocationStatus.Revoked,
|
|
Source = source,
|
|
ProducedAt = DateTimeOffset.UtcNow,
|
|
RevocationTime = revocationTime,
|
|
RevocationReason = reason
|
|
};
|
|
|
|
/// <summary>
|
|
/// Creates a result indicating the status is unknown.
|
|
/// </summary>
|
|
public static CertificateStatusResult Unknown(RevocationSource source, string? error = null) => new()
|
|
{
|
|
Status = RevocationStatus.Unknown,
|
|
Source = source,
|
|
ProducedAt = DateTimeOffset.UtcNow,
|
|
Error = error
|
|
};
|
|
|
|
/// <summary>
|
|
/// Creates a result indicating the check is unavailable.
|
|
/// </summary>
|
|
public static CertificateStatusResult Unavailable(string error) => new()
|
|
{
|
|
Status = RevocationStatus.Unavailable,
|
|
Source = RevocationSource.None,
|
|
ProducedAt = DateTimeOffset.UtcNow,
|
|
Error = error
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of checking an entire certificate chain.
|
|
/// </summary>
|
|
public sealed record ChainStatusResult
|
|
{
|
|
/// <summary>
|
|
/// Gets whether the entire chain is valid.
|
|
/// </summary>
|
|
public bool IsValid => CertificateResults.All(r => r.Status == RevocationStatus.Good);
|
|
|
|
/// <summary>
|
|
/// Gets the status results for each certificate in the chain.
|
|
/// </summary>
|
|
public required IReadOnlyList<CertificateStatusResult> CertificateResults { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets the overall chain status.
|
|
/// </summary>
|
|
public RevocationStatus OverallStatus
|
|
{
|
|
get
|
|
{
|
|
if (CertificateResults.Any(r => r.Status == RevocationStatus.Revoked))
|
|
return RevocationStatus.Revoked;
|
|
if (CertificateResults.Any(r => r.Status == RevocationStatus.Unknown))
|
|
return RevocationStatus.Unknown;
|
|
if (CertificateResults.Any(r => r.Status == RevocationStatus.Unavailable))
|
|
return RevocationStatus.Unavailable;
|
|
return RevocationStatus.Good;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the first revoked certificate result if any.
|
|
/// </summary>
|
|
public CertificateStatusResult? FirstRevoked =>
|
|
CertificateResults.FirstOrDefault(r => r.Status == RevocationStatus.Revoked);
|
|
}
|