Add new features and tests for AirGap and Time modules
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Introduced `SbomService` tasks documentation.
- Updated `StellaOps.sln` to include new projects: `StellaOps.AirGap.Time` and `StellaOps.AirGap.Importer`.
- Added unit tests for `BundleImportPlanner`, `DsseVerifier`, `ImportValidator`, and other components in the `StellaOps.AirGap.Importer.Tests` namespace.
- Implemented `InMemoryBundleRepositories` for testing bundle catalog and item repositories.
- Created `MerkleRootCalculator`, `RootRotationPolicy`, and `TufMetadataValidator` tests.
- Developed `StalenessCalculator` and `TimeAnchorLoader` tests in the `StellaOps.AirGap.Time.Tests` namespace.
- Added `fetch-sbomservice-deps.sh` script for offline dependency fetching.
This commit is contained in:
master
2025-11-20 23:29:54 +02:00
parent 65b1599229
commit 79b8e53441
182 changed files with 6660 additions and 1242 deletions

View File

@@ -0,0 +1,90 @@
using System.Security.Cryptography;
using System.Text;
using StellaOps.AirGap.Importer.Contracts;
namespace StellaOps.AirGap.Importer.Validation;
/// <summary>
/// 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.
/// </summary>
public sealed class DsseVerifier
{
private const string PaePrefix = "DSSEv1";
public BundleValidationResult Verify(DsseEnvelope envelope, TrustRootConfig trustRoots)
{
if (trustRoots.TrustedKeyFingerprints.Count == 0 || trustRoots.PublicKeys.Count == 0)
{
return BundleValidationResult.Failure("trust-roots-required");
}
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))
{
return BundleValidationResult.Success("dsse-signature-verified");
}
}
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 static bool TryVerifyRsaPss(byte[] publicKey, byte[] pae, string signatureBase64)
{
try
{
using var rsa = RSA.Create();
rsa.ImportSubjectPublicKeyInfo(publicKey, out _);
var sig = Convert.FromBase64String(signatureBase64);
return rsa.VerifyData(pae, sig, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
}
catch
{
return false;
}
}
private static string ComputeFingerprint(byte[] publicKey)
{
var hash = SHA256.HashData(publicKey);
return Convert.ToHexString(hash).ToLowerInvariant();
}
}