save progress

This commit is contained in:
StellaOps Bot
2026-01-02 15:52:31 +02:00
parent 2dec7e6a04
commit f46bde5575
174 changed files with 20793 additions and 8307 deletions

View File

@@ -5,6 +5,7 @@
// Description: Fluent builder for constructing Sigstore bundles
// -----------------------------------------------------------------------------
using System.Globalization;
using StellaOps.Attestor.Bundle.Models;
using StellaOps.Attestor.Bundle.Serialization;
@@ -38,11 +39,24 @@ public sealed class SigstoreBundleBuilder
ArgumentException.ThrowIfNullOrWhiteSpace(payload);
ArgumentNullException.ThrowIfNull(signatures);
EnsureBase64(payload, nameof(payload));
var signatureList = signatures.ToList();
if (signatureList.Count == 0)
{
throw new ArgumentException("At least one signature is required.", nameof(signatures));
}
foreach (var signature in signatureList)
{
ArgumentNullException.ThrowIfNull(signature);
EnsureBase64(signature.Sig, nameof(signatures));
}
_dsseEnvelope = new BundleDsseEnvelope
{
PayloadType = payloadType,
Payload = payload,
Signatures = signatures.ToList()
Signatures = signatureList
};
return this;
@@ -83,6 +97,7 @@ public sealed class SigstoreBundleBuilder
public SigstoreBundleBuilder WithCertificateBase64(string base64DerCertificate)
{
ArgumentException.ThrowIfNullOrWhiteSpace(base64DerCertificate);
EnsureBase64(base64DerCertificate, nameof(base64DerCertificate));
_certificate = new CertificateInfo
{
RawBytes = base64DerCertificate
@@ -140,6 +155,13 @@ public sealed class SigstoreBundleBuilder
string version = "0.0.1",
InclusionProof? inclusionProof = null)
{
EnsureNumber(logIndex, nameof(logIndex));
EnsureNumber(integratedTime, nameof(integratedTime));
EnsureBase64(logIdKeyId, nameof(logIdKeyId));
EnsureBase64(canonicalizedBody, nameof(canonicalizedBody));
ArgumentException.ThrowIfNullOrWhiteSpace(kind);
ArgumentException.ThrowIfNullOrWhiteSpace(version);
var entry = new TransparencyLogEntry
{
LogIndex = logIndex,
@@ -260,4 +282,29 @@ public sealed class SigstoreBundleBuilder
var bundle = Build();
return SigstoreBundleSerializer.SerializeToUtf8Bytes(bundle);
}
private static void EnsureBase64(string value, string paramName)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Value must be provided.", paramName);
}
try
{
Convert.FromBase64String(value);
}
catch (FormatException ex)
{
throw new ArgumentException("Value must be base64-encoded.", paramName, ex);
}
}
private static void EnsureNumber(string value, string paramName)
{
if (!long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
throw new ArgumentException("Value must be an integer string.", paramName);
}
}
}

View File

@@ -144,6 +144,24 @@ public static class SigstoreBundleSerializer
throw new SigstoreBundleException("Bundle verificationMaterial is required");
}
if (bundle.VerificationMaterial.Certificate is null &&
bundle.VerificationMaterial.PublicKey is null)
{
throw new SigstoreBundleException("Bundle verificationMaterial must include certificate or publicKey");
}
if (bundle.VerificationMaterial.Certificate is not null &&
string.IsNullOrWhiteSpace(bundle.VerificationMaterial.Certificate.RawBytes))
{
throw new SigstoreBundleException("Bundle certificate rawBytes is required");
}
if (bundle.VerificationMaterial.PublicKey is not null &&
string.IsNullOrWhiteSpace(bundle.VerificationMaterial.PublicKey.RawBytes))
{
throw new SigstoreBundleException("Bundle publicKey rawBytes is required");
}
if (bundle.DsseEnvelope is null)
{
throw new SigstoreBundleException("Bundle dsseEnvelope is required");

View File

@@ -7,4 +7,4 @@ Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.
| --- | --- | --- |
| AUDIT-0045-M | DONE | Maintainability audit for StellaOps.Attestor.Bundle. |
| AUDIT-0045-T | DONE | Test coverage audit for StellaOps.Attestor.Bundle. |
| AUDIT-0045-A | TODO | Pending approval for changes. |
| AUDIT-0045-A | DONE | Applied bundle validation hardening, verifier fixes, and test coverage. |

View File

@@ -8,6 +8,7 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Globalization;
using Microsoft.Extensions.Logging;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
@@ -287,7 +288,22 @@ public sealed class SigstoreBundleVerifier
}
// Construct PAE (Pre-Authentication Encoding) for DSSE
var payloadBytes = Convert.FromBase64String(envelope.Payload);
byte[] payloadBytes;
try
{
payloadBytes = Convert.FromBase64String(envelope.Payload);
}
catch (FormatException ex)
{
errors.Add(new BundleVerificationError
{
Code = BundleVerificationErrorCode.DsseSignatureInvalid,
Message = "DSSE envelope payload is not valid base64",
Exception = ex
});
return new VerificationCheckResult(false, CheckResult.Failed, errors);
}
var paeMessage = ConstructPae(envelope.PayloadType, payloadBytes);
// Verify at least one signature
@@ -304,6 +320,15 @@ public sealed class SigstoreBundleVerifier
break;
}
}
catch (FormatException ex)
{
errors.Add(new BundleVerificationError
{
Code = BundleVerificationErrorCode.DsseSignatureInvalid,
Message = "DSSE signature is not valid base64",
Exception = ex
});
}
catch (Exception ex)
{
_logger?.LogDebug(ex, "Signature verification attempt failed");
@@ -320,7 +345,9 @@ public sealed class SigstoreBundleVerifier
return new VerificationCheckResult(false, CheckResult.Failed, errors);
}
return new VerificationCheckResult(true, CheckResult.Passed, errors);
return errors.Count == 0
? new VerificationCheckResult(true, CheckResult.Passed, errors)
: new VerificationCheckResult(false, CheckResult.Failed, errors);
}
private static byte[] ConstructPae(string payloadType, byte[] payload)
@@ -331,8 +358,8 @@ public sealed class SigstoreBundleVerifier
const byte Space = 0x20;
var typeBytes = Encoding.UTF8.GetBytes(payloadType);
var typeLenBytes = Encoding.UTF8.GetBytes(typeBytes.Length.ToString());
var payloadLenBytes = Encoding.UTF8.GetBytes(payload.Length.ToString());
var typeLenBytes = Encoding.UTF8.GetBytes(typeBytes.Length.ToString(CultureInfo.InvariantCulture));
var payloadLenBytes = Encoding.UTF8.GetBytes(payload.Length.ToString(CultureInfo.InvariantCulture));
var prefixBytes = Encoding.UTF8.GetBytes(DssePrefix);
var totalLength = prefixBytes.Length + 1 + typeLenBytes.Length + 1 +
@@ -426,23 +453,29 @@ public sealed class SigstoreBundleVerifier
await Task.CompletedTask; // Async for future extensibility
var errors = new List<BundleVerificationError>();
var sawProof = false;
foreach (var entry in tlogEntries)
{
if (entry.InclusionProof is null)
{
// Skip entries without inclusion proofs
errors.Add(new BundleVerificationError
{
Code = BundleVerificationErrorCode.InclusionProofInvalid,
Message = $"Missing inclusion proof for log index {entry.LogIndex}"
});
continue;
}
try
{
var valid = VerifyMerkleInclusionProof(entry);
sawProof = true;
var valid = VerifyMerkleInclusionProof(entry, out var errorCode);
if (!valid)
{
errors.Add(new BundleVerificationError
{
Code = BundleVerificationErrorCode.InclusionProofInvalid,
Code = errorCode,
Message = $"Merkle inclusion proof verification failed for log index {entry.LogIndex}"
});
}
@@ -458,6 +491,15 @@ public sealed class SigstoreBundleVerifier
}
}
if (!sawProof && tlogEntries.Count > 0)
{
errors.Add(new BundleVerificationError
{
Code = BundleVerificationErrorCode.InclusionProofInvalid,
Message = "No inclusion proofs present in transparency log entries"
});
}
if (errors.Count > 0)
{
return new VerificationCheckResult(false, CheckResult.Failed, errors);
@@ -466,8 +508,10 @@ public sealed class SigstoreBundleVerifier
return new VerificationCheckResult(true, CheckResult.Passed, errors);
}
private bool VerifyMerkleInclusionProof(TransparencyLogEntry entry)
private bool VerifyMerkleInclusionProof(TransparencyLogEntry entry, out BundleVerificationErrorCode errorCode)
{
errorCode = BundleVerificationErrorCode.InclusionProofInvalid;
if (entry.InclusionProof is null)
{
return false;
@@ -475,9 +519,14 @@ public sealed class SigstoreBundleVerifier
var proof = entry.InclusionProof;
if (!string.Equals(proof.LogIndex, entry.LogIndex, StringComparison.Ordinal))
{
return false;
}
// Parse values
if (!long.TryParse(proof.LogIndex, out var leafIndex) ||
!long.TryParse(proof.TreeSize, out var treeSize))
if (!long.TryParse(proof.LogIndex, NumberStyles.Integer, CultureInfo.InvariantCulture, out var leafIndex) ||
!long.TryParse(proof.TreeSize, NumberStyles.Integer, CultureInfo.InvariantCulture, out var treeSize))
{
return false;
}
@@ -500,7 +549,13 @@ public sealed class SigstoreBundleVerifier
// Verify Merkle path
var computedRoot = ComputeMerkleRoot(leafHash, leafIndex, treeSize, hashes);
return computedRoot.SequenceEqual(expectedRoot);
if (!computedRoot.SequenceEqual(expectedRoot))
{
errorCode = BundleVerificationErrorCode.RootHashMismatch;
return false;
}
return true;
}
private static byte[] ComputeLeafHash(byte[] data)