consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user