Add comprehensive security tests for OWASP A02, A05, A07, and A08 categories
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled

- Implemented tests for Cryptographic Failures (A02) to ensure proper handling of sensitive data, secure algorithms, and key management.
- Added tests for Security Misconfiguration (A05) to validate production configurations, security headers, CORS settings, and feature management.
- Developed tests for Authentication Failures (A07) to enforce strong password policies, rate limiting, session management, and MFA support.
- Created tests for Software and Data Integrity Failures (A08) to verify artifact signatures, SBOM integrity, attestation chains, and feed updates.
This commit is contained in:
master
2025-12-16 16:40:19 +02:00
parent 415eff1207
commit 2170a58734
206 changed files with 30547 additions and 534 deletions

View File

@@ -0,0 +1,154 @@
using StellaOps.Attestor.Core.Verification;
using Xunit;
namespace StellaOps.Attestor.Tests;
/// <summary>
/// Tests for CheckpointSignatureVerifier.
/// SPRINT_3000_0001_0001 - T3: Checkpoint signature verification tests
/// </summary>
public sealed class CheckpointSignatureVerifierTests
{
// Sample checkpoint format (Rekor production format)
private const string ValidCheckpointBody = """
rekor.sigstore.dev - 2605736670972794746
123456789
abc123def456ghi789jkl012mno345pqr678stu901vwx234=
1702345678
""";
private const string InvalidFormatCheckpoint = "not a valid checkpoint";
[Fact]
public void ParseCheckpoint_ValidFormat_ExtractsFields()
{
// Act
var result = CheckpointSignatureVerifier.ParseCheckpoint(ValidCheckpointBody);
// Assert
Assert.NotNull(result.Origin);
Assert.Contains("rekor.sigstore.dev", result.Origin);
Assert.Equal(123456789L, result.TreeSize);
Assert.NotNull(result.RootHash);
}
[Fact]
public void ParseCheckpoint_InvalidFormat_ReturnsFailure()
{
// Act
var result = CheckpointSignatureVerifier.ParseCheckpoint(InvalidFormatCheckpoint);
// Assert
Assert.False(result.Verified);
Assert.Contains("Invalid", result.FailureReason);
}
[Fact]
public void ParseCheckpoint_EmptyString_ReturnsFailure()
{
// Act
var result = CheckpointSignatureVerifier.ParseCheckpoint("");
// Assert
Assert.False(result.Verified);
Assert.NotNull(result.FailureReason);
}
[Fact]
public void ParseCheckpoint_MinimalValidFormat_ExtractsFields()
{
// Arrange - minimal checkpoint without timestamp
var checkpoint = """
origin-name
42
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
""";
// Act
var result = CheckpointSignatureVerifier.ParseCheckpoint(checkpoint);
// Assert
Assert.Equal("origin-name", result.Origin);
Assert.Equal(42L, result.TreeSize);
Assert.NotNull(result.RootHash);
Assert.Equal(32, result.RootHash!.Length); // SHA-256 hash
}
[Fact]
public void ParseCheckpoint_InvalidBase64Root_ReturnsFailure()
{
// Arrange - invalid base64 in root hash
var checkpoint = """
origin-name
42
not-valid-base64!!!
""";
// Act
var result = CheckpointSignatureVerifier.ParseCheckpoint(checkpoint);
// Assert
Assert.False(result.Verified);
Assert.Contains("Invalid root hash", result.FailureReason);
}
[Fact]
public void ParseCheckpoint_InvalidTreeSize_ReturnsFailure()
{
// Arrange - non-numeric tree size
var checkpoint = """
origin-name
not-a-number
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
""";
// Act
var result = CheckpointSignatureVerifier.ParseCheckpoint(checkpoint);
// Assert
Assert.False(result.Verified);
Assert.Contains("Invalid tree size", result.FailureReason);
}
[Fact]
public void VerifyCheckpoint_NullCheckpoint_ThrowsArgumentNull()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
CheckpointSignatureVerifier.VerifyCheckpoint(null!, [], []));
}
[Fact]
public void VerifyCheckpoint_NullSignature_ThrowsArgumentNull()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
CheckpointSignatureVerifier.VerifyCheckpoint("checkpoint", null!, []));
}
[Fact]
public void VerifyCheckpoint_NullPublicKey_ThrowsArgumentNull()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
CheckpointSignatureVerifier.VerifyCheckpoint("checkpoint", [], null!));
}
[Fact]
public void VerifyCheckpoint_InvalidFormat_ReturnsFailure()
{
// Arrange
var signature = new byte[64];
var publicKey = new byte[65]; // P-256 uncompressed
// Act
var result = CheckpointSignatureVerifier.VerifyCheckpoint(
InvalidFormatCheckpoint,
signature,
publicKey);
// Assert
Assert.False(result.Verified);
Assert.Contains("Invalid checkpoint format", result.FailureReason);
}
}

