save progress
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.AirGap.Importer.Contracts;
|
||||
using StellaOps.Cryptography;
|
||||
@@ -12,10 +11,10 @@ namespace StellaOps.AirGap.Importer.Validation;
|
||||
/// </summary>
|
||||
public sealed class DsseVerifier
|
||||
{
|
||||
private const string PaePrefix = "DSSEv1";
|
||||
private readonly ICryptoProviderRegistry _cryptoRegistry;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public DsseVerifier(ICryptoProviderRegistry? cryptoRegistry = null)
|
||||
public DsseVerifier(ICryptoProviderRegistry? cryptoRegistry = null, TimeProvider? timeProvider = null)
|
||||
{
|
||||
if (cryptoRegistry is null)
|
||||
{
|
||||
@@ -27,6 +26,8 @@ public sealed class DsseVerifier
|
||||
{
|
||||
_cryptoRegistry = cryptoRegistry;
|
||||
}
|
||||
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public BundleValidationResult Verify(DsseEnvelope envelope, TrustRootConfig trustRoots, ILogger? logger = null)
|
||||
@@ -41,35 +42,86 @@ public sealed class DsseVerifier
|
||||
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<string>(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 (!trustRoots.TrustedKeyFingerprints.Contains(fingerprint))
|
||||
if (!fingerprints.Contains(fingerprint))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pae = BuildPreAuthEncoding(envelope.PayloadType, envelope.Payload);
|
||||
if (TryVerifyRsaPss(keyBytes, pae, signature.Signature))
|
||||
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");
|
||||
}
|
||||
return BundleValidationResult.Success("dsse-signature-verified");
|
||||
}
|
||||
}
|
||||
|
||||
logger?.LogWarning(
|
||||
@@ -80,31 +132,7 @@ public sealed class DsseVerifier
|
||||
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)
|
||||
private bool TryVerifyRsaPss(byte[] publicKey, byte[] pae, byte[] signatureBytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -112,8 +140,7 @@ public sealed class DsseVerifier
|
||||
var verifier = _cryptoRegistry.ResolveOrThrow(CryptoCapability.Verification, "PS256")
|
||||
.CreateEphemeralVerifier("PS256", publicKey);
|
||||
|
||||
var sig = Convert.FromBase64String(signatureBase64);
|
||||
var result = verifier.VerifyAsync(pae, sig).GetAwaiter().GetResult();
|
||||
var result = verifier.VerifyAsync(pae, signatureBytes).GetAwaiter().GetResult();
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
@@ -128,5 +155,57 @@ public sealed class DsseVerifier
|
||||
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<byte>();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAlgorithmAllowed(IReadOnlyCollection<string> 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user