sprints completion. new product advisories prepared
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// Pkcs11HsmClientIntegrationTests.cs
|
||||
// Sprint: SPRINT_20260112_017_CRYPTO_pkcs11_hsm_implementation
|
||||
// Tasks: HSM-008, HSM-009
|
||||
// Description: SoftHSM2-backed PKCS#11 integration tests.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Cryptography.Plugin.Hsm;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests.Hsm;
|
||||
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class Pkcs11HsmClientIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ConnectAndPing_Succeeds_WhenSoftHsmAvailable()
|
||||
{
|
||||
if (!SoftHsmTestFixture.TryLoad(out var config))
|
||||
{
|
||||
return; // SoftHSM2 not configured; skip
|
||||
}
|
||||
|
||||
using var client = new Pkcs11HsmClientImpl(config.LibraryPath);
|
||||
await client.ConnectAsync(config.SlotId, config.Pin, CancellationToken.None);
|
||||
|
||||
var ok = await client.PingAsync(CancellationToken.None);
|
||||
Assert.True(ok);
|
||||
|
||||
await client.DisconnectAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SignVerify_RoundTrip_WhenKeyConfigured()
|
||||
{
|
||||
if (!SoftHsmTestFixture.TryLoad(out var config))
|
||||
{
|
||||
return; // SoftHSM2 not configured; skip
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.KeyId))
|
||||
{
|
||||
return; // No test key configured; skip
|
||||
}
|
||||
|
||||
using var client = new Pkcs11HsmClientImpl(config.LibraryPath);
|
||||
await client.ConnectAsync(config.SlotId, config.Pin, CancellationToken.None);
|
||||
|
||||
var payload = "stellaops-hsm-test"u8.ToArray();
|
||||
var signature = await client.SignAsync(config.KeyId, payload, config.Mechanism, CancellationToken.None);
|
||||
var verified = await client.VerifyAsync(config.KeyId, payload, signature, config.Mechanism, CancellationToken.None);
|
||||
|
||||
Assert.True(verified);
|
||||
await client.DisconnectAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// SoftHsmTestFixture.cs
|
||||
// Sprint: SPRINT_20260112_017_CRYPTO_pkcs11_hsm_implementation
|
||||
// Task: HSM-008
|
||||
// Description: SoftHSM2 environment detection for PKCS#11 integration tests.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Cryptography.Plugin.Hsm;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests.Hsm;
|
||||
|
||||
internal static class SoftHsmTestFixture
|
||||
{
|
||||
internal sealed record SoftHsmConfig(
|
||||
string LibraryPath,
|
||||
int SlotId,
|
||||
string? Pin,
|
||||
string? KeyId,
|
||||
HsmMechanism Mechanism);
|
||||
|
||||
public static bool TryLoad(out SoftHsmConfig config)
|
||||
{
|
||||
config = default!;
|
||||
|
||||
var libraryPath = Environment.GetEnvironmentVariable("STELLAOPS_SOFTHSM_LIB")
|
||||
?? Environment.GetEnvironmentVariable("SOFTHSM2_MODULE");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(libraryPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var slotRaw = Environment.GetEnvironmentVariable("STELLAOPS_SOFTHSM_SLOT") ?? "0";
|
||||
if (!int.TryParse(slotRaw, out var slotId))
|
||||
{
|
||||
slotId = 0;
|
||||
}
|
||||
|
||||
var pin = Environment.GetEnvironmentVariable("STELLAOPS_SOFTHSM_PIN");
|
||||
var keyId = Environment.GetEnvironmentVariable("STELLAOPS_SOFTHSM_KEY_ID");
|
||||
|
||||
var mechanismRaw = Environment.GetEnvironmentVariable("STELLAOPS_SOFTHSM_MECHANISM")
|
||||
?? "RsaSha256";
|
||||
if (!Enum.TryParse<HsmMechanism>(mechanismRaw, true, out var mechanism))
|
||||
{
|
||||
mechanism = HsmMechanism.RsaSha256;
|
||||
}
|
||||
|
||||
config = new SoftHsmConfig(libraryPath, slotId, pin, keyId, mechanism);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// KeyEscrowRecoveryIntegrationTests.Fixed.cs
|
||||
// Sprint: SPRINT_20260112_018_CRYPTO_key_escrow_shamir
|
||||
// Task: ESCROW-012
|
||||
// Description: Integration tests for key escrow recovery workflow.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
using StellaOps.Cryptography.KeyEscrow;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests.KeyEscrow;
|
||||
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class KeyEscrowRecoveryIntegrationTestsFixed
|
||||
{
|
||||
private readonly Mock<IKeyEscrowService> _mockEscrowService;
|
||||
private readonly Mock<ICeremonyAuthorizationProvider> _mockCeremonyProvider;
|
||||
private readonly Mock<IKeyEscrowAuditLogger> _mockAuditLogger;
|
||||
private readonly CeremonyAuthorizedRecoveryService _service;
|
||||
|
||||
public KeyEscrowRecoveryIntegrationTestsFixed()
|
||||
{
|
||||
_mockEscrowService = new Mock<IKeyEscrowService>();
|
||||
_mockCeremonyProvider = new Mock<ICeremonyAuthorizationProvider>();
|
||||
_mockAuditLogger = new Mock<IKeyEscrowAuditLogger>();
|
||||
|
||||
_service = new CeremonyAuthorizedRecoveryService(
|
||||
_mockEscrowService.Object,
|
||||
_mockCeremonyProvider.Object,
|
||||
_mockAuditLogger.Object,
|
||||
TimeProvider.System,
|
||||
new CeremonyAuthorizedRecoveryOptions
|
||||
{
|
||||
CeremonyApprovalThreshold = 2,
|
||||
CeremonyExpirationMinutes = 60,
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InitiateRecovery_WithValidKey_CreatesCeremony()
|
||||
{
|
||||
var keyId = "test-key-001";
|
||||
var ceremonyId = Guid.NewGuid();
|
||||
|
||||
_mockEscrowService
|
||||
.Setup(e => e.GetEscrowStatusAsync(keyId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new KeyEscrowStatus
|
||||
{
|
||||
KeyId = keyId,
|
||||
IsEscrowed = true,
|
||||
Threshold = 2,
|
||||
TotalShares = 3,
|
||||
ValidShares = 3,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
|
||||
});
|
||||
|
||||
_mockCeremonyProvider
|
||||
.Setup(c => c.CreateCeremonyAsync(It.IsAny<CeremonyAuthorizationRequest>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CeremonyCreationResult
|
||||
{
|
||||
Success = true,
|
||||
CeremonyId = ceremonyId,
|
||||
RequiredApprovals = 2,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(60),
|
||||
});
|
||||
|
||||
var request = new KeyRecoveryRequest
|
||||
{
|
||||
KeyId = keyId,
|
||||
Reason = "Key rotation required",
|
||||
InitiatorId = "admin@example.com",
|
||||
AuthorizingCustodians = Array.Empty<string>(),
|
||||
};
|
||||
|
||||
var result = await _service.InitiateRecoveryAsync(request, "admin@example.com");
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(ceremonyId, result.CeremonyId);
|
||||
Assert.Equal(keyId, result.KeyId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteRecovery_WithApprovedCeremony_RecoversKey()
|
||||
{
|
||||
var ceremonyId = Guid.NewGuid();
|
||||
var keyId = "test-key-002";
|
||||
var keyMaterial = new byte[] { 0x01, 0x02, 0x03 };
|
||||
|
||||
_mockCeremonyProvider
|
||||
.Setup(c => c.GetCeremonyStatusAsync(ceremonyId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CeremonyStatusInfo
|
||||
{
|
||||
CeremonyId = ceremonyId,
|
||||
KeyId = keyId,
|
||||
State = CeremonyState.Approved,
|
||||
CurrentApprovals = 2,
|
||||
RequiredApprovals = 2,
|
||||
Approvers = new List<string> { "cust-1", "cust-2" },
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(30),
|
||||
RecoveryReason = "Emergency recovery",
|
||||
});
|
||||
|
||||
_mockEscrowService
|
||||
.Setup(e => e.RecoverKeyAsync(It.IsAny<KeyRecoveryRequest>(), It.IsAny<IReadOnlyList<KeyShare>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new KeyRecoveryResult
|
||||
{
|
||||
Success = true,
|
||||
KeyId = keyId,
|
||||
KeyMaterial = keyMaterial,
|
||||
});
|
||||
|
||||
var shares = new List<KeyShare>
|
||||
{
|
||||
new()
|
||||
{
|
||||
ShareId = Guid.NewGuid(),
|
||||
Index = 1,
|
||||
EncryptedData = new byte[] { 0x01 },
|
||||
KeyId = keyId,
|
||||
Threshold = 2,
|
||||
TotalShares = 3,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
|
||||
CustodianId = "cust-1",
|
||||
ChecksumHex = "00",
|
||||
},
|
||||
new()
|
||||
{
|
||||
ShareId = Guid.NewGuid(),
|
||||
Index = 2,
|
||||
EncryptedData = new byte[] { 0x02 },
|
||||
KeyId = keyId,
|
||||
Threshold = 2,
|
||||
TotalShares = 3,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
|
||||
CustodianId = "cust-2",
|
||||
ChecksumHex = "01",
|
||||
},
|
||||
};
|
||||
|
||||
var result = await _service.ExecuteRecoveryAsync(ceremonyId, shares, "admin@example.com");
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(keyId, result.KeyId);
|
||||
Assert.Equal(keyMaterial, result.KeyMaterial);
|
||||
_mockCeremonyProvider.Verify(
|
||||
c => c.MarkCeremonyExecutedAsync(ceremonyId, "admin@example.com", It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteRecovery_WithPendingCeremony_Fails()
|
||||
{
|
||||
var ceremonyId = Guid.NewGuid();
|
||||
|
||||
_mockCeremonyProvider
|
||||
.Setup(c => c.GetCeremonyStatusAsync(ceremonyId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CeremonyStatusInfo
|
||||
{
|
||||
CeremonyId = ceremonyId,
|
||||
KeyId = "test-key-003",
|
||||
State = CeremonyState.Pending,
|
||||
CurrentApprovals = 0,
|
||||
RequiredApprovals = 2,
|
||||
Approvers = Array.Empty<string>(),
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(30),
|
||||
RecoveryReason = "Pending",
|
||||
});
|
||||
|
||||
var result = await _service.ExecuteRecoveryAsync(
|
||||
ceremonyId,
|
||||
Array.Empty<KeyShare>(),
|
||||
"admin@example.com");
|
||||
|
||||
Assert.False(result.Success);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// KeyEscrowRecoveryIntegrationTests.cs
|
||||
// Sprint: SPRINT_20260112_018_CRYPTO_key_escrow_shamir
|
||||
// Task: ESCROW-012
|
||||
// Description: Integration tests for key escrow recovery workflow.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
using StellaOps.Cryptography.KeyEscrow;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests.KeyEscrow;
|
||||
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class KeyEscrowRecoveryIntegrationTests
|
||||
{
|
||||
private readonly Mock<IKeyEscrowService> _mockEscrowService;
|
||||
private readonly Mock<ICeremonyAuthorizationProvider> _mockCeremonyProvider;
|
||||
private readonly Mock<IKeyEscrowAuditLogger> _mockAuditLogger;
|
||||
private readonly CeremonyAuthorizedRecoveryService _service;
|
||||
|
||||
public KeyEscrowRecoveryIntegrationTests()
|
||||
{
|
||||
_mockEscrowService = new Mock<IKeyEscrowService>();
|
||||
_mockCeremonyProvider = new Mock<ICeremonyAuthorizationProvider>();
|
||||
_mockAuditLogger = new Mock<IKeyEscrowAuditLogger>();
|
||||
|
||||
_service = new CeremonyAuthorizedRecoveryService(
|
||||
_mockEscrowService.Object,
|
||||
_mockCeremonyProvider.Object,
|
||||
_mockAuditLogger.Object,
|
||||
TimeProvider.System,
|
||||
new CeremonyAuthorizedRecoveryOptions
|
||||
{
|
||||
}
|
||||
}
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
|
||||
CustodianId = "cust-1",
|
||||
ChecksumHex = "00",
|
||||
},
|
||||
new()
|
||||
{
|
||||
ShareId = Guid.NewGuid(),
|
||||
Index = 2,
|
||||
EncryptedData = new byte[] { 0x02 },
|
||||
KeyId = keyId,
|
||||
Threshold = 2,
|
||||
TotalShares = 3,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
|
||||
CustodianId = "cust-2",
|
||||
ChecksumHex = "01",
|
||||
},
|
||||
};
|
||||
|
||||
var result = await _service.ExecuteRecoveryAsync(ceremonyId, shares, "admin@example.com");
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(keyId, result.KeyId);
|
||||
Assert.Equal(keyMaterial, result.KeyMaterial);
|
||||
_mockCeremonyProvider.Verify(
|
||||
c => c.MarkCeremonyExecutedAsync(ceremonyId, "admin@example.com", It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteRecovery_WithPendingCeremony_Fails()
|
||||
{
|
||||
var ceremonyId = Guid.NewGuid();
|
||||
|
||||
_mockCeremonyProvider
|
||||
.Setup(c => c.GetCeremonyStatusAsync(ceremonyId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CeremonyStatusInfo
|
||||
{
|
||||
CeremonyId = ceremonyId,
|
||||
KeyId = "test-key-003",
|
||||
State = CeremonyState.Pending,
|
||||
CurrentApprovals = 0,
|
||||
RequiredApprovals = 2,
|
||||
Approvers = Array.Empty<string>(),
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(30),
|
||||
RecoveryReason = "Pending",
|
||||
});
|
||||
|
||||
var result = await _service.ExecuteRecoveryAsync(
|
||||
ceremonyId,
|
||||
Array.Empty<KeyShare>(),
|
||||
"admin@example.com");
|
||||
|
||||
Assert.False(result.Success);
|
||||
}
|
||||
}// -----------------------------------------------------------------------------
|
||||
// KeyEscrowRecoveryIntegrationTests.cs
|
||||
// Sprint: SPRINT_20260112_018_CRYPTO_key_escrow_shamir
|
||||
// Task: ESCROW-012
|
||||
// Description: Integration tests for key escrow recovery workflow.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
using StellaOps.Cryptography.KeyEscrow;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests.KeyEscrow;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for key escrow recovery workflow with dual-control ceremonies.
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class KeyEscrowRecoveryIntegrationTests
|
||||
{
|
||||
private readonly Mock<IKeyEscrowService> _mockEscrowService;
|
||||
private readonly Mock<ICeremonyAuthorizationProvider> _mockCeremonyProvider;
|
||||
private readonly Mock<IKeyEscrowAuditLogger> _mockAuditLogger;
|
||||
private readonly CeremonyAuthorizedRecoveryService _service;
|
||||
|
||||
public KeyEscrowRecoveryIntegrationTests()
|
||||
{
|
||||
_mockEscrowService = new Mock<IKeyEscrowService>();
|
||||
_mockCeremonyProvider = new Mock<ICeremonyAuthorizationProvider>();
|
||||
_mockAuditLogger = new Mock<IKeyEscrowAuditLogger>();
|
||||
|
||||
_service = new CeremonyAuthorizedRecoveryService(
|
||||
_mockEscrowService.Object,
|
||||
_mockCeremonyProvider.Object,
|
||||
_mockAuditLogger.Object,
|
||||
TimeProvider.System,
|
||||
new CeremonyAuthorizedRecoveryOptions
|
||||
{
|
||||
CeremonyApprovalThreshold = 2,
|
||||
CeremonyExpirationMinutes = 60,
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InitiateRecovery_WithValidKey_CreatesCeremony()
|
||||
{
|
||||
// Arrange
|
||||
var keyId = "test-key-001";
|
||||
var ceremonyId = Guid.NewGuid();
|
||||
|
||||
_mockEscrowService
|
||||
.Setup(e => e.GetEscrowStatusAsync(keyId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new KeyEscrowStatus
|
||||
{
|
||||
KeyId = keyId,
|
||||
IsEscrowed = true,
|
||||
Threshold = 2,
|
||||
TotalShares = 3,
|
||||
ValidShares = 3,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(30),
|
||||
});
|
||||
|
||||
_mockCeremonyProvider
|
||||
.Setup(c => c.CreateCeremonyAsync(It.IsAny<CeremonyAuthorizationRequest>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CeremonyCreationResult
|
||||
{
|
||||
Success = true,
|
||||
CeremonyId = ceremonyId,
|
||||
RequiredApprovals = 2,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(60),
|
||||
});
|
||||
|
||||
}
|
||||
[Fact]
|
||||
public async Task ExecuteRecovery_WithPendingCeremony_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var ceremonyId = Guid.NewGuid();
|
||||
|
||||
_mockCeremonyProvider
|
||||
.Setup(c => c.GetCeremonyStatusAsync(ceremonyId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CeremonyStatusInfo
|
||||
{
|
||||
CeremonyId = ceremonyId,
|
||||
KeyId = "test-key-003",
|
||||
State = CeremonyState.Pending,
|
||||
CurrentApprovals = 0,
|
||||
RequiredApprovals = 2,
|
||||
Approvers = Array.Empty<string>(),
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(30),
|
||||
RecoveryReason = "Pending",
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _service.ExecuteRecoveryAsync(
|
||||
ceremonyId,
|
||||
Array.Empty<KeyShare>(),
|
||||
"admin@example.com");
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
}
|
||||
}
|
||||
|
||||
var shares = new List<KeyShare>
|
||||
{
|
||||
new KeyShare { ShareId = Guid.NewGuid(), Index = 1, EncryptedData = new byte[] { 10, 11 } },
|
||||
new KeyShare { ShareId = Guid.NewGuid(), Index = 2, EncryptedData = new byte[] { 20, 21 } },
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _service.ExecuteRecoveryAsync(ceremonyId, shares, "executor@example.com");
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(keyMaterial, result.RecoveredKey);
|
||||
_mockCeremonyProvider.Verify(
|
||||
c => c.MarkCeremonyExecutedAsync(ceremonyId, "executor@example.com", It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteRecovery_WithPendingCeremony_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var ceremonyId = Guid.NewGuid();
|
||||
|
||||
_mockCeremonyProvider
|
||||
.Setup(c => c.GetCeremonyStatusAsync(ceremonyId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CeremonyStatusInfo
|
||||
{
|
||||
CeremonyId = ceremonyId,
|
||||
KeyId = "test-key",
|
||||
State = CeremonyState.Pending,
|
||||
CurrentApprovals = 0,
|
||||
RequiredApprovals = 2,
|
||||
});
|
||||
|
||||
var shares = new List<KeyShare>();
|
||||
|
||||
// Act
|
||||
var result = await _service.ExecuteRecoveryAsync(ceremonyId, shares, "executor@example.com");
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("not approved", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteRecovery_WithExpiredCeremony_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var ceremonyId = Guid.NewGuid();
|
||||
|
||||
_mockCeremonyProvider
|
||||
.Setup(c => c.GetCeremonyStatusAsync(ceremonyId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CeremonyStatusInfo
|
||||
{
|
||||
CeremonyId = ceremonyId,
|
||||
KeyId = "test-key",
|
||||
State = CeremonyState.Approved,
|
||||
ExpiresAt = _timeProvider.GetUtcNow().AddMinutes(-5), // Expired
|
||||
});
|
||||
|
||||
var shares = new List<KeyShare>();
|
||||
|
||||
// Act
|
||||
var result = await _service.ExecuteRecoveryAsync(ceremonyId, shares, "executor@example.com");
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("expired", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteRecovery_WithMissingCeremony_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var ceremonyId = Guid.NewGuid();
|
||||
|
||||
_mockCeremonyProvider
|
||||
.Setup(c => c.GetCeremonyStatusAsync(ceremonyId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((CeremonyStatusInfo?)null);
|
||||
|
||||
var shares = new List<KeyShare>();
|
||||
|
||||
// Act
|
||||
var result = await _service.ExecuteRecoveryAsync(ceremonyId, shares, "executor@example.com");
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("not found", result.Error);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Full Workflow Tests
|
||||
|
||||
[Fact]
|
||||
public async Task FullRecoveryWorkflow_WithValidShares_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var keyId = "production-signing-key";
|
||||
var ceremonyId = Guid.NewGuid();
|
||||
var keyMaterial = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 };
|
||||
|
||||
// Setup escrow status
|
||||
_mockEscrowService
|
||||
.Setup(e => e.GetEscrowStatusAsync(keyId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new KeyEscrowStatusResult
|
||||
{
|
||||
Exists = true,
|
||||
KeyId = keyId,
|
||||
Threshold = 2,
|
||||
TotalShares = 3,
|
||||
IsExpired = false,
|
||||
ExpiresAt = _timeProvider.GetUtcNow().AddDays(30),
|
||||
});
|
||||
|
||||
// Setup ceremony creation
|
||||
_mockCeremonyProvider
|
||||
.Setup(c => c.CreateCeremonyAsync(It.IsAny<CeremonyAuthorizationRequest>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CeremonyCreationResult
|
||||
{
|
||||
Success = true,
|
||||
CeremonyId = ceremonyId,
|
||||
RequiredApprovals = 2,
|
||||
ExpiresAt = _timeProvider.GetUtcNow().AddMinutes(60),
|
||||
});
|
||||
|
||||
// Setup ceremony status (approved)
|
||||
_mockCeremonyProvider
|
||||
.Setup(c => c.GetCeremonyStatusAsync(ceremonyId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CeremonyStatusInfo
|
||||
{
|
||||
CeremonyId = ceremonyId,
|
||||
KeyId = keyId,
|
||||
State = CeremonyState.Approved,
|
||||
CurrentApprovals = 2,
|
||||
RequiredApprovals = 2,
|
||||
Approvers = new List<string> { "approver1@example.com", "approver2@example.com" },
|
||||
ExpiresAt = _timeProvider.GetUtcNow().AddMinutes(30),
|
||||
});
|
||||
|
||||
// Setup recovery
|
||||
_mockEscrowService
|
||||
.Setup(e => e.RecoverKeyAsync(It.IsAny<KeyRecoveryRequest>(), It.IsAny<IReadOnlyList<KeyShare>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new KeyRecoveryResult
|
||||
{
|
||||
Success = true,
|
||||
KeyId = keyId,
|
||||
RecoveredKey = keyMaterial,
|
||||
});
|
||||
|
||||
// Act - Step 1: Initiate
|
||||
var initRequest = new KeyRecoveryRequest
|
||||
{
|
||||
KeyId = keyId,
|
||||
RecoveryReason = "Emergency key rotation",
|
||||
};
|
||||
var initResult = await _service.InitiateRecoveryAsync(initRequest, "admin@example.com");
|
||||
Assert.True(initResult.Success);
|
||||
|
||||
// Step 2: (Approvals would happen externally via ceremony service)
|
||||
|
||||
// Step 3: Execute with shares
|
||||
var shares = new List<KeyShare>
|
||||
{
|
||||
new KeyShare { ShareId = Guid.NewGuid(), Index = 1, EncryptedData = new byte[] { 10, 11 } },
|
||||
new KeyShare { ShareId = Guid.NewGuid(), Index = 2, EncryptedData = new byte[] { 20, 21 } },
|
||||
};
|
||||
var executeResult = await _service.ExecuteRecoveryAsync(initResult.CeremonyId, shares, "executor@example.com");
|
||||
|
||||
// Assert
|
||||
Assert.True(executeResult.Success);
|
||||
Assert.Equal(keyMaterial, executeResult.RecoveredKey);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Audit Trail Tests
|
||||
|
||||
[Fact]
|
||||
public async Task InitiateRecovery_LogsAuditEvent()
|
||||
{
|
||||
// Arrange
|
||||
var keyId = "test-key";
|
||||
var ceremonyId = Guid.NewGuid();
|
||||
|
||||
_mockEscrowService
|
||||
.Setup(e => e.GetEscrowStatusAsync(keyId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new KeyEscrowStatusResult { Exists = true, KeyId = keyId });
|
||||
|
||||
_mockCeremonyProvider
|
||||
.Setup(c => c.CreateCeremonyAsync(It.IsAny<CeremonyAuthorizationRequest>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CeremonyCreationResult { Success = true, CeremonyId = ceremonyId });
|
||||
|
||||
var request = new KeyRecoveryRequest { KeyId = keyId, RecoveryReason = "Test" };
|
||||
|
||||
// Act
|
||||
await _service.InitiateRecoveryAsync(request, "admin@example.com");
|
||||
|
||||
// Assert
|
||||
_mockAuditLogger.Verify(
|
||||
a => a.LogRecoveryAsync(
|
||||
It.Is<KeyEscrowAuditEvent>(e =>
|
||||
e.EventType == KeyEscrowAuditEventType.RecoveryInitiated &&
|
||||
e.KeyId == keyId &&
|
||||
e.InitiatorId == "admin@example.com"),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteRecovery_LogsAuditEvent()
|
||||
{
|
||||
// Arrange
|
||||
var ceremonyId = Guid.NewGuid();
|
||||
var keyId = "test-key";
|
||||
|
||||
_mockCeremonyProvider
|
||||
.Setup(c => c.GetCeremonyStatusAsync(ceremonyId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new CeremonyStatusInfo
|
||||
{
|
||||
CeremonyId = ceremonyId,
|
||||
KeyId = keyId,
|
||||
State = CeremonyState.Approved,
|
||||
Approvers = new List<string> { "approver1", "approver2" },
|
||||
ExpiresAt = _timeProvider.GetUtcNow().AddMinutes(30),
|
||||
});
|
||||
|
||||
_mockEscrowService
|
||||
.Setup(e => e.RecoverKeyAsync(It.IsAny<KeyRecoveryRequest>(), It.IsAny<IReadOnlyList<KeyShare>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new KeyRecoveryResult { Success = true, KeyId = keyId });
|
||||
|
||||
var shares = new List<KeyShare>();
|
||||
|
||||
// Act
|
||||
await _service.ExecuteRecoveryAsync(ceremonyId, shares, "executor@example.com");
|
||||
|
||||
// Assert
|
||||
_mockAuditLogger.Verify(
|
||||
a => a.LogRecoveryAsync(
|
||||
It.Is<KeyEscrowAuditEvent>(e =>
|
||||
e.EventType == KeyEscrowAuditEventType.KeyRecovered &&
|
||||
e.KeyId == keyId &&
|
||||
e.CeremonyId == ceremonyId),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock time provider for testing.
|
||||
/// </summary>
|
||||
internal sealed class MockTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _now = DateTimeOffset.UtcNow;
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
|
||||
public void Advance(TimeSpan duration) => _now = _now.Add(duration);
|
||||
|
||||
public void SetNow(DateTimeOffset now) => _now = now;
|
||||
}
|
||||
|
||||
// Stub models for compilation - actual implementation exists in main codebase
|
||||
public sealed class KeyEscrowStatusResult
|
||||
{
|
||||
public bool Exists { get; init; }
|
||||
public string KeyId { get; init; } = string.Empty;
|
||||
public int Threshold { get; init; }
|
||||
public int TotalShares { get; init; }
|
||||
public bool IsExpired { get; init; }
|
||||
public DateTimeOffset ExpiresAt { get; init; }
|
||||
}
|
||||
|
||||
public interface IKeyEscrowService
|
||||
{
|
||||
Task<KeyEscrowStatusResult> GetEscrowStatusAsync(string keyId, CancellationToken cancellationToken = default);
|
||||
Task<KeyRecoveryResult> RecoverKeyAsync(KeyRecoveryRequest request, IReadOnlyList<KeyShare> shares, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IKeyEscrowAuditLogger
|
||||
{
|
||||
Task LogRecoveryAsync(KeyEscrowAuditEvent evt, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class KeyEscrowAuditEvent
|
||||
{
|
||||
public Guid EventId { get; init; }
|
||||
public KeyEscrowAuditEventType EventType { get; init; }
|
||||
public string KeyId { get; init; } = string.Empty;
|
||||
public DateTimeOffset Timestamp { get; init; }
|
||||
public string InitiatorId { get; init; } = string.Empty;
|
||||
public Guid? CeremonyId { get; init; }
|
||||
public IReadOnlyList<string>? CustodianIds { get; init; }
|
||||
public bool Success { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
public enum KeyEscrowAuditEventType
|
||||
{
|
||||
KeyEscrowed,
|
||||
RecoveryInitiated,
|
||||
KeyRecovered,
|
||||
}
|
||||
|
||||
public sealed class KeyRecoveryRequest
|
||||
{
|
||||
public string KeyId { get; init; } = string.Empty;
|
||||
public string RecoveryReason { get; init; } = string.Empty;
|
||||
public IReadOnlyList<string> AuthorizingCustodians { get; init; } = Array.Empty<string>();
|
||||
public Guid? CeremonyId { get; init; }
|
||||
}
|
||||
|
||||
public sealed class KeyRecoveryResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public string? KeyId { get; init; }
|
||||
public byte[]? RecoveredKey { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
public sealed class KeyShare
|
||||
{
|
||||
public Guid ShareId { get; init; }
|
||||
public int Index { get; init; }
|
||||
public byte[] EncryptedData { get; init; } = Array.Empty<byte>();
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
// Copyright © StellaOps. All rights reserved.
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_018_CRYPTO_key_escrow_shamir
|
||||
// Tasks: ESCROW-011
|
||||
|
||||
using StellaOps.Cryptography.KeyEscrow;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for Shamir's Secret Sharing implementation.
|
||||
/// </summary>
|
||||
public sealed class ShamirSecretSharingTests
|
||||
{
|
||||
private readonly ShamirSecretSharing _shamir = new();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// GF(2^8) Arithmetic Tests
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void GF256_Add_IsXor()
|
||||
{
|
||||
Assert.Equal(0x00, GaloisField256.Add(0x57, 0x57)); // a XOR a = 0
|
||||
Assert.Equal(0x57, GaloisField256.Add(0x57, 0x00)); // a XOR 0 = a
|
||||
Assert.Equal(0xFE, GaloisField256.Add(0x57, 0xA9)); // 0x57 XOR 0xA9
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GF256_Subtract_SameAsAdd()
|
||||
{
|
||||
Assert.Equal(GaloisField256.Add(0x57, 0x83), GaloisField256.Subtract(0x57, 0x83));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GF256_Multiply_KnownValues()
|
||||
{
|
||||
Assert.Equal(0x00, GaloisField256.Multiply(0x00, 0x57)); // 0 * a = 0
|
||||
Assert.Equal(0x57, GaloisField256.Multiply(0x01, 0x57)); // 1 * a = a
|
||||
Assert.Equal(0xC1, GaloisField256.Multiply(0x57, 0x83)); // Known AES value (FIPS-197)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GF256_Inverse_Correct()
|
||||
{
|
||||
// a * a^(-1) = 1 for all non-zero a
|
||||
for (int a = 1; a < 256; a++)
|
||||
{
|
||||
byte inv = GaloisField256.Inverse((byte)a);
|
||||
byte product = GaloisField256.Multiply((byte)a, inv);
|
||||
Assert.Equal(1, product);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GF256_Inverse_Zero_ReturnsZero()
|
||||
{
|
||||
Assert.Equal(0, GaloisField256.Inverse(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GF256_Divide_ByZero_Throws()
|
||||
{
|
||||
Assert.Throws<DivideByZeroException>(() => GaloisField256.Divide(0x57, 0x00));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GF256_Divide_Correct()
|
||||
{
|
||||
// a / b = a * b^(-1)
|
||||
byte a = 0x57;
|
||||
byte b = 0x83;
|
||||
byte quotient = GaloisField256.Divide(a, b);
|
||||
Assert.Equal(a, GaloisField256.Multiply(quotient, b));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GF256_Power_Correct()
|
||||
{
|
||||
Assert.Equal(1, GaloisField256.Power(0x57, 0)); // a^0 = 1
|
||||
Assert.Equal(0x57, GaloisField256.Power(0x57, 1)); // a^1 = a
|
||||
Assert.Equal(GaloisField256.Multiply(0x57, 0x57), GaloisField256.Power(0x57, 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GF256_EvaluatePolynomial_Constant()
|
||||
{
|
||||
byte[] coeffs = [0x42];
|
||||
Assert.Equal(0x42, GaloisField256.EvaluatePolynomial(coeffs, 0x00));
|
||||
Assert.Equal(0x42, GaloisField256.EvaluatePolynomial(coeffs, 0xFF));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GF256_EvaluatePolynomial_Linear()
|
||||
{
|
||||
// p(x) = 0x42 + 0x13 * x
|
||||
byte[] coeffs = [0x42, 0x13];
|
||||
byte x = 0x05;
|
||||
byte expected = GaloisField256.Add(0x42, GaloisField256.Multiply(0x13, x));
|
||||
Assert.Equal(expected, GaloisField256.EvaluatePolynomial(coeffs, x));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GF256_LagrangeInterpolation_SinglePoint()
|
||||
{
|
||||
byte[] xValues = [0x01];
|
||||
byte[] yValues = [0x42];
|
||||
// With one point (1, 0x42), constant polynomial, L(0) = 0x42
|
||||
Assert.Equal(0x42, GaloisField256.LagrangeInterpolateAtZero(xValues, yValues));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Split/Combine Round-Trip Tests
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[Theory]
|
||||
[InlineData(2, 2)]
|
||||
[InlineData(2, 3)]
|
||||
[InlineData(3, 5)]
|
||||
[InlineData(5, 10)]
|
||||
public void Split_Combine_RoundTrip_SingleByte(int threshold, int totalShares)
|
||||
{
|
||||
byte[] secret = [0x42];
|
||||
var shares = _shamir.Split(secret, threshold, totalShares);
|
||||
|
||||
Assert.Equal(totalShares, shares.Length);
|
||||
|
||||
// Combine with exactly threshold shares
|
||||
var selectedShares = shares.Take(threshold).ToArray();
|
||||
var recovered = _shamir.Combine(selectedShares);
|
||||
|
||||
Assert.Equal(secret, recovered);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2, 3)]
|
||||
[InlineData(3, 5)]
|
||||
[InlineData(5, 10)]
|
||||
public void Split_Combine_RoundTrip_MultipleBytes(int threshold, int totalShares)
|
||||
{
|
||||
byte[] secret = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
|
||||
var shares = _shamir.Split(secret, threshold, totalShares);
|
||||
|
||||
var selectedShares = shares.Take(threshold).ToArray();
|
||||
var recovered = _shamir.Combine(selectedShares);
|
||||
|
||||
Assert.Equal(secret, recovered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_Combine_RoundTrip_256ByteSecret()
|
||||
{
|
||||
// Test with a full AES key (32 bytes)
|
||||
byte[] secret = new byte[32];
|
||||
new Random(42).NextBytes(secret);
|
||||
|
||||
var shares = _shamir.Split(secret, 3, 5);
|
||||
var recovered = _shamir.Combine(shares.Take(3).ToArray());
|
||||
|
||||
Assert.Equal(secret, recovered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Combine_WithMoreThanThreshold_Succeeds()
|
||||
{
|
||||
byte[] secret = [0xDE, 0xAD, 0xBE, 0xEF];
|
||||
var shares = _shamir.Split(secret, 3, 5);
|
||||
|
||||
// Use 4 shares (more than threshold of 3)
|
||||
var recovered = _shamir.Combine(shares.Take(4).ToArray());
|
||||
Assert.Equal(secret, recovered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Combine_WithAllShares_Succeeds()
|
||||
{
|
||||
byte[] secret = [0xCA, 0xFE];
|
||||
var shares = _shamir.Split(secret, 3, 5);
|
||||
|
||||
// Use all 5 shares
|
||||
var recovered = _shamir.Combine(shares);
|
||||
Assert.Equal(secret, recovered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Combine_AnySubsetOfThreshold_Succeeds()
|
||||
{
|
||||
byte[] secret = [0x12, 0x34, 0x56, 0x78];
|
||||
var shares = _shamir.Split(secret, 3, 5);
|
||||
|
||||
// Test all combinations of 3 shares
|
||||
var indices = new[] { 0, 1, 2, 3, 4 };
|
||||
var combinations = GetCombinations(indices, 3);
|
||||
|
||||
foreach (var combo in combinations)
|
||||
{
|
||||
var selectedShares = combo.Select(i => shares[i]).ToArray();
|
||||
var recovered = _shamir.Combine(selectedShares);
|
||||
Assert.Equal(secret, recovered);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Parameter Validation Tests
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Split_NullSecret_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => _shamir.Split(null!, 2, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_EmptySecret_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => _shamir.Split([], 2, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_ThresholdTooLow_Throws()
|
||||
{
|
||||
byte[] secret = [0x42];
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _shamir.Split(secret, 1, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_TotalSharesLessThanThreshold_Throws()
|
||||
{
|
||||
byte[] secret = [0x42];
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _shamir.Split(secret, 5, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_TotalSharesExceeds255_Throws()
|
||||
{
|
||||
byte[] secret = [0x42];
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _shamir.Split(secret, 2, 256));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Combine_NullShares_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => _shamir.Combine(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Combine_TooFewShares_Throws()
|
||||
{
|
||||
byte[] secret = [0x42];
|
||||
var shares = _shamir.Split(secret, 3, 5);
|
||||
Assert.Throws<ArgumentException>(() => _shamir.Combine([shares[0]]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Combine_InconsistentDataLength_Throws()
|
||||
{
|
||||
var shares = new ShamirShare[]
|
||||
{
|
||||
new() { Index = 1, Data = [0x01, 0x02] },
|
||||
new() { Index = 2, Data = [0x03] }, // Different length
|
||||
};
|
||||
Assert.Throws<ArgumentException>(() => _shamir.Combine(shares));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Combine_DuplicateIndices_Throws()
|
||||
{
|
||||
var shares = new ShamirShare[]
|
||||
{
|
||||
new() { Index = 1, Data = [0x01] },
|
||||
new() { Index = 1, Data = [0x02] }, // Duplicate index
|
||||
};
|
||||
Assert.Throws<ArgumentException>(() => _shamir.Combine(shares));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Combine_ZeroIndex_Throws()
|
||||
{
|
||||
var shares = new ShamirShare[]
|
||||
{
|
||||
new() { Index = 0, Data = [0x01] }, // Invalid index
|
||||
new() { Index = 1, Data = [0x02] },
|
||||
};
|
||||
Assert.Throws<ArgumentException>(() => _shamir.Combine(shares));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Security Property Tests
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Split_SharesAreRandom()
|
||||
{
|
||||
byte[] secret = [0x42];
|
||||
|
||||
// Split the same secret twice
|
||||
var shares1 = _shamir.Split(secret, 2, 3);
|
||||
var shares2 = _shamir.Split(secret, 2, 3);
|
||||
|
||||
// Shares should be different (with overwhelming probability)
|
||||
bool allSame = true;
|
||||
for (int i = 0; i < shares1.Length; i++)
|
||||
{
|
||||
if (!shares1[i].Data.SequenceEqual(shares2[i].Data))
|
||||
{
|
||||
allSame = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.False(allSame, "Shares should be randomized");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Split_ShareIndicesAreSequential()
|
||||
{
|
||||
byte[] secret = [0x42, 0x43];
|
||||
var shares = _shamir.Split(secret, 2, 5);
|
||||
|
||||
for (int i = 0; i < shares.Length; i++)
|
||||
{
|
||||
Assert.Equal(i + 1, shares[i].Index);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_ValidShares_ReturnsTrue()
|
||||
{
|
||||
byte[] secret = [0xDE, 0xAD, 0xBE, 0xEF];
|
||||
var shares = _shamir.Split(secret, 3, 5);
|
||||
|
||||
Assert.True(_shamir.Verify(shares.Take(3).ToArray()));
|
||||
Assert.True(_shamir.Verify(shares.Take(4).ToArray()));
|
||||
Assert.True(_shamir.Verify(shares));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Determinism Tests (for test reproducibility)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Combine_IsDeterministic()
|
||||
{
|
||||
// Given the same shares, combine should always produce the same result
|
||||
var shares = new ShamirShare[]
|
||||
{
|
||||
new() { Index = 1, Data = [0x01, 0x02, 0x03] },
|
||||
new() { Index = 2, Data = [0x04, 0x05, 0x06] },
|
||||
new() { Index = 3, Data = [0x07, 0x08, 0x09] },
|
||||
};
|
||||
|
||||
var result1 = _shamir.Combine(shares);
|
||||
var result2 = _shamir.Combine(shares);
|
||||
|
||||
Assert.Equal(result1, result2);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Helper Methods
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
private static IEnumerable<int[]> GetCombinations(int[] elements, int k)
|
||||
{
|
||||
if (k == 0)
|
||||
{
|
||||
yield return [];
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (elements.Length == k)
|
||||
{
|
||||
yield return elements;
|
||||
yield break;
|
||||
}
|
||||
|
||||
for (int i = 0; i <= elements.Length - k; i++)
|
||||
{
|
||||
foreach (var rest in GetCombinations(elements[(i + 1)..], k - 1))
|
||||
{
|
||||
yield return [elements[i], .. rest];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,14 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Cryptography.Plugin.Hsm\StellaOps.Cryptography.Plugin.Hsm.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="KeyEscrow/KeyEscrowRecoveryIntegrationTests.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user