View File

@@ -0,0 +1,318 @@
using System.Text;
using System.Text.Json;
using StellaOps.Attestor.Core.Verification;
using Xunit;
namespace StellaOps.Attestor.Tests;
/// <summary>
/// Integration tests for Rekor inclusion proof verification.
/// SPRINT_3000_0001_0001 - T10: Integration tests with mock Rekor responses
/// </summary>
public sealed class RekorInclusionVerificationIntegrationTests
{
/// <summary>
/// Golden test fixture: a valid inclusion proof from Rekor production.
/// This is a simplified representation of a real Rekor entry.
/// </summary>
private static readonly MockRekorEntry ValidEntry = new()
{
LogIndex = 12345678,
TreeSize = 20000000,
LeafHash = Convert.FromBase64String("n4bQgYhMfWWaL-qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="),
ProofHashes =
[
Convert.FromBase64String("1B2M2Y8AsgTpgAmY7PhCfg=="),
Convert.FromBase64String("47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU="),
Convert.FromBase64String("fRjPxJ7P6CcH_HiMzOZz3rkbwsC4HbTYP8Qe7L9j1Po="),
],
RootHash = Convert.FromBase64String("rMj3G9LfM9C6Xt0qpV3pHbM2q5lPvKjS0mOmV8jXwAk="),
Checkpoint = """
rekor.sigstore.dev - 2605736670972794746
20000000
rMj3G9LfM9C6Xt0qpV3pHbM2q5lPvKjS0mOmV8jXwAk=
1702345678
""",
};
[Fact]
public void VerifyInclusion_SingleLeafTree_Succeeds()
{
// Arrange - single leaf tree (tree size = 1)
var leafHash = new byte[32];
Random.Shared.NextBytes(leafHash);
// Act
var result = MerkleProofVerifier.VerifyInclusion(
leafHash,
leafIndex: 0,
treeSize: 1,
proofHashes: [],
expectedRootHash: leafHash); // Root equals leaf for single node
// Assert
Assert.True(result);
}
[Fact]
public void VerifyInclusion_TwoLeafTree_LeftLeaf_Succeeds()
{
// Arrange - two-leaf tree, verify left leaf
var leftLeaf = new byte[32];
var rightLeaf = new byte[32];
Random.Shared.NextBytes(leftLeaf);
Random.Shared.NextBytes(rightLeaf);
// Compute expected root
var expectedRoot = ComputeInteriorHash(leftLeaf, rightLeaf);
// Act - verify left leaf (index 0)
var result = MerkleProofVerifier.VerifyInclusion(
leftLeaf,
leafIndex: 0,
treeSize: 2,
proofHashes: [rightLeaf],
expectedRootHash: expectedRoot);
// Assert
Assert.True(result);
}
[Fact]
public void VerifyInclusion_TwoLeafTree_RightLeaf_Succeeds()
{
// Arrange - two-leaf tree, verify right leaf
var leftLeaf = new byte[32];
var rightLeaf = new byte[32];
Random.Shared.NextBytes(leftLeaf);
Random.Shared.NextBytes(rightLeaf);
// Compute expected root
var expectedRoot = ComputeInteriorHash(leftLeaf, rightLeaf);
// Act - verify right leaf (index 1)
var result = MerkleProofVerifier.VerifyInclusion(
rightLeaf,
leafIndex: 1,
treeSize: 2,
proofHashes: [leftLeaf],
expectedRootHash: expectedRoot);
// Assert
Assert.True(result);
}
[Fact]
public void VerifyInclusion_FourLeafTree_AllPositions_Succeed()
{
// Arrange - four-leaf balanced tree
var leaves = new byte[4][];
for (int i = 0; i < 4; i++)
{
leaves[i] = new byte[32];
Random.Shared.NextBytes(leaves[i]);
}
// Build tree:
// root
// / \
// h01 h23
// / \ / \
// L0 L1 L2 L3
var h01 = ComputeInteriorHash(leaves[0], leaves[1]);
var h23 = ComputeInteriorHash(leaves[2], leaves[3]);
var root = ComputeInteriorHash(h01, h23);
// Test each leaf position
var testCases = new (int index, byte[][] proof)[]
{
(0, [leaves[1], h23]), // L0: sibling is L1, then h23
(1, [leaves[0], h23]), // L1: sibling is L0, then h23
(2, [leaves[3], h01]), // L2: sibling is L3, then h01
(3, [leaves[2], h01]), // L3: sibling is L2, then h01
};
foreach (var (index, proof) in testCases)
{
// Act
var result = MerkleProofVerifier.VerifyInclusion(
leaves[index],
leafIndex: index,
treeSize: 4,
proofHashes: proof,
expectedRootHash: root);
// Assert
Assert.True(result, $"Verification failed for leaf index {index}");
}
}
[Fact]
public void VerifyInclusion_WrongLeafHash_Fails()
{
// Arrange
var correctLeaf = new byte[32];
var wrongLeaf = new byte[32];
var sibling = new byte[32];
Random.Shared.NextBytes(correctLeaf);
Random.Shared.NextBytes(wrongLeaf);
Random.Shared.NextBytes(sibling);
var root = ComputeInteriorHash(correctLeaf, sibling);
// Act - try to verify with wrong leaf
var result = MerkleProofVerifier.VerifyInclusion(
wrongLeaf,
leafIndex: 0,
treeSize: 2,
proofHashes: [sibling],
expectedRootHash: root);
// Assert
Assert.False(result);
}
[Fact]
public void VerifyInclusion_WrongRootHash_Fails()
{
// Arrange
var leaf = new byte[32];
var sibling = new byte[32];
var wrongRoot = new byte[32];
Random.Shared.NextBytes(leaf);
Random.Shared.NextBytes(sibling);
Random.Shared.NextBytes(wrongRoot);
// Act
var result = MerkleProofVerifier.VerifyInclusion(
leaf,
leafIndex: 0,
treeSize: 2,
proofHashes: [sibling],
expectedRootHash: wrongRoot);
// Assert
Assert.False(result);
}
[Fact]
public void VerifyInclusion_InvalidLeafIndex_Fails()
{
// Arrange
var leaf = new byte[32];
Random.Shared.NextBytes(leaf);
// Act - index >= tree size
var result = MerkleProofVerifier.VerifyInclusion(
leaf,
leafIndex: 5,
treeSize: 4,
proofHashes: [],
expectedRootHash: leaf);
// Assert
Assert.False(result);
}
[Fact]
public void VerifyInclusion_NegativeLeafIndex_Fails()
{
// Arrange
var leaf = new byte[32];
Random.Shared.NextBytes(leaf);
// Act
var result = MerkleProofVerifier.VerifyInclusion(
leaf,
leafIndex: -1,
treeSize: 4,
proofHashes: [],
expectedRootHash: leaf);
// Assert
Assert.False(result);
}
[Fact]
public void VerifyInclusion_ZeroTreeSize_Fails()
{
// Arrange
var leaf = new byte[32];
Random.Shared.NextBytes(leaf);
// Act
var result = MerkleProofVerifier.VerifyInclusion(
leaf,
leafIndex: 0,
treeSize: 0,
proofHashes: [],
expectedRootHash: leaf);
// Assert
Assert.False(result);
}
[Fact]
public void ComputeRootFromPath_EmptyProof_SingleLeaf_ReturnsLeafHash()
{
// Arrange
var leaf = new byte[32];
Random.Shared.NextBytes(leaf);
// Act
var result = MerkleProofVerifier.ComputeRootFromPath(
leaf,
leafIndex: 0,
treeSize: 1,
proofHashes: []);
// Assert
Assert.NotNull(result);
Assert.Equal(leaf, result);
}
[Fact]
public void ComputeRootFromPath_EmptyProof_MultiLeaf_ReturnsNull()
{
// Arrange - empty proof for multi-leaf tree is invalid
var leaf = new byte[32];
Random.Shared.NextBytes(leaf);
// Act
var result = MerkleProofVerifier.ComputeRootFromPath(
leaf,
leafIndex: 0,
treeSize: 4,
proofHashes: []);
// Assert
Assert.Null(result);
}
/// <summary>
/// Computes an interior node hash per RFC 6962.
/// H(0x01 || left || right)
/// </summary>
private static byte[] ComputeInteriorHash(byte[] left, byte[] right)
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
var combined = new byte[1 + left.Length + right.Length];
combined[0] = 0x01; // Interior node prefix
left.CopyTo(combined, 1);
right.CopyTo(combined, 1 + left.Length);
return sha256.ComputeHash(combined);
}
/// <summary>
/// Mock Rekor entry for testing.
/// </summary>
private sealed class MockRekorEntry
{
public long LogIndex { get; init; }
public long TreeSize { get; init; }
public byte[] LeafHash { get; init; } = [];
public byte[][] ProofHashes { get; init; } = [];
public byte[] RootHash { get; init; } = [];
public string Checkpoint { get; init; } = "";
}
}

View File

@@ -0,0 +1,210 @@
using StellaOps.Attestor.Core.Verification;
using Xunit;
namespace StellaOps.Attestor.Tests;
public class TimeSkewValidatorTests
{
private readonly TimeSkewOptions _defaultOptions = new()
{
Enabled = true,
WarnThresholdSeconds = 60,
RejectThresholdSeconds = 300,
MaxFutureSkewSeconds = 60,
FailOnReject = true
};
[Fact]
public void Validate_WhenDisabled_ReturnsSkipped()
{
// Arrange
var options = new TimeSkewOptions { Enabled = false };
var validator = new TimeSkewValidator(options);
var integratedTime = DateTimeOffset.UtcNow.AddSeconds(-10);
// Act
var result = validator.Validate(integratedTime);
// Assert
Assert.True(result.IsValid);
Assert.Equal(TimeSkewStatus.Skipped, result.Status);
Assert.Contains("disabled", result.Message);
}
[Fact]
public void Validate_WhenNoIntegratedTime_ReturnsSkipped()
{
// Arrange
var validator = new TimeSkewValidator(_defaultOptions);
// Act
var result = validator.Validate(integratedTime: null);
// Assert
Assert.True(result.IsValid);
Assert.Equal(TimeSkewStatus.Skipped, result.Status);
Assert.Contains("No integrated time", result.Message);
}
[Theory]
[InlineData(0)] // No skew
[InlineData(5)] // 5 seconds ago
[InlineData(30)] // 30 seconds ago
[InlineData(59)] // Just under warn threshold
public void Validate_WhenSkewBelowWarnThreshold_ReturnsOk(int secondsAgo)
{
// Arrange
var validator = new TimeSkewValidator(_defaultOptions);
var localTime = DateTimeOffset.UtcNow;
var integratedTime = localTime.AddSeconds(-secondsAgo);
// Act
var result = validator.Validate(integratedTime, localTime);
// Assert
Assert.True(result.IsValid);
Assert.Equal(TimeSkewStatus.Ok, result.Status);
Assert.InRange(result.SkewSeconds, secondsAgo - 1, secondsAgo + 1);
}
[Theory]
[InlineData(60)] // At warn threshold
[InlineData(120)] // 2 minutes
[InlineData(299)] // Just under reject threshold
public void Validate_WhenSkewBetweenWarnAndReject_ReturnsWarning(int secondsAgo)
{
// Arrange
var validator = new TimeSkewValidator(_defaultOptions);
var localTime = DateTimeOffset.UtcNow;
var integratedTime = localTime.AddSeconds(-secondsAgo);
// Act
var result = validator.Validate(integratedTime, localTime);
// Assert
Assert.True(result.IsValid); // Warning still passes
Assert.Equal(TimeSkewStatus.Warning, result.Status);
Assert.Contains("warning threshold", result.Message);
}
[Theory]
[InlineData(300)] // At reject threshold
[InlineData(600)] // 10 minutes
[InlineData(3600)] // 1 hour
public void Validate_WhenSkewExceedsRejectThreshold_ReturnsRejected(int secondsAgo)
{
// Arrange
var validator = new TimeSkewValidator(_defaultOptions);
var localTime = DateTimeOffset.UtcNow;
var integratedTime = localTime.AddSeconds(-secondsAgo);
// Act
var result = validator.Validate(integratedTime, localTime);
// Assert
Assert.False(result.IsValid);
Assert.Equal(TimeSkewStatus.Rejected, result.Status);
Assert.Contains("rejection threshold", result.Message);
}
[Theory]
[InlineData(5)] // 5 seconds in future (OK)
[InlineData(30)] // 30 seconds in future (OK)
[InlineData(60)] // At max future threshold (OK)
public void Validate_WhenSmallFutureSkew_ReturnsOk(int secondsInFuture)
{
// Arrange
var validator = new TimeSkewValidator(_defaultOptions);
var localTime = DateTimeOffset.UtcNow;
var integratedTime = localTime.AddSeconds(secondsInFuture);
// Act
var result = validator.Validate(integratedTime, localTime);
// Assert
Assert.True(result.IsValid);
Assert.Equal(TimeSkewStatus.Ok, result.Status);
Assert.True(result.SkewSeconds < 0); // Negative means future
}
[Theory]
[InlineData(61)] // Just over max future
[InlineData(120)] // 2 minutes in future
[InlineData(3600)] // 1 hour in future
public void Validate_WhenLargeFutureSkew_ReturnsFutureTimestamp(int secondsInFuture)
{
// Arrange
var validator = new TimeSkewValidator(_defaultOptions);
var localTime = DateTimeOffset.UtcNow;
var integratedTime = localTime.AddSeconds(secondsInFuture);
// Act
var result = validator.Validate(integratedTime, localTime);
// Assert
Assert.False(result.IsValid);
Assert.Equal(TimeSkewStatus.FutureTimestamp, result.Status);
Assert.Contains("Future timestamp", result.Message);
}
[Fact]
public void Validate_UsesCurrentTimeWhenLocalTimeNotProvided()
{
// Arrange
var validator = new TimeSkewValidator(_defaultOptions);
var integratedTime = DateTimeOffset.UtcNow.AddSeconds(-10);
// Act
var result = validator.Validate(integratedTime);
// Assert
Assert.True(result.IsValid);
Assert.InRange(result.SkewSeconds, 9, 12); // Allow for test execution time
}
[Fact]
public void Validate_CustomThresholds_AreRespected()
{
// Arrange
var options = new TimeSkewOptions
{
Enabled = true,
WarnThresholdSeconds = 10,
RejectThresholdSeconds = 30,
MaxFutureSkewSeconds = 5
};
var validator = new TimeSkewValidator(options);
var localTime = DateTimeOffset.UtcNow;
// Act - 15 seconds should warn with custom thresholds
var result = validator.Validate(localTime.AddSeconds(-15), localTime);
// Assert
Assert.True(result.IsValid);
Assert.Equal(TimeSkewStatus.Warning, result.Status);
}
[Fact]
public void Validate_ReturnsCorrectTimestamps()
{
// Arrange
var validator = new TimeSkewValidator(_defaultOptions);
var localTime = new DateTimeOffset(2025, 12, 16, 12, 0, 0, TimeSpan.Zero);
var integratedTime = new DateTimeOffset(2025, 12, 16, 11, 59, 30, TimeSpan.Zero);
// Act
var result = validator.Validate(integratedTime, localTime);
// Assert
Assert.Equal(integratedTime, result.IntegratedTime);
Assert.Equal(localTime, result.LocalTime);
Assert.Equal(30, result.SkewSeconds, precision: 0);
}
[Fact]
public void Constructor_ThrowsOnNullOptions()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new TimeSkewValidator(null!));
}
}