using System.Text; 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 const string PaePrefix = "DSSEv1"; private readonly ICryptoProviderRegistry _cryptoRegistry; public DsseVerifier(ICryptoProviderRegistry? cryptoRegistry = 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; } } 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"); } logger?.LogDebug( "offlinekit.dsse.verify start payload_type={payload_type} signatures={signatures} public_keys={public_keys}", envelope.PayloadType, envelope.Signatures.Count, trustRoots.PublicKeys.Count); foreach (var signature in envelope.Signatures) { if (!trustRoots.PublicKeys.TryGetValue(signature.KeyId, out var keyBytes)) { continue; } var fingerprint = ComputeFingerprint(keyBytes); if (!trustRoots.TrustedKeyFingerprints.Contains(fingerprint)) { continue; } var pae = BuildPreAuthEncoding(envelope.PayloadType, envelope.Payload); if (TryVerifyRsaPss(keyBytes, pae, signature.Signature)) { 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 static byte[] BuildPreAuthEncoding(string payloadType, string payloadBase64) { var payloadBytes = Convert.FromBase64String(payloadBase64); var parts = new[] { PaePrefix, payloadType, Encoding.UTF8.GetString(payloadBytes) }; var paeBuilder = new StringBuilder(); paeBuilder.Append("PAE:"); paeBuilder.Append(parts.Length); foreach (var part in parts) { paeBuilder.Append(' '); paeBuilder.Append(part.Length); paeBuilder.Append(' '); paeBuilder.Append(part); } return Encoding.UTF8.GetBytes(paeBuilder.ToString()); } private bool TryVerifyRsaPss(byte[] publicKey, byte[] pae, string signatureBase64) { try { // Use cryptographic abstraction for verification var verifier = _cryptoRegistry.ResolveOrThrow(CryptoCapability.Verification, "PS256") .CreateEphemeralVerifier("PS256", publicKey); var sig = Convert.FromBase64String(signatureBase64); var result = verifier.VerifyAsync(pae, sig).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(); } }