feat(api): Implement Console Export Client and Models
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled
- Added ConsoleExportClient for managing export requests and responses. - Introduced ConsoleExportRequest and ConsoleExportResponse models. - Implemented methods for creating and retrieving exports with appropriate headers. feat(crypto): Add Software SM2/SM3 Cryptography Provider - Implemented SmSoftCryptoProvider for software-only SM2/SM3 cryptography. - Added support for signing and verification using SM2 algorithm. - Included hashing functionality with SM3 algorithm. - Configured options for loading keys from files and environment gate checks. test(crypto): Add unit tests for SmSoftCryptoProvider - Created comprehensive tests for signing, verifying, and hashing functionalities. - Ensured correct behavior for key management and error handling. feat(api): Enhance Console Export Models - Expanded ConsoleExport models to include detailed status and event types. - Added support for various export formats and notification options. test(time): Implement TimeAnchorPolicyService tests - Developed tests for TimeAnchorPolicyService to validate time anchors. - Covered scenarios for anchor validation, drift calculation, and policy enforcement.
This commit is contained in:
@@ -1,32 +1,218 @@
|
||||
using System.Formats.Asn1;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.Pkcs;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using StellaOps.AirGap.Time.Models;
|
||||
using StellaOps.AirGap.Time.Parsing;
|
||||
|
||||
namespace StellaOps.AirGap.Time.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies RFC 3161 timestamp tokens using SignedCms and X509 certificate chain validation.
|
||||
/// Per AIRGAP-TIME-57-001: Provides trusted time-anchor service with real crypto verification.
|
||||
/// </summary>
|
||||
public sealed class Rfc3161Verifier : ITimeTokenVerifier
|
||||
{
|
||||
// RFC 3161 OIDs
|
||||
private static readonly Oid TstInfoOid = new("1.2.840.113549.1.9.16.1.4"); // id-ct-TSTInfo
|
||||
private static readonly Oid SigningTimeOid = new("1.2.840.113549.1.9.5");
|
||||
|
||||
public TimeTokenFormat Format => TimeTokenFormat.Rfc3161;
|
||||
|
||||
public TimeAnchorValidationResult Verify(ReadOnlySpan<byte> tokenBytes, IReadOnlyList<TimeTrustRoot> trustRoots, out TimeAnchor anchor)
|
||||
{
|
||||
anchor = TimeAnchor.Unknown;
|
||||
|
||||
if (trustRoots.Count == 0)
|
||||
{
|
||||
return TimeAnchorValidationResult.Failure("trust-roots-required");
|
||||
return TimeAnchorValidationResult.Failure("rfc3161-trust-roots-required");
|
||||
}
|
||||
|
||||
if (tokenBytes.IsEmpty)
|
||||
{
|
||||
return TimeAnchorValidationResult.Failure("token-empty");
|
||||
return TimeAnchorValidationResult.Failure("rfc3161-token-empty");
|
||||
}
|
||||
|
||||
// Stub verification: derive anchor deterministically; rely on presence of trust roots for gating.
|
||||
var digest = Convert.ToHexString(SHA256.HashData(tokenBytes)).ToLowerInvariant();
|
||||
var seconds = BitConverter.ToUInt64(SHA256.HashData(tokenBytes).AsSpan(0, 8));
|
||||
var anchorTime = DateTimeOffset.UnixEpoch.AddSeconds(seconds % (3600 * 24 * 365));
|
||||
var signerKeyId = trustRoots.FirstOrDefault()?.KeyId ?? "unknown";
|
||||
anchor = new TimeAnchor(anchorTime, "rfc3161-token", "RFC3161", signerKeyId, digest);
|
||||
return TimeAnchorValidationResult.Success("rfc3161-stub-verified");
|
||||
// Compute token digest for reference
|
||||
var tokenDigest = Convert.ToHexString(SHA256.HashData(tokenBytes)).ToLowerInvariant();
|
||||
|
||||
try
|
||||
{
|
||||
// Parse the SignedCms structure
|
||||
var signedCms = new SignedCms();
|
||||
signedCms.Decode(tokenBytes.ToArray());
|
||||
|
||||
// Verify signature (basic check without chain building)
|
||||
try
|
||||
{
|
||||
signedCms.CheckSignature(verifySignatureOnly: true);
|
||||
}
|
||||
catch (CryptographicException ex)
|
||||
{
|
||||
return TimeAnchorValidationResult.Failure($"rfc3161-signature-invalid:{ex.Message}");
|
||||
}
|
||||
|
||||
// Extract the signing certificate
|
||||
if (signedCms.SignerInfos.Count == 0)
|
||||
{
|
||||
return TimeAnchorValidationResult.Failure("rfc3161-no-signer");
|
||||
}
|
||||
|
||||
var signerInfo = signedCms.SignerInfos[0];
|
||||
var signerCert = signerInfo.Certificate;
|
||||
|
||||
if (signerCert is null)
|
||||
{
|
||||
return TimeAnchorValidationResult.Failure("rfc3161-no-signer-certificate");
|
||||
}
|
||||
|
||||
// Validate signer certificate against trust roots
|
||||
var validRoot = ValidateAgainstTrustRoots(signerCert, trustRoots);
|
||||
if (validRoot is null)
|
||||
{
|
||||
return TimeAnchorValidationResult.Failure("rfc3161-certificate-not-trusted");
|
||||
}
|
||||
|
||||
// Extract signing time from the TSTInfo or signed attributes
|
||||
var signingTime = ExtractSigningTime(signedCms, signerInfo);
|
||||
if (signingTime is null)
|
||||
{
|
||||
return TimeAnchorValidationResult.Failure("rfc3161-no-signing-time");
|
||||
}
|
||||
|
||||
// Compute certificate fingerprint
|
||||
var certFingerprint = Convert.ToHexString(SHA256.HashData(signerCert.RawData)).ToLowerInvariant()[..16];
|
||||
|
||||
anchor = new TimeAnchor(
|
||||
signingTime.Value,
|
||||
$"rfc3161:{validRoot.KeyId}",
|
||||
"RFC3161",
|
||||
certFingerprint,
|
||||
tokenDigest);
|
||||
|
||||
return TimeAnchorValidationResult.Success("rfc3161-verified");
|
||||
}
|
||||
catch (CryptographicException ex)
|
||||
{
|
||||
return TimeAnchorValidationResult.Failure($"rfc3161-decode-error:{ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return TimeAnchorValidationResult.Failure($"rfc3161-error:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static TimeTrustRoot? ValidateAgainstTrustRoots(X509Certificate2 signerCert, IReadOnlyList<TimeTrustRoot> trustRoots)
|
||||
{
|
||||
foreach (var root in trustRoots)
|
||||
{
|
||||
// Match by certificate thumbprint or subject key identifier
|
||||
try
|
||||
{
|
||||
// Try direct certificate match
|
||||
var rootCert = X509CertificateLoader.LoadCertificate(root.PublicKey);
|
||||
if (signerCert.Thumbprint.Equals(rootCert.Thumbprint, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return root;
|
||||
}
|
||||
|
||||
// Try chain validation against root
|
||||
using var chain = new X509Chain();
|
||||
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
|
||||
chain.ChainPolicy.CustomTrustStore.Add(rootCert);
|
||||
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; // Offline mode
|
||||
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
|
||||
|
||||
if (chain.Build(signerCert))
|
||||
{
|
||||
return root;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Invalid root certificate format, try next
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static DateTimeOffset? ExtractSigningTime(SignedCms signedCms, SignerInfo signerInfo)
|
||||
{
|
||||
// Try to get signing time from signed attributes
|
||||
foreach (var attr in signerInfo.SignedAttributes)
|
||||
{
|
||||
if (attr.Oid.Value == SigningTimeOid.Value)
|
||||
{
|
||||
try
|
||||
{
|
||||
var reader = new AsnReader(attr.Values[0].RawData, AsnEncodingRules.DER);
|
||||
var time = reader.ReadUtcTime();
|
||||
return time;
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract from TSTInfo content
|
||||
try
|
||||
{
|
||||
var content = signedCms.ContentInfo;
|
||||
if (content.ContentType.Value == TstInfoOid.Value)
|
||||
{
|
||||
var tstInfo = ParseTstInfo(content.Content);
|
||||
if (tstInfo.HasValue)
|
||||
{
|
||||
return tstInfo.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall through
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static DateTimeOffset? ParseTstInfo(ReadOnlyMemory<byte> tstInfoBytes)
|
||||
{
|
||||
// TSTInfo ::= SEQUENCE {
|
||||
// version INTEGER,
|
||||
// policy OBJECT IDENTIFIER,
|
||||
// messageImprint MessageImprint,
|
||||
// serialNumber INTEGER,
|
||||
// genTime GeneralizedTime,
|
||||
// ...
|
||||
// }
|
||||
try
|
||||
{
|
||||
var reader = new AsnReader(tstInfoBytes, AsnEncodingRules.DER);
|
||||
var sequenceReader = reader.ReadSequence();
|
||||
|
||||
// Skip version
|
||||
sequenceReader.ReadInteger();
|
||||
|
||||
// Skip policy OID
|
||||
sequenceReader.ReadObjectIdentifier();
|
||||
|
||||
// Skip messageImprint (SEQUENCE)
|
||||
sequenceReader.ReadSequence();
|
||||
|
||||
// Skip serialNumber
|
||||
sequenceReader.ReadInteger();
|
||||
|
||||
// Read genTime (GeneralizedTime)
|
||||
var genTime = sequenceReader.ReadGeneralizedTime();
|
||||
return genTime;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user