sprints work.
This commit is contained in:
@@ -0,0 +1,353 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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);
|
||||
|
||||
// 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
|
||||
{
|
||||
warnings.Add("Expected timestamp attribute not found");
|
||||
}
|
||||
}
|
||||
|
||||
// Check LTV completeness if required
|
||||
var isLtvComplete = false;
|
||||
if (options.VerifyLtvCompleteness && level >= CadesLevel.CadesLT)
|
||||
{
|
||||
isLtvComplete = VerifyLtvCompleteness(signedCms);
|
||||
if (!isLtvComplete)
|
||||
{
|
||||
warnings.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 sealed class TstInfoData
|
||||
{
|
||||
public required string DigestAlgorithm { get; init; }
|
||||
public required byte[] MessageImprint { get; init; }
|
||||
public DateTimeOffset? GenerationTime { get; init; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user