using Microsoft.Extensions.Logging; using StellaOps.AirGap.Importer.Contracts; using StellaOps.Cryptography; namespace StellaOps.AirGap.Importer.Validation; /// /// Minimal DSSE verifier supporting RSA-PSS/SHA256. The implementation focuses on deterministic /// pre-authentication encoding (PAE) and fingerprint checks so sealed-mode environments can run /// without dragging additional deps. /// public sealed class DsseVerifier { private readonly ICryptoProviderRegistry _cryptoRegistry; private readonly TimeProvider _timeProvider; public DsseVerifier(ICryptoProviderRegistry? cryptoRegistry = null, TimeProvider? timeProvider = null) { if (cryptoRegistry is null) { // For offline/airgap scenarios, use OfflineVerificationCryptoProvider by default var offlineProvider = new StellaOps.Cryptography.Plugin.OfflineVerification.OfflineVerificationCryptoProvider(); _cryptoRegistry = new CryptoProviderRegistry([offlineProvider]); } else { _cryptoRegistry = cryptoRegistry; } _timeProvider = timeProvider ?? TimeProvider.System; } public BundleValidationResult Verify(DsseEnvelope envelope, TrustRootConfig trustRoots, ILogger? logger = null) { if (trustRoots.TrustedKeyFingerprints.Count == 0 || trustRoots.PublicKeys.Count == 0) { logger?.LogWarning( "offlinekit.dsse.verify failed reason_code={reason_code} trusted_fingerprints={trusted_fingerprints} public_keys={public_keys}", "TRUST_ROOTS_REQUIRED", trustRoots.TrustedKeyFingerprints.Count, trustRoots.PublicKeys.Count); return BundleValidationResult.Failure("trust-roots-required"); } if (!IsAlgorithmAllowed(trustRoots.AllowedSignatureAlgorithms)) { logger?.LogWarning( "offlinekit.dsse.verify failed reason_code={reason_code} allowed_algorithms={allowed_algorithms}", "ALGORITHM_NOT_ALLOWED", trustRoots.AllowedSignatureAlgorithms.Count); return BundleValidationResult.Failure("signature-algorithm-disallowed"); } if (!IsWithinTrustWindow(trustRoots, _timeProvider.GetUtcNow(), out var windowReason)) { logger?.LogWarning( "offlinekit.dsse.verify failed reason_code={reason_code} trust_window={trust_window}", "TRUST_WINDOW_INVALID", windowReason); return BundleValidationResult.Failure("trust-window-invalid"); } logger?.LogDebug( "offlinekit.dsse.verify start payload_type={payload_type} signatures={signatures} public_keys={public_keys}", envelope.PayloadType, envelope.Signatures.Count, trustRoots.PublicKeys.Count); if (!TryDecodeBase64(envelope.Payload, out var payloadBytes)) { logger?.LogWarning( "offlinekit.dsse.verify failed reason_code={reason_code}", "PAYLOAD_BASE64_INVALID"); return BundleValidationResult.Failure("payload-base64-invalid"); } var fingerprints = new HashSet(trustRoots.TrustedKeyFingerprints, StringComparer.OrdinalIgnoreCase); var signatureKeyIds = envelope.Signatures .Where(sig => !string.IsNullOrWhiteSpace(sig.KeyId)) .Select(sig => sig.KeyId!) .ToList(); if (signatureKeyIds.Count == 0) { logger?.LogWarning( "offlinekit.dsse.verify failed reason_code={reason_code}", "SIGNATURE_KEYID_MISSING"); return BundleValidationResult.Failure("signature-keyid-missing"); } var pae = DssePreAuthenticationEncoding.Encode(envelope.PayloadType, payloadBytes); foreach (var signature in envelope.Signatures) { if (string.IsNullOrWhiteSpace(signature.KeyId)) { continue; } if (!trustRoots.PublicKeys.TryGetValue(signature.KeyId, out var keyBytes)) { continue; } var fingerprint = ComputeFingerprint(keyBytes); if (!fingerprints.Contains(fingerprint)) { continue; } if (!TryDecodeBase64(signature.Signature, out var sigBytes)) { continue; } if (TryVerifyRsaPss(keyBytes, pae, sigBytes)) { logger?.LogInformation( "offlinekit.dsse.verify succeeded key_id={key_id} fingerprint={fingerprint} payload_type={payload_type}", signature.KeyId, fingerprint, envelope.PayloadType); return BundleValidationResult.Success("dsse-signature-verified"); } } logger?.LogWarning( "offlinekit.dsse.verify failed reason_code={reason_code} signatures={signatures} public_keys={public_keys}", "DSSE_SIGNATURE_INVALID", envelope.Signatures.Count, trustRoots.PublicKeys.Count); return BundleValidationResult.Failure("dsse-signature-untrusted-or-invalid"); } private bool TryVerifyRsaPss(byte[] publicKey, byte[] pae, byte[] signatureBytes) { try { // Use cryptographic abstraction for verification var verifier = _cryptoRegistry.ResolveOrThrow(CryptoCapability.Verification, "PS256") .CreateEphemeralVerifier("PS256", publicKey); var result = verifier.VerifyAsync(pae, signatureBytes).GetAwaiter().GetResult(); return result; } catch { return false; } } private string ComputeFingerprint(byte[] publicKey) { var hasherResolution = _cryptoRegistry.ResolveHasher("SHA-256"); var hash = hasherResolution.Hasher.ComputeHash(publicKey); return Convert.ToHexString(hash).ToLowerInvariant(); } private static bool TryDecodeBase64(string value, out byte[] bytes) { try { bytes = Convert.FromBase64String(value); return true; } catch { bytes = Array.Empty(); return false; } } private static bool IsAlgorithmAllowed(IReadOnlyCollection allowedAlgorithms) { if (allowedAlgorithms.Count == 0) { return true; } foreach (var algorithm in allowedAlgorithms) { if (string.Equals(algorithm, "PS256", StringComparison.OrdinalIgnoreCase) || string.Equals(algorithm, "RSASSA-PSS-SHA256", StringComparison.OrdinalIgnoreCase) || string.Equals(algorithm, "RSA-PSS-SHA256", StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static bool IsWithinTrustWindow(TrustRootConfig trustRoots, DateTimeOffset nowUtc, out string reason) { reason = string.Empty; if (trustRoots.NotBeforeUtc is { } notBefore && nowUtc < notBefore) { reason = "not-before"; return false; } if (trustRoots.NotAfterUtc is { } notAfter && nowUtc > notAfter) { reason = "not-after"; return false; } return true; } }