save progress
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user