chore(sprints): archive 20260226 advisories and expand deterministic tests

This commit is contained in:
master
2026-03-04 03:09:23 +02:00
parent 4fe8eb56ae
commit aaad8104cb
35 changed files with 4686 additions and 1 deletions

View File

@@ -0,0 +1,346 @@
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Symbols.Bundle;
using StellaOps.Symbols.Bundle.Abstractions;
using StellaOps.Symbols.Core.Models;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using Xunit;
using BundleManifest = StellaOps.Symbols.Bundle.Models.BundleManifest;
using CoreSymbolEntry = StellaOps.Symbols.Core.Models.SymbolEntry;
namespace StellaOps.Symbols.Tests.Bundle;
public sealed class BundleBuilderVerificationTests
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
[Fact]
public async Task VerifyAsync_SignedAndRekorBundle_PassesWithRequiredGates()
{
var fixture = await CreateFixtureAsync(sign: true, submitRekor: true);
try
{
var verify = await fixture.Builder.VerifyAsync(
fixture.BundlePath,
new BundleVerifyOptions
{
RequireSignature = true,
RequireRekorProof = true,
VerifyRekorOffline = true
});
Assert.True(verify.Valid, string.Join("; ", verify.Errors));
Assert.Equal(SignatureStatus.Valid, verify.SignatureStatus);
Assert.Equal(RekorVerifyStatus.VerifiedOffline, verify.RekorStatus);
Assert.NotNull(verify.Manifest);
Assert.StartsWith("blake3:", verify.Manifest!.BundleId, StringComparison.Ordinal);
Assert.All(verify.Manifest.Entries, entry =>
{
Assert.StartsWith("blake3:", entry.ManifestHash, StringComparison.Ordinal);
Assert.StartsWith("blake3:", entry.BlobHash, StringComparison.Ordinal);
});
}
finally
{
fixture.Dispose();
}
}
[Fact]
public async Task VerifyAsync_TamperedSignature_FailsDeterministically()
{
var fixture = await CreateFixtureAsync(sign: true, submitRekor: false);
try
{
await RewriteManifestAsync(
fixture.BundlePath,
manifest =>
{
var signature = manifest.Signature!;
return manifest with
{
Signature = signature with
{
Signature = TamperBase64(signature.Signature!)
}
};
});
var verify = await fixture.Builder.VerifyAsync(
fixture.BundlePath,
new BundleVerifyOptions { RequireSignature = true });
Assert.False(verify.Valid);
Assert.Equal(SignatureStatus.Invalid, verify.SignatureStatus);
Assert.Contains(
verify.Errors,
error => error.StartsWith("signature_verification_failed:signature_mismatch", StringComparison.Ordinal));
}
finally
{
fixture.Dispose();
}
}
[Fact]
public async Task VerifyAsync_UnsignedBundle_WhenSignatureRequired_Fails()
{
var fixture = await CreateFixtureAsync(sign: false, submitRekor: false);
try
{
var verify = await fixture.Builder.VerifyAsync(
fixture.BundlePath,
new BundleVerifyOptions { RequireSignature = true });
Assert.False(verify.Valid);
Assert.Equal(SignatureStatus.Unsigned, verify.SignatureStatus);
Assert.Contains(
verify.Errors,
error => error.StartsWith("signature_verification_failed:signature_not_present", StringComparison.Ordinal));
}
finally
{
fixture.Dispose();
}
}
[Fact]
public async Task VerifyAsync_WhenRekorProofRequiredButCheckpointMissing_FailsDeterministically()
{
var fixture = await CreateFixtureAsync(sign: true, submitRekor: false);
try
{
var verify = await fixture.Builder.VerifyAsync(
fixture.BundlePath,
new BundleVerifyOptions
{
RequireSignature = true,
RequireRekorProof = true,
VerifyRekorOffline = true
});
Assert.False(verify.Valid);
Assert.Null(verify.RekorStatus);
Assert.Contains("rekor_proof_required:missing_checkpoint", verify.Errors);
}
finally
{
fixture.Dispose();
}
}
[Fact]
public async Task VerifyAsync_TruncatedInclusionProof_FailsDeterministically()
{
var fixture = await CreateFixtureAsync(sign: true, submitRekor: true);
try
{
await RewriteManifestAsync(
fixture.BundlePath,
manifest =>
{
var checkpoint = manifest.RekorCheckpoint!;
var proof = checkpoint.InclusionProof!;
return manifest with
{
RekorCheckpoint = checkpoint with
{
InclusionProof = proof with
{
Hashes = proof.Hashes.Take(1).ToArray()
}
}
};
});
var verify = await fixture.Builder.VerifyAsync(
fixture.BundlePath,
new BundleVerifyOptions
{
RequireSignature = true,
RequireRekorProof = true,
VerifyRekorOffline = true
});
Assert.False(verify.Valid);
Assert.Equal(RekorVerifyStatus.Invalid, verify.RekorStatus);
Assert.Contains(
verify.Errors,
error => error.StartsWith("rekor_inclusion_proof_failed:proof_nodes_truncated", StringComparison.Ordinal));
}
finally
{
fixture.Dispose();
}
}
[Fact]
public async Task VerifyAsync_CorruptedInclusionProofRoot_FailsDeterministically()
{
var fixture = await CreateFixtureAsync(sign: true, submitRekor: true);
try
{
await RewriteManifestAsync(
fixture.BundlePath,
manifest =>
{
var checkpoint = manifest.RekorCheckpoint!;
var proof = checkpoint.InclusionProof!;
return manifest with
{
RekorCheckpoint = checkpoint with
{
InclusionProof = proof with
{
RootHash = "blake3:" + new string('0', 64)
}
}
};
});
var verify = await fixture.Builder.VerifyAsync(
fixture.BundlePath,
new BundleVerifyOptions
{
RequireSignature = true,
RequireRekorProof = true,
VerifyRekorOffline = true
});
Assert.False(verify.Valid);
Assert.Equal(RekorVerifyStatus.Invalid, verify.RekorStatus);
Assert.Contains(
verify.Errors,
error => error.StartsWith("rekor_inclusion_proof_failed:proof_root_mismatch", StringComparison.Ordinal));
}
finally
{
fixture.Dispose();
}
}
private static async Task<TestFixture> CreateFixtureAsync(bool sign, bool submitRekor)
{
var rootDir = Path.Combine(Path.GetTempPath(), "stella-symbols-tests", Guid.NewGuid().ToString("N"));
var sourceDir = Path.Combine(rootDir, "source");
var outputDir = Path.Combine(rootDir, "out");
Directory.CreateDirectory(sourceDir);
Directory.CreateDirectory(outputDir);
const string debugId = "DBG001";
var manifest = new SymbolManifest
{
ManifestId = "blake3:manifest-dbg001",
DebugId = debugId,
CodeId = "code-001",
BinaryName = "libsample.so",
Platform = "linux-x64",
Format = BinaryFormat.Elf,
Symbols =
[
new CoreSymbolEntry
{
Address = 0x1000,
Size = 16,
MangledName = "_ZL6samplev",
DemangledName = "sample()"
}
],
TenantId = "tenant-default",
BlobUri = "cas://symbols/tenant-default/dbg001/blob",
CreatedAt = new DateTimeOffset(2026, 2, 26, 12, 0, 0, TimeSpan.Zero)
};
await File.WriteAllTextAsync(
Path.Combine(sourceDir, $"{debugId}.symbols.json"),
JsonSerializer.Serialize(manifest, JsonOptions));
await File.WriteAllBytesAsync(
Path.Combine(sourceDir, $"{debugId}.sym"),
Encoding.UTF8.GetBytes("deterministic-symbol-blob-content"));
string? signingKeyPath = null;
if (sign)
{
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
signingKeyPath = Path.Combine(rootDir, "signing-key.pem");
await File.WriteAllTextAsync(signingKeyPath, ecdsa.ExportECPrivateKeyPem());
}
var builder = new BundleBuilder(NullLogger<BundleBuilder>.Instance);
var build = await builder.BuildAsync(new BundleBuildOptions
{
Name = "symbols-fixture",
Version = "1.0.0",
SourceDir = sourceDir,
OutputDir = outputDir,
Sign = sign,
SigningKeyPath = signingKeyPath,
SubmitRekor = submitRekor,
RekorUrl = "https://rekor.example.test"
});
Assert.True(build.Success, build.Error);
Assert.NotNull(build.BundlePath);
return new TestFixture(builder, build.BundlePath!, rootDir);
}
private static async Task RewriteManifestAsync(
string bundlePath,
Func<BundleManifest, BundleManifest> rewrite)
{
var tempDir = Path.Combine(Path.GetTempPath(), "stella-symbols-mutate", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
ZipFile.ExtractToDirectory(bundlePath, tempDir);
var manifestPath = Path.Combine(tempDir, "manifest.json");
var manifest = JsonSerializer.Deserialize<BundleManifest>(await File.ReadAllTextAsync(manifestPath))
?? throw new InvalidOperationException("Bundle manifest is missing or invalid.");
var mutated = rewrite(manifest);
await File.WriteAllTextAsync(manifestPath, JsonSerializer.Serialize(mutated, JsonOptions));
File.Delete(bundlePath);
ZipFile.CreateFromDirectory(tempDir, bundlePath, CompressionLevel.NoCompression, includeBaseDirectory: false);
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
private static string TamperBase64(string input)
{
if (string.IsNullOrEmpty(input))
{
return input;
}
var replacement = input[0] == 'A' ? 'B' : 'A';
return replacement + input[1..];
}
private sealed record TestFixture(BundleBuilder Builder, string BundlePath, string RootDir) : IDisposable
{
public void Dispose()
{
if (Directory.Exists(RootDir))
{
Directory.Delete(RootDir, recursive: true);
}
}
}
}