consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -15,11 +15,15 @@ using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Concelier.SbomIntegration.Parsing;
using StellaOps.Policy.Licensing;
using StellaOps.Policy.NtiaCompliance;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using System.Collections.Immutable;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Formats.Asn1;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -1111,28 +1115,136 @@ public static class SbomCommandGroup
{
try
{
if (string.IsNullOrWhiteSpace(trustRootPath))
{
return new SbomVerificationCheck(
"DSSE envelope signature",
false,
"trust-root-missing: supply --trust-root with trusted key/certificate material");
}
if (!File.Exists(trustRootPath) && !Directory.Exists(trustRootPath))
{
return new SbomVerificationCheck(
"DSSE envelope signature",
false,
$"trust-root-not-found: {trustRootPath}");
}
var trustKeys = LoadTrustVerificationKeys(trustRootPath);
if (trustKeys.Count == 0)
{
return new SbomVerificationCheck(
"DSSE envelope signature",
false,
"trust-root-empty: no usable RSA/ECDSA/Ed25519 public keys found");
}
var dsseJson = await File.ReadAllTextAsync(dssePath, ct);
var dsse = JsonSerializer.Deserialize<JsonElement>(dsseJson);
if (!dsse.TryGetProperty("payloadType", out var payloadType) ||
!dsse.TryGetProperty("payload", out _) ||
!dsse.TryGetProperty("payload", out var payloadBase64Element) ||
!dsse.TryGetProperty("signatures", out var sigs) ||
sigs.ValueKind != JsonValueKind.Array ||
sigs.GetArrayLength() == 0)
{
return new SbomVerificationCheck("DSSE envelope signature", false, "Invalid DSSE structure");
return new SbomVerificationCheck(
"DSSE envelope signature",
false,
"dsse-structure-invalid: missing payloadType/payload/signatures");
}
// Validate payload type
var payloadTypeStr = payloadType.GetString();
if (string.IsNullOrEmpty(payloadTypeStr))
{
return new SbomVerificationCheck("DSSE envelope signature", false, "Missing payloadType");
return new SbomVerificationCheck(
"DSSE envelope signature",
false,
"dsse-payload-type-missing");
}
// In production, this would verify the actual signature using certificates
// For now, validate structure
var sigCount = sigs.GetArrayLength();
return new SbomVerificationCheck("DSSE envelope signature", true, $"Valid ({sigCount} signature(s), type: {payloadTypeStr})");
var payloadBase64 = payloadBase64Element.GetString();
if (string.IsNullOrWhiteSpace(payloadBase64))
{
return new SbomVerificationCheck(
"DSSE envelope signature",
false,
"dsse-payload-missing");
}
byte[] payloadBytes;
try
{
payloadBytes = Convert.FromBase64String(payloadBase64);
}
catch (FormatException)
{
return new SbomVerificationCheck(
"DSSE envelope signature",
false,
"dsse-payload-invalid-base64");
}
var pae = BuildDssePae(payloadTypeStr, payloadBytes);
var signatureCount = 0;
var decodeErrorCount = 0;
var verificationErrorCount = 0;
foreach (var signatureElement in sigs.EnumerateArray())
{
signatureCount++;
if (!signatureElement.TryGetProperty("sig", out var sigValue))
{
decodeErrorCount++;
continue;
}
var signatureBase64 = sigValue.GetString();
if (string.IsNullOrWhiteSpace(signatureBase64))
{
decodeErrorCount++;
continue;
}
byte[] signatureBytes;
try
{
signatureBytes = Convert.FromBase64String(signatureBase64);
}
catch (FormatException)
{
decodeErrorCount++;
continue;
}
foreach (var trustKey in trustKeys)
{
if (VerifyWithTrustKey(trustKey, pae, signatureBytes))
{
return new SbomVerificationCheck(
"DSSE envelope signature",
true,
$"dsse-signature-verified: signature {signatureCount} verified with {trustKey.Algorithm} key ({trustKey.Source})");
}
}
verificationErrorCount++;
}
if (decodeErrorCount > 0 && verificationErrorCount == 0)
{
return new SbomVerificationCheck(
"DSSE envelope signature",
false,
$"dsse-signature-invalid-base64: {decodeErrorCount} signature(s) not decodable");
}
return new SbomVerificationCheck(
"DSSE envelope signature",
false,
$"dsse-signature-verification-failed: checked {signatureCount} signature(s) against {trustKeys.Count} trust key(s)");
}
catch (Exception ex)
{
@@ -1140,6 +1252,270 @@ public static class SbomCommandGroup
}
}
private static byte[] BuildDssePae(string payloadType, byte[] payload)
{
var header = Encoding.UTF8.GetBytes("DSSEv1");
var payloadTypeBytes = Encoding.UTF8.GetBytes(payloadType);
var payloadTypeLengthBytes = Encoding.UTF8.GetBytes(payloadTypeBytes.Length.ToString());
var payloadLengthBytes = Encoding.UTF8.GetBytes(payload.Length.ToString());
var space = new[] { (byte)' ' };
var output = new byte[
header.Length + space.Length + payloadTypeLengthBytes.Length + space.Length +
payloadTypeBytes.Length + space.Length + payloadLengthBytes.Length + space.Length +
payload.Length];
var offset = 0;
Buffer.BlockCopy(header, 0, output, offset, header.Length); offset += header.Length;
Buffer.BlockCopy(space, 0, output, offset, space.Length); offset += space.Length;
Buffer.BlockCopy(payloadTypeLengthBytes, 0, output, offset, payloadTypeLengthBytes.Length); offset += payloadTypeLengthBytes.Length;
Buffer.BlockCopy(space, 0, output, offset, space.Length); offset += space.Length;
Buffer.BlockCopy(payloadTypeBytes, 0, output, offset, payloadTypeBytes.Length); offset += payloadTypeBytes.Length;
Buffer.BlockCopy(space, 0, output, offset, space.Length); offset += space.Length;
Buffer.BlockCopy(payloadLengthBytes, 0, output, offset, payloadLengthBytes.Length); offset += payloadLengthBytes.Length;
Buffer.BlockCopy(space, 0, output, offset, space.Length); offset += space.Length;
Buffer.BlockCopy(payload, 0, output, offset, payload.Length);
return output;
}
private static List<TrustVerificationKey> LoadTrustVerificationKeys(string trustRootPath)
{
var files = new List<string>();
if (File.Exists(trustRootPath))
{
files.Add(trustRootPath);
}
else if (Directory.Exists(trustRootPath))
{
files.AddRange(
Directory.EnumerateFiles(trustRootPath, "*", SearchOption.TopDirectoryOnly)
.Where(path =>
{
var ext = Path.GetExtension(path);
return ext.Equals(".pem", StringComparison.OrdinalIgnoreCase) ||
ext.Equals(".crt", StringComparison.OrdinalIgnoreCase) ||
ext.Equals(".cer", StringComparison.OrdinalIgnoreCase) ||
ext.Equals(".pub", StringComparison.OrdinalIgnoreCase) ||
ext.Equals(".key", StringComparison.OrdinalIgnoreCase) ||
ext.Equals(".txt", StringComparison.OrdinalIgnoreCase);
})
.OrderBy(path => path, StringComparer.Ordinal));
}
var keys = new List<TrustVerificationKey>();
foreach (var file in files)
{
var source = Path.GetFileName(file);
TryLoadCertificateKey(file, source, keys);
TryLoadPublicKeysFromPem(file, source, keys);
}
return keys;
}
private static void TryLoadCertificateKey(string filePath, string source, List<TrustVerificationKey> keys)
{
try
{
using var certificate = X509CertificateLoader.LoadCertificateFromFile(filePath);
if (certificate.GetRSAPublicKey() is not null)
{
keys.Add(new TrustVerificationKey(source, "rsa", certificate.PublicKey.ExportSubjectPublicKeyInfo()));
return;
}
if (certificate.GetECDsaPublicKey() is not null)
{
keys.Add(new TrustVerificationKey(source, "ecdsa", certificate.PublicKey.ExportSubjectPublicKeyInfo()));
return;
}
if (IsEd25519SubjectPublicKeyInfo(certificate.PublicKey.ExportSubjectPublicKeyInfo()) &&
TryExtractRawEd25519PublicKey(certificate.PublicKey.ExportSubjectPublicKeyInfo(), out var ed25519Key))
{
keys.Add(new TrustVerificationKey(source, "ed25519", ed25519Key));
}
}
catch
{
// Not a certificate file; PEM key parsing path handles it.
}
}
private static void TryLoadPublicKeysFromPem(string filePath, string source, List<TrustVerificationKey> keys)
{
string content;
try
{
content = File.ReadAllText(filePath);
}
catch
{
return;
}
const string begin = "-----BEGIN PUBLIC KEY-----";
const string end = "-----END PUBLIC KEY-----";
var cursor = 0;
while (true)
{
var beginIndex = content.IndexOf(begin, cursor, StringComparison.Ordinal);
if (beginIndex < 0)
{
break;
}
var endIndex = content.IndexOf(end, beginIndex, StringComparison.Ordinal);
if (endIndex < 0)
{
break;
}
var base64Start = beginIndex + begin.Length;
var base64 = content.Substring(base64Start, endIndex - base64Start);
var normalized = new string(base64.Where(static ch => !char.IsWhiteSpace(ch)).ToArray());
byte[] der;
try
{
der = Convert.FromBase64String(normalized);
}
catch (FormatException)
{
cursor = endIndex + end.Length;
continue;
}
if (IsEd25519SubjectPublicKeyInfo(der) && TryExtractRawEd25519PublicKey(der, out var ed25519Key))
{
keys.Add(new TrustVerificationKey(source, "ed25519", ed25519Key));
}
else if (CanImportRsa(der))
{
keys.Add(new TrustVerificationKey(source, "rsa", der));
}
else if (CanImportEcdsa(der))
{
keys.Add(new TrustVerificationKey(source, "ecdsa", der));
}
cursor = endIndex + end.Length;
}
}
private static bool CanImportRsa(byte[] der)
{
try
{
using var rsa = RSA.Create();
rsa.ImportSubjectPublicKeyInfo(der, out _);
return true;
}
catch
{
return false;
}
}
private static bool CanImportEcdsa(byte[] der)
{
try
{
using var ecdsa = ECDsa.Create();
ecdsa.ImportSubjectPublicKeyInfo(der, out _);
return true;
}
catch
{
return false;
}
}
private static bool VerifyWithTrustKey(TrustVerificationKey key, byte[] pae, byte[] signature)
{
try
{
return key.Algorithm switch
{
"rsa" => VerifyRsa(key.KeyMaterial, pae, signature),
"ecdsa" => VerifyEcdsa(key.KeyMaterial, pae, signature),
"ed25519" => VerifyEd25519(key.KeyMaterial, pae, signature),
_ => false
};
}
catch
{
return false;
}
}
private static bool VerifyRsa(byte[] publicKeyDer, byte[] data, byte[] signature)
{
using var rsa = RSA.Create();
rsa.ImportSubjectPublicKeyInfo(publicKeyDer, out _);
return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) ||
rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
}
private static bool VerifyEcdsa(byte[] publicKeyDer, byte[] data, byte[] signature)
{
using var ecdsa = ECDsa.Create();
ecdsa.ImportSubjectPublicKeyInfo(publicKeyDer, out _);
return ecdsa.VerifyData(data, signature, HashAlgorithmName.SHA256);
}
private static bool VerifyEd25519(byte[] publicKey, byte[] data, byte[] signature)
{
if (publicKey.Length != 32 || signature.Length != 64)
{
return false;
}
var verifier = new Ed25519Signer();
verifier.Init(forSigning: false, new Ed25519PublicKeyParameters(publicKey, 0));
verifier.BlockUpdate(data, 0, data.Length);
return verifier.VerifySignature(signature);
}
private static bool IsEd25519SubjectPublicKeyInfo(ReadOnlySpan<byte> der)
{
try
{
var reader = new AsnReader(der.ToArray(), AsnEncodingRules.DER);
var spki = reader.ReadSequence();
var algorithm = spki.ReadSequence();
var oid = algorithm.ReadObjectIdentifier();
return string.Equals(oid, "1.3.101.112", StringComparison.Ordinal);
}
catch
{
return false;
}
}
private static bool TryExtractRawEd25519PublicKey(byte[] spki, out byte[] publicKey)
{
publicKey = Array.Empty<byte>();
try
{
var reader = new AsnReader(spki, AsnEncodingRules.DER);
var sequence = reader.ReadSequence();
_ = sequence.ReadSequence();
publicKey = sequence.ReadBitString(out _);
return publicKey.Length == 32;
}
catch
{
return false;
}
}
private sealed record TrustVerificationKey(string Source, string Algorithm, byte[] KeyMaterial);
private static string? FindSbomFile(string archiveDir)
{
var spdxPath = Path.Combine(archiveDir, "sbom.spdx.json");