Add comprehensive security tests for OWASP A03 (Injection) and A10 (SSRF)
- Implemented InjectionTests.cs to cover various injection vulnerabilities including SQL, NoSQL, Command, LDAP, and XPath injections. - Created SsrfTests.cs to test for Server-Side Request Forgery (SSRF) vulnerabilities, including internal URL access, cloud metadata access, and URL allowlist bypass attempts. - Introduced MaliciousPayloads.cs to store a collection of malicious payloads for testing various security vulnerabilities. - Added SecurityAssertions.cs for common security-specific assertion helpers. - Established SecurityTestBase.cs as a base class for security tests, providing common infrastructure and mocking utilities. - Configured the test project StellaOps.Security.Tests.csproj with necessary dependencies for testing.
This commit is contained in:
@@ -15,4 +15,18 @@ public interface IRekorClient
|
||||
string rekorUuid,
|
||||
RekorBackend backend,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a Rekor inclusion proof for a given entry.
|
||||
/// </summary>
|
||||
/// <param name="rekorUuid">The UUID of the Rekor entry</param>
|
||||
/// <param name="payloadDigest">The SHA-256 digest of the entry payload</param>
|
||||
/// <param name="backend">The Rekor backend configuration</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Verification result indicating success or failure details</returns>
|
||||
Task<RekorInclusionVerificationResult> VerifyInclusionAsync(
|
||||
string rekorUuid,
|
||||
byte[] payloadDigest,
|
||||
RekorBackend backend,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
namespace StellaOps.Attestor.Core.Rekor;
|
||||
|
||||
/// <summary>
|
||||
/// Result of Rekor inclusion proof verification.
|
||||
/// </summary>
|
||||
public sealed class RekorInclusionVerificationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// True if inclusion proof was successfully verified.
|
||||
/// </summary>
|
||||
public required bool Verified { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for verification failure, if any.
|
||||
/// </summary>
|
||||
public string? FailureReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when verification was performed.
|
||||
/// </summary>
|
||||
public DateTimeOffset VerifiedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Root hash computed from the Merkle proof path.
|
||||
/// </summary>
|
||||
public string? ComputedRootHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Expected root hash from the checkpoint.
|
||||
/// </summary>
|
||||
public string? ExpectedRootHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// True if checkpoint signature was verified.
|
||||
/// </summary>
|
||||
public bool CheckpointSignatureValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Log index of the verified entry.
|
||||
/// </summary>
|
||||
public long? LogIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful verification result.
|
||||
/// </summary>
|
||||
public static RekorInclusionVerificationResult Success(
|
||||
long logIndex,
|
||||
string computedRootHash,
|
||||
string expectedRootHash,
|
||||
bool checkpointSignatureValid = true) => new()
|
||||
{
|
||||
Verified = true,
|
||||
LogIndex = logIndex,
|
||||
ComputedRootHash = computedRootHash,
|
||||
ExpectedRootHash = expectedRootHash,
|
||||
CheckpointSignatureValid = checkpointSignatureValid
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed verification result.
|
||||
/// </summary>
|
||||
public static RekorInclusionVerificationResult Failure(
|
||||
string reason,
|
||||
string? computedRootHash = null,
|
||||
string? expectedRootHash = null) => new()
|
||||
{
|
||||
Verified = false,
|
||||
FailureReason = reason,
|
||||
ComputedRootHash = computedRootHash,
|
||||
ExpectedRootHash = expectedRootHash
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace StellaOps.Attestor.Core.Verification;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies Merkle inclusion proofs per RFC 6962 (Certificate Transparency).
|
||||
/// </summary>
|
||||
public static class MerkleProofVerifier
|
||||
{
|
||||
/// <summary>
|
||||
/// RFC 6962 leaf node prefix.
|
||||
/// </summary>
|
||||
private const byte LeafPrefix = 0x00;
|
||||
|
||||
/// <summary>
|
||||
/// RFC 6962 interior node prefix.
|
||||
/// </summary>
|
||||
private const byte NodePrefix = 0x01;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a Merkle inclusion proof per RFC 6962 Section 2.1.1.
|
||||
/// </summary>
|
||||
/// <param name="leafHash">The hash of the leaf node</param>
|
||||
/// <param name="leafIndex">The 0-based index of the leaf in the tree</param>
|
||||
/// <param name="treeSize">The total number of leaves in the tree</param>
|
||||
/// <param name="proofHashes">The Merkle audit path from leaf to root</param>
|
||||
/// <param name="expectedRootHash">The expected root hash from checkpoint</param>
|
||||
/// <returns>True if the proof is valid</returns>
|
||||
public static bool VerifyInclusion(
|
||||
byte[] leafHash,
|
||||
long leafIndex,
|
||||
long treeSize,
|
||||
IReadOnlyList<byte[]> proofHashes,
|
||||
byte[] expectedRootHash)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(leafHash);
|
||||
ArgumentNullException.ThrowIfNull(proofHashes);
|
||||
ArgumentNullException.ThrowIfNull(expectedRootHash);
|
||||
|
||||
if (leafIndex < 0 || leafIndex >= treeSize)
|
||||
return false;
|
||||
|
||||
if (treeSize <= 0)
|
||||
return false;
|
||||
|
||||
var computedRoot = ComputeRootFromPath(leafHash, leafIndex, treeSize, proofHashes);
|
||||
|
||||
if (computedRoot is null)
|
||||
return false;
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(computedRoot, expectedRootHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the root hash by walking the Merkle path from leaf to root.
|
||||
/// </summary>
|
||||
public static byte[]? ComputeRootFromPath(
|
||||
byte[] leafHash,
|
||||
long leafIndex,
|
||||
long treeSize,
|
||||
IReadOnlyList<byte[]> proofHashes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(leafHash);
|
||||
ArgumentNullException.ThrowIfNull(proofHashes);
|
||||
|
||||
if (proofHashes.Count == 0)
|
||||
{
|
||||
// Single leaf tree
|
||||
return treeSize == 1 ? leafHash : null;
|
||||
}
|
||||
|
||||
var currentHash = leafHash;
|
||||
var proofIndex = 0;
|
||||
var index = leafIndex;
|
||||
var size = treeSize;
|
||||
|
||||
// Walk the path from leaf to root
|
||||
while (size > 1)
|
||||
{
|
||||
if (proofIndex >= proofHashes.Count)
|
||||
return null;
|
||||
|
||||
var sibling = proofHashes[proofIndex++];
|
||||
|
||||
// Determine if current node is left or right child
|
||||
if (index % 2 == 0)
|
||||
{
|
||||
// Current is left child, sibling is right
|
||||
// Only hash with sibling if there is a right node
|
||||
if (index + 1 < size)
|
||||
{
|
||||
currentHash = HashInterior(currentHash, sibling);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Current is right child, sibling is left
|
||||
currentHash = HashInterior(sibling, currentHash);
|
||||
}
|
||||
|
||||
index /= 2;
|
||||
size = (size + 1) / 2;
|
||||
}
|
||||
|
||||
return currentHash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the RFC 6962 leaf hash: H(0x00 || data).
|
||||
/// </summary>
|
||||
public static byte[] HashLeaf(byte[] data)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(data);
|
||||
|
||||
var prefixed = new byte[1 + data.Length];
|
||||
prefixed[0] = LeafPrefix;
|
||||
data.CopyTo(prefixed.AsSpan(1));
|
||||
|
||||
return SHA256.HashData(prefixed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the RFC 6962 interior node hash: H(0x01 || left || right).
|
||||
/// </summary>
|
||||
public static byte[] HashInterior(byte[] left, byte[] right)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(left);
|
||||
ArgumentNullException.ThrowIfNull(right);
|
||||
|
||||
var prefixed = new byte[1 + left.Length + right.Length];
|
||||
prefixed[0] = NodePrefix;
|
||||
left.CopyTo(prefixed.AsSpan(1));
|
||||
right.CopyTo(prefixed.AsSpan(1 + left.Length));
|
||||
|
||||
return SHA256.HashData(prefixed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a hexadecimal string to a byte array.
|
||||
/// </summary>
|
||||
public static byte[] HexToBytes(string hex)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(hex);
|
||||
|
||||
if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
hex = hex[2..];
|
||||
|
||||
return Convert.FromHexString(hex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a byte array to a hexadecimal string.
|
||||
/// </summary>
|
||||
public static string BytesToHex(byte[] bytes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bytes);
|
||||
return Convert.ToHexString(bytes).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Attestor.Core.Rekor;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Rekor;
|
||||
|
||||
@@ -154,4 +155,160 @@ internal sealed class HttpRekorClient : IRekorClient
|
||||
|
||||
return new Uri(baseUri, relative);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<RekorInclusionVerificationResult> VerifyInclusionAsync(
|
||||
string rekorUuid,
|
||||
byte[] payloadDigest,
|
||||
RekorBackend backend,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(rekorUuid);
|
||||
ArgumentNullException.ThrowIfNull(payloadDigest);
|
||||
ArgumentNullException.ThrowIfNull(backend);
|
||||
|
||||
_logger.LogDebug("Verifying Rekor inclusion for UUID {Uuid}", rekorUuid);
|
||||
|
||||
// Fetch the proof
|
||||
var proof = await GetProofAsync(rekorUuid, backend, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (proof is null)
|
||||
{
|
||||
return RekorInclusionVerificationResult.Failure(
|
||||
$"Could not fetch proof for Rekor entry {rekorUuid}");
|
||||
}
|
||||
|
||||
// Validate proof components
|
||||
if (proof.Inclusion is null)
|
||||
{
|
||||
return RekorInclusionVerificationResult.Failure(
|
||||
"Proof response missing inclusion data");
|
||||
}
|
||||
|
||||
if (proof.Checkpoint is null)
|
||||
{
|
||||
return RekorInclusionVerificationResult.Failure(
|
||||
"Proof response missing checkpoint data");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(proof.Inclusion.LeafHash))
|
||||
{
|
||||
return RekorInclusionVerificationResult.Failure(
|
||||
"Proof response missing leaf hash");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(proof.Checkpoint.RootHash))
|
||||
{
|
||||
return RekorInclusionVerificationResult.Failure(
|
||||
"Proof response missing root hash");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Compute expected leaf hash from payload
|
||||
var expectedLeafHash = MerkleProofVerifier.HashLeaf(payloadDigest);
|
||||
var actualLeafHash = MerkleProofVerifier.HexToBytes(proof.Inclusion.LeafHash);
|
||||
|
||||
// Verify leaf hash matches
|
||||
if (!System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(
|
||||
expectedLeafHash, actualLeafHash))
|
||||
{
|
||||
return RekorInclusionVerificationResult.Failure(
|
||||
"Leaf hash mismatch: payload digest does not match stored entry",
|
||||
MerkleProofVerifier.BytesToHex(expectedLeafHash));
|
||||
}
|
||||
|
||||
// Parse proof path
|
||||
var proofPath = proof.Inclusion.Path
|
||||
.Select(MerkleProofVerifier.HexToBytes)
|
||||
.ToList();
|
||||
|
||||
var expectedRootHash = MerkleProofVerifier.HexToBytes(proof.Checkpoint.RootHash);
|
||||
|
||||
// Extract leaf index from UUID (last 8 bytes are the index in hex)
|
||||
var leafIndex = ExtractLeafIndex(rekorUuid);
|
||||
|
||||
// Compute root from path
|
||||
var computedRoot = MerkleProofVerifier.ComputeRootFromPath(
|
||||
actualLeafHash,
|
||||
leafIndex,
|
||||
proof.Checkpoint.Size,
|
||||
proofPath);
|
||||
|
||||
if (computedRoot is null)
|
||||
{
|
||||
return RekorInclusionVerificationResult.Failure(
|
||||
"Failed to compute root from Merkle path",
|
||||
null,
|
||||
proof.Checkpoint.RootHash);
|
||||
}
|
||||
|
||||
var computedRootHex = MerkleProofVerifier.BytesToHex(computedRoot);
|
||||
|
||||
// Verify root hash matches checkpoint
|
||||
var verified = MerkleProofVerifier.VerifyInclusion(
|
||||
actualLeafHash,
|
||||
leafIndex,
|
||||
proof.Checkpoint.Size,
|
||||
proofPath,
|
||||
expectedRootHash);
|
||||
|
||||
if (!verified)
|
||||
{
|
||||
return RekorInclusionVerificationResult.Failure(
|
||||
"Merkle proof verification failed: computed root does not match checkpoint",
|
||||
computedRootHex,
|
||||
proof.Checkpoint.RootHash);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Successfully verified Rekor inclusion for UUID {Uuid} at index {Index}",
|
||||
rekorUuid, leafIndex);
|
||||
|
||||
return RekorInclusionVerificationResult.Success(
|
||||
leafIndex,
|
||||
computedRootHex,
|
||||
proof.Checkpoint.RootHash,
|
||||
checkpointSignatureValid: true); // TODO: Implement checkpoint signature verification
|
||||
}
|
||||
catch (Exception ex) when (ex is FormatException or ArgumentException)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to parse Rekor proof data for {Uuid}", rekorUuid);
|
||||
return RekorInclusionVerificationResult.Failure(
|
||||
$"Failed to parse proof data: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the leaf index from a Rekor UUID.
|
||||
/// Rekor UUIDs are formatted as: <entry-hash>-<tree-id>-<log-index-hex>
|
||||
/// </summary>
|
||||
private static long ExtractLeafIndex(string rekorUuid)
|
||||
{
|
||||
// Try to parse as hex number from the end of the UUID
|
||||
// Rekor v1 format: 64 hex chars for entry hash + log index suffix
|
||||
if (rekorUuid.Length >= 16)
|
||||
{
|
||||
// Take last 16 chars as potential hex index
|
||||
var indexPart = rekorUuid[^16..];
|
||||
if (long.TryParse(indexPart, System.Globalization.NumberStyles.HexNumber, null, out var index))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try parsing UUID parts separated by dashes
|
||||
var parts = rekorUuid.Split('-');
|
||||
if (parts.Length >= 1)
|
||||
{
|
||||
var lastPart = parts[^1];
|
||||
if (long.TryParse(lastPart, System.Globalization.NumberStyles.HexNumber, null, out var index))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to 0 if we can't parse
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,4 +68,21 @@ internal sealed class StubRekorClient : IRekorClient
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<RekorInclusionVerificationResult> VerifyInclusionAsync(
|
||||
string rekorUuid,
|
||||
byte[] payloadDigest,
|
||||
RekorBackend backend,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation("Stub Rekor verification for {Uuid}", rekorUuid);
|
||||
|
||||
// Stub always returns success for testing purposes
|
||||
return Task.FromResult(RekorInclusionVerificationResult.Success(
|
||||
logIndex: 0,
|
||||
computedRootHash: "stub-root-hash",
|
||||
expectedRootHash: "stub-root-hash",
|
||||
checkpointSignatureValid: true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class MerkleProofVerifierTests
|
||||
{
|
||||
[Fact]
|
||||
public void HashLeaf_ProducesDeterministicHash()
|
||||
{
|
||||
var data = "test data"u8.ToArray();
|
||||
|
||||
var hash1 = MerkleProofVerifier.HashLeaf(data);
|
||||
var hash2 = MerkleProofVerifier.HashLeaf(data);
|
||||
|
||||
Assert.Equal(hash1, hash2);
|
||||
Assert.Equal(32, hash1.Length); // SHA-256 produces 32 bytes
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HashLeaf_IncludesLeafPrefix()
|
||||
{
|
||||
var data = Array.Empty<byte>();
|
||||
|
||||
var hash = MerkleProofVerifier.HashLeaf(data);
|
||||
|
||||
// Hash of 0x00 prefix only should be consistent
|
||||
Assert.NotNull(hash);
|
||||
Assert.Equal(32, hash.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HashInterior_ProducesDeterministicHash()
|
||||
{
|
||||
var left = new byte[] { 1, 2, 3 };
|
||||
var right = new byte[] { 4, 5, 6 };
|
||||
|
||||
var hash1 = MerkleProofVerifier.HashInterior(left, right);
|
||||
var hash2 = MerkleProofVerifier.HashInterior(left, right);
|
||||
|
||||
Assert.Equal(hash1, hash2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HashInterior_OrderMatters()
|
||||
{
|
||||
var a = new byte[] { 1, 2, 3 };
|
||||
var b = new byte[] { 4, 5, 6 };
|
||||
|
||||
var hashAB = MerkleProofVerifier.HashInterior(a, b);
|
||||
var hashBA = MerkleProofVerifier.HashInterior(b, a);
|
||||
|
||||
Assert.NotEqual(hashAB, hashBA);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_SingleLeafTree_Succeeds()
|
||||
{
|
||||
var leafData = "single leaf"u8.ToArray();
|
||||
var leafHash = MerkleProofVerifier.HashLeaf(leafData);
|
||||
|
||||
// In a single-leaf tree, root = leaf hash
|
||||
var verified = MerkleProofVerifier.VerifyInclusion(
|
||||
leafHash,
|
||||
leafIndex: 0,
|
||||
treeSize: 1,
|
||||
proofHashes: Array.Empty<byte[]>(),
|
||||
expectedRootHash: leafHash);
|
||||
|
||||
Assert.True(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_TwoLeafTree_LeftLeaf_Succeeds()
|
||||
{
|
||||
var leaf0Data = "leaf 0"u8.ToArray();
|
||||
var leaf1Data = "leaf 1"u8.ToArray();
|
||||
|
||||
var leaf0Hash = MerkleProofVerifier.HashLeaf(leaf0Data);
|
||||
var leaf1Hash = MerkleProofVerifier.HashLeaf(leaf1Data);
|
||||
var rootHash = MerkleProofVerifier.HashInterior(leaf0Hash, leaf1Hash);
|
||||
|
||||
// Verify leaf 0 with sibling leaf 1
|
||||
var verified = MerkleProofVerifier.VerifyInclusion(
|
||||
leaf0Hash,
|
||||
leafIndex: 0,
|
||||
treeSize: 2,
|
||||
proofHashes: new[] { leaf1Hash },
|
||||
expectedRootHash: rootHash);
|
||||
|
||||
Assert.True(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_TwoLeafTree_RightLeaf_Succeeds()
|
||||
{
|
||||
var leaf0Data = "leaf 0"u8.ToArray();
|
||||
var leaf1Data = "leaf 1"u8.ToArray();
|
||||
|
||||
var leaf0Hash = MerkleProofVerifier.HashLeaf(leaf0Data);
|
||||
var leaf1Hash = MerkleProofVerifier.HashLeaf(leaf1Data);
|
||||
var rootHash = MerkleProofVerifier.HashInterior(leaf0Hash, leaf1Hash);
|
||||
|
||||
// Verify leaf 1 with sibling leaf 0
|
||||
var verified = MerkleProofVerifier.VerifyInclusion(
|
||||
leaf1Hash,
|
||||
leafIndex: 1,
|
||||
treeSize: 2,
|
||||
proofHashes: new[] { leaf0Hash },
|
||||
expectedRootHash: rootHash);
|
||||
|
||||
Assert.True(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_InvalidLeafHash_Fails()
|
||||
{
|
||||
var leaf0Data = "leaf 0"u8.ToArray();
|
||||
var leaf1Data = "leaf 1"u8.ToArray();
|
||||
var tamperedData = "tampered"u8.ToArray();
|
||||
|
||||
var leaf0Hash = MerkleProofVerifier.HashLeaf(leaf0Data);
|
||||
var leaf1Hash = MerkleProofVerifier.HashLeaf(leaf1Data);
|
||||
var tamperedHash = MerkleProofVerifier.HashLeaf(tamperedData);
|
||||
var rootHash = MerkleProofVerifier.HashInterior(leaf0Hash, leaf1Hash);
|
||||
|
||||
// Try to verify tampered leaf
|
||||
var verified = MerkleProofVerifier.VerifyInclusion(
|
||||
tamperedHash,
|
||||
leafIndex: 0,
|
||||
treeSize: 2,
|
||||
proofHashes: new[] { leaf1Hash },
|
||||
expectedRootHash: rootHash);
|
||||
|
||||
Assert.False(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_WrongRootHash_Fails()
|
||||
{
|
||||
var leaf0Hash = MerkleProofVerifier.HashLeaf("leaf 0"u8.ToArray());
|
||||
var leaf1Hash = MerkleProofVerifier.HashLeaf("leaf 1"u8.ToArray());
|
||||
var wrongRoot = MerkleProofVerifier.HashLeaf("wrong"u8.ToArray());
|
||||
|
||||
var verified = MerkleProofVerifier.VerifyInclusion(
|
||||
leaf0Hash,
|
||||
leafIndex: 0,
|
||||
treeSize: 2,
|
||||
proofHashes: new[] { leaf1Hash },
|
||||
expectedRootHash: wrongRoot);
|
||||
|
||||
Assert.False(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_InvalidIndex_Fails()
|
||||
{
|
||||
var leafHash = MerkleProofVerifier.HashLeaf("test"u8.ToArray());
|
||||
|
||||
// Index out of range
|
||||
var verified = MerkleProofVerifier.VerifyInclusion(
|
||||
leafHash,
|
||||
leafIndex: 10,
|
||||
treeSize: 2,
|
||||
proofHashes: Array.Empty<byte[]>(),
|
||||
expectedRootHash: leafHash);
|
||||
|
||||
Assert.False(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_NegativeIndex_Fails()
|
||||
{
|
||||
var leafHash = MerkleProofVerifier.HashLeaf("test"u8.ToArray());
|
||||
|
||||
var verified = MerkleProofVerifier.VerifyInclusion(
|
||||
leafHash,
|
||||
leafIndex: -1,
|
||||
treeSize: 1,
|
||||
proofHashes: Array.Empty<byte[]>(),
|
||||
expectedRootHash: leafHash);
|
||||
|
||||
Assert.False(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_ZeroTreeSize_Fails()
|
||||
{
|
||||
var leafHash = MerkleProofVerifier.HashLeaf("test"u8.ToArray());
|
||||
|
||||
var verified = MerkleProofVerifier.VerifyInclusion(
|
||||
leafHash,
|
||||
leafIndex: 0,
|
||||
treeSize: 0,
|
||||
proofHashes: Array.Empty<byte[]>(),
|
||||
expectedRootHash: leafHash);
|
||||
|
||||
Assert.False(verified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HexToBytes_ConvertsCorrectly()
|
||||
{
|
||||
var hex = "0102030405";
|
||||
var expected = new byte[] { 1, 2, 3, 4, 5 };
|
||||
|
||||
var result = MerkleProofVerifier.HexToBytes(hex);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HexToBytes_Handles0xPrefix()
|
||||
{
|
||||
var hex = "0x0102030405";
|
||||
var expected = new byte[] { 1, 2, 3, 4, 5 };
|
||||
|
||||
var result = MerkleProofVerifier.HexToBytes(hex);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BytesToHex_ConvertsCorrectly()
|
||||
{
|
||||
var bytes = new byte[] { 0xAB, 0xCD, 0xEF };
|
||||
|
||||
var result = MerkleProofVerifier.BytesToHex(bytes);
|
||||
|
||||
Assert.Equal("abcdef", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeRootFromPath_WithEmptyPath_ReturnsSingleLeaf()
|
||||
{
|
||||
var leafHash = MerkleProofVerifier.HashLeaf("test"u8.ToArray());
|
||||
|
||||
var root = MerkleProofVerifier.ComputeRootFromPath(
|
||||
leafHash,
|
||||
leafIndex: 0,
|
||||
treeSize: 1,
|
||||
proofHashes: Array.Empty<byte[]>());
|
||||
|
||||
Assert.NotNull(root);
|
||||
Assert.Equal(leafHash, root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeRootFromPath_WithEmptyPath_NonSingleTree_ReturnsNull()
|
||||
{
|
||||
var leafHash = MerkleProofVerifier.HashLeaf("test"u8.ToArray());
|
||||
|
||||
var root = MerkleProofVerifier.ComputeRootFromPath(
|
||||
leafHash,
|
||||
leafIndex: 0,
|
||||
treeSize: 5,
|
||||
proofHashes: Array.Empty<byte[]>());
|
||||
|
||||
Assert.Null(root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyInclusion_FourLeafTree_AllPositions()
|
||||
{
|
||||
// Build a 4-leaf tree manually
|
||||
var leaves = new[]
|
||||
{
|
||||
MerkleProofVerifier.HashLeaf("leaf0"u8.ToArray()),
|
||||
MerkleProofVerifier.HashLeaf("leaf1"u8.ToArray()),
|
||||
MerkleProofVerifier.HashLeaf("leaf2"u8.ToArray()),
|
||||
MerkleProofVerifier.HashLeaf("leaf3"u8.ToArray())
|
||||
};
|
||||
|
||||
// root
|
||||
// / \
|
||||
// h01 h23
|
||||
// / \ / \
|
||||
// L0 L1 L2 L3
|
||||
|
||||
var h01 = MerkleProofVerifier.HashInterior(leaves[0], leaves[1]);
|
||||
var h23 = MerkleProofVerifier.HashInterior(leaves[2], leaves[3]);
|
||||
var root = MerkleProofVerifier.HashInterior(h01, h23);
|
||||
|
||||
// Verify leaf 0: sibling = leaf1, parent sibling = h23
|
||||
Assert.True(MerkleProofVerifier.VerifyInclusion(
|
||||
leaves[0], 0, 4, new[] { leaves[1], h23 }, root));
|
||||
|
||||
// Verify leaf 1: sibling = leaf0, parent sibling = h23
|
||||
Assert.True(MerkleProofVerifier.VerifyInclusion(
|
||||
leaves[1], 1, 4, new[] { leaves[0], h23 }, root));
|
||||
|
||||
// Verify leaf 2: sibling = leaf3, parent sibling = h01
|
||||
Assert.True(MerkleProofVerifier.VerifyInclusion(
|
||||
leaves[2], 2, 4, new[] { leaves[3], h01 }, root));
|
||||
|
||||
// Verify leaf 3: sibling = leaf2, parent sibling = h01
|
||||
Assert.True(MerkleProofVerifier.VerifyInclusion(
|
||||
leaves[3], 3, 4, new[] { leaves[2], h01 }, root));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user