Files
git.stella-ops.org/src/Cryptography/StellaOps.Cryptography.Plugin.Eidas/Timestamping/QualifiedTimestampVerifier.cs

404 lines
15 KiB
C#

// -----------------------------------------------------------------------------
// QualifiedTimestampVerifier.cs
// Sprint: SPRINT_20260119_011 eIDAS Qualified Timestamp Support
// Task: QTS-006 - Verification for Qualified Timestamps
// Description: Implementation of qualified timestamp verification.
// -----------------------------------------------------------------------------
using System.Formats.Asn1;
using System.Security.Cryptography;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
namespace StellaOps.Cryptography.Plugin.Eidas.Timestamping;
/// <summary>
/// Implementation of <see cref="IQualifiedTimestampVerifier"/>.
/// </summary>
public sealed class QualifiedTimestampVerifier : IQualifiedTimestampVerifier
{
private readonly IEuTrustListService _trustListService;
private readonly ILogger<QualifiedTimestampVerifier> _logger;
private const string SignatureTimestampOid = "1.2.840.113549.1.9.16.2.14";
private const string TstInfoOid = "1.2.840.113549.1.9.16.1.4";
/// <summary>
/// Initializes a new instance of the <see cref="QualifiedTimestampVerifier"/> class.
/// </summary>
public QualifiedTimestampVerifier(
IEuTrustListService trustListService,
ILogger<QualifiedTimestampVerifier> logger)
{
_trustListService = trustListService;
_logger = logger;
}
/// <inheritdoc />
public async Task<QualifiedTimestampVerificationResult> VerifyAsync(
ReadOnlyMemory<byte> timestampToken,
ReadOnlyMemory<byte> originalData,
QualifiedTimestampVerificationOptions? options = null,
CancellationToken cancellationToken = default)
{
options ??= new QualifiedTimestampVerificationOptions();
var errors = new List<string>();
var warnings = new List<string>();
try
{
// Decode the timestamp token (RFC 3161 TimeStampResp)
var signedCms = new SignedCms();
signedCms.Decode(timestampToken.ToArray());
// Verify CMS signature
try
{
signedCms.CheckSignature(verifySignatureOnly: false);
}
catch (CryptographicException ex)
{
return QualifiedTimestampVerificationResult.Failed($"Signature verification failed: {ex.Message}");
}
// Get TSA certificate
var tsaCert = signedCms.SignerInfos[0].Certificate;
if (tsaCert is null)
{
return QualifiedTimestampVerificationResult.Failed("TSA certificate not found in timestamp");
}
// Parse TSTInfo
var tstInfo = ParseTstInfo(signedCms.ContentInfo.Content);
if (tstInfo is null)
{
return QualifiedTimestampVerificationResult.Failed("Failed to parse TSTInfo");
}
// Verify message imprint
var expectedHash = ComputeHash(originalData.ToArray(), tstInfo.DigestAlgorithm);
if (!tstInfo.MessageImprint.SequenceEqual(expectedHash))
{
return QualifiedTimestampVerificationResult.Failed("Message imprint mismatch");
}
// Check TSA qualification
var isQualified = await _trustListService.IsQualifiedTsaAsync(tsaCert, cancellationToken);
var wasQualifiedAtTime = isQualified;
if (options.CheckHistoricalQualification && tstInfo.GenerationTime.HasValue)
{
wasQualifiedAtTime = await _trustListService.WasQualifiedTsaAtTimeAsync(
tsaCert,
tstInfo.GenerationTime.Value,
cancellationToken);
}
if (options.RequireQualifiedTsa && !isQualified)
{
errors.Add("TSA is not on the EU Trusted List as a qualified TSA");
}
if (options.RequireQualifiedTsa && !wasQualifiedAtTime)
{
errors.Add("TSA was not qualified at the time of timestamping");
}
// Get trust list entry for additional info
TrustListEntry? trustListEntry = null;
if (isQualified)
{
trustListEntry = await _trustListService.GetTsaQualificationAsync(
tsaCert.Subject,
cancellationToken);
}
_logger.LogDebug(
"Timestamp verification: valid={Valid}, qualified={Qualified}, time={Time}",
errors.Count == 0,
isQualified,
tstInfo.GenerationTime);
return new QualifiedTimestampVerificationResult
{
IsValid = errors.Count == 0,
IsQualifiedTsa = isQualified,
WasQualifiedAtTime = wasQualifiedAtTime,
GenerationTime = tstInfo.GenerationTime,
TsaCertificate = tsaCert,
TsaName = tsaCert.Subject,
DigestAlgorithm = tstInfo.DigestAlgorithm,
Errors = errors.Count > 0 ? errors : null,
Warnings = warnings.Count > 0 ? warnings : null,
TrustListEntry = trustListEntry
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Timestamp verification failed");
return QualifiedTimestampVerificationResult.Failed(ex.Message);
}
}
/// <inheritdoc />
public async Task<CadesVerificationResult> VerifyCadesAsync(
ReadOnlyMemory<byte> signature,
ReadOnlyMemory<byte> originalData,
QualifiedTimestampVerificationOptions? options = null,
CancellationToken cancellationToken = default)
{
options ??= new QualifiedTimestampVerificationOptions();
var errors = new List<string>();
var warnings = new List<string>();
try
{
// Decode CAdES signature
var signedCms = new SignedCms(new ContentInfo(originalData.ToArray()), detached: true);
try
{
signedCms.Decode(signature.ToArray());
signedCms.CheckSignature(verifySignatureOnly: false);
}
catch (CryptographicException ex)
{
return CadesVerificationResult.Failed($"Signature verification failed: {ex.Message}");
}
var signerInfo = signedCms.SignerInfos[0];
var signerCert = signerInfo.Certificate;
// Detect CAdES level
var level = DetectCadesLevel(signedCms);
ValidateCadesFormat(signedCms, level, errors);
// Get signing time
DateTimeOffset? signingTime = null;
var signingTimeAttr = signerInfo.SignedAttributes
.Cast<CryptographicAttributeObject>()
.FirstOrDefault(a => a.Oid?.Value == "1.2.840.113549.1.9.5");
if (signingTimeAttr is not null)
{
var pkcs9Time = new Pkcs9SigningTime(signingTimeAttr.Values[0].RawData);
signingTime = pkcs9Time.SigningTime;
}
// Verify timestamp if present
QualifiedTimestampVerificationResult? timestampResult = null;
var isTimestampValid = false;
if (level >= CadesLevel.CadesT)
{
var tsAttr = signerInfo.UnsignedAttributes
.Cast<CryptographicAttributeObject>()
.FirstOrDefault(a => a.Oid?.Value == SignatureTimestampOid);
if (tsAttr is not null)
{
var signatureValue = signerInfo.GetSignature();
timestampResult = await VerifyAsync(
tsAttr.Values[0].RawData,
signatureValue,
options,
cancellationToken);
isTimestampValid = timestampResult.IsValid;
if (!isTimestampValid)
{
errors.Add("Timestamp verification failed");
}
}
else
{
errors.Add("Expected timestamp attribute not found");
}
}
// Check LTV completeness if required
var isLtvComplete = false;
var shouldCheckLtv = options.VerifyLtvCompleteness || level >= CadesLevel.CadesLT;
if (shouldCheckLtv)
{
isLtvComplete = VerifyLtvCompleteness(signedCms);
if (!isLtvComplete)
{
errors.Add("LTV data is incomplete");
}
}
return new CadesVerificationResult
{
IsSignatureValid = errors.Count == 0,
IsTimestampValid = isTimestampValid,
DetectedLevel = level,
TimestampResult = timestampResult,
SignerCertificate = signerCert,
SigningTime = signingTime ?? timestampResult?.GenerationTime,
IsLtvComplete = isLtvComplete,
Errors = errors.Count > 0 ? errors : null,
Warnings = warnings.Count > 0 ? warnings : null
};
}
catch (Exception ex)
{
_logger.LogError(ex, "CAdES verification failed");
return CadesVerificationResult.Failed(ex.Message);
}
}
private static TstInfoData? ParseTstInfo(byte[] content)
{
try
{
var reader = new AsnReader(content, AsnEncodingRules.DER);
var sequence = reader.ReadSequence();
// Version
_ = sequence.ReadInteger();
// Policy
_ = sequence.ReadObjectIdentifier();
// MessageImprint
var imprintSeq = sequence.ReadSequence();
var algorithmSeq = imprintSeq.ReadSequence();
var algorithmOid = algorithmSeq.ReadObjectIdentifier();
var messageImprint = imprintSeq.ReadOctetString();
// SerialNumber
_ = sequence.ReadInteger();
// GenTime
var genTime = sequence.ReadGeneralizedTime();
return new TstInfoData
{
DigestAlgorithm = MapOidToAlgorithm(algorithmOid),
MessageImprint = messageImprint,
GenerationTime = genTime
};
}
catch
{
return null;
}
}
private static string MapOidToAlgorithm(string oid) => oid switch
{
"2.16.840.1.101.3.4.2.1" => "SHA256",
"2.16.840.1.101.3.4.2.2" => "SHA384",
"2.16.840.1.101.3.4.2.3" => "SHA512",
"1.3.14.3.2.26" => "SHA1",
_ => oid
};
private static byte[] ComputeHash(byte[] data, string algorithm) => algorithm switch
{
"SHA384" => SHA384.HashData(data),
"SHA512" => SHA512.HashData(data),
_ => SHA256.HashData(data)
};
private static CadesLevel DetectCadesLevel(SignedCms signedCms)
{
var signerInfo = signedCms.SignerInfos[0];
var unsignedAttrs = signerInfo.UnsignedAttributes;
const string archiveTimestampOid = "1.2.840.113549.1.9.16.2.48";
const string revocationValuesOid = "1.2.840.113549.1.9.16.2.24";
const string revocationRefsOid = "1.2.840.113549.1.9.16.2.22";
var hasArchiveTs = unsignedAttrs.Cast<CryptographicAttributeObject>()
.Any(a => a.Oid?.Value == archiveTimestampOid);
if (hasArchiveTs) return CadesLevel.CadesLTA;
var hasRevocationValues = unsignedAttrs.Cast<CryptographicAttributeObject>()
.Any(a => a.Oid?.Value == revocationValuesOid);
if (hasRevocationValues) return CadesLevel.CadesLT;
var hasRevocationRefs = unsignedAttrs.Cast<CryptographicAttributeObject>()
.Any(a => a.Oid?.Value == revocationRefsOid);
if (hasRevocationRefs) return CadesLevel.CadesC;
var hasSignatureTs = unsignedAttrs.Cast<CryptographicAttributeObject>()
.Any(a => a.Oid?.Value == SignatureTimestampOid);
if (hasSignatureTs) return CadesLevel.CadesT;
return CadesLevel.CadesB;
}
private static bool VerifyLtvCompleteness(SignedCms signedCms)
{
var signerInfo = signedCms.SignerInfos[0];
var unsignedAttrs = signerInfo.UnsignedAttributes;
const string revocationValuesOid = "1.2.840.113549.1.9.16.2.24";
const string certValuesOid = "1.2.840.113549.1.9.16.2.23";
var hasRevocationValues = unsignedAttrs.Cast<CryptographicAttributeObject>()
.Any(a => a.Oid?.Value == revocationValuesOid);
var hasCertValues = unsignedAttrs.Cast<CryptographicAttributeObject>()
.Any(a => a.Oid?.Value == certValuesOid);
return hasRevocationValues && hasCertValues;
}
private static void ValidateCadesFormat(SignedCms signedCms, CadesLevel level, List<string> errors)
{
var signerInfo = signedCms.SignerInfos[0];
var unsignedAttrs = signerInfo.UnsignedAttributes;
const string archiveTimestampOid = "1.2.840.113549.1.9.16.2.48";
const string revocationValuesOid = "1.2.840.113549.1.9.16.2.24";
const string certValuesOid = "1.2.840.113549.1.9.16.2.23";
const string revocationRefsOid = "1.2.840.113549.1.9.16.2.22";
if (level >= CadesLevel.CadesT &&
!HasUnsignedAttribute(unsignedAttrs, SignatureTimestampOid))
{
errors.Add("Missing signature timestamp attribute for CAdES-T");
}
if (level >= CadesLevel.CadesC &&
!HasUnsignedAttribute(unsignedAttrs, revocationRefsOid))
{
errors.Add("Missing revocation references for CAdES-C");
}
if (level >= CadesLevel.CadesLT)
{
if (!HasUnsignedAttribute(unsignedAttrs, revocationValuesOid))
{
errors.Add("Missing revocation values for CAdES-LT");
}
if (!HasUnsignedAttribute(unsignedAttrs, certValuesOid))
{
errors.Add("Missing certificate values for CAdES-LT");
}
}
if (level >= CadesLevel.CadesLTA &&
!HasUnsignedAttribute(unsignedAttrs, archiveTimestampOid))
{
errors.Add("Missing archive timestamp for CAdES-LTA");
}
}
private static bool HasUnsignedAttribute(CryptographicAttributeObjectCollection attributes, string oid)
{
return attributes.Cast<CryptographicAttributeObject>()
.Any(a => a.Oid?.Value == oid);
}
private sealed class TstInfoData
{
public required string DigestAlgorithm { get; init; }
public required byte[] MessageImprint { get; init; }
public DateTimeOffset? GenerationTime { get; init; }
}
}