work work hard work
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using StellaOps.AirGap.Importer.Validation;
|
||||
|
||||
namespace StellaOps.AirGap.Importer.Tests.Validation;
|
||||
|
||||
public sealed class RekorOfflineReceiptVerifierTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ValidReceiptAndCheckpoint_Succeeds()
|
||||
{
|
||||
var temp = Path.Combine(Path.GetTempPath(), "stellaops-rekor-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(temp);
|
||||
|
||||
try
|
||||
{
|
||||
// Leaf 0 is the DSSE digest we verify for inclusion.
|
||||
var dsseSha256 = SHA256.HashData(Encoding.UTF8.GetBytes("dsse-envelope"));
|
||||
var otherDsseSha256 = SHA256.HashData(Encoding.UTF8.GetBytes("other-envelope"));
|
||||
|
||||
var leaf0 = HashLeaf(dsseSha256);
|
||||
var leaf1 = HashLeaf(otherDsseSha256);
|
||||
var root = HashInterior(leaf0, leaf1);
|
||||
|
||||
var rootBase64 = Convert.ToBase64String(root);
|
||||
var treeSize = 2L;
|
||||
var origin = "rekor.sigstore.dev - 2605736670972794746";
|
||||
var timestamp = "1700000000";
|
||||
var canonicalBody = $"{origin}\n{treeSize}\n{rootBase64}\n{timestamp}\n";
|
||||
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var signature = ecdsa.SignData(Encoding.UTF8.GetBytes(canonicalBody), HashAlgorithmName.SHA256);
|
||||
var signatureBase64 = Convert.ToBase64String(signature);
|
||||
|
||||
var checkpointPath = Path.Combine(temp, "checkpoint.sig");
|
||||
await File.WriteAllTextAsync(
|
||||
checkpointPath,
|
||||
canonicalBody + $"sig {signatureBase64}\n",
|
||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
|
||||
var publicKeyPath = Path.Combine(temp, "rekor-pub.pem");
|
||||
await File.WriteAllTextAsync(
|
||||
publicKeyPath,
|
||||
WrapPem("PUBLIC KEY", ecdsa.ExportSubjectPublicKeyInfo()),
|
||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
|
||||
var receiptPath = Path.Combine(temp, "rekor-receipt.json");
|
||||
var receiptJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
uuid = "uuid-1",
|
||||
logIndex = 0,
|
||||
rootHash = Convert.ToHexString(root).ToLowerInvariant(),
|
||||
hashes = new[] { Convert.ToHexString(leaf1).ToLowerInvariant() },
|
||||
checkpoint = "checkpoint.sig"
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true });
|
||||
await File.WriteAllTextAsync(receiptPath, receiptJson, new UTF8Encoding(false));
|
||||
|
||||
var result = await RekorOfflineReceiptVerifier.VerifyAsync(receiptPath, dsseSha256, publicKeyPath, CancellationToken.None);
|
||||
|
||||
result.Verified.Should().BeTrue();
|
||||
result.CheckpointSignatureVerified.Should().BeTrue();
|
||||
result.RekorUuid.Should().Be("uuid-1");
|
||||
result.LogIndex.Should().Be(0);
|
||||
result.TreeSize.Should().Be(2);
|
||||
result.ExpectedRootHash.Should().Be(Convert.ToHexString(root).ToLowerInvariant());
|
||||
result.ComputedRootHash.Should().Be(Convert.ToHexString(root).ToLowerInvariant());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(temp, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_TamperedCheckpointSignature_Fails()
|
||||
{
|
||||
var temp = Path.Combine(Path.GetTempPath(), "stellaops-rekor-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(temp);
|
||||
|
||||
try
|
||||
{
|
||||
var dsseSha256 = SHA256.HashData(Encoding.UTF8.GetBytes("dsse-envelope"));
|
||||
var otherDsseSha256 = SHA256.HashData(Encoding.UTF8.GetBytes("other-envelope"));
|
||||
|
||||
var leaf0 = HashLeaf(dsseSha256);
|
||||
var leaf1 = HashLeaf(otherDsseSha256);
|
||||
var root = HashInterior(leaf0, leaf1);
|
||||
|
||||
var rootBase64 = Convert.ToBase64String(root);
|
||||
var treeSize = 2L;
|
||||
var origin = "rekor.sigstore.dev - 2605736670972794746";
|
||||
var timestamp = "1700000000";
|
||||
var canonicalBody = $"{origin}\n{treeSize}\n{rootBase64}\n{timestamp}\n";
|
||||
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var signature = ecdsa.SignData(Encoding.UTF8.GetBytes(canonicalBody), HashAlgorithmName.SHA256);
|
||||
signature[0] ^= 0xFF; // tamper
|
||||
|
||||
var checkpointPath = Path.Combine(temp, "checkpoint.sig");
|
||||
await File.WriteAllTextAsync(
|
||||
checkpointPath,
|
||||
canonicalBody + $"sig {Convert.ToBase64String(signature)}\n",
|
||||
new UTF8Encoding(false));
|
||||
|
||||
var publicKeyPath = Path.Combine(temp, "rekor-pub.pem");
|
||||
await File.WriteAllTextAsync(
|
||||
publicKeyPath,
|
||||
WrapPem("PUBLIC KEY", ecdsa.ExportSubjectPublicKeyInfo()),
|
||||
new UTF8Encoding(false));
|
||||
|
||||
var receiptPath = Path.Combine(temp, "rekor-receipt.json");
|
||||
var receiptJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
uuid = "uuid-1",
|
||||
logIndex = 0,
|
||||
rootHash = Convert.ToHexString(root).ToLowerInvariant(),
|
||||
hashes = new[] { Convert.ToHexString(leaf1).ToLowerInvariant() },
|
||||
checkpoint = "checkpoint.sig"
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true });
|
||||
await File.WriteAllTextAsync(receiptPath, receiptJson, new UTF8Encoding(false));
|
||||
|
||||
var result = await RekorOfflineReceiptVerifier.VerifyAsync(receiptPath, dsseSha256, publicKeyPath, CancellationToken.None);
|
||||
|
||||
result.Verified.Should().BeFalse();
|
||||
result.FailureReason.Should().Contain("checkpoint signature", because: result.FailureReason);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(temp, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] HashLeaf(byte[] leafData)
|
||||
{
|
||||
var buffer = new byte[1 + leafData.Length];
|
||||
buffer[0] = 0x00;
|
||||
leafData.CopyTo(buffer, 1);
|
||||
return SHA256.HashData(buffer);
|
||||
}
|
||||
|
||||
private static byte[] HashInterior(byte[] left, byte[] right)
|
||||
{
|
||||
var buffer = new byte[1 + left.Length + right.Length];
|
||||
buffer[0] = 0x01;
|
||||
left.CopyTo(buffer, 1);
|
||||
right.CopyTo(buffer, 1 + left.Length);
|
||||
return SHA256.HashData(buffer);
|
||||
}
|
||||
|
||||
private static string WrapPem(string label, byte[] derBytes)
|
||||
{
|
||||
var base64 = Convert.ToBase64String(derBytes);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"-----BEGIN {label}-----");
|
||||
for (var i = 0; i < base64.Length; i += 64)
|
||||
{
|
||||
sb.AppendLine(base64.Substring(i, Math.Min(64, base64.Length - i)));
|
||||
}
|
||||
sb.AppendLine($"-----END {label}-----");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user