license switch agpl -> busl1, sprints work, new product advisories
This commit is contained in:
@@ -4,6 +4,7 @@ using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Org.BouncyCastle.Security;
|
||||
using Org.BouncyCastle.X509;
|
||||
using StellaOps.Cryptography.Plugin.Eidas.Timestamping;
|
||||
using StellaOps.Plugin.Abstractions;
|
||||
using StellaOps.Plugin.Abstractions.Capabilities;
|
||||
using StellaOps.Plugin.Abstractions.Context;
|
||||
@@ -21,6 +22,10 @@ public sealed class EidasPlugin : CryptoPluginBase
|
||||
private X509Certificate2? _signingCertificate;
|
||||
private X509Certificate? _bcCertificate;
|
||||
private RSA? _privateKey;
|
||||
private ICadesSignatureBuilder? _cadesBuilder;
|
||||
private ITimestampModeSelector? _timestampModeSelector;
|
||||
private IQualifiedTimestampVerifier? _timestampVerifier;
|
||||
private QualifiedTimestampingConfiguration? _timestampingConfiguration;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PluginInfo Info => new(
|
||||
@@ -29,7 +34,7 @@ public sealed class EidasPlugin : CryptoPluginBase
|
||||
Version: "1.0.0",
|
||||
Vendor: "Stella Ops",
|
||||
Description: "EU eIDAS qualified electronic signatures (ETSI TS 119 312)",
|
||||
LicenseId: "AGPL-3.0-or-later");
|
||||
LicenseId: "BUSL-1.1");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<string> SupportedAlgorithms => new[]
|
||||
@@ -40,6 +45,9 @@ public sealed class EidasPlugin : CryptoPluginBase
|
||||
"eIDAS-ECDSA-SHA256",
|
||||
"eIDAS-ECDSA-SHA384",
|
||||
"eIDAS-CAdES-BES",
|
||||
"eIDAS-CAdES-T",
|
||||
"eIDAS-CAdES-LT",
|
||||
"eIDAS-CAdES-LTA",
|
||||
"eIDAS-XAdES-BES"
|
||||
};
|
||||
|
||||
@@ -63,6 +71,12 @@ public sealed class EidasPlugin : CryptoPluginBase
|
||||
Context?.Logger.Warning("eIDAS provider initialized without certificate - signing operations will fail");
|
||||
}
|
||||
|
||||
_timestampingConfiguration = context.Configuration.Bind<QualifiedTimestampingConfiguration>("timestamping")
|
||||
?? new QualifiedTimestampingConfiguration();
|
||||
_cadesBuilder = context.Services.GetService<ICadesSignatureBuilder>();
|
||||
_timestampModeSelector = context.Services.GetService<ITimestampModeSelector>();
|
||||
_timestampVerifier = context.Services.GetService<IQualifiedTimestampVerifier>();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -74,7 +88,7 @@ public sealed class EidasPlugin : CryptoPluginBase
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<byte[]> SignAsync(ReadOnlyMemory<byte> data, CryptoSignOptions options, CancellationToken ct)
|
||||
public override async Task<byte[]> SignAsync(ReadOnlyMemory<byte> data, CryptoSignOptions options, CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
ct.ThrowIfCancellationRequested();
|
||||
@@ -89,7 +103,7 @@ public sealed class EidasPlugin : CryptoPluginBase
|
||||
|
||||
if (algorithm.Contains("CAdES", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
signature = CreateCadesSignature(data.ToArray());
|
||||
signature = await SignCadesAsync(data, options, ct).ConfigureAwait(false);
|
||||
}
|
||||
else if (algorithm.Contains("ECDSA", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -104,11 +118,11 @@ public sealed class EidasPlugin : CryptoPluginBase
|
||||
|
||||
Context?.Logger.Debug("Signed {DataLength} bytes with {Algorithm}", data.Length, algorithm);
|
||||
|
||||
return Task.FromResult(signature);
|
||||
return signature;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CryptoVerifyOptions options, CancellationToken ct)
|
||||
public override async Task<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CryptoVerifyOptions options, CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
ct.ThrowIfCancellationRequested();
|
||||
@@ -118,7 +132,7 @@ public sealed class EidasPlugin : CryptoPluginBase
|
||||
|
||||
if (algorithm.Contains("CAdES", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
isValid = VerifyCadesSignature(data.ToArray(), signature.ToArray());
|
||||
isValid = await VerifyCadesSignatureAsync(data, signature, options, ct).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -149,7 +163,171 @@ public sealed class EidasPlugin : CryptoPluginBase
|
||||
|
||||
Context?.Logger.Debug("Verified signature: {IsValid}", isValid);
|
||||
|
||||
return Task.FromResult(isValid);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
private async Task<byte[]> SignCadesAsync(ReadOnlyMemory<byte> data, CryptoSignOptions options, CancellationToken ct)
|
||||
{
|
||||
if (_signingCertificate == null)
|
||||
{
|
||||
throw new InvalidOperationException("Certificate not loaded for CAdES signing.");
|
||||
}
|
||||
|
||||
var timestampContext = BuildTimestampContext(options.Metadata);
|
||||
var policy = ResolveTimestampPolicy(timestampContext);
|
||||
var requestedLevel = ResolveCadesLevel(options.Algorithm, policy);
|
||||
var provider = FindProvider(policy.TsaProvider);
|
||||
var signatureOptions = new CadesSignatureOptions
|
||||
{
|
||||
DigestAlgorithm = ResolveDigestAlgorithm(options.Algorithm),
|
||||
TsaProvider = provider?.Name ?? policy.TsaProvider,
|
||||
TsaUrl = provider?.Url ?? _options?.TimestampAuthorityUrl,
|
||||
RequireQualifiedTsa = policy.IsQualified,
|
||||
IncludeRevocationData = requestedLevel >= CadesLevel.CadesLT
|
||||
};
|
||||
|
||||
if (_cadesBuilder is null)
|
||||
{
|
||||
Context?.Logger.Warning("CAdES builder not available; falling back to CAdES-BES.");
|
||||
return CreateCadesSignature(data.ToArray());
|
||||
}
|
||||
|
||||
return requestedLevel switch
|
||||
{
|
||||
CadesLevel.CadesB => await _cadesBuilder.CreateCadesBAsync(data, _signingCertificate, signatureOptions, ct).ConfigureAwait(false),
|
||||
CadesLevel.CadesT => await _cadesBuilder.CreateCadesTAsync(data, _signingCertificate, signatureOptions, ct).ConfigureAwait(false),
|
||||
CadesLevel.CadesLT => await _cadesBuilder.CreateCadesLTAsync(data, _signingCertificate, signatureOptions, ct).ConfigureAwait(false),
|
||||
CadesLevel.CadesLTA => await _cadesBuilder.CreateCadesLTAAsync(data, _signingCertificate, signatureOptions, ct).ConfigureAwait(false),
|
||||
CadesLevel.CadesC => await _cadesBuilder.CreateCadesLTAsync(data, _signingCertificate, signatureOptions, ct).ConfigureAwait(false),
|
||||
_ => await _cadesBuilder.CreateCadesBAsync(data, _signingCertificate, signatureOptions, ct).ConfigureAwait(false)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<bool> VerifyCadesSignatureAsync(
|
||||
ReadOnlyMemory<byte> data,
|
||||
ReadOnlyMemory<byte> signature,
|
||||
CryptoVerifyOptions options,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (_timestampVerifier is null)
|
||||
{
|
||||
return VerifyCadesSignature(data.ToArray(), signature.ToArray());
|
||||
}
|
||||
|
||||
var requestedLevel = ResolveCadesLevel(options.Algorithm, ResolveTimestampPolicy(BuildTimestampContext(null)));
|
||||
var mode = _options?.TimestampMode ?? _timestampingConfiguration?.DefaultMode ?? TimestampMode.Rfc3161;
|
||||
var verifyOptions = new QualifiedTimestampVerificationOptions
|
||||
{
|
||||
RequireQualifiedTsa = mode == TimestampMode.Qualified || mode == TimestampMode.QualifiedLtv,
|
||||
VerifyLtvCompleteness = requestedLevel >= CadesLevel.CadesLT
|
||||
};
|
||||
|
||||
var result = await _timestampVerifier.VerifyCadesAsync(signature, data, verifyOptions, ct).ConfigureAwait(false);
|
||||
return result.IsSignatureValid;
|
||||
}
|
||||
|
||||
private TimestampPolicy ResolveTimestampPolicy(TimestampContext context)
|
||||
{
|
||||
if (_timestampModeSelector is not null)
|
||||
{
|
||||
return _timestampModeSelector.GetPolicy(context);
|
||||
}
|
||||
|
||||
var mode = _options?.TimestampMode ?? _timestampingConfiguration?.DefaultMode ?? TimestampMode.Rfc3161;
|
||||
var provider = ResolveDefaultProvider(mode);
|
||||
|
||||
return new TimestampPolicy
|
||||
{
|
||||
Mode = mode,
|
||||
TsaProvider = provider,
|
||||
SignatureFormat = _options?.SignatureFormat ?? CadesLevel.CadesT,
|
||||
MatchedPolicy = "options"
|
||||
};
|
||||
}
|
||||
|
||||
private TimestampContext BuildTimestampContext(IReadOnlyDictionary<string, string>? metadata)
|
||||
{
|
||||
if (metadata is null)
|
||||
{
|
||||
return new TimestampContext();
|
||||
}
|
||||
|
||||
metadata.TryGetValue("environment", out var environment);
|
||||
metadata.TryGetValue("repository", out var repository);
|
||||
metadata.TryGetValue("tags", out var tagsValue);
|
||||
metadata.TryGetValue("artifactType", out var artifactType);
|
||||
metadata.TryGetValue("artifactDigest", out var artifactDigest);
|
||||
|
||||
return new TimestampContext
|
||||
{
|
||||
Environment = environment,
|
||||
Repository = repository,
|
||||
Tags = ParseTags(tagsValue),
|
||||
ArtifactType = artifactType,
|
||||
ArtifactDigest = artifactDigest,
|
||||
Properties = metadata
|
||||
};
|
||||
}
|
||||
|
||||
private QualifiedTsaProvider? FindProvider(string providerName)
|
||||
{
|
||||
return _timestampingConfiguration?.Providers
|
||||
.FirstOrDefault(p => p.Name.Equals(providerName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private string ResolveDefaultProvider(TimestampMode mode)
|
||||
{
|
||||
var isQualified = mode == TimestampMode.Qualified || mode == TimestampMode.QualifiedLtv;
|
||||
var provider = _timestampingConfiguration?.Providers
|
||||
.FirstOrDefault(p => p.Qualified == isQualified);
|
||||
return provider?.Name ?? _timestampingConfiguration?.Providers.FirstOrDefault()?.Name ?? "default";
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string>? ParseTags(string? tagsValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagsValue))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return tagsValue
|
||||
.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string ResolveDigestAlgorithm(string algorithm)
|
||||
{
|
||||
if (algorithm.Contains("512", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "SHA512";
|
||||
}
|
||||
|
||||
if (algorithm.Contains("384", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "SHA384";
|
||||
}
|
||||
|
||||
return "SHA256";
|
||||
}
|
||||
|
||||
private static CadesLevel ResolveCadesLevel(string algorithm, TimestampPolicy policy)
|
||||
{
|
||||
var level = algorithm switch
|
||||
{
|
||||
var a when a.Contains("CAdES-LTA", StringComparison.OrdinalIgnoreCase) => CadesLevel.CadesLTA,
|
||||
var a when a.Contains("CAdES-LT", StringComparison.OrdinalIgnoreCase) => CadesLevel.CadesLT,
|
||||
var a when a.Contains("CAdES-T", StringComparison.OrdinalIgnoreCase) => CadesLevel.CadesT,
|
||||
var a when a.Contains("CAdES-B", StringComparison.OrdinalIgnoreCase) => CadesLevel.CadesB,
|
||||
_ => policy.SignatureFormat
|
||||
};
|
||||
|
||||
if (policy.Mode == TimestampMode.None)
|
||||
{
|
||||
return CadesLevel.CadesB;
|
||||
}
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -322,6 +500,16 @@ public sealed class EidasOptions
|
||||
/// </summary>
|
||||
public string? TimestampAuthorityUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp mode to use (Rfc3161, Qualified, QualifiedLtv).
|
||||
/// </summary>
|
||||
public TimestampMode? TimestampMode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CAdES signature format level.
|
||||
/// </summary>
|
||||
public CadesLevel? SignatureFormat { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to validate certificate chain during operations.
|
||||
/// </summary>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="System.Security.Cryptography.Pkcs" />
|
||||
<PackageReference Include="System.Security.Cryptography.Xml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
// Description: Implementation of EU Trusted List service.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Cryptography.Xml;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -258,6 +261,8 @@ public sealed class EuTrustListService : IEuTrustListService
|
||||
}
|
||||
}
|
||||
|
||||
var certificates = ParseServiceCertificates(serviceInfo);
|
||||
|
||||
entries.Add(new TrustListEntry
|
||||
{
|
||||
TspName = tspName,
|
||||
@@ -269,7 +274,8 @@ public sealed class EuTrustListService : IEuTrustListService
|
||||
ServiceTypeIdentifier = serviceType ?? "",
|
||||
CountryCode = ExtractCountryCode(tspName),
|
||||
ServiceSupplyPoints = supplyPoints,
|
||||
StatusHistory = historyList
|
||||
StatusHistory = historyList,
|
||||
ServiceCertificates = certificates
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -336,9 +342,64 @@ public sealed class EuTrustListService : IEuTrustListService
|
||||
|
||||
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
|
||||
|
||||
var xmlDoc = new XmlDocument
|
||||
{
|
||||
PreserveWhitespace = true,
|
||||
XmlResolver = null
|
||||
};
|
||||
xmlDoc.LoadXml(xmlContent);
|
||||
|
||||
var nsManager = new XmlNamespaceManager(xmlDoc.NameTable);
|
||||
nsManager.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl);
|
||||
|
||||
var signatureNode = xmlDoc.SelectSingleNode("//ds:Signature", nsManager) as XmlElement;
|
||||
if (signatureNode is null)
|
||||
{
|
||||
throw new CryptographicException("Trust list signature element not found.");
|
||||
}
|
||||
|
||||
var signedXml = new SignedXml(xmlDoc);
|
||||
signedXml.LoadXml(signatureNode);
|
||||
|
||||
if (!signedXml.CheckSignature())
|
||||
{
|
||||
throw new CryptographicException("Trust list signature validation failed.");
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<X509Certificate2>? ParseServiceCertificates(XElement serviceInfo)
|
||||
{
|
||||
var certElements = serviceInfo.Descendants()
|
||||
.Where(e => e.Name.LocalName.Equals("X509Certificate", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(e => e.Value)
|
||||
.Where(v => !string.IsNullOrWhiteSpace(v))
|
||||
.ToList();
|
||||
|
||||
if (certElements.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var certificates = new List<X509Certificate2>();
|
||||
foreach (var certBase64 in certElements)
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = Convert.FromBase64String(certBase64.Trim());
|
||||
certificates.Add(X509CertificateLoader.LoadCertificate(raw));
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// Ignore malformed certificate entries.
|
||||
}
|
||||
catch (CryptographicException)
|
||||
{
|
||||
// Ignore malformed certificate entries.
|
||||
}
|
||||
}
|
||||
|
||||
return certificates.Count > 0 ? certificates : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +171,7 @@ public sealed class QualifiedTimestampVerifier : IQualifiedTimestampVerifier
|
||||
|
||||
// Detect CAdES level
|
||||
var level = DetectCadesLevel(signedCms);
|
||||
ValidateCadesFormat(signedCms, level, errors);
|
||||
|
||||
// Get signing time
|
||||
DateTimeOffset? signingTime = null;
|
||||
@@ -211,18 +212,19 @@ public sealed class QualifiedTimestampVerifier : IQualifiedTimestampVerifier
|
||||
}
|
||||
else
|
||||
{
|
||||
warnings.Add("Expected timestamp attribute not found");
|
||||
errors.Add("Expected timestamp attribute not found");
|
||||
}
|
||||
}
|
||||
|
||||
// Check LTV completeness if required
|
||||
var isLtvComplete = false;
|
||||
if (options.VerifyLtvCompleteness && level >= CadesLevel.CadesLT)
|
||||
var shouldCheckLtv = options.VerifyLtvCompleteness || level >= CadesLevel.CadesLT;
|
||||
if (shouldCheckLtv)
|
||||
{
|
||||
isLtvComplete = VerifyLtvCompleteness(signedCms);
|
||||
if (!isLtvComplete)
|
||||
{
|
||||
warnings.Add("LTV data is incomplete");
|
||||
errors.Add("LTV data is incomplete");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,6 +346,54 @@ public sealed class QualifiedTimestampVerifier : IQualifiedTimestampVerifier
|
||||
return hasRevocationValues && hasCertValues;
|
||||
}
|
||||
|
||||
private static void ValidateCadesFormat(SignedCms signedCms, CadesLevel level, List<string> errors)
|
||||
{
|
||||
var signerInfo = signedCms.SignerInfos[0];
|
||||
var unsignedAttrs = signerInfo.UnsignedAttributes;
|
||||
|
||||
const string archiveTimestampOid = "1.2.840.113549.1.9.16.2.48";
|
||||
const string revocationValuesOid = "1.2.840.113549.1.9.16.2.24";
|
||||
const string certValuesOid = "1.2.840.113549.1.9.16.2.23";
|
||||
const string revocationRefsOid = "1.2.840.113549.1.9.16.2.22";
|
||||
|
||||
if (level >= CadesLevel.CadesT &&
|
||||
!HasUnsignedAttribute(unsignedAttrs, SignatureTimestampOid))
|
||||
{
|
||||
errors.Add("Missing signature timestamp attribute for CAdES-T");
|
||||
}
|
||||
|
||||
if (level >= CadesLevel.CadesC &&
|
||||
!HasUnsignedAttribute(unsignedAttrs, revocationRefsOid))
|
||||
{
|
||||
errors.Add("Missing revocation references for CAdES-C");
|
||||
}
|
||||
|
||||
if (level >= CadesLevel.CadesLT)
|
||||
{
|
||||
if (!HasUnsignedAttribute(unsignedAttrs, revocationValuesOid))
|
||||
{
|
||||
errors.Add("Missing revocation values for CAdES-LT");
|
||||
}
|
||||
|
||||
if (!HasUnsignedAttribute(unsignedAttrs, certValuesOid))
|
||||
{
|
||||
errors.Add("Missing certificate values for CAdES-LT");
|
||||
}
|
||||
}
|
||||
|
||||
if (level >= CadesLevel.CadesLTA &&
|
||||
!HasUnsignedAttribute(unsignedAttrs, archiveTimestampOid))
|
||||
{
|
||||
errors.Add("Missing archive timestamp for CAdES-LTA");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasUnsignedAttribute(CryptographicAttributeObjectCollection attributes, string oid)
|
||||
{
|
||||
return attributes.Cast<CryptographicAttributeObject>()
|
||||
.Any(a => a.Oid?.Value == oid);
|
||||
}
|
||||
|
||||
private sealed class TstInfoData
|
||||
{
|
||||
public required string DigestAlgorithm { get; init; }
|
||||
|
||||
@@ -61,13 +61,16 @@ public sealed partial class TimestampModeSelector : ITimestampModeSelector
|
||||
overridePolicy.Mode,
|
||||
provider);
|
||||
|
||||
return new TimestampPolicy
|
||||
var policy = new TimestampPolicy
|
||||
{
|
||||
Mode = overridePolicy.Mode,
|
||||
TsaProvider = provider,
|
||||
SignatureFormat = overridePolicy.SignatureFormat ?? CadesLevel.CadesT,
|
||||
MatchedPolicy = $"override:{context.Environment ?? "default"}"
|
||||
};
|
||||
|
||||
LogDecision(context, policy);
|
||||
return policy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,26 +83,45 @@ public sealed partial class TimestampModeSelector : ITimestampModeSelector
|
||||
"Provider {Provider} required for context, using qualified mode",
|
||||
provider.Name);
|
||||
|
||||
return new TimestampPolicy
|
||||
var policy = new TimestampPolicy
|
||||
{
|
||||
Mode = TimestampMode.Qualified,
|
||||
TsaProvider = provider.Name,
|
||||
SignatureFormat = provider.SignatureFormat,
|
||||
MatchedPolicy = $"provider:{provider.Name}"
|
||||
};
|
||||
|
||||
LogDecision(context, policy);
|
||||
return policy;
|
||||
}
|
||||
}
|
||||
|
||||
// Use defaults
|
||||
var defaultProvider = GetDefaultProviderForMode(_config.DefaultMode);
|
||||
|
||||
return new TimestampPolicy
|
||||
var defaultPolicy = new TimestampPolicy
|
||||
{
|
||||
Mode = _config.DefaultMode,
|
||||
TsaProvider = defaultProvider,
|
||||
SignatureFormat = CadesLevel.CadesT,
|
||||
MatchedPolicy = "default"
|
||||
};
|
||||
|
||||
LogDecision(context, defaultPolicy);
|
||||
return defaultPolicy;
|
||||
}
|
||||
|
||||
private void LogDecision(TimestampContext context, TimestampPolicy policy)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Timestamp policy decision: mode={Mode}, provider={Provider}, format={Format}, policy={Policy}, env={Environment}, repo={Repository}, tags={Tags}",
|
||||
policy.Mode,
|
||||
policy.TsaProvider,
|
||||
policy.SignatureFormat,
|
||||
policy.MatchedPolicy ?? "unknown",
|
||||
context.Environment ?? "(none)",
|
||||
context.Repository ?? "(none)",
|
||||
context.Tags is null ? "(none)" : string.Join(",", context.Tags));
|
||||
}
|
||||
|
||||
private static bool MatchesOverride(TimestampContext context, TimestampPolicyOverride policy)
|
||||
|
||||
@@ -4,7 +4,7 @@ plugin:
|
||||
version: 1.0.0
|
||||
vendor: Stella Ops
|
||||
description: EU eIDAS qualified electronic signatures (ETSI TS 119 312)
|
||||
license: AGPL-3.0-or-later
|
||||
license: BUSL-1.1
|
||||
|
||||
entryPoint: StellaOps.Cryptography.Plugin.Eidas.EidasPlugin
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed class FipsPlugin : CryptoPluginBase
|
||||
Version: "1.0.0",
|
||||
Vendor: "Stella Ops",
|
||||
Description: "US FIPS 140-2 compliant cryptographic algorithms",
|
||||
LicenseId: "AGPL-3.0-or-later");
|
||||
LicenseId: "BUSL-1.1");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<string> SupportedAlgorithms => new[]
|
||||
|
||||
@@ -4,7 +4,7 @@ plugin:
|
||||
version: 1.0.0
|
||||
vendor: Stella Ops
|
||||
description: US FIPS 140-2 compliant cryptographic algorithms
|
||||
license: AGPL-3.0-or-later
|
||||
license: BUSL-1.1
|
||||
|
||||
entryPoint: StellaOps.Cryptography.Plugin.Fips.FipsPlugin
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ public sealed class GostPlugin : CryptoPluginBase
|
||||
Version: "1.0.0",
|
||||
Vendor: "Stella Ops",
|
||||
Description: "Russian GOST R 34.10-2012 and R 34.11-2012 cryptographic algorithms",
|
||||
LicenseId: "AGPL-3.0-or-later");
|
||||
LicenseId: "BUSL-1.1");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<string> SupportedAlgorithms => new[]
|
||||
|
||||
@@ -4,7 +4,7 @@ plugin:
|
||||
version: 1.0.0
|
||||
vendor: Stella Ops
|
||||
description: Russian GOST R 34.10-2012 and R 34.11-2012 cryptographic algorithms
|
||||
license: AGPL-3.0-or-later
|
||||
license: BUSL-1.1
|
||||
|
||||
entryPoint: StellaOps.Cryptography.Plugin.Gost.GostPlugin
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed class HsmPlugin : CryptoPluginBase
|
||||
Version: "1.0.0",
|
||||
Vendor: "Stella Ops",
|
||||
Description: "Hardware Security Module integration via PKCS#11",
|
||||
LicenseId: "AGPL-3.0-or-later");
|
||||
LicenseId: "BUSL-1.1");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<string> SupportedAlgorithms => new[]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright © StellaOps. All rights reserved.
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Sprint: SPRINT_20260112_017_CRYPTO_pkcs11_hsm_implementation
|
||||
// Tasks: HSM-002, HSM-003, HSM-004, HSM-005, HSM-006, HSM-007
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ plugin:
|
||||
version: 1.0.0
|
||||
vendor: Stella Ops
|
||||
description: Hardware Security Module integration via PKCS#11
|
||||
license: AGPL-3.0-or-later
|
||||
license: BUSL-1.1
|
||||
|
||||
entryPoint: StellaOps.Cryptography.Plugin.Hsm.HsmPlugin
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ public sealed class SmPlugin : CryptoPluginBase
|
||||
Version: "1.0.0",
|
||||
Vendor: "Stella Ops",
|
||||
Description: "Chinese national cryptographic standards SM2/SM3/SM4 (GM/T 0003-0004)",
|
||||
LicenseId: "AGPL-3.0-or-later");
|
||||
LicenseId: "BUSL-1.1");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<string> SupportedAlgorithms => new[]
|
||||
|
||||
@@ -4,7 +4,7 @@ plugin:
|
||||
version: 1.0.0
|
||||
vendor: Stella Ops
|
||||
description: Chinese national cryptographic standards SM2/SM3/SM4 (GM/T 0003-0004)
|
||||
license: AGPL-3.0-or-later
|
||||
license: BUSL-1.1
|
||||
|
||||
entryPoint: StellaOps.Cryptography.Plugin.Sm.SmPlugin
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright © StellaOps. All rights reserved.
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Sprint: SPRINT_20260112_018_CRYPTO_key_escrow_shamir
|
||||
// Tasks: ESCROW-001, ESCROW-002
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright © StellaOps. All rights reserved.
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Sprint: SPRINT_20260112_018_CRYPTO_key_escrow_shamir
|
||||
// Tasks: ESCROW-006, ESCROW-007
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright © StellaOps. All rights reserved.
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Sprint: SPRINT_20260112_018_CRYPTO_key_escrow_shamir
|
||||
// Tasks: ESCROW-003, ESCROW-004
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright © StellaOps. All rights reserved.
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Sprint: SPRINT_20260112_018_CRYPTO_key_escrow_shamir
|
||||
// Tasks: ESCROW-003, ESCROW-005
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright © StellaOps. All rights reserved.
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Sprint: SPRINT_20260112_018_CRYPTO_key_escrow_shamir
|
||||
// Tasks: ESCROW-004, ESCROW-006, ESCROW-008, ESCROW-009
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright © StellaOps. All rights reserved.
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Sprint: SPRINT_20260112_018_CRYPTO_key_escrow_shamir
|
||||
// Tasks: ESCROW-001, ESCROW-002
|
||||
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// QualifiedTsaProviderTests.cs
|
||||
// Sprint: SPRINT_20260119_011 eIDAS Qualified Timestamp Support
|
||||
// Task: QTS-001 - Unit tests for qualification checks
|
||||
// Description: Unit tests for qualified TSA provider configuration.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Cryptography.Plugin.Eidas.Timestamping;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests.Eidas;
|
||||
|
||||
public class QualifiedTsaProviderTests
|
||||
{
|
||||
#region Provider Configuration
|
||||
|
||||
[Fact]
|
||||
public void QualifiedTsaProvider_DefaultProperties_AreCorrect()
|
||||
{
|
||||
// Arrange & Act
|
||||
var provider = new QualifiedTsaProvider
|
||||
{
|
||||
Name = "test-tsa",
|
||||
Url = "https://tsa.test.com"
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.False(provider.Qualified);
|
||||
Assert.Null(provider.TrustListRef);
|
||||
Assert.Null(provider.RequiredForEnvironments);
|
||||
Assert.Null(provider.RequiredForTags);
|
||||
Assert.Equal(CadesLevel.CadesT, provider.SignatureFormat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QualifiedTsaProvider_QualifiedProvider_HasCorrectFlags()
|
||||
{
|
||||
// Arrange & Act
|
||||
var provider = new QualifiedTsaProvider
|
||||
{
|
||||
Name = "d-trust-qts",
|
||||
Url = "https://qts.d-trust.net/tsp",
|
||||
Qualified = true,
|
||||
TrustListRef = "eu-tl",
|
||||
SignatureFormat = CadesLevel.CadesLT
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.True(provider.Qualified);
|
||||
Assert.Equal("eu-tl", provider.TrustListRef);
|
||||
Assert.Equal(CadesLevel.CadesLT, provider.SignatureFormat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QualifiedTsaProvider_RequiredForEnvironments_ConfiguredCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
var provider = new QualifiedTsaProvider
|
||||
{
|
||||
Name = "prod-qts",
|
||||
Url = "https://qts.example.com",
|
||||
Qualified = true,
|
||||
RequiredForEnvironments = ["production", "staging"]
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(provider.RequiredForEnvironments);
|
||||
Assert.Equal(2, provider.RequiredForEnvironments.Count);
|
||||
Assert.Contains("production", provider.RequiredForEnvironments);
|
||||
Assert.Contains("staging", provider.RequiredForEnvironments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QualifiedTsaProvider_RequiredForTags_ConfiguredCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
var provider = new QualifiedTsaProvider
|
||||
{
|
||||
Name = "compliance-qts",
|
||||
Url = "https://qts.example.com",
|
||||
Qualified = true,
|
||||
RequiredForTags = ["eidas-required", "pci-dss", "regulated"]
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(provider.RequiredForTags);
|
||||
Assert.Equal(3, provider.RequiredForTags.Count);
|
||||
Assert.Contains("eidas-required", provider.RequiredForTags);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Configuration Validation
|
||||
|
||||
[Fact]
|
||||
public void QualifiedTimestampingConfiguration_DefaultMode_Rfc3161()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new QualifiedTimestampingConfiguration();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimestampMode.Rfc3161, config.DefaultMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QualifiedTimestampingConfiguration_Providers_EmptyByDefault()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new QualifiedTimestampingConfiguration();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(config.Providers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QualifiedTimestampingConfiguration_Overrides_EmptyByDefault()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new QualifiedTimestampingConfiguration();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(config.Overrides);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QualifiedTimestampingConfiguration_CompleteSetup_IsValid()
|
||||
{
|
||||
// Arrange & Act
|
||||
var config = new QualifiedTimestampingConfiguration
|
||||
{
|
||||
DefaultMode = TimestampMode.Rfc3161,
|
||||
Providers =
|
||||
[
|
||||
new QualifiedTsaProvider
|
||||
{
|
||||
Name = "digicert",
|
||||
Url = "https://timestamp.digicert.com",
|
||||
Qualified = false
|
||||
},
|
||||
new QualifiedTsaProvider
|
||||
{
|
||||
Name = "d-trust",
|
||||
Url = "https://qts.d-trust.net/tsp",
|
||||
Qualified = true,
|
||||
TrustListRef = "eu-tl"
|
||||
}
|
||||
],
|
||||
Overrides =
|
||||
[
|
||||
new TimestampPolicyOverride
|
||||
{
|
||||
Match = new OverrideMatchCriteria { Environments = ["production"] },
|
||||
Mode = TimestampMode.Qualified,
|
||||
TsaProvider = "d-trust"
|
||||
}
|
||||
],
|
||||
TrustList = new EuTrustListConfiguration
|
||||
{
|
||||
LotlUrl = "https://ec.europa.eu/tools/lotl/eu-lotl.xml",
|
||||
CacheTtl = TimeSpan.FromHours(24),
|
||||
OfflinePath = "/etc/stella/eu-lotl.xml"
|
||||
}
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, config.Providers.Count);
|
||||
Assert.Single(config.Overrides);
|
||||
Assert.NotNull(config.TrustList);
|
||||
Assert.Equal(24, config.TrustList.CacheTtl.TotalHours);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Match Criteria Tests
|
||||
|
||||
[Fact]
|
||||
public void OverrideMatchCriteria_EmptyMatch_MatchesNothing()
|
||||
{
|
||||
// Arrange
|
||||
var match = new OverrideMatchCriteria();
|
||||
|
||||
// Assert - empty match has no requirements
|
||||
Assert.Null(match.Environments);
|
||||
Assert.Null(match.Tags);
|
||||
Assert.Null(match.Repositories);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OverrideMatchCriteria_WithEnvironments_ConfiguredCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
var match = new OverrideMatchCriteria
|
||||
{
|
||||
Environments = ["production", "staging"]
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(match.Environments);
|
||||
Assert.Equal(2, match.Environments.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OverrideMatchCriteria_WithRepositoryPatterns_ConfiguredCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
var match = new OverrideMatchCriteria
|
||||
{
|
||||
Repositories = ["finance-*", "banking-*", "core-platform"]
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(match.Repositories);
|
||||
Assert.Equal(3, match.Repositories.Count);
|
||||
Assert.Contains("finance-*", match.Repositories);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Timestamp Policy Tests
|
||||
|
||||
[Fact]
|
||||
public void TimestampPolicy_RequiredProperties_AreSet()
|
||||
{
|
||||
// Arrange & Act
|
||||
var policy = new TimestampPolicy
|
||||
{
|
||||
Mode = TimestampMode.Qualified,
|
||||
TsaProvider = "test-qts",
|
||||
SignatureFormat = CadesLevel.CadesLT,
|
||||
MatchedPolicy = "override:production"
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimestampMode.Qualified, policy.Mode);
|
||||
Assert.Equal("test-qts", policy.TsaProvider);
|
||||
Assert.Equal(CadesLevel.CadesLT, policy.SignatureFormat);
|
||||
Assert.Equal("override:production", policy.MatchedPolicy);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CAdES Level Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(CadesLevel.CadesB, "CadesB")]
|
||||
[InlineData(CadesLevel.CadesT, "CadesT")]
|
||||
[InlineData(CadesLevel.CadesC, "CadesC")]
|
||||
[InlineData(CadesLevel.CadesLT, "CadesLT")]
|
||||
[InlineData(CadesLevel.CadesLTA, "CadesLTA")]
|
||||
public void CadesLevel_AllLevels_HaveCorrectNames(CadesLevel level, string expectedName)
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal(expectedName, level.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CadesLevel_Ordering_IsCorrect()
|
||||
{
|
||||
// Arrange - levels should be ordered from basic to most complete
|
||||
var levels = new[] { CadesLevel.CadesB, CadesLevel.CadesT, CadesLevel.CadesC, CadesLevel.CadesLT, CadesLevel.CadesLTA };
|
||||
|
||||
// Assert
|
||||
for (var i = 0; i < levels.Length - 1; i++)
|
||||
{
|
||||
Assert.True(levels[i] < levels[i + 1], $"{levels[i]} should be less than {levels[i + 1]}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Timestamp Mode Tests
|
||||
|
||||
[Theory]
|
||||
[InlineData(TimestampMode.None, false)]
|
||||
[InlineData(TimestampMode.Rfc3161, false)]
|
||||
[InlineData(TimestampMode.Qualified, true)]
|
||||
[InlineData(TimestampMode.QualifiedLtv, true)]
|
||||
public void TimestampMode_QualifiedModes_IdentifiedCorrectly(TimestampMode mode, bool isQualified)
|
||||
{
|
||||
// Act
|
||||
var actual = mode == TimestampMode.Qualified || mode == TimestampMode.QualifiedLtv;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(isQualified, actual);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// TimestampModeSelectorTests.cs
|
||||
// Sprint: SPRINT_20260119_011 eIDAS Qualified Timestamp Support
|
||||
// Task: QTS-005 - Unit tests for override scenarios
|
||||
// Description: Unit tests for TimestampModeSelector.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Cryptography.Plugin.Eidas.Timestamping;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests.Eidas;
|
||||
|
||||
public class TimestampModeSelectorTests
|
||||
{
|
||||
private static ITimestampModeSelector CreateSelector(QualifiedTimestampingConfiguration config)
|
||||
{
|
||||
var options = Options.Create(config);
|
||||
var logger = NullLogger<TimestampModeSelector>.Instance;
|
||||
return new TimestampModeSelector(options, logger);
|
||||
}
|
||||
|
||||
#region Default Mode Selection
|
||||
|
||||
[Fact]
|
||||
public void SelectMode_NoContext_ReturnsDefaultMode()
|
||||
{
|
||||
// Arrange
|
||||
var config = new QualifiedTimestampingConfiguration
|
||||
{
|
||||
DefaultMode = TimestampMode.Rfc3161,
|
||||
Providers = [new QualifiedTsaProvider { Name = "default-tsa", Url = "https://tsa.example.com", Qualified = false }]
|
||||
};
|
||||
var selector = CreateSelector(config);
|
||||
var context = new TimestampContext();
|
||||
|
||||
// Act
|
||||
var mode = selector.SelectMode(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimestampMode.Rfc3161, mode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectMode_DefaultQualifiedMode_ReturnsQualified()
|
||||
{
|
||||
// Arrange
|
||||
var config = new QualifiedTimestampingConfiguration
|
||||
{
|
||||
DefaultMode = TimestampMode.Qualified,
|
||||
Providers = [new QualifiedTsaProvider { Name = "qualified-tsa", Url = "https://qts.example.com", Qualified = true }]
|
||||
};
|
||||
var selector = CreateSelector(config);
|
||||
var context = new TimestampContext();
|
||||
|
||||
// Act
|
||||
var mode = selector.SelectMode(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimestampMode.Qualified, mode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Environment Override
|
||||
|
||||
[Fact]
|
||||
public void SelectMode_MatchingEnvironment_ReturnsOverrideMode()
|
||||
{
|
||||
// Arrange
|
||||
var config = new QualifiedTimestampingConfiguration
|
||||
{
|
||||
DefaultMode = TimestampMode.Rfc3161,
|
||||
Providers =
|
||||
[
|
||||
new QualifiedTsaProvider { Name = "default-tsa", Url = "https://tsa.example.com", Qualified = false },
|
||||
new QualifiedTsaProvider { Name = "qualified-tsa", Url = "https://qts.example.com", Qualified = true }
|
||||
],
|
||||
Overrides =
|
||||
[
|
||||
new TimestampPolicyOverride
|
||||
{
|
||||
Match = new OverrideMatchCriteria { Environments = ["production"] },
|
||||
Mode = TimestampMode.Qualified,
|
||||
TsaProvider = "qualified-tsa"
|
||||
}
|
||||
]
|
||||
};
|
||||
var selector = CreateSelector(config);
|
||||
var context = new TimestampContext { Environment = "production" };
|
||||
|
||||
// Act
|
||||
var mode = selector.SelectMode(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimestampMode.Qualified, mode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectMode_NonMatchingEnvironment_ReturnsDefaultMode()
|
||||
{
|
||||
// Arrange
|
||||
var config = new QualifiedTimestampingConfiguration
|
||||
{
|
||||
DefaultMode = TimestampMode.Rfc3161,
|
||||
Providers = [new QualifiedTsaProvider { Name = "default-tsa", Url = "https://tsa.example.com", Qualified = false }],
|
||||
Overrides =
|
||||
[
|
||||
new TimestampPolicyOverride
|
||||
{
|
||||
Match = new OverrideMatchCriteria { Environments = ["production"] },
|
||||
Mode = TimestampMode.Qualified
|
||||
}
|
||||
]
|
||||
};
|
||||
var selector = CreateSelector(config);
|
||||
var context = new TimestampContext { Environment = "staging" };
|
||||
|
||||
// Act
|
||||
var mode = selector.SelectMode(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimestampMode.Rfc3161, mode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tag Override
|
||||
|
||||
[Fact]
|
||||
public void SelectMode_MatchingTag_ReturnsOverrideMode()
|
||||
{
|
||||
// Arrange
|
||||
var config = new QualifiedTimestampingConfiguration
|
||||
{
|
||||
DefaultMode = TimestampMode.Rfc3161,
|
||||
Providers =
|
||||
[
|
||||
new QualifiedTsaProvider { Name = "default-tsa", Url = "https://tsa.example.com", Qualified = false },
|
||||
new QualifiedTsaProvider { Name = "qualified-tsa", Url = "https://qts.example.com", Qualified = true }
|
||||
],
|
||||
Overrides =
|
||||
[
|
||||
new TimestampPolicyOverride
|
||||
{
|
||||
Match = new OverrideMatchCriteria { Tags = ["eidas-required", "pci-dss"] },
|
||||
Mode = TimestampMode.Qualified,
|
||||
TsaProvider = "qualified-tsa"
|
||||
}
|
||||
]
|
||||
};
|
||||
var selector = CreateSelector(config);
|
||||
var context = new TimestampContext { Tags = ["eidas-required"] };
|
||||
|
||||
// Act
|
||||
var mode = selector.SelectMode(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimestampMode.Qualified, mode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectMode_MultipleTagsOneMatch_ReturnsOverrideMode()
|
||||
{
|
||||
// Arrange
|
||||
var config = new QualifiedTimestampingConfiguration
|
||||
{
|
||||
DefaultMode = TimestampMode.Rfc3161,
|
||||
Providers = [new QualifiedTsaProvider { Name = "qualified-tsa", Url = "https://qts.example.com", Qualified = true }],
|
||||
Overrides =
|
||||
[
|
||||
new TimestampPolicyOverride
|
||||
{
|
||||
Match = new OverrideMatchCriteria { Tags = ["high-value"] },
|
||||
Mode = TimestampMode.QualifiedLtv,
|
||||
TsaProvider = "qualified-tsa"
|
||||
}
|
||||
]
|
||||
};
|
||||
var selector = CreateSelector(config);
|
||||
var context = new TimestampContext { Tags = ["internal", "high-value", "release"] };
|
||||
|
||||
// Act
|
||||
var mode = selector.SelectMode(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimestampMode.QualifiedLtv, mode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Repository Pattern Override
|
||||
|
||||
[Fact]
|
||||
public void SelectMode_WildcardRepositoryMatch_ReturnsOverrideMode()
|
||||
{
|
||||
// Arrange
|
||||
var config = new QualifiedTimestampingConfiguration
|
||||
{
|
||||
DefaultMode = TimestampMode.Rfc3161,
|
||||
Providers = [new QualifiedTsaProvider { Name = "qualified-tsa", Url = "https://qts.example.com", Qualified = true }],
|
||||
Overrides =
|
||||
[
|
||||
new TimestampPolicyOverride
|
||||
{
|
||||
Match = new OverrideMatchCriteria { Repositories = ["finance-*"] },
|
||||
Mode = TimestampMode.Qualified,
|
||||
TsaProvider = "qualified-tsa"
|
||||
}
|
||||
]
|
||||
};
|
||||
var selector = CreateSelector(config);
|
||||
var context = new TimestampContext { Repository = "finance-core" };
|
||||
|
||||
// Act
|
||||
var mode = selector.SelectMode(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimestampMode.Qualified, mode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectMode_ExactRepositoryMatch_ReturnsOverrideMode()
|
||||
{
|
||||
// Arrange
|
||||
var config = new QualifiedTimestampingConfiguration
|
||||
{
|
||||
DefaultMode = TimestampMode.Rfc3161,
|
||||
Providers = [new QualifiedTsaProvider { Name = "qualified-tsa", Url = "https://qts.example.com", Qualified = true }],
|
||||
Overrides =
|
||||
[
|
||||
new TimestampPolicyOverride
|
||||
{
|
||||
Match = new OverrideMatchCriteria { Repositories = ["core-platform"] },
|
||||
Mode = TimestampMode.Qualified,
|
||||
TsaProvider = "qualified-tsa"
|
||||
}
|
||||
]
|
||||
};
|
||||
var selector = CreateSelector(config);
|
||||
var context = new TimestampContext { Repository = "core-platform" };
|
||||
|
||||
// Act
|
||||
var mode = selector.SelectMode(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimestampMode.Qualified, mode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Provider Selection
|
||||
|
||||
[Fact]
|
||||
public void SelectProvider_WithOverride_ReturnsOverrideProvider()
|
||||
{
|
||||
// Arrange
|
||||
var config = new QualifiedTimestampingConfiguration
|
||||
{
|
||||
DefaultMode = TimestampMode.Rfc3161,
|
||||
Providers =
|
||||
[
|
||||
new QualifiedTsaProvider { Name = "default-tsa", Url = "https://tsa.example.com", Qualified = false },
|
||||
new QualifiedTsaProvider { Name = "d-trust-qts", Url = "https://qts.d-trust.net", Qualified = true }
|
||||
],
|
||||
Overrides =
|
||||
[
|
||||
new TimestampPolicyOverride
|
||||
{
|
||||
Match = new OverrideMatchCriteria { Environments = ["production"] },
|
||||
Mode = TimestampMode.Qualified,
|
||||
TsaProvider = "d-trust-qts"
|
||||
}
|
||||
]
|
||||
};
|
||||
var selector = CreateSelector(config);
|
||||
var context = new TimestampContext { Environment = "production" };
|
||||
|
||||
// Act
|
||||
var provider = selector.SelectProvider(context, TimestampMode.Qualified);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("d-trust-qts", provider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPolicy_ReturnsCompletePolicy()
|
||||
{
|
||||
// Arrange
|
||||
var config = new QualifiedTimestampingConfiguration
|
||||
{
|
||||
DefaultMode = TimestampMode.Rfc3161,
|
||||
Providers =
|
||||
[
|
||||
new QualifiedTsaProvider { Name = "default-tsa", Url = "https://tsa.example.com", Qualified = false },
|
||||
new QualifiedTsaProvider { Name = "eu-qts", Url = "https://qts.eu.example.com", Qualified = true }
|
||||
],
|
||||
Overrides =
|
||||
[
|
||||
new TimestampPolicyOverride
|
||||
{
|
||||
Match = new OverrideMatchCriteria { Tags = ["regulated"] },
|
||||
Mode = TimestampMode.QualifiedLtv,
|
||||
TsaProvider = "eu-qts",
|
||||
SignatureFormat = CadesLevel.CadesLT
|
||||
}
|
||||
]
|
||||
};
|
||||
var selector = CreateSelector(config);
|
||||
var context = new TimestampContext { Tags = ["regulated", "production"] };
|
||||
|
||||
// Act
|
||||
var policy = selector.GetPolicy(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimestampMode.QualifiedLtv, policy.Mode);
|
||||
Assert.Equal("eu-qts", policy.TsaProvider);
|
||||
Assert.Equal(CadesLevel.CadesLT, policy.SignatureFormat);
|
||||
Assert.Contains("override", policy.MatchedPolicy);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Provider Requirements
|
||||
|
||||
[Fact]
|
||||
public void SelectMode_ProviderRequiredForEnvironment_ReturnsQualified()
|
||||
{
|
||||
// Arrange
|
||||
var config = new QualifiedTimestampingConfiguration
|
||||
{
|
||||
DefaultMode = TimestampMode.Rfc3161,
|
||||
Providers =
|
||||
[
|
||||
new QualifiedTsaProvider
|
||||
{
|
||||
Name = "special-qts",
|
||||
Url = "https://qts.example.com",
|
||||
Qualified = true,
|
||||
RequiredForEnvironments = ["production", "staging"]
|
||||
}
|
||||
]
|
||||
};
|
||||
var selector = CreateSelector(config);
|
||||
var context = new TimestampContext { Environment = "production" };
|
||||
|
||||
// Act
|
||||
var mode = selector.SelectMode(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimestampMode.Qualified, mode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectMode_ProviderRequiredForTag_ReturnsQualified()
|
||||
{
|
||||
// Arrange
|
||||
var config = new QualifiedTimestampingConfiguration
|
||||
{
|
||||
DefaultMode = TimestampMode.Rfc3161,
|
||||
Providers =
|
||||
[
|
||||
new QualifiedTsaProvider
|
||||
{
|
||||
Name = "compliance-qts",
|
||||
Url = "https://qts.example.com",
|
||||
Qualified = true,
|
||||
RequiredForTags = ["sox-compliance"]
|
||||
}
|
||||
]
|
||||
};
|
||||
var selector = CreateSelector(config);
|
||||
var context = new TimestampContext { Tags = ["internal", "sox-compliance"] };
|
||||
|
||||
// Act
|
||||
var mode = selector.SelectMode(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(TimestampMode.Qualified, mode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright © StellaOps. All rights reserved.
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// Sprint: SPRINT_20260112_018_CRYPTO_key_escrow_shamir
|
||||
// Tasks: ESCROW-011
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Cryptography.Plugin.Eidas\StellaOps.Cryptography.Plugin.Eidas.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Cryptography.Plugin.Hsm\StellaOps.Cryptography.Plugin.Hsm.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user