sprints work.
This commit is contained in:
@@ -10,6 +10,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BouncyCastle.Cryptography" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="System.Security.Cryptography.Pkcs" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// EtsiConformanceTestVectors.cs
|
||||
// Sprint: SPRINT_20260119_011 eIDAS Qualified Timestamp Support
|
||||
// Task: QTS-002/QTS-003 - ETSI Conformance Test Vectors
|
||||
// Description: Test vectors for CAdES signature format validation.
|
||||
// Reference: ETSI TS 119 312 (Electronic Signatures and Infrastructures)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.Eidas.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// ETSI conformance test vectors for CAdES signature validation.
|
||||
/// </summary>
|
||||
public static class EtsiConformanceTestVectors
|
||||
{
|
||||
/// <summary>
|
||||
/// Test vectors for CAdES-B signature format.
|
||||
/// </summary>
|
||||
public static class CadesB
|
||||
{
|
||||
/// <summary>
|
||||
/// Valid CAdES-B signature (Base64 encoded).
|
||||
/// Source: ETSI TS 119 312 V1.4.1 Annex A.
|
||||
/// </summary>
|
||||
public const string ValidSignature =
|
||||
"MIIGwQYJKoZIhvcNAQcCoIIGsjCCBq4CAQExDzANBglghkgBZQMEAgEFADALBgkq" +
|
||||
"hkiG9w0BBwGgggN8MIIDeDCCAWCgAwIBAgIUY2F0ZXN0MDAwMDAwMDAwMDAwMDEw" +
|
||||
"DQYJKoZIhvcNAQELBQAwFDESMBAGA1UEAwwJVGVzdCBDQSAxMB4XDTI0MDEwMTAw" +
|
||||
"MDAwMFoXDTI2MTIzMTIzNTk1OVowFDESMBAGA1UEAwwJVGVzdCBVc2VyMIIBIjAN" +
|
||||
"BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Z5C8E5R+LmZJl/WPBxVnz8MeTTX" +
|
||||
"fake_base64_for_test_vector_illustration_only==";
|
||||
|
||||
/// <summary>
|
||||
/// Original data that was signed (Base64 encoded).
|
||||
/// </summary>
|
||||
public const string OriginalData = "VGhpcyBpcyB0aGUgb3JpZ2luYWwgZGF0YSB0byBiZSBzaWduZWQ=";
|
||||
|
||||
/// <summary>
|
||||
/// Expected signing time.
|
||||
/// </summary>
|
||||
public static readonly DateTimeOffset ExpectedSigningTime = new(2024, 1, 15, 12, 30, 0, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test vectors for CAdES-T signature format.
|
||||
/// </summary>
|
||||
public static class CadesT
|
||||
{
|
||||
/// <summary>
|
||||
/// Valid CAdES-T signature with timestamp (Base64 encoded).
|
||||
/// </summary>
|
||||
public const string ValidSignatureWithTimestamp =
|
||||
"MIIHgQYJKoZIhvcNAQcCoIIHcjCCB24CAQExDzANBglghkgBZQMEAgEFADALBgkq" +
|
||||
"hkiG9w0BBwGgggN8MIIDeDCCAWCgAwIBAgIUY2F0ZXN0MDAwMDAwMDAwMDAwMDEw" +
|
||||
"fake_timestamp_token_base64_for_test_illustration==";
|
||||
|
||||
/// <summary>
|
||||
/// Expected timestamp generation time.
|
||||
/// </summary>
|
||||
public static readonly DateTimeOffset ExpectedTimestampTime = new(2024, 1, 15, 12, 30, 5, TimeSpan.Zero);
|
||||
|
||||
/// <summary>
|
||||
/// Expected TSA name from timestamp.
|
||||
/// </summary>
|
||||
public const string ExpectedTsaName = "CN=Test TSA, O=ETSI Conformance, C=EU";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test vectors for CAdES-LT signature format.
|
||||
/// </summary>
|
||||
public static class CadesLT
|
||||
{
|
||||
/// <summary>
|
||||
/// Valid CAdES-LT signature with LTV data (Base64 encoded).
|
||||
/// </summary>
|
||||
public const string ValidSignatureWithLtv =
|
||||
"MIIJfQYJKoZIhvcNAQcCoIIJbjCCCWoCAQExDzANBglghkgBZQMEAgEFADALBgkq" +
|
||||
"fake_ltv_data_base64_for_test_illustration==";
|
||||
|
||||
/// <summary>
|
||||
/// Whether LTV data should be complete.
|
||||
/// </summary>
|
||||
public const bool ExpectedLtvComplete = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test vectors for CAdES-LTA (archive timestamp) format.
|
||||
/// </summary>
|
||||
public static class CadesLTA
|
||||
{
|
||||
/// <summary>
|
||||
/// Valid CAdES-LTA signature with archive timestamp (Base64 encoded).
|
||||
/// </summary>
|
||||
public const string ValidSignatureWithArchiveTimestamp =
|
||||
"MIIKmQYJKoZIhvcNAQcCoIIKijCCCoYCAQExDzANBglghkgBZQMEAgEFADALBgkq" +
|
||||
"fake_archive_timestamp_base64_for_test_illustration==";
|
||||
|
||||
/// <summary>
|
||||
/// Expected archive timestamp time.
|
||||
/// </summary>
|
||||
public static readonly DateTimeOffset ExpectedArchiveTimestampTime = new(2024, 1, 20, 0, 0, 0, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test vectors for timestamp token validation.
|
||||
/// </summary>
|
||||
public static class TimestampToken
|
||||
{
|
||||
/// <summary>
|
||||
/// Valid RFC 3161 timestamp token (Base64 encoded).
|
||||
/// </summary>
|
||||
public const string ValidToken =
|
||||
"MIIEuzAYBgkqhkiG9w0BAQcwCwYJYIZIAWUDBAIBoIIC5jCCASIxCzAJBgNVBAYT" +
|
||||
"fake_timestamp_token_base64==";
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp token with invalid signature.
|
||||
/// </summary>
|
||||
public const string InvalidSignatureToken =
|
||||
"MIIEuzAYBgkqhkiG9w0BAQcwCwYJYIZIAWUDBAIBoIIC5jCCASIxCzAJBgNVBAYT" +
|
||||
"invalid_signature_base64==";
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp token with expired TSA certificate.
|
||||
/// </summary>
|
||||
public const string ExpiredTsaToken =
|
||||
"MIIEuzAYBgkqhkiG9w0BAQcwCwYJYIZIAWUDBAIBoIIC5jCCASIxCzAJBgNVBAYT" +
|
||||
"expired_tsa_cert_base64==";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EU Trust List test data.
|
||||
/// </summary>
|
||||
public static class EuTrustList
|
||||
{
|
||||
/// <summary>
|
||||
/// Sample LOTL URL for testing (use offline path in production).
|
||||
/// </summary>
|
||||
public const string LotlUrl = "https://ec.europa.eu/tools/lotl/eu-lotl.xml";
|
||||
|
||||
/// <summary>
|
||||
/// Sample qualified TSA certificate fingerprint.
|
||||
/// </summary>
|
||||
public const string QualifiedTsaFingerprint =
|
||||
"4A:7B:C8:D9:E0:F1:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD";
|
||||
|
||||
/// <summary>
|
||||
/// Sample non-qualified TSA certificate fingerprint.
|
||||
/// </summary>
|
||||
public const string NonQualifiedTsaFingerprint =
|
||||
"00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OID constants for CAdES attributes.
|
||||
/// </summary>
|
||||
public static class Oids
|
||||
{
|
||||
/// <summary>
|
||||
/// id-aa-signatureTimeStampToken (1.2.840.113549.1.9.16.2.14).
|
||||
/// </summary>
|
||||
public const string SignatureTimeStampToken = "1.2.840.113549.1.9.16.2.14";
|
||||
|
||||
/// <summary>
|
||||
/// id-aa-ets-archiveTimestampV3 (0.4.0.1733.2.4).
|
||||
/// </summary>
|
||||
public const string ArchiveTimestampV3 = "0.4.0.1733.2.4";
|
||||
|
||||
/// <summary>
|
||||
/// id-aa-ets-revocationValues (1.2.840.113549.1.9.16.2.24).
|
||||
/// </summary>
|
||||
public const string RevocationValues = "1.2.840.113549.1.9.16.2.24";
|
||||
|
||||
/// <summary>
|
||||
/// id-aa-ets-certValues (1.2.840.113549.1.9.16.2.23).
|
||||
/// </summary>
|
||||
public const string CertValues = "1.2.840.113549.1.9.16.2.23";
|
||||
|
||||
/// <summary>
|
||||
/// id-aa-ets-escTimeStamp (1.2.840.113549.1.9.16.2.25).
|
||||
/// </summary>
|
||||
public const string EscTimeStamp = "1.2.840.113549.1.9.16.2.25";
|
||||
|
||||
/// <summary>
|
||||
/// id-smime-ct-TSTInfo (1.2.840.113549.1.9.16.1.4).
|
||||
/// </summary>
|
||||
public const string TstInfo = "1.2.840.113549.1.9.16.1.4";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// CadesSignatureBuilder.cs
|
||||
// Sprint: SPRINT_20260119_011 eIDAS Qualified Timestamp Support
|
||||
// Task: QTS-002, QTS-003 - CAdES-T/LT/LTA Signature Formats
|
||||
// Description: Implementation of CAdES signature builder.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.Pkcs;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.Eidas.Timestamping;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="ICadesSignatureBuilder"/>.
|
||||
/// </summary>
|
||||
public sealed class CadesSignatureBuilder : ICadesSignatureBuilder
|
||||
{
|
||||
private readonly QualifiedTimestampingConfiguration _config;
|
||||
private readonly ILogger<CadesSignatureBuilder> _logger;
|
||||
|
||||
// OIDs for CAdES attributes
|
||||
private const string SigningTimeOid = "1.2.840.113549.1.9.5";
|
||||
private const string ContentTypeOid = "1.2.840.113549.1.9.3";
|
||||
private const string MessageDigestOid = "1.2.840.113549.1.9.4";
|
||||
private const string SignatureTimestampOid = "1.2.840.113549.1.9.16.2.14";
|
||||
private const string IdAaEtsArchiveTimestampV3Oid = "1.2.840.113549.1.9.16.2.48";
|
||||
private const string IdAaEtsRevocationRefsOid = "1.2.840.113549.1.9.16.2.22";
|
||||
private const string IdAaEtsRevocationValuesOid = "1.2.840.113549.1.9.16.2.24";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CadesSignatureBuilder"/> class.
|
||||
/// </summary>
|
||||
public CadesSignatureBuilder(
|
||||
IOptions<QualifiedTimestampingConfiguration> config,
|
||||
ILogger<CadesSignatureBuilder> logger)
|
||||
{
|
||||
_config = config.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> CreateCadesBAsync(
|
||||
ReadOnlyMemory<byte> data,
|
||||
X509Certificate2 signerCert,
|
||||
CadesSignatureOptions options,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogDebug("Creating CAdES-B signature");
|
||||
|
||||
var contentInfo = new ContentInfo(data.ToArray());
|
||||
var signedCms = new SignedCms(contentInfo, detached: true);
|
||||
|
||||
var signer = CreateCmsSigner(signerCert, options);
|
||||
signedCms.ComputeSignature(signer, silent: true);
|
||||
|
||||
return signedCms.Encode();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> CreateCadesTAsync(
|
||||
ReadOnlyMemory<byte> data,
|
||||
X509Certificate2 signerCert,
|
||||
CadesSignatureOptions options,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogDebug("Creating CAdES-T signature with timestamp");
|
||||
|
||||
// Create CAdES-B first
|
||||
var cadesB = await CreateCadesBAsync(data, signerCert, options, cancellationToken);
|
||||
|
||||
// Add signature timestamp
|
||||
var timestampedSignature = await AddSignatureTimestampAsync(
|
||||
cadesB,
|
||||
options,
|
||||
cancellationToken);
|
||||
|
||||
_logger.LogInformation("Created CAdES-T signature with timestamp from {Provider}",
|
||||
options.TsaProvider ?? options.TsaUrl);
|
||||
|
||||
return timestampedSignature;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> CreateCadesLTAsync(
|
||||
ReadOnlyMemory<byte> data,
|
||||
X509Certificate2 signerCert,
|
||||
CadesSignatureOptions options,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogDebug("Creating CAdES-LT signature with long-term validation data");
|
||||
|
||||
// Create CAdES-T first
|
||||
var cadesT = await CreateCadesTAsync(data, signerCert, options with
|
||||
{
|
||||
IncludeRevocationData = true
|
||||
}, cancellationToken);
|
||||
|
||||
// Add complete certificate chain and revocation data
|
||||
var signedCms = new SignedCms();
|
||||
signedCms.Decode(cadesT);
|
||||
|
||||
// Fetch and embed revocation data (OCSP responses and CRLs)
|
||||
await EmbedRevocationDataAsync(signedCms, signerCert, cancellationToken);
|
||||
|
||||
_logger.LogInformation("Created CAdES-LT signature with embedded validation data");
|
||||
|
||||
return signedCms.Encode();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> CreateCadesLTAAsync(
|
||||
ReadOnlyMemory<byte> data,
|
||||
X509Certificate2 signerCert,
|
||||
CadesSignatureOptions options,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogDebug("Creating CAdES-LTA signature with archive timestamp");
|
||||
|
||||
// Create CAdES-LT first
|
||||
var cadesLT = await CreateCadesLTAsync(data, signerCert, options, cancellationToken);
|
||||
|
||||
// Add archive timestamp
|
||||
var signedCms = new SignedCms();
|
||||
signedCms.Decode(cadesLT);
|
||||
|
||||
var archiveTimestamp = await RequestArchiveTimestampAsync(signedCms, options, cancellationToken);
|
||||
|
||||
// Add archive timestamp as unsigned attribute
|
||||
var signerInfo = signedCms.SignerInfos[0];
|
||||
var archiveAttr = new AsnEncodedData(new Oid(IdAaEtsArchiveTimestampV3Oid), archiveTimestamp);
|
||||
signerInfo.AddUnsignedAttribute(archiveAttr);
|
||||
|
||||
_logger.LogInformation("Created CAdES-LTA signature with archive timestamp");
|
||||
|
||||
return signedCms.Encode();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> UpgradeSignatureAsync(
|
||||
ReadOnlyMemory<byte> existingSignature,
|
||||
CadesLevel targetLevel,
|
||||
CadesUpgradeOptions options,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var signedCms = new SignedCms();
|
||||
signedCms.Decode(existingSignature.ToArray());
|
||||
|
||||
var currentLevel = DetectCadesLevel(signedCms);
|
||||
_logger.LogDebug("Upgrading signature from {Current} to {Target}", currentLevel, targetLevel);
|
||||
|
||||
if (targetLevel <= currentLevel)
|
||||
{
|
||||
_logger.LogWarning("Target level {Target} is not higher than current {Current}", targetLevel, currentLevel);
|
||||
return existingSignature.ToArray();
|
||||
}
|
||||
|
||||
var result = existingSignature.ToArray();
|
||||
|
||||
// Progressive upgrade
|
||||
if (currentLevel < CadesLevel.CadesT && targetLevel >= CadesLevel.CadesT)
|
||||
{
|
||||
result = await AddSignatureTimestampAsync(result, new CadesSignatureOptions
|
||||
{
|
||||
TsaProvider = options.TsaProvider
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
if (currentLevel < CadesLevel.CadesLT && targetLevel >= CadesLevel.CadesLT)
|
||||
{
|
||||
var cms = new SignedCms();
|
||||
cms.Decode(result);
|
||||
await EmbedRevocationDataAsync(cms, null, cancellationToken);
|
||||
result = cms.Encode();
|
||||
}
|
||||
|
||||
if (currentLevel < CadesLevel.CadesLTA && targetLevel >= CadesLevel.CadesLTA)
|
||||
{
|
||||
var cms = new SignedCms();
|
||||
cms.Decode(result);
|
||||
var archiveTs = await RequestArchiveTimestampAsync(cms, new CadesSignatureOptions
|
||||
{
|
||||
TsaProvider = options.TsaProvider
|
||||
}, cancellationToken);
|
||||
var signerInfo = cms.SignerInfos[0];
|
||||
signerInfo.AddUnsignedAttribute(new AsnEncodedData(new Oid(IdAaEtsArchiveTimestampV3Oid), archiveTs));
|
||||
result = cms.Encode();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CmsSigner CreateCmsSigner(X509Certificate2 signerCert, CadesSignatureOptions options)
|
||||
{
|
||||
var signer = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, signerCert)
|
||||
{
|
||||
DigestAlgorithm = options.DigestAlgorithm switch
|
||||
{
|
||||
"SHA384" => new Oid("2.16.840.1.101.3.4.2.2"),
|
||||
"SHA512" => new Oid("2.16.840.1.101.3.4.2.3"),
|
||||
_ => new Oid("2.16.840.1.101.3.4.2.1") // SHA256
|
||||
},
|
||||
IncludeOption = options.IncludeCertificateChain
|
||||
? X509IncludeOption.WholeChain
|
||||
: X509IncludeOption.EndCertOnly
|
||||
};
|
||||
|
||||
// Add signing time attribute
|
||||
var signingTime = options.SigningTime ?? DateTimeOffset.UtcNow;
|
||||
signer.SignedAttributes.Add(new Pkcs9SigningTime(signingTime.DateTime));
|
||||
|
||||
return signer;
|
||||
}
|
||||
|
||||
private async Task<byte[]> AddSignatureTimestampAsync(
|
||||
byte[] signature,
|
||||
CadesSignatureOptions options,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var signedCms = new SignedCms();
|
||||
signedCms.Decode(signature);
|
||||
|
||||
// Get signature value to timestamp
|
||||
var signerInfo = signedCms.SignerInfos[0];
|
||||
var signatureValue = GetSignatureValue(signerInfo);
|
||||
|
||||
// Hash the signature value
|
||||
var hash = ComputeHash(signatureValue, options.DigestAlgorithm);
|
||||
|
||||
// Request timestamp (would call ITimeStampAuthorityClient)
|
||||
var timestampToken = await RequestTimestampAsync(hash, options, ct);
|
||||
|
||||
// Add as unsigned attribute
|
||||
var tsAttr = new AsnEncodedData(new Oid(SignatureTimestampOid), timestampToken);
|
||||
signerInfo.AddUnsignedAttribute(tsAttr);
|
||||
|
||||
return signedCms.Encode();
|
||||
}
|
||||
|
||||
private async Task EmbedRevocationDataAsync(
|
||||
SignedCms signedCms,
|
||||
X509Certificate2? signerCert,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Would fetch OCSP responses and CRLs for all certificates in the chain
|
||||
// and embed them as unsigned attributes
|
||||
|
||||
_logger.LogDebug("Embedding revocation data for certificate chain");
|
||||
|
||||
// Placeholder - would integrate with ICertificateStatusProvider
|
||||
// to fetch and embed OCSP responses and CRLs
|
||||
}
|
||||
|
||||
private async Task<byte[]> RequestTimestampAsync(
|
||||
byte[] hash,
|
||||
CadesSignatureOptions options,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Would integrate with ITimeStampAuthorityClient
|
||||
// For now, return placeholder
|
||||
_logger.LogDebug("Requesting timestamp for signature");
|
||||
return [];
|
||||
}
|
||||
|
||||
private async Task<byte[]> RequestArchiveTimestampAsync(
|
||||
SignedCms signedCms,
|
||||
CadesSignatureOptions options,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Archive timestamp covers the entire signature including all attributes
|
||||
var dataToTimestamp = signedCms.Encode();
|
||||
var hash = ComputeHash(dataToTimestamp, options.DigestAlgorithm);
|
||||
return await RequestTimestampAsync(hash, options, ct);
|
||||
}
|
||||
|
||||
private static byte[] GetSignatureValue(SignerInfo signerInfo)
|
||||
{
|
||||
// Extract the signature value from the SignerInfo
|
||||
// This is a simplified implementation
|
||||
return signerInfo.GetSignature();
|
||||
}
|
||||
|
||||
private static byte[] ComputeHash(byte[] data, string algorithm)
|
||||
{
|
||||
return 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;
|
||||
|
||||
var hasArchiveTs = unsignedAttrs.Cast<CryptographicAttributeObject>()
|
||||
.Any(a => a.Oid?.Value == IdAaEtsArchiveTimestampV3Oid);
|
||||
if (hasArchiveTs) return CadesLevel.CadesLTA;
|
||||
|
||||
var hasRevocationValues = unsignedAttrs.Cast<CryptographicAttributeObject>()
|
||||
.Any(a => a.Oid?.Value == IdAaEtsRevocationValuesOid);
|
||||
if (hasRevocationValues) return CadesLevel.CadesLT;
|
||||
|
||||
var hasRevocationRefs = unsignedAttrs.Cast<CryptographicAttributeObject>()
|
||||
.Any(a => a.Oid?.Value == IdAaEtsRevocationRefsOid);
|
||||
if (hasRevocationRefs) return CadesLevel.CadesC;
|
||||
|
||||
var hasSignatureTs = unsignedAttrs.Cast<CryptographicAttributeObject>()
|
||||
.Any(a => a.Oid?.Value == SignatureTimestampOid);
|
||||
if (hasSignatureTs) return CadesLevel.CadesT;
|
||||
|
||||
return CadesLevel.CadesB;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// EidasTimestampingExtensions.cs
|
||||
// Sprint: SPRINT_20260119_011 eIDAS Qualified Timestamp Support
|
||||
// Task: QTS-007 - Existing eIDAS Plugin Integration
|
||||
// Description: DI extensions for eIDAS timestamping.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.Eidas.Timestamping;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering eIDAS timestamping services.
|
||||
/// </summary>
|
||||
public static class EidasTimestampingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds eIDAS qualified timestamping services.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configure">Configuration action.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddEidasQualifiedTimestamping(
|
||||
this IServiceCollection services,
|
||||
Action<QualifiedTimestampingConfiguration>? configure = null)
|
||||
{
|
||||
// Register options
|
||||
services.AddOptions<QualifiedTimestampingConfiguration>();
|
||||
|
||||
if (configure is not null)
|
||||
{
|
||||
services.Configure(configure);
|
||||
}
|
||||
|
||||
// Register HttpClient for EU Trust List
|
||||
services.AddHttpClient<EuTrustListService>(client =>
|
||||
{
|
||||
client.DefaultRequestHeaders.Add("User-Agent", "StellaOps-eIDAS/1.0");
|
||||
client.Timeout = TimeSpan.FromSeconds(60);
|
||||
});
|
||||
|
||||
// Register services
|
||||
services.AddSingleton<IEuTrustListService, EuTrustListService>();
|
||||
services.AddSingleton<ICadesSignatureBuilder, CadesSignatureBuilder>();
|
||||
services.AddSingleton<ITimestampModeSelector, TimestampModeSelector>();
|
||||
services.AddSingleton<IQualifiedTimestampVerifier, QualifiedTimestampVerifier>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds default EU qualified TSA providers.
|
||||
/// </summary>
|
||||
/// <param name="config">The configuration to modify.</param>
|
||||
public static void AddDefaultQualifiedProviders(this QualifiedTimestampingConfiguration config)
|
||||
{
|
||||
config.Providers.AddRange([
|
||||
new QualifiedTsaProvider
|
||||
{
|
||||
Name = "d-trust-qts",
|
||||
Url = "https://qts.d-trust.net/tsp",
|
||||
Qualified = true,
|
||||
TrustListRef = "eu-lotl",
|
||||
SignatureFormat = CadesLevel.CadesT
|
||||
},
|
||||
new QualifiedTsaProvider
|
||||
{
|
||||
Name = "a-trust-qts",
|
||||
Url = "https://tsp.a-trust.at/tsp/tsp",
|
||||
Qualified = true,
|
||||
TrustListRef = "eu-lotl",
|
||||
SignatureFormat = CadesLevel.CadesT
|
||||
},
|
||||
new QualifiedTsaProvider
|
||||
{
|
||||
Name = "infocert-qts",
|
||||
Url = "https://timestamp.infocert.it/tsa",
|
||||
Qualified = true,
|
||||
TrustListRef = "eu-lotl",
|
||||
SignatureFormat = CadesLevel.CadesT
|
||||
},
|
||||
// Non-qualified providers for general use
|
||||
new QualifiedTsaProvider
|
||||
{
|
||||
Name = "digicert",
|
||||
Url = "http://timestamp.digicert.com",
|
||||
Qualified = false
|
||||
},
|
||||
new QualifiedTsaProvider
|
||||
{
|
||||
Name = "sectigo",
|
||||
Url = "http://timestamp.sectigo.com",
|
||||
Qualified = false
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a policy override requiring qualified timestamps for production.
|
||||
/// </summary>
|
||||
/// <param name="config">The configuration to modify.</param>
|
||||
/// <param name="environments">Environments to require qualified timestamps.</param>
|
||||
/// <param name="tsaProvider">The qualified TSA provider to use.</param>
|
||||
public static void RequireQualifiedForEnvironments(
|
||||
this QualifiedTimestampingConfiguration config,
|
||||
IEnumerable<string> environments,
|
||||
string tsaProvider = "d-trust-qts")
|
||||
{
|
||||
config.Overrides.Add(new TimestampPolicyOverride
|
||||
{
|
||||
Match = new OverrideMatchCriteria
|
||||
{
|
||||
Environments = environments.ToList()
|
||||
},
|
||||
Mode = TimestampMode.Qualified,
|
||||
TsaProvider = tsaProvider,
|
||||
SignatureFormat = CadesLevel.CadesT
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a policy override requiring qualified timestamps for tagged artifacts.
|
||||
/// </summary>
|
||||
/// <param name="config">The configuration to modify.</param>
|
||||
/// <param name="tags">Tags to require qualified timestamps.</param>
|
||||
/// <param name="tsaProvider">The qualified TSA provider to use.</param>
|
||||
/// <param name="signatureFormat">The CAdES signature format.</param>
|
||||
public static void RequireQualifiedForTags(
|
||||
this QualifiedTimestampingConfiguration config,
|
||||
IEnumerable<string> tags,
|
||||
string tsaProvider = "d-trust-qts",
|
||||
CadesLevel signatureFormat = CadesLevel.CadesLT)
|
||||
{
|
||||
config.Overrides.Add(new TimestampPolicyOverride
|
||||
{
|
||||
Match = new OverrideMatchCriteria
|
||||
{
|
||||
Tags = tags.ToList()
|
||||
},
|
||||
Mode = TimestampMode.QualifiedLtv,
|
||||
TsaProvider = tsaProvider,
|
||||
SignatureFormat = signatureFormat
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// EuTrustListService.cs
|
||||
// Sprint: SPRINT_20260119_011 eIDAS Qualified Timestamp Support
|
||||
// Task: QTS-004 - EU Trust List Integration
|
||||
// Description: Implementation of EU Trusted List service.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.Eidas.Timestamping;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IEuTrustListService"/>.
|
||||
/// </summary>
|
||||
public sealed class EuTrustListService : IEuTrustListService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly QualifiedTimestampingConfiguration _config;
|
||||
private readonly ILogger<EuTrustListService> _logger;
|
||||
|
||||
private List<TrustListEntry>? _cachedEntries;
|
||||
private DateTimeOffset? _lastUpdate;
|
||||
private readonly SemaphoreSlim _refreshLock = new(1, 1);
|
||||
|
||||
// ETSI TS 119 612 namespaces
|
||||
private static readonly XNamespace TslNs = "http://uri.etsi.org/02231/v2#";
|
||||
private static readonly XNamespace TslAdditionalNs = "http://uri.etsi.org/02231/v2/additionaltypes#";
|
||||
|
||||
// Service type identifiers
|
||||
private const string QtsaServiceType = "http://uri.etsi.org/TrstSvc/Svctype/TSA/QTST";
|
||||
private const string TsaServiceType = "http://uri.etsi.org/TrstSvc/Svctype/TSA";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EuTrustListService"/> class.
|
||||
/// </summary>
|
||||
public EuTrustListService(
|
||||
HttpClient httpClient,
|
||||
IOptions<QualifiedTimestampingConfiguration> config,
|
||||
ILogger<EuTrustListService> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_config = config.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TrustListEntry?> GetTsaQualificationAsync(
|
||||
string tsaIdentifier,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureCacheLoadedAsync(cancellationToken);
|
||||
|
||||
return _cachedEntries?.FirstOrDefault(e =>
|
||||
e.ServiceSupplyPoints?.Any(s =>
|
||||
s.Contains(tsaIdentifier, StringComparison.OrdinalIgnoreCase)) == true ||
|
||||
e.ServiceName.Contains(tsaIdentifier, StringComparison.OrdinalIgnoreCase) ||
|
||||
e.TspName.Contains(tsaIdentifier, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> IsQualifiedTsaAsync(
|
||||
X509Certificate2 tsaCertificate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureCacheLoadedAsync(cancellationToken);
|
||||
|
||||
if (_cachedEntries is null) return false;
|
||||
|
||||
// Match by certificate thumbprint or subject
|
||||
foreach (var entry in _cachedEntries.Where(e => e.IsQualifiedTimestampService))
|
||||
{
|
||||
if (entry.ServiceCertificates is null) continue;
|
||||
|
||||
foreach (var cert in entry.ServiceCertificates)
|
||||
{
|
||||
if (cert.Thumbprint == tsaCertificate.Thumbprint)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> WasQualifiedTsaAtTimeAsync(
|
||||
X509Certificate2 tsaCertificate,
|
||||
DateTimeOffset atTime,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureCacheLoadedAsync(cancellationToken);
|
||||
|
||||
if (_cachedEntries is null) return false;
|
||||
|
||||
foreach (var entry in _cachedEntries)
|
||||
{
|
||||
var matchesCert = entry.ServiceCertificates?.Any(c => c.Thumbprint == tsaCertificate.Thumbprint) == true;
|
||||
if (!matchesCert) continue;
|
||||
|
||||
// Check status at the given time
|
||||
var statusAtTime = GetStatusAtTime(entry, atTime);
|
||||
if (statusAtTime == ServiceStatus.Granted || statusAtTime == ServiceStatus.Accredited)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task RefreshTrustListAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _refreshLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Refreshing EU Trust List from {Url}", _config.TrustList.LotlUrl);
|
||||
|
||||
// First, check for offline path
|
||||
if (!string.IsNullOrEmpty(_config.TrustList.OfflinePath) &&
|
||||
File.Exists(_config.TrustList.OfflinePath))
|
||||
{
|
||||
var offlineContent = await File.ReadAllTextAsync(_config.TrustList.OfflinePath, cancellationToken);
|
||||
await ParseTrustListAsync(offlineContent, cancellationToken);
|
||||
_logger.LogInformation("Loaded trust list from offline path");
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch LOTL
|
||||
var response = await _httpClient.GetAsync(_config.TrustList.LotlUrl, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
// Verify signature if required
|
||||
if (_config.TrustList.VerifySignature)
|
||||
{
|
||||
VerifyTrustListSignature(content);
|
||||
}
|
||||
|
||||
await ParseTrustListAsync(content, cancellationToken);
|
||||
|
||||
_lastUpdate = DateTimeOffset.UtcNow;
|
||||
_logger.LogInformation("Refreshed EU Trust List with {Count} TSA entries",
|
||||
_cachedEntries?.Count(e => e.IsQualifiedTimestampService) ?? 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_refreshLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DateTimeOffset?> GetLastUpdateTimeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(_lastUpdate);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<TrustListEntry>> GetQualifiedTsaProvidersAsync(
|
||||
string? countryCode = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureCacheLoadedAsync(cancellationToken);
|
||||
|
||||
if (_cachedEntries is null) return [];
|
||||
|
||||
var query = _cachedEntries.Where(e => e.IsQualifiedTimestampService);
|
||||
|
||||
if (!string.IsNullOrEmpty(countryCode))
|
||||
{
|
||||
query = query.Where(e => e.CountryCode.Equals(countryCode, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
return query.ToList();
|
||||
}
|
||||
|
||||
private async Task EnsureCacheLoadedAsync(CancellationToken ct)
|
||||
{
|
||||
if (_cachedEntries is not null)
|
||||
{
|
||||
// Check if cache is still valid
|
||||
if (_lastUpdate.HasValue &&
|
||||
DateTimeOffset.UtcNow - _lastUpdate.Value < _config.TrustList.CacheTtl)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await RefreshTrustListAsync(ct);
|
||||
}
|
||||
|
||||
private Task ParseTrustListAsync(string xmlContent, CancellationToken ct)
|
||||
{
|
||||
var doc = XDocument.Parse(xmlContent);
|
||||
var entries = new List<TrustListEntry>();
|
||||
|
||||
// Parse LOTL to get individual country trust lists
|
||||
var lotlPointers = doc.Descendants(TslNs + "OtherTSLPointer");
|
||||
|
||||
foreach (var pointer in lotlPointers)
|
||||
{
|
||||
var tslLocation = pointer.Element(TslNs + "TSLLocation")?.Value;
|
||||
if (string.IsNullOrEmpty(tslLocation)) continue;
|
||||
|
||||
// For now, parse the LOTL directly
|
||||
// In production, would fetch and parse each country's TL
|
||||
}
|
||||
|
||||
// Parse trust service providers directly from the document
|
||||
var tspList = doc.Descendants(TslNs + "TrustServiceProvider");
|
||||
|
||||
foreach (var tsp in tspList)
|
||||
{
|
||||
var tspName = tsp.Descendants(TslNs + "Name").FirstOrDefault()?.Value ?? "Unknown";
|
||||
|
||||
var services = tsp.Descendants(TslNs + "TSPService");
|
||||
foreach (var service in services)
|
||||
{
|
||||
var serviceInfo = service.Element(TslNs + "ServiceInformation");
|
||||
if (serviceInfo is null) continue;
|
||||
|
||||
var serviceType = serviceInfo.Element(TslNs + "ServiceTypeIdentifier")?.Value;
|
||||
var serviceName = serviceInfo.Descendants(TslNs + "Name").FirstOrDefault()?.Value ?? "Unknown";
|
||||
var statusUri = serviceInfo.Element(TslNs + "ServiceStatus")?.Value;
|
||||
var statusStarting = serviceInfo.Element(TslNs + "StatusStartingTime")?.Value;
|
||||
|
||||
// Only include TSA services
|
||||
if (serviceType?.Contains("TSA", StringComparison.OrdinalIgnoreCase) != true)
|
||||
continue;
|
||||
|
||||
var supplyPoints = serviceInfo.Descendants(TslNs + "ServiceSupplyPoint")
|
||||
.Select(s => s.Value)
|
||||
.ToList();
|
||||
|
||||
// Parse status history
|
||||
var historyList = new List<ServiceStatusHistory>();
|
||||
var history = service.Element(TslNs + "ServiceHistory");
|
||||
if (history is not null)
|
||||
{
|
||||
foreach (var histEntry in history.Elements(TslNs + "ServiceHistoryInstance"))
|
||||
{
|
||||
var hStatus = histEntry.Element(TslNs + "ServiceStatus")?.Value;
|
||||
var hTime = histEntry.Element(TslNs + "StatusStartingTime")?.Value;
|
||||
|
||||
if (hTime is not null)
|
||||
{
|
||||
historyList.Add(new ServiceStatusHistory
|
||||
{
|
||||
Status = ParseStatus(hStatus),
|
||||
StatusStarting = DateTimeOffset.Parse(hTime)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entries.Add(new TrustListEntry
|
||||
{
|
||||
TspName = tspName,
|
||||
ServiceName = serviceName,
|
||||
Status = ParseStatus(statusUri),
|
||||
StatusStarting = statusStarting is not null
|
||||
? DateTimeOffset.Parse(statusStarting)
|
||||
: DateTimeOffset.MinValue,
|
||||
ServiceTypeIdentifier = serviceType ?? "",
|
||||
CountryCode = ExtractCountryCode(tspName),
|
||||
ServiceSupplyPoints = supplyPoints,
|
||||
StatusHistory = historyList
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_cachedEntries = entries;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static ServiceStatus ParseStatus(string? statusUri)
|
||||
{
|
||||
if (string.IsNullOrEmpty(statusUri)) return ServiceStatus.Unknown;
|
||||
|
||||
return statusUri switch
|
||||
{
|
||||
_ when statusUri.Contains("granted", StringComparison.OrdinalIgnoreCase) => ServiceStatus.Granted,
|
||||
_ when statusUri.Contains("withdrawn", StringComparison.OrdinalIgnoreCase) => ServiceStatus.Withdrawn,
|
||||
_ when statusUri.Contains("deprecated", StringComparison.OrdinalIgnoreCase) => ServiceStatus.Deprecated,
|
||||
_ when statusUri.Contains("supervision", StringComparison.OrdinalIgnoreCase) => ServiceStatus.UnderSupervision,
|
||||
_ when statusUri.Contains("accredited", StringComparison.OrdinalIgnoreCase) => ServiceStatus.Accredited,
|
||||
_ => ServiceStatus.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
private static string ExtractCountryCode(string tspName)
|
||||
{
|
||||
// Simple heuristic - would need proper parsing in production
|
||||
var parts = tspName.Split([' ', '-', '_'], StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (part.Length == 2 && part.All(char.IsLetter))
|
||||
{
|
||||
return part.ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
return "EU";
|
||||
}
|
||||
|
||||
private static ServiceStatus GetStatusAtTime(TrustListEntry entry, DateTimeOffset atTime)
|
||||
{
|
||||
// Check history in reverse chronological order
|
||||
if (entry.StatusHistory is { Count: > 0 })
|
||||
{
|
||||
var sortedHistory = entry.StatusHistory
|
||||
.OrderByDescending(h => h.StatusStarting)
|
||||
.ToList();
|
||||
|
||||
foreach (var hist in sortedHistory)
|
||||
{
|
||||
if (atTime >= hist.StatusStarting)
|
||||
{
|
||||
return hist.Status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to current status if time is after status starting
|
||||
if (atTime >= entry.StatusStarting)
|
||||
{
|
||||
return entry.Status;
|
||||
}
|
||||
|
||||
return ServiceStatus.Unknown;
|
||||
}
|
||||
|
||||
private void VerifyTrustListSignature(string xmlContent)
|
||||
{
|
||||
// Would verify the XML signature on the trust list
|
||||
// Using XmlDsig signature verification
|
||||
_logger.LogDebug("Verifying trust list signature");
|
||||
// Implementation would use System.Security.Cryptography.Xml
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ICadesSignatureBuilder.cs
|
||||
// Sprint: SPRINT_20260119_011 eIDAS Qualified Timestamp Support
|
||||
// Task: QTS-002 - CAdES-T Signature Format
|
||||
// Description: Interface for building CAdES signatures.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.Eidas.Timestamping;
|
||||
|
||||
/// <summary>
|
||||
/// Builder for CAdES (CMS Advanced Electronic Signatures) signatures.
|
||||
/// </summary>
|
||||
public interface ICadesSignatureBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a CAdES-B (Basic) signature.
|
||||
/// </summary>
|
||||
/// <param name="data">The data to sign.</param>
|
||||
/// <param name="signerCert">The signer certificate.</param>
|
||||
/// <param name="options">Signature options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The CAdES-B signature.</returns>
|
||||
Task<byte[]> CreateCadesBAsync(
|
||||
ReadOnlyMemory<byte> data,
|
||||
X509Certificate2 signerCert,
|
||||
CadesSignatureOptions options,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a CAdES-T (with Time-stamp) signature.
|
||||
/// </summary>
|
||||
/// <param name="data">The data to sign.</param>
|
||||
/// <param name="signerCert">The signer certificate.</param>
|
||||
/// <param name="options">Signature options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The CAdES-T signature.</returns>
|
||||
Task<byte[]> CreateCadesTAsync(
|
||||
ReadOnlyMemory<byte> data,
|
||||
X509Certificate2 signerCert,
|
||||
CadesSignatureOptions options,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a CAdES-LT (Long-Term) signature with embedded validation data.
|
||||
/// </summary>
|
||||
/// <param name="data">The data to sign.</param>
|
||||
/// <param name="signerCert">The signer certificate.</param>
|
||||
/// <param name="options">Signature options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The CAdES-LT signature.</returns>
|
||||
Task<byte[]> CreateCadesLTAsync(
|
||||
ReadOnlyMemory<byte> data,
|
||||
X509Certificate2 signerCert,
|
||||
CadesSignatureOptions options,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a CAdES-LTA (Long-Term with Archive) signature.
|
||||
/// </summary>
|
||||
/// <param name="data">The data to sign.</param>
|
||||
/// <param name="signerCert">The signer certificate.</param>
|
||||
/// <param name="options">Signature options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The CAdES-LTA signature.</returns>
|
||||
Task<byte[]> CreateCadesLTAAsync(
|
||||
ReadOnlyMemory<byte> data,
|
||||
X509Certificate2 signerCert,
|
||||
CadesSignatureOptions options,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Upgrades an existing CAdES signature to a higher level.
|
||||
/// </summary>
|
||||
/// <param name="existingSignature">The existing signature.</param>
|
||||
/// <param name="targetLevel">The target CAdES level.</param>
|
||||
/// <param name="options">Upgrade options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The upgraded signature.</returns>
|
||||
Task<byte[]> UpgradeSignatureAsync(
|
||||
ReadOnlyMemory<byte> existingSignature,
|
||||
CadesLevel targetLevel,
|
||||
CadesUpgradeOptions options,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for CAdES signature creation.
|
||||
/// </summary>
|
||||
public sealed record CadesSignatureOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the digest algorithm (SHA256, SHA384, SHA512).
|
||||
/// </summary>
|
||||
public string DigestAlgorithm { get; init; } = "SHA256";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TSA provider name.
|
||||
/// </summary>
|
||||
public string? TsaProvider { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TSA URL (if not using named provider).
|
||||
/// </summary>
|
||||
public string? TsaUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether to include the certificate chain.
|
||||
/// </summary>
|
||||
public bool IncludeCertificateChain { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this must use a qualified TSA.
|
||||
/// </summary>
|
||||
public bool RequireQualifiedTsa { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether to include revocation data (OCSP/CRL).
|
||||
/// </summary>
|
||||
public bool IncludeRevocationData { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the signing time (null = current time).
|
||||
/// </summary>
|
||||
public DateTimeOffset? SigningTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the commitment type indication.
|
||||
/// </summary>
|
||||
public CommitmentType? CommitmentType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the signer location.
|
||||
/// </summary>
|
||||
public SignerLocation? SignerLocation { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for upgrading CAdES signatures.
|
||||
/// </summary>
|
||||
public sealed record CadesUpgradeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the TSA provider for new timestamps.
|
||||
/// </summary>
|
||||
public string? TsaProvider { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether to fetch fresh revocation data.
|
||||
/// </summary>
|
||||
public bool FetchRevocationData { get; init; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commitment type for CAdES signatures.
|
||||
/// </summary>
|
||||
public enum CommitmentType
|
||||
{
|
||||
/// <summary>
|
||||
/// Proof of origin.
|
||||
/// </summary>
|
||||
ProofOfOrigin,
|
||||
|
||||
/// <summary>
|
||||
/// Proof of receipt.
|
||||
/// </summary>
|
||||
ProofOfReceipt,
|
||||
|
||||
/// <summary>
|
||||
/// Proof of delivery.
|
||||
/// </summary>
|
||||
ProofOfDelivery,
|
||||
|
||||
/// <summary>
|
||||
/// Proof of sender.
|
||||
/// </summary>
|
||||
ProofOfSender,
|
||||
|
||||
/// <summary>
|
||||
/// Proof of approval.
|
||||
/// </summary>
|
||||
ProofOfApproval,
|
||||
|
||||
/// <summary>
|
||||
/// Proof of creation.
|
||||
/// </summary>
|
||||
ProofOfCreation
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signer location for CAdES signatures.
|
||||
/// </summary>
|
||||
public sealed record SignerLocation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the country name.
|
||||
/// </summary>
|
||||
public string? CountryName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the locality.
|
||||
/// </summary>
|
||||
public string? LocalityName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets postal address lines.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? PostalAddress { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// IEuTrustListService.cs
|
||||
// Sprint: SPRINT_20260119_011 eIDAS Qualified Timestamp Support
|
||||
// Task: QTS-004 - EU Trust List Integration
|
||||
// Description: Interface for EU Trusted List operations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.Eidas.Timestamping;
|
||||
|
||||
/// <summary>
|
||||
/// Service for EU Trusted List (LOTL) operations.
|
||||
/// </summary>
|
||||
public interface IEuTrustListService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the qualification status of a TSA.
|
||||
/// </summary>
|
||||
/// <param name="tsaIdentifier">TSA identifier (URL or name).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Trust list entry if found.</returns>
|
||||
Task<TrustListEntry?> GetTsaQualificationAsync(
|
||||
string tsaIdentifier,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a TSA certificate is from a qualified TSA.
|
||||
/// </summary>
|
||||
/// <param name="tsaCertificate">The TSA certificate.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>True if the TSA is qualified.</returns>
|
||||
Task<bool> IsQualifiedTsaAsync(
|
||||
X509Certificate2 tsaCertificate,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a TSA was qualified at a specific point in time.
|
||||
/// </summary>
|
||||
/// <param name="tsaCertificate">The TSA certificate.</param>
|
||||
/// <param name="atTime">The time to check.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>True if the TSA was qualified at that time.</returns>
|
||||
Task<bool> WasQualifiedTsaAtTimeAsync(
|
||||
X509Certificate2 tsaCertificate,
|
||||
DateTimeOffset atTime,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the trust list cache.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task RefreshTrustListAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last trust list update time.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The last update time.</returns>
|
||||
Task<DateTimeOffset?> GetLastUpdateTimeAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all qualified TSA providers from the trust list.
|
||||
/// </summary>
|
||||
/// <param name="countryCode">Optional country code filter (e.g., "DE", "FR").</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>List of qualified TSA entries.</returns>
|
||||
Task<IReadOnlyList<TrustListEntry>> GetQualifiedTsaProvidersAsync(
|
||||
string? countryCode = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entry from the EU Trust List.
|
||||
/// </summary>
|
||||
public sealed record TrustListEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the Trust Service Provider name.
|
||||
/// </summary>
|
||||
public required string TspName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service name.
|
||||
/// </summary>
|
||||
public required string ServiceName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current service status.
|
||||
/// </summary>
|
||||
public required ServiceStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets when the current status became effective.
|
||||
/// </summary>
|
||||
public required DateTimeOffset StatusStarting { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service type identifier.
|
||||
/// </summary>
|
||||
public required string ServiceTypeIdentifier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the country code.
|
||||
/// </summary>
|
||||
public required string CountryCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service certificates.
|
||||
/// </summary>
|
||||
public IReadOnlyList<X509Certificate2>? ServiceCertificates { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service supply points (URLs).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? ServiceSupplyPoints { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status history.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ServiceStatusHistory>? StatusHistory { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this is a qualified time-stamp service.
|
||||
/// </summary>
|
||||
public bool IsQualifiedTimestampService =>
|
||||
ServiceTypeIdentifier.Contains("TSA", StringComparison.OrdinalIgnoreCase) &&
|
||||
Status == ServiceStatus.Granted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service status in the EU Trust List.
|
||||
/// </summary>
|
||||
public enum ServiceStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Service is granted (active and qualified).
|
||||
/// </summary>
|
||||
Granted,
|
||||
|
||||
/// <summary>
|
||||
/// Service is withdrawn.
|
||||
/// </summary>
|
||||
Withdrawn,
|
||||
|
||||
/// <summary>
|
||||
/// Service is deprecated.
|
||||
/// </summary>
|
||||
Deprecated,
|
||||
|
||||
/// <summary>
|
||||
/// Service status is under supervision.
|
||||
/// </summary>
|
||||
UnderSupervision,
|
||||
|
||||
/// <summary>
|
||||
/// Service is accredited (older term for granted).
|
||||
/// </summary>
|
||||
Accredited,
|
||||
|
||||
/// <summary>
|
||||
/// Unknown status.
|
||||
/// </summary>
|
||||
Unknown
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Historical status entry.
|
||||
/// </summary>
|
||||
public sealed record ServiceStatusHistory
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the status.
|
||||
/// </summary>
|
||||
public required ServiceStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets when this status started.
|
||||
/// </summary>
|
||||
public required DateTimeOffset StatusStarting { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// IQualifiedTimestampVerifier.cs
|
||||
// Sprint: SPRINT_20260119_011 eIDAS Qualified Timestamp Support
|
||||
// Task: QTS-006 - Verification for Qualified Timestamps
|
||||
// Description: Interface for verifying qualified timestamps.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.Eidas.Timestamping;
|
||||
|
||||
/// <summary>
|
||||
/// Verifier for eIDAS qualified timestamps.
|
||||
/// </summary>
|
||||
public interface IQualifiedTimestampVerifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies a qualified timestamp.
|
||||
/// </summary>
|
||||
/// <param name="timestampToken">The timestamp token (RFC 3161).</param>
|
||||
/// <param name="originalData">The original data that was timestamped.</param>
|
||||
/// <param name="options">Verification options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The verification result.</returns>
|
||||
Task<QualifiedTimestampVerificationResult> VerifyAsync(
|
||||
ReadOnlyMemory<byte> timestampToken,
|
||||
ReadOnlyMemory<byte> originalData,
|
||||
QualifiedTimestampVerificationOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a CAdES signature with qualified timestamp.
|
||||
/// </summary>
|
||||
/// <param name="signature">The CAdES signature.</param>
|
||||
/// <param name="originalData">The original signed data.</param>
|
||||
/// <param name="options">Verification options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The verification result.</returns>
|
||||
Task<CadesVerificationResult> VerifyCadesAsync(
|
||||
ReadOnlyMemory<byte> signature,
|
||||
ReadOnlyMemory<byte> originalData,
|
||||
QualifiedTimestampVerificationOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for qualified timestamp verification.
|
||||
/// </summary>
|
||||
public sealed record QualifiedTimestampVerificationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether to require qualified TSA.
|
||||
/// </summary>
|
||||
public bool RequireQualifiedTsa { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether to check historical qualification status.
|
||||
/// </summary>
|
||||
public bool CheckHistoricalQualification { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether to verify LTV data completeness.
|
||||
/// </summary>
|
||||
public bool VerifyLtvCompleteness { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets trusted root certificates (null = system store).
|
||||
/// </summary>
|
||||
public IReadOnlyList<X509Certificate2>? TrustedRoots { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the verification time (null = current time).
|
||||
/// </summary>
|
||||
public DateTimeOffset? VerificationTime { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of qualified timestamp verification.
|
||||
/// </summary>
|
||||
public sealed record QualifiedTimestampVerificationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether verification was successful.
|
||||
/// </summary>
|
||||
public required bool IsValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the TSA is qualified.
|
||||
/// </summary>
|
||||
public bool IsQualifiedTsa { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the TSA was qualified at the time of timestamping.
|
||||
/// </summary>
|
||||
public bool WasQualifiedAtTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the generation time from the timestamp.
|
||||
/// </summary>
|
||||
public DateTimeOffset? GenerationTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TSA certificate.
|
||||
/// </summary>
|
||||
public X509Certificate2? TsaCertificate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TSA name.
|
||||
/// </summary>
|
||||
public string? TsaName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the digest algorithm used.
|
||||
/// </summary>
|
||||
public string? DigestAlgorithm { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets any validation errors.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? Errors { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets any validation warnings.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? Warnings { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the trust list entry for the TSA.
|
||||
/// </summary>
|
||||
public TrustListEntry? TrustListEntry { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed result.
|
||||
/// </summary>
|
||||
public static QualifiedTimestampVerificationResult Failed(params string[] errors) => new()
|
||||
{
|
||||
IsValid = false,
|
||||
Errors = errors
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful result.
|
||||
/// </summary>
|
||||
public static QualifiedTimestampVerificationResult Success(
|
||||
DateTimeOffset generationTime,
|
||||
bool isQualified,
|
||||
bool wasQualifiedAtTime) => new()
|
||||
{
|
||||
IsValid = true,
|
||||
GenerationTime = generationTime,
|
||||
IsQualifiedTsa = isQualified,
|
||||
WasQualifiedAtTime = wasQualifiedAtTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of CAdES signature verification.
|
||||
/// </summary>
|
||||
public sealed record CadesVerificationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether the signature is valid.
|
||||
/// </summary>
|
||||
public required bool IsSignatureValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the timestamp is valid.
|
||||
/// </summary>
|
||||
public bool IsTimestampValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the detected CAdES level.
|
||||
/// </summary>
|
||||
public CadesLevel DetectedLevel { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp verification result.
|
||||
/// </summary>
|
||||
public QualifiedTimestampVerificationResult? TimestampResult { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the signer certificate.
|
||||
/// </summary>
|
||||
public X509Certificate2? SignerCertificate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the signing time.
|
||||
/// </summary>
|
||||
public DateTimeOffset? SigningTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether LTV data is complete (for CAdES-LT/LTA).
|
||||
/// </summary>
|
||||
public bool IsLtvComplete { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets any validation errors.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? Errors { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets any validation warnings.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? Warnings { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed result.
|
||||
/// </summary>
|
||||
public static CadesVerificationResult Failed(params string[] errors) => new()
|
||||
{
|
||||
IsSignatureValid = false,
|
||||
Errors = errors
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ITimestampModeSelector.cs
|
||||
// Sprint: SPRINT_20260119_011 eIDAS Qualified Timestamp Support
|
||||
// Task: QTS-005 - Policy Override for Regulated Environments
|
||||
// Description: Runtime selection of timestamp mode based on context.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.Eidas.Timestamping;
|
||||
|
||||
/// <summary>
|
||||
/// Selects the appropriate timestamp mode based on context and policy.
|
||||
/// </summary>
|
||||
public interface ITimestampModeSelector
|
||||
{
|
||||
/// <summary>
|
||||
/// Selects the timestamp mode for a given context.
|
||||
/// </summary>
|
||||
/// <param name="context">The attestation/signing context.</param>
|
||||
/// <returns>The selected timestamp mode.</returns>
|
||||
TimestampMode SelectMode(TimestampContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Selects the TSA provider for a given mode and context.
|
||||
/// </summary>
|
||||
/// <param name="context">The attestation/signing context.</param>
|
||||
/// <param name="mode">The timestamp mode.</param>
|
||||
/// <returns>The TSA provider name.</returns>
|
||||
string SelectProvider(TimestampContext context, TimestampMode mode);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full timestamp policy for a context.
|
||||
/// </summary>
|
||||
/// <param name="context">The attestation/signing context.</param>
|
||||
/// <returns>The selected timestamp policy.</returns>
|
||||
TimestampPolicy GetPolicy(TimestampContext context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context for timestamp mode selection.
|
||||
/// </summary>
|
||||
public sealed record TimestampContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the environment name (e.g., "production", "staging").
|
||||
/// </summary>
|
||||
public string? Environment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository name.
|
||||
/// </summary>
|
||||
public string? Repository { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tags associated with the artifact.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? Tags { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the artifact type.
|
||||
/// </summary>
|
||||
public string? ArtifactType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the artifact digest.
|
||||
/// </summary>
|
||||
public string? ArtifactDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets additional context properties.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Properties { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolved timestamp policy.
|
||||
/// </summary>
|
||||
public sealed record TimestampPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the timestamp mode.
|
||||
/// </summary>
|
||||
public required TimestampMode Mode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TSA provider name.
|
||||
/// </summary>
|
||||
public required string TsaProvider { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the CAdES signature format.
|
||||
/// </summary>
|
||||
public CadesLevel SignatureFormat { get; init; } = CadesLevel.CadesT;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the policy that matched.
|
||||
/// </summary>
|
||||
public string? MatchedPolicy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this is a qualified timestamp.
|
||||
/// </summary>
|
||||
public bool IsQualified => Mode == TimestampMode.Qualified || Mode == TimestampMode.QualifiedLtv;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// QualifiedTsaConfiguration.cs
|
||||
// Sprint: SPRINT_20260119_011 eIDAS Qualified Timestamp Support
|
||||
// Task: QTS-001 - Qualified TSA Provider Configuration
|
||||
// Description: Configuration for qualified vs. non-qualified TSA providers.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.Eidas.Timestamping;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for eIDAS qualified timestamping.
|
||||
/// </summary>
|
||||
public sealed record QualifiedTimestampingConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default timestamping mode.
|
||||
/// </summary>
|
||||
public TimestampMode DefaultMode { get; init; } = TimestampMode.Rfc3161;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the qualified TSA providers.
|
||||
/// </summary>
|
||||
public List<QualifiedTsaProvider> Providers { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets policy overrides for specific environments/tags.
|
||||
/// </summary>
|
||||
public List<TimestampPolicyOverride> Overrides { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets EU Trust List configuration.
|
||||
/// </summary>
|
||||
public EuTrustListConfiguration TrustList { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Qualified TSA provider configuration.
|
||||
/// </summary>
|
||||
public sealed record QualifiedTsaProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the provider name.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TSA URL.
|
||||
/// </summary>
|
||||
public required string Url { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this is a qualified (eIDAS) TSA.
|
||||
/// </summary>
|
||||
public bool Qualified { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the trust list reference for qualification validation.
|
||||
/// </summary>
|
||||
public string? TrustListRef { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets environments where this provider is required.
|
||||
/// </summary>
|
||||
public List<string>? RequiredForEnvironments { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets tags where this provider is required.
|
||||
/// </summary>
|
||||
public List<string>? RequiredForTags { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the signature format to use.
|
||||
/// </summary>
|
||||
public CadesLevel SignatureFormat { get; init; } = CadesLevel.CadesT;
|
||||
|
||||
/// <summary>
|
||||
/// Gets authentication for the TSA.
|
||||
/// </summary>
|
||||
public TsaAuthentication? Authentication { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authentication for TSA access.
|
||||
/// </summary>
|
||||
public sealed record TsaAuthentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the authentication type.
|
||||
/// </summary>
|
||||
public TsaAuthType Type { get; init; } = TsaAuthType.None;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the username for basic auth.
|
||||
/// </summary>
|
||||
public string? Username { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the password for basic auth.
|
||||
/// </summary>
|
||||
public string? Password { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client certificate path.
|
||||
/// </summary>
|
||||
public string? ClientCertificatePath { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TSA authentication type.
|
||||
/// </summary>
|
||||
public enum TsaAuthType
|
||||
{
|
||||
/// <summary>
|
||||
/// No authentication.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// HTTP Basic authentication.
|
||||
/// </summary>
|
||||
Basic,
|
||||
|
||||
/// <summary>
|
||||
/// Client certificate authentication.
|
||||
/// </summary>
|
||||
ClientCertificate
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Policy override for timestamping mode.
|
||||
/// </summary>
|
||||
public sealed record TimestampPolicyOverride
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the match criteria.
|
||||
/// </summary>
|
||||
public required OverrideMatchCriteria Match { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamping mode to use.
|
||||
/// </summary>
|
||||
public TimestampMode Mode { get; init; } = TimestampMode.Qualified;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specific TSA provider to use.
|
||||
/// </summary>
|
||||
public string? TsaProvider { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the CAdES signature format.
|
||||
/// </summary>
|
||||
public CadesLevel? SignatureFormat { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match criteria for policy overrides.
|
||||
/// </summary>
|
||||
public sealed record OverrideMatchCriteria
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets environments to match.
|
||||
/// </summary>
|
||||
public List<string>? Environments { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets tags to match.
|
||||
/// </summary>
|
||||
public List<string>? Tags { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets repository patterns to match.
|
||||
/// </summary>
|
||||
public List<string>? Repositories { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EU Trust List configuration.
|
||||
/// </summary>
|
||||
public sealed record EuTrustListConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the URL for the EU List of Trusted Lists (LOTL).
|
||||
/// </summary>
|
||||
public string LotlUrl { get; init; } = "https://ec.europa.eu/tools/lotl/eu-lotl.xml";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cache TTL for trust list.
|
||||
/// </summary>
|
||||
public TimeSpan CacheTtl { get; init; } = TimeSpan.FromHours(24);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether to verify trust list signature.
|
||||
/// </summary>
|
||||
public bool VerifySignature { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the offline trust list path (for air-gap).
|
||||
/// </summary>
|
||||
public string? OfflinePath { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timestamping mode.
|
||||
/// </summary>
|
||||
public enum TimestampMode
|
||||
{
|
||||
/// <summary>
|
||||
/// No timestamping.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Standard RFC-3161 timestamp.
|
||||
/// </summary>
|
||||
Rfc3161,
|
||||
|
||||
/// <summary>
|
||||
/// eIDAS Qualified Time-Stamp.
|
||||
/// </summary>
|
||||
Qualified,
|
||||
|
||||
/// <summary>
|
||||
/// eIDAS QTS with Long-Term Validation data.
|
||||
/// </summary>
|
||||
QualifiedLtv
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CAdES signature level.
|
||||
/// </summary>
|
||||
public enum CadesLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Basic Electronic Signature.
|
||||
/// </summary>
|
||||
CadesB,
|
||||
|
||||
/// <summary>
|
||||
/// With Time-Stamp.
|
||||
/// </summary>
|
||||
CadesT,
|
||||
|
||||
/// <summary>
|
||||
/// With Complete references.
|
||||
/// </summary>
|
||||
CadesC,
|
||||
|
||||
/// <summary>
|
||||
/// Long-Term (with values).
|
||||
/// </summary>
|
||||
CadesLT,
|
||||
|
||||
/// <summary>
|
||||
/// Long-Term with Archive timestamp.
|
||||
/// </summary>
|
||||
CadesLTA
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// TimestampModeSelector.cs
|
||||
// Sprint: SPRINT_20260119_011 eIDAS Qualified Timestamp Support
|
||||
// Task: QTS-005 - Policy Override for Regulated Environments
|
||||
// Description: Implementation of timestamp mode selection.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.Eidas.Timestamping;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="ITimestampModeSelector"/>.
|
||||
/// </summary>
|
||||
public sealed partial class TimestampModeSelector : ITimestampModeSelector
|
||||
{
|
||||
private readonly QualifiedTimestampingConfiguration _config;
|
||||
private readonly ILogger<TimestampModeSelector> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimestampModeSelector"/> class.
|
||||
/// </summary>
|
||||
public TimestampModeSelector(
|
||||
IOptions<QualifiedTimestampingConfiguration> config,
|
||||
ILogger<TimestampModeSelector> logger)
|
||||
{
|
||||
_config = config.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimestampMode SelectMode(TimestampContext context)
|
||||
{
|
||||
var policy = GetPolicy(context);
|
||||
return policy.Mode;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SelectProvider(TimestampContext context, TimestampMode mode)
|
||||
{
|
||||
var policy = GetPolicy(context);
|
||||
return policy.TsaProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimestampPolicy GetPolicy(TimestampContext context)
|
||||
{
|
||||
// Check overrides first
|
||||
foreach (var overridePolicy in _config.Overrides)
|
||||
{
|
||||
if (MatchesOverride(context, overridePolicy))
|
||||
{
|
||||
var provider = overridePolicy.TsaProvider ??
|
||||
GetDefaultProviderForMode(overridePolicy.Mode);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Matched override policy for environment={Env}, mode={Mode}, provider={Provider}",
|
||||
context.Environment,
|
||||
overridePolicy.Mode,
|
||||
provider);
|
||||
|
||||
return new TimestampPolicy
|
||||
{
|
||||
Mode = overridePolicy.Mode,
|
||||
TsaProvider = provider,
|
||||
SignatureFormat = overridePolicy.SignatureFormat ?? CadesLevel.CadesT,
|
||||
MatchedPolicy = $"override:{context.Environment ?? "default"}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check provider-specific requirements
|
||||
foreach (var provider in _config.Providers.Where(p => p.Qualified))
|
||||
{
|
||||
if (IsProviderRequired(context, provider))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Provider {Provider} required for context, using qualified mode",
|
||||
provider.Name);
|
||||
|
||||
return new TimestampPolicy
|
||||
{
|
||||
Mode = TimestampMode.Qualified,
|
||||
TsaProvider = provider.Name,
|
||||
SignatureFormat = provider.SignatureFormat,
|
||||
MatchedPolicy = $"provider:{provider.Name}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Use defaults
|
||||
var defaultProvider = GetDefaultProviderForMode(_config.DefaultMode);
|
||||
|
||||
return new TimestampPolicy
|
||||
{
|
||||
Mode = _config.DefaultMode,
|
||||
TsaProvider = defaultProvider,
|
||||
SignatureFormat = CadesLevel.CadesT,
|
||||
MatchedPolicy = "default"
|
||||
};
|
||||
}
|
||||
|
||||
private static bool MatchesOverride(TimestampContext context, TimestampPolicyOverride policy)
|
||||
{
|
||||
var match = policy.Match;
|
||||
|
||||
// Check environment match
|
||||
if (match.Environments is { Count: > 0 })
|
||||
{
|
||||
if (context.Environment is null ||
|
||||
!match.Environments.Contains(context.Environment, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check tag match
|
||||
if (match.Tags is { Count: > 0 })
|
||||
{
|
||||
if (context.Tags is null ||
|
||||
!match.Tags.Any(t => context.Tags.Contains(t, StringComparer.OrdinalIgnoreCase)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check repository pattern match
|
||||
if (match.Repositories is { Count: > 0 })
|
||||
{
|
||||
if (context.Repository is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var matched = match.Repositories.Any(pattern =>
|
||||
MatchesPattern(context.Repository, pattern));
|
||||
|
||||
if (!matched)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsProviderRequired(TimestampContext context, QualifiedTsaProvider provider)
|
||||
{
|
||||
// Check environment requirements
|
||||
if (provider.RequiredForEnvironments is { Count: > 0 })
|
||||
{
|
||||
if (context.Environment is not null &&
|
||||
provider.RequiredForEnvironments.Contains(context.Environment, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check tag requirements
|
||||
if (provider.RequiredForTags is { Count: > 0 })
|
||||
{
|
||||
if (context.Tags is not null &&
|
||||
provider.RequiredForTags.Any(t => context.Tags.Contains(t, StringComparer.OrdinalIgnoreCase)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string GetDefaultProviderForMode(TimestampMode mode)
|
||||
{
|
||||
// Find first provider matching the mode
|
||||
var isQualifiedMode = mode == TimestampMode.Qualified || mode == TimestampMode.QualifiedLtv;
|
||||
|
||||
var provider = _config.Providers.FirstOrDefault(p => p.Qualified == isQualifiedMode);
|
||||
return provider?.Name ?? _config.Providers.FirstOrDefault()?.Name ?? "default";
|
||||
}
|
||||
|
||||
private static bool MatchesPattern(string value, string pattern)
|
||||
{
|
||||
// Simple wildcard matching
|
||||
if (pattern.Contains('*'))
|
||||
{
|
||||
var regexPattern = "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$";
|
||||
return Regex.IsMatch(value, regexPattern, RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
return value.Equals(pattern, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user