license switch agpl -> busl1, sprints work, new product advisories
This commit is contained in:
@@ -19,7 +19,11 @@ public sealed class Rfc3161Verifier : ITimeTokenVerifier
|
||||
|
||||
public TimeTokenFormat Format => TimeTokenFormat.Rfc3161;
|
||||
|
||||
public TimeAnchorValidationResult Verify(ReadOnlySpan<byte> tokenBytes, IReadOnlyList<TimeTrustRoot> trustRoots, out TimeAnchor anchor)
|
||||
public TimeAnchorValidationResult Verify(
|
||||
ReadOnlySpan<byte> tokenBytes,
|
||||
IReadOnlyList<TimeTrustRoot> trustRoots,
|
||||
out TimeAnchor anchor,
|
||||
TimeTokenVerificationOptions? options = null)
|
||||
{
|
||||
anchor = TimeAnchor.Unknown;
|
||||
|
||||
@@ -66,13 +70,6 @@ public sealed class Rfc3161Verifier : ITimeTokenVerifier
|
||||
return TimeAnchorValidationResult.Failure("rfc3161-no-signer-certificate");
|
||||
}
|
||||
|
||||
// Validate signer certificate against trust roots
|
||||
var validRoot = ValidateAgainstTrustRoots(signerCert, trustRoots);
|
||||
if (validRoot is null)
|
||||
{
|
||||
return TimeAnchorValidationResult.Failure("rfc3161-certificate-not-trusted");
|
||||
}
|
||||
|
||||
// Extract signing time from the TSTInfo or signed attributes
|
||||
var signingTime = ExtractSigningTime(signedCms, signerInfo);
|
||||
if (signingTime is null)
|
||||
@@ -80,6 +77,27 @@ public sealed class Rfc3161Verifier : ITimeTokenVerifier
|
||||
return TimeAnchorValidationResult.Failure("rfc3161-no-signing-time");
|
||||
}
|
||||
|
||||
// Validate signer certificate against trust roots
|
||||
var extraCertificates = BuildExtraCertificates(signedCms, options);
|
||||
var verificationTime = options?.VerificationTime ?? signingTime.Value;
|
||||
var validRoot = ValidateAgainstTrustRoots(
|
||||
signerCert,
|
||||
trustRoots,
|
||||
extraCertificates,
|
||||
verificationTime);
|
||||
if (validRoot is null)
|
||||
{
|
||||
return TimeAnchorValidationResult.Failure("rfc3161-certificate-not-trusted");
|
||||
}
|
||||
|
||||
if (options?.Offline == true)
|
||||
{
|
||||
if (!TryVerifyOfflineRevocation(options, out var revocationReason))
|
||||
{
|
||||
return TimeAnchorValidationResult.Failure(revocationReason);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute certificate fingerprint
|
||||
var certFingerprint = Convert.ToHexString(SHA256.HashData(signerCert.RawData)).ToLowerInvariant()[..16];
|
||||
|
||||
@@ -102,7 +120,11 @@ public sealed class Rfc3161Verifier : ITimeTokenVerifier
|
||||
}
|
||||
}
|
||||
|
||||
private static TimeTrustRoot? ValidateAgainstTrustRoots(X509Certificate2 signerCert, IReadOnlyList<TimeTrustRoot> trustRoots)
|
||||
private static TimeTrustRoot? ValidateAgainstTrustRoots(
|
||||
X509Certificate2 signerCert,
|
||||
IReadOnlyList<TimeTrustRoot> trustRoots,
|
||||
IReadOnlyList<X509Certificate2> extraCertificates,
|
||||
DateTimeOffset verificationTime)
|
||||
{
|
||||
foreach (var root in trustRoots)
|
||||
{
|
||||
@@ -122,6 +144,15 @@ public sealed class Rfc3161Verifier : ITimeTokenVerifier
|
||||
chain.ChainPolicy.CustomTrustStore.Add(rootCert);
|
||||
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; // Offline mode
|
||||
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
|
||||
chain.ChainPolicy.VerificationTime = verificationTime.UtcDateTime;
|
||||
|
||||
foreach (var cert in extraCertificates)
|
||||
{
|
||||
if (!string.Equals(cert.Thumbprint, rootCert.Thumbprint, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
chain.ChainPolicy.ExtraStore.Add(cert);
|
||||
}
|
||||
}
|
||||
|
||||
if (chain.Build(signerCert))
|
||||
{
|
||||
@@ -138,6 +169,86 @@ public sealed class Rfc3161Verifier : ITimeTokenVerifier
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<X509Certificate2> BuildExtraCertificates(
|
||||
SignedCms signedCms,
|
||||
TimeTokenVerificationOptions? options)
|
||||
{
|
||||
var extra = new List<X509Certificate2>();
|
||||
if (options?.CertificateChain is { Count: > 0 })
|
||||
{
|
||||
extra.AddRange(options.CertificateChain);
|
||||
}
|
||||
|
||||
foreach (var cert in signedCms.Certificates.Cast<X509Certificate2>())
|
||||
{
|
||||
if (!extra.Any(existing =>
|
||||
existing.Thumbprint.Equals(cert.Thumbprint, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
extra.Add(cert);
|
||||
}
|
||||
}
|
||||
|
||||
return extra;
|
||||
}
|
||||
|
||||
private static bool TryVerifyOfflineRevocation(
|
||||
TimeTokenVerificationOptions options,
|
||||
out string reason)
|
||||
{
|
||||
var hasOcsp = options.OcspResponses.Count > 0;
|
||||
var hasCrl = options.Crls.Count > 0;
|
||||
|
||||
if (!hasOcsp && !hasCrl)
|
||||
{
|
||||
reason = "rfc3161-revocation-missing";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasOcsp && options.OcspResponses.Any(IsOcspSuccess))
|
||||
{
|
||||
reason = "rfc3161-revocation-ocsp";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasCrl && options.Crls.Any(IsCrlParseable))
|
||||
{
|
||||
reason = "rfc3161-revocation-crl";
|
||||
return true;
|
||||
}
|
||||
|
||||
reason = "rfc3161-revocation-invalid";
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsOcspSuccess(byte[] response)
|
||||
{
|
||||
try
|
||||
{
|
||||
var reader = new AsnReader(response, AsnEncodingRules.DER);
|
||||
var sequence = reader.ReadSequence();
|
||||
var status = sequence.ReadEnumeratedValue<OcspResponseStatus>();
|
||||
return status == OcspResponseStatus.Successful;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsCrlParseable(byte[] crl)
|
||||
{
|
||||
try
|
||||
{
|
||||
var reader = new AsnReader(crl, AsnEncodingRules.DER);
|
||||
reader.ReadSequence();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static DateTimeOffset? ExtractSigningTime(SignedCms signedCms, SignerInfo signerInfo)
|
||||
{
|
||||
// Try to get signing time from signed attributes
|
||||
@@ -215,4 +326,14 @@ public sealed class Rfc3161Verifier : ITimeTokenVerifier
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private enum OcspResponseStatus
|
||||
{
|
||||
Successful = 0,
|
||||
MalformedRequest = 1,
|
||||
InternalError = 2,
|
||||
TryLater = 3,
|
||||
SigRequired = 5,
|
||||
Unauthorized = 6
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user