diff --git a/README.md b/README.md index 51be538..36a264c 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ This is a `(t, n)` threshold scheme: - **Byte-Oriented:** Primarily designed to split `byte[]` secrets. Convenience methods for `string` secrets (using UTF-8 encoding by default) are also provided. - **Finite Field Arithmetic:** Performs calculations in GF(257) by default, suitable for byte-wise secret sharing (each byte 0-255 becomes a field element). The prime can be configured. - **Share Serialization:** Includes methods to serialize shares to and from strings for easier storage or transmission. +- **Authenticated Shares:** Support for cryptographically authenticated shares with HMAC-SHA256 signatures to detect tampering and temporal validity tracking. - **Unit Tested:** Comes with a set of MSTest unit tests to verify correctness. ## How to Use @@ -121,6 +122,55 @@ The `Share` record provides methods for serialization: - `share.ToString()`: Converts a `Share` object to a string like `"X:Y0,Y1,Y2,..."`. - `Share.Parse(string s)`: Converts a serialized string back into a `Share` object. +### Authenticated Shares + +For enhanced security, the library provides authenticated shares that include cryptographic signatures to detect tampering: + +```csharp +using ShamirSecretSharing; + +// Create an authenticator with a shared key +var key = Encoding.UTF8.GetBytes("YourSecureKeyAtLeast16BytesLong!"); +var authenticator = new HmacShareAuthenticator(key); + +// Or create with a random key +var (authenticator, generatedKey) = HmacShareAuthenticator.CreateWithRandomKey(); + +// Create the authenticated service +var authService = new AuthenticatedShamirService(authenticator); + +// Split a secret with authentication +string secret = "Sensitive data requiring tamper protection"; +int n = 5; // Total shares +int t = 3; // Threshold +var expiresIn = TimeSpan.FromDays(7); // Optional expiration + +AuthenticatedShare[] authShares = authService.SplitAuthenticatedSecret( + secret, n, t, expiresIn); + +// Serialize authenticated shares for storage/transmission +foreach (var authShare in authShares) +{ + string serialized = authShare.ToString(); + Console.WriteLine($"Authenticated Share {authShare.Share.X}: {serialized}"); +} + +// Reconstruct from authenticated shares +var collectedShares = authShares.Take(t).ToList(); // Use any t shares +string reconstructed = authService.ReconstructAuthenticatedSecretString( + collectedShares, t); + +// The service automatically validates signatures and expiration +// If any share is tampered with or expired, reconstruction will fail with an exception +``` + +#### Key Features of Authenticated Shares: + +- **Tamper Detection:** Each share includes an HMAC-SHA256 signature that is verified during reconstruction +- **Temporal Validity:** Shares can have optional expiration times +- **Validation:** The `ValidateShares` method allows checking share integrity before reconstruction +- **Flexible Authentication:** The `IShareAuthenticator` interface allows custom authentication implementations + ### Prime Number (`_field.Prime`) - The default prime used is 257. This is suitable for splitting `byte[]` secrets, as each byte (0-255) can be a field element. @@ -131,7 +181,8 @@ The `Share` record provides methods for serialization: - **Randomness:** The security of SSS relies on the cryptographic randomness of the coefficients chosen for the polynomial. This implementation uses `System.Security.Cryptography.RandomNumberGenerator` for this purpose. - **Share Security:** Each individual share must be kept secret. If an attacker obtains `t` or more shares, they can reconstruct the secret. SSS protects against the loss/compromise of *up to* `t-1` shares. -- **Integrity/Authenticity:** This basic SSS implementation does not inherently protect against malicious shares (a participant providing a fake or altered share during reconstruction). For such scenarios, Verifiable Secret Sharing (VSS) schemes are needed. +- **Integrity/Authenticity:** The library now provides authenticated shares using HMAC-SHA256 signatures to protect against malicious or corrupted shares. The `AuthenticatedShamirService` automatically validates share integrity during reconstruction, rejecting tampered shares. +- **Key Management:** When using authenticated shares, the HMAC key must be securely shared among all authorized parties. Consider using asymmetric authentication (RSA/ECDSA) for scenarios where key distribution is challenging. - **Side Channels:** As with any cryptographic implementation, consider potential side-channel attacks depending on the environment where share generation or reconstruction occurs. ## Limitations @@ -144,6 +195,10 @@ The `Share` record provides methods for serialization: - `FiniteField.cs`: Implements arithmetic operations in a Galois Field GF(p). - `Share.cs`: Defines the `Share` record and its serialization/deserialization logic. - `ShamirSecretSharingService.cs`: Contains the core logic for splitting and reconstructing secrets. +- `AuthenticatedShare.cs`: Extends shares with cryptographic signatures and timestamps. +- `IShareAuthenticator.cs`: Interface for implementing different authentication strategies. +- `HmacShareAuthenticator.cs`: HMAC-SHA256 based share authentication implementation. +- `AuthenticatedShamirService.cs`: Service for creating and verifying authenticated shares. - `ShamirSecretSharingTests/` (Separate Project): Contains MSTest unit tests. - `ShamirSecretSharing.Console/` (Separate Project): Contains a console application for testing the library interactively. diff --git a/ShamirSecretSharing/AuthenticatedShamirService.cs b/ShamirSecretSharing/AuthenticatedShamirService.cs new file mode 100644 index 0000000..450d1c2 --- /dev/null +++ b/ShamirSecretSharing/AuthenticatedShamirService.cs @@ -0,0 +1,225 @@ +using System.Text; + +namespace ShamirSecretSharing; + +/// +/// Provides authenticated Shamir's Secret Sharing functionality with tamper detection and temporal validity. +/// +/// +/// This service extends the basic Shamir's Secret Sharing scheme by adding cryptographic signatures +/// to shares, enabling detection of tampered or corrupted shares before reconstruction. +/// +public class AuthenticatedShamirService +{ + private readonly ShamirSecretSharingService _shamirService; + private readonly IShareAuthenticator _authenticator; + + /// + /// Initializes a new instance of the class. + /// + /// The authenticator to use for signing and verifying shares. + /// The prime modulus for the finite field (default: 257). + /// Thrown if authenticator is null. + public AuthenticatedShamirService(IShareAuthenticator authenticator, int prime = 257) + { + _authenticator = authenticator ?? throw new ArgumentNullException(nameof(authenticator)); + _shamirService = new ShamirSecretSharingService(prime); + } + + /// + /// Splits a secret byte array into authenticated shares with integrity protection. + /// + /// The secret data to split. + /// The total number of shares to create. + /// The threshold of shares required to reconstruct the secret. + /// Optional duration after which shares expire. + /// An array of authenticated shares. + /// Thrown if the secret is invalid. + /// Thrown if n or t are invalid. + public AuthenticatedShare[] SplitAuthenticatedSecret(byte[] secret, int n, int t, TimeSpan? expiresIn = null) + { + // Use the underlying service to split the secret + var shares = _shamirService.SplitSecret(secret, n, t); + + var createdAt = DateTimeOffset.UtcNow; + var expiresAt = expiresIn.HasValue ? createdAt.Add(expiresIn.Value) : (DateTimeOffset?)null; + + var authenticatedShares = new AuthenticatedShare[shares.Length]; + for (var i = 0; i < shares.Length; i++) + { + var signature = _authenticator.SignShare(shares[i], createdAt, expiresAt); + authenticatedShares[i] = new AuthenticatedShare(shares[i], signature, createdAt, expiresAt); + } + + return authenticatedShares; + } + + /// + /// Splits a secret string into authenticated shares with integrity protection. + /// + /// The secret string to split. + /// The total number of shares to create. + /// The threshold of shares required to reconstruct the secret. + /// Optional duration after which shares expire. + /// The encoding to use (default: UTF-8). + /// An array of authenticated shares. + public AuthenticatedShare[] SplitAuthenticatedSecret(string secret, int n, int t, TimeSpan? expiresIn = null, Encoding? encoding = null) + { + encoding ??= Encoding.UTF8; + return SplitAuthenticatedSecret(encoding.GetBytes(secret), n, t, expiresIn); + } + + /// + /// Reconstructs the secret from authenticated shares after validating their integrity. + /// + /// The authenticated shares to use for reconstruction. + /// The threshold number of shares required. + /// Whether to check if shares have expired (default: true). + /// The reconstructed secret as a byte array. + /// Thrown if shares are invalid, tampered, or expired. + /// Thrown if share validation fails. + public byte[] ReconstructAuthenticatedSecret(IReadOnlyList authenticatedShares, int t, bool validateExpiry = true) + { + if (authenticatedShares == null || authenticatedShares.Count == 0) + throw new ArgumentException("Authenticated shares list cannot be null or empty.", nameof(authenticatedShares)); + + if (authenticatedShares.Count < t) + throw new ArgumentException($"Not enough shares provided. Need {t}, got {authenticatedShares.Count}.", nameof(authenticatedShares)); + + // Validate all shares before reconstruction + var validationResults = ValidateShares(authenticatedShares, validateExpiry); + + // Check if we have enough valid shares + var validShares = validationResults + .Where(r => r.IsValid && r.Share != null) + .Select(r => r.Share!) + .ToList(); + + if (validShares.Count < t) + { + var invalidCount = validationResults.Count(r => !r.IsValid); + var reasons = validationResults + .Where(r => !r.IsValid) + .Select(r => r.FailureReason) + .Distinct() + .ToList(); + + throw new InvalidOperationException( + $"Not enough valid shares for reconstruction. {invalidCount} shares failed validation. " + + $"Reasons: {string.Join(", ", reasons)}"); + } + + // Extract the underlying shares for reconstruction + var shares = validShares + .Select(s => s.Share) + .Take(t) // Only use the required threshold number + .ToList(); + + return _shamirService.ReconstructSecret(shares, t); + } + + /// + /// Reconstructs a secret string from authenticated shares. + /// + /// The authenticated shares to use for reconstruction. + /// The threshold number of shares required. + /// Whether to check if shares have expired (default: true). + /// The encoding to use (default: UTF-8). + /// The reconstructed secret string. + public string ReconstructAuthenticatedSecretString( + IReadOnlyList authenticatedShares, + int t, + bool validateExpiry = true, + Encoding? encoding = null) + { + encoding ??= Encoding.UTF8; + var reconstructedBytes = ReconstructAuthenticatedSecret(authenticatedShares, t, validateExpiry); + return encoding.GetString(reconstructedBytes); + } + + /// + /// Validates a collection of authenticated shares. + /// + /// The shares to validate. + /// Whether to check for expired shares (default: true). + /// A list of validation results for each share. + public IReadOnlyList ValidateShares( + IReadOnlyList authenticatedShares, + bool checkExpiry = true) + { + if (authenticatedShares == null) + throw new ArgumentNullException(nameof(authenticatedShares)); + + var results = new List(); + var currentTime = DateTimeOffset.UtcNow; + + foreach (var share in authenticatedShares) + { + if (share == null) + { + results.Add(new ShareValidationResult(null, false, "Share is null")); + continue; + } + + // Check expiry + if (checkExpiry && share.IsExpiredAt(currentTime)) + { + results.Add(new ShareValidationResult(share, false, "Share has expired")); + continue; + } + + // Verify signature + bool signatureValid; + try + { + signatureValid = _authenticator.VerifyShare(share); + } + catch (Exception ex) + { + results.Add(new ShareValidationResult(share, false, $"Signature verification failed: {ex.Message}")); + continue; + } + + if (!signatureValid) + { + results.Add(new ShareValidationResult(share, false, "Invalid signature - share may be tampered")); + continue; + } + + results.Add(new ShareValidationResult(share, true, null)); + } + + return results; + } + + /// + /// Represents the result of validating an authenticated share. + /// + public record ShareValidationResult + { + /// + /// Gets the share that was validated. + /// + public AuthenticatedShare? Share { get; } + + /// + /// Gets whether the share passed validation. + /// + public bool IsValid { get; } + + /// + /// Gets the reason for validation failure, if any. + /// + public string? FailureReason { get; } + + /// + /// Initializes a new instance of the record. + /// + public ShareValidationResult(AuthenticatedShare? share, bool isValid, string? failureReason) + { + Share = share; + IsValid = isValid; + FailureReason = failureReason; + } + } +} \ No newline at end of file diff --git a/ShamirSecretSharing/AuthenticatedShare.cs b/ShamirSecretSharing/AuthenticatedShare.cs new file mode 100644 index 0000000..e1a7108 --- /dev/null +++ b/ShamirSecretSharing/AuthenticatedShare.cs @@ -0,0 +1,134 @@ +using System.Globalization; +using System.Text; + +namespace ShamirSecretSharing; + +/// +/// Represents an authenticated share in Shamir's Secret Sharing scheme with integrity verification. +/// +/// +/// This class wraps a regular Share with authentication metadata including a signature +/// for tamper detection and timestamps for temporal validity tracking. +/// +public record AuthenticatedShare +{ + /// + /// Gets the underlying share containing the actual secret share data. + /// + public Share Share { get; } + + /// + /// Gets the cryptographic signature for verifying share integrity. + /// + public byte[] Signature { get; } + + /// + /// Gets the timestamp when this share was created. + /// + public DateTimeOffset CreatedAt { get; } + + /// + /// Gets the optional expiration timestamp for this share. + /// + public DateTimeOffset? ExpiresAt { get; } + + /// + /// Initializes a new instance of the record. + /// + /// The underlying share data. + /// The cryptographic signature for the share. + /// The creation timestamp. + /// The optional expiration timestamp. + /// Thrown if share or signature is null. + public AuthenticatedShare(Share share, byte[] signature, DateTimeOffset createdAt, DateTimeOffset? expiresAt = null) + { + Share = share ?? throw new ArgumentNullException(nameof(share)); + Signature = signature ?? throw new ArgumentNullException(nameof(signature)); + CreatedAt = createdAt; + ExpiresAt = expiresAt; + } + + /// + /// Determines whether this share has expired based on the current time. + /// + /// True if the share has expired; otherwise, false. + public bool IsExpired() => IsExpiredAt(DateTimeOffset.UtcNow); + + /// + /// Determines whether this share has expired at a specific point in time. + /// + /// The time to check expiration against. + /// True if the share has expired; otherwise, false. + public bool IsExpiredAt(DateTimeOffset atTime) => ExpiresAt.HasValue && atTime > ExpiresAt.Value; + + /// + /// Serializes the authenticated share to a string representation. + /// Format: Share|Base64Signature|ISO8601CreatedAt[|ISO8601ExpiresAt] + /// + /// A string representation of the authenticated share. + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(Share.ToString()); + sb.Append('|'); + sb.Append(Convert.ToBase64String(Signature)); + sb.Append('|'); + sb.Append(CreatedAt.ToString("O", CultureInfo.InvariantCulture)); + + if (ExpiresAt.HasValue) + { + sb.Append('|'); + sb.Append(ExpiresAt.Value.ToString("O", CultureInfo.InvariantCulture)); + } + + return sb.ToString(); + } + + /// + /// Deserializes an authenticated share from its string representation. + /// + /// The string representation of the authenticated share. + /// An object. + /// Thrown if is null or empty. + /// Thrown if is not in the expected format. + public static AuthenticatedShare Parse(string authenticatedShareString) + { + if (string.IsNullOrEmpty(authenticatedShareString)) + throw new ArgumentException("Authenticated share string cannot be null or empty.", nameof(authenticatedShareString)); + + var parts = authenticatedShareString.Split('|'); + if (parts.Length < 3 || parts.Length > 4) + throw new ArgumentException( + "Authenticated share string must contain 3 or 4 parts separated by '|'.", + nameof(authenticatedShareString)); + + // Parse the share + var share = Share.Parse(parts[0]); + + // Parse the signature + byte[] signature; + try + { + signature = Convert.FromBase64String(parts[1]); + } + catch (FormatException ex) + { + throw new ArgumentException("Invalid signature format in authenticated share string.", nameof(authenticatedShareString), ex); + } + + // Parse the creation timestamp + if (!DateTimeOffset.TryParseExact(parts[2], "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var createdAt)) + throw new ArgumentException($"Invalid creation timestamp format: {parts[2]}", nameof(authenticatedShareString)); + + // Parse the optional expiration timestamp + DateTimeOffset? expiresAt = null; + if (parts.Length == 4) + { + if (!DateTimeOffset.TryParseExact(parts[3], "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var expires)) + throw new ArgumentException($"Invalid expiration timestamp format: {parts[3]}", nameof(authenticatedShareString)); + expiresAt = expires; + } + + return new AuthenticatedShare(share, signature, createdAt, expiresAt); + } +} \ No newline at end of file diff --git a/ShamirSecretSharing/HmacShareAuthenticator.cs b/ShamirSecretSharing/HmacShareAuthenticator.cs new file mode 100644 index 0000000..435a6d6 --- /dev/null +++ b/ShamirSecretSharing/HmacShareAuthenticator.cs @@ -0,0 +1,143 @@ +using System.Security.Cryptography; +using System.Text; + +namespace ShamirSecretSharing; + +/// +/// Implements share authentication using HMAC-SHA256 for symmetric key-based integrity verification. +/// +/// +/// This authenticator uses a shared secret key to generate and verify HMAC signatures. +/// All parties must possess the same key to authenticate shares. +/// +public class HmacShareAuthenticator : IShareAuthenticator, IDisposable +{ + private readonly byte[] _key; + private readonly HMACSHA256 _hmac; + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The shared secret key for HMAC operations. + /// Thrown if the key is null. + /// Thrown if the key is empty or too short. + public HmacShareAuthenticator(byte[] key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + if (key.Length == 0) + throw new ArgumentException("Key cannot be empty.", nameof(key)); + if (key.Length < 16) + throw new ArgumentException("Key should be at least 16 bytes for security.", nameof(key)); + + _key = (byte[])key.Clone(); + _hmac = new HMACSHA256(_key); + } + + /// + /// Creates a new authenticator with a randomly generated key. + /// + /// The size of the key in bytes (default: 32). + /// A tuple containing the authenticator and the generated key. + public static (HmacShareAuthenticator Authenticator, byte[] Key) CreateWithRandomKey(int keySize = 32) + { + if (keySize < 16) + throw new ArgumentOutOfRangeException(nameof(keySize), "Key size should be at least 16 bytes."); + + var key = new byte[keySize]; + RandomNumberGenerator.Fill(key); + return (new HmacShareAuthenticator(key), key); + } + + /// + public string AlgorithmName => "HMAC-SHA256"; + + /// + public byte[] SignShare(Share share, DateTimeOffset createdAt, DateTimeOffset? expiresAt = null) + { + if (share == null) + throw new ArgumentNullException(nameof(share)); + + var data = GetShareData(share, createdAt, expiresAt); + + lock (_hmac) + { + return _hmac.ComputeHash(data); + } + } + + /// + public bool VerifyShare(AuthenticatedShare authenticatedShare) + { + if (authenticatedShare == null) + throw new ArgumentNullException(nameof(authenticatedShare)); + + var data = GetShareData( + authenticatedShare.Share, + authenticatedShare.CreatedAt, + authenticatedShare.ExpiresAt); + + byte[] computedSignature; + lock (_hmac) + { + computedSignature = _hmac.ComputeHash(data); + } + + return CryptographicOperations.FixedTimeEquals(computedSignature, authenticatedShare.Signature); + } + + /// + /// Generates the data to be signed from a share and its metadata. + /// + private static byte[] GetShareData(Share share, DateTimeOffset createdAt, DateTimeOffset? expiresAt) + { + var sb = new StringBuilder(); + + // Include share data + sb.Append(share.ToString()); + sb.Append('|'); + + // Include creation timestamp + sb.Append(createdAt.ToUnixTimeMilliseconds()); + + // Include expiration if present + if (expiresAt.HasValue) + { + sb.Append('|'); + sb.Append(expiresAt.Value.ToUnixTimeMilliseconds()); + } + + return Encoding.UTF8.GetBytes(sb.ToString()); + } + + /// + /// Disposes of the HMAC instance and clears the key from memory. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes of managed and unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _hmac?.Dispose(); + + // Clear the key from memory + if (_key != null) + { + Array.Clear(_key, 0, _key.Length); + } + } + _disposed = true; + } + } +} \ No newline at end of file diff --git a/ShamirSecretSharing/IShareAuthenticator.cs b/ShamirSecretSharing/IShareAuthenticator.cs new file mode 100644 index 0000000..b3b3c53 --- /dev/null +++ b/ShamirSecretSharing/IShareAuthenticator.cs @@ -0,0 +1,32 @@ +namespace ShamirSecretSharing; + +/// +/// Defines the contract for implementing share authentication strategies. +/// +/// +/// Implementations can use various cryptographic approaches such as HMAC, RSA, or ECDSA +/// to provide integrity and authenticity verification for shares. +/// +public interface IShareAuthenticator +{ + /// + /// Generates a cryptographic signature for a share. + /// + /// The share to sign. + /// The timestamp when the share was created. + /// The optional expiration timestamp. + /// The cryptographic signature as a byte array. + byte[] SignShare(Share share, DateTimeOffset createdAt, DateTimeOffset? expiresAt = null); + + /// + /// Verifies the cryptographic signature of an authenticated share. + /// + /// The authenticated share to verify. + /// True if the signature is valid; otherwise, false. + bool VerifyShare(AuthenticatedShare authenticatedShare); + + /// + /// Gets the algorithm name used for authentication. + /// + string AlgorithmName { get; } +} \ No newline at end of file diff --git a/ShamirSecretSharing/Share.cs b/ShamirSecretSharing/Share.cs index 786d0df..07c331b 100644 --- a/ShamirSecretSharing/Share.cs +++ b/ShamirSecretSharing/Share.cs @@ -54,7 +54,7 @@ public void Deconstruct(out int x, out int[] yValues) public override string ToString() { var sb = new StringBuilder(); - sb.Append(X.ToString("X")); + sb.Append(X.ToString("X2")); // Ensure X is at least 2 hex digits sb.Append(':'); foreach (var y in YValues) { diff --git a/ShamirSecretSharingTests/AuthenticatedShareTests.cs b/ShamirSecretSharingTests/AuthenticatedShareTests.cs new file mode 100644 index 0000000..10109f6 --- /dev/null +++ b/ShamirSecretSharingTests/AuthenticatedShareTests.cs @@ -0,0 +1,330 @@ +using System.Text; +using ShamirSecretSharing; + +namespace ShamirSecretSharingTests; + +[TestClass] +public class AuthenticatedShareTests +{ + private HmacShareAuthenticator _authenticator = null!; + private AuthenticatedShamirService _service = null!; + private byte[] _testKey = null!; + + [TestInitialize] + public void Setup() + { + _testKey = Encoding.UTF8.GetBytes("ThisIsATestKeyForHMACAuthentication123!"); + _authenticator = new HmacShareAuthenticator(_testKey); + _service = new AuthenticatedShamirService(_authenticator); + } + + [TestCleanup] + public void Cleanup() + { + _authenticator?.Dispose(); + } + + [TestMethod] + public void AuthenticatedShare_SerializationRoundTrip_Success() + { + // Arrange + var share = new Share(1, [10, 20, 30]); + var createdAt = DateTimeOffset.UtcNow; + var expiresAt = createdAt.AddHours(1); + var signature = _authenticator.SignShare(share, createdAt, expiresAt); + var authShare = new AuthenticatedShare(share, signature, createdAt, expiresAt); + + // Act + var serialized = authShare.ToString(); + var deserialized = AuthenticatedShare.Parse(serialized); + + // Assert + Assert.AreEqual(authShare.Share.X, deserialized.Share.X); + CollectionAssert.AreEqual(authShare.Share.YValues, deserialized.Share.YValues); + CollectionAssert.AreEqual(authShare.Signature, deserialized.Signature); + Assert.AreEqual(authShare.CreatedAt, deserialized.CreatedAt); + Assert.AreEqual(authShare.ExpiresAt, deserialized.ExpiresAt); + } + + [TestMethod] + public void AuthenticatedShare_SerializationWithoutExpiry_Success() + { + // Arrange + var share = new Share(5, [100, 200]); + var createdAt = DateTimeOffset.UtcNow; + var signature = _authenticator.SignShare(share, createdAt, null); + var authShare = new AuthenticatedShare(share, signature, createdAt, null); + + // Act + var serialized = authShare.ToString(); + var deserialized = AuthenticatedShare.Parse(serialized); + + // Assert + Assert.AreEqual(authShare.Share.X, deserialized.Share.X); + Assert.IsNull(deserialized.ExpiresAt); + } + + [TestMethod] + public void AuthenticatedShare_IsExpired_CorrectlyIdentifiesExpiredShares() + { + // Arrange + var share = new Share(1, [10]); + var createdAt = DateTimeOffset.UtcNow.AddHours(-2); + var expiresAt = DateTimeOffset.UtcNow.AddHours(-1); // Expired 1 hour ago + var signature = _authenticator.SignShare(share, createdAt, expiresAt); + var expiredShare = new AuthenticatedShare(share, signature, createdAt, expiresAt); + + var futureExpiry = DateTimeOffset.UtcNow.AddHours(1); + var validShare = new AuthenticatedShare(share, signature, createdAt, futureExpiry); + + // Act & Assert + Assert.IsTrue(expiredShare.IsExpired()); + Assert.IsFalse(validShare.IsExpired()); + } + + [TestMethod] + public void HmacShareAuthenticator_SignAndVerify_Success() + { + // Arrange + var share = new Share(1, [10, 20, 30]); + var createdAt = DateTimeOffset.UtcNow; + var expiresAt = createdAt.AddHours(1); + + // Act + var signature = _authenticator.SignShare(share, createdAt, expiresAt); + var authShare = new AuthenticatedShare(share, signature, createdAt, expiresAt); + var isValid = _authenticator.VerifyShare(authShare); + + // Assert + Assert.IsTrue(isValid); + } + + [TestMethod] + public void HmacShareAuthenticator_VerifyTamperedShare_Fails() + { + // Arrange + var share = new Share(1, [10, 20, 30]); + var createdAt = DateTimeOffset.UtcNow; + var signature = _authenticator.SignShare(share, createdAt, null); + + // Tamper with the share + var tamperedShare = new Share(1, [10, 20, 31]); // Changed last value + var authShare = new AuthenticatedShare(tamperedShare, signature, createdAt, null); + + // Act + var isValid = _authenticator.VerifyShare(authShare); + + // Assert + Assert.IsFalse(isValid); + } + + [TestMethod] + public void HmacShareAuthenticator_VerifyWithWrongKey_Fails() + { + // Arrange + var share = new Share(1, [10, 20, 30]); + var createdAt = DateTimeOffset.UtcNow; + var signature = _authenticator.SignShare(share, createdAt, null); + var authShare = new AuthenticatedShare(share, signature, createdAt, null); + + // Create authenticator with different key + var wrongKey = Encoding.UTF8.GetBytes("ThisIsADifferentKeyThatShouldNotWork!"); + using var wrongAuthenticator = new HmacShareAuthenticator(wrongKey); + + // Act + var isValid = wrongAuthenticator.VerifyShare(authShare); + + // Assert + Assert.IsFalse(isValid); + } + + [TestMethod] + public void AuthenticatedShamirService_SplitAndReconstruct_Success() + { + // Arrange + var secret = "This is a secret message!"; + var n = 5; + var t = 3; + + // Act + var shares = _service.SplitAuthenticatedSecret(secret, n, t); + var reconstructed = _service.ReconstructAuthenticatedSecretString(shares.Take(t).ToList(), t); + + // Assert + Assert.AreEqual(secret, reconstructed); + Assert.AreEqual(n, shares.Length); + } + + [TestMethod] + public void AuthenticatedShamirService_ReconstructWithTamperedShare_Fails() + { + // Arrange + var secret = Encoding.UTF8.GetBytes("Secret data"); + var n = 5; + var t = 3; + var shares = _service.SplitAuthenticatedSecret(secret, n, t); + + // Tamper with one share + var tamperedShare = shares[0]; + var modifiedShareData = new Share(tamperedShare.Share.X, [255, 255, 255]); // Completely wrong values + shares[0] = new AuthenticatedShare( + modifiedShareData, + tamperedShare.Signature, + tamperedShare.CreatedAt, + tamperedShare.ExpiresAt); + + // Act & Assert + var ex = Assert.ThrowsException(() => + _service.ReconstructAuthenticatedSecret(shares.Take(t).ToList(), t)); + + Assert.IsTrue(ex.Message.Contains("Invalid signature")); + } + + [TestMethod] + public void AuthenticatedShamirService_ReconstructWithExpiredShare_Fails() + { + // Arrange + var secret = "Test secret"; + var n = 5; + var t = 3; + var expiresIn = TimeSpan.FromMilliseconds(-1000); // Already expired + var shares = _service.SplitAuthenticatedSecret(secret, n, t, expiresIn); + + // Act & Assert + var ex = Assert.ThrowsException(() => + _service.ReconstructAuthenticatedSecretString(shares.Take(t).ToList(), t)); + + Assert.IsTrue(ex.Message.Contains("expired")); + } + + [TestMethod] + public void AuthenticatedShamirService_ReconstructWithExpiredShareButValidationDisabled_Success() + { + // Arrange + var secret = "Test secret"; + var n = 5; + var t = 3; + var expiresIn = TimeSpan.FromMilliseconds(-1000); // Already expired + var shares = _service.SplitAuthenticatedSecret(secret, n, t, expiresIn); + + // Act + var reconstructed = _service.ReconstructAuthenticatedSecretString( + shares.Take(t).ToList(), t, validateExpiry: false); + + // Assert + Assert.AreEqual(secret, reconstructed); + } + + [TestMethod] + public void AuthenticatedShamirService_ValidateShares_IdentifiesAllIssues() + { + // Arrange + var secret = Encoding.UTF8.GetBytes("Test"); + var shares = _service.SplitAuthenticatedSecret(secret, 5, 3, TimeSpan.FromHours(1)); + + // Create various problematic shares + var sharesList = new List + { + shares[0], // Valid share + null!, // Null share + new AuthenticatedShare( // Expired share + shares[1].Share, + shares[1].Signature, + DateTimeOffset.UtcNow.AddHours(-2), + DateTimeOffset.UtcNow.AddHours(-1)), + new AuthenticatedShare( // Tampered share + new Share(shares[2].Share.X, [255, 255]), + shares[2].Signature, + shares[2].CreatedAt, + shares[2].ExpiresAt) + }; + + // Act + var results = _service.ValidateShares(sharesList); + + // Assert + Assert.AreEqual(4, results.Count); + Assert.IsTrue(results[0].IsValid); // First share is valid + Assert.IsFalse(results[1].IsValid); // Null share + Assert.IsTrue(results[1].FailureReason!.Contains("null")); + Assert.IsFalse(results[2].IsValid); // Expired share + Assert.IsTrue(results[2].FailureReason!.Contains("expired")); + Assert.IsFalse(results[3].IsValid); // Tampered share + Assert.IsTrue(results[3].FailureReason!.Contains("signature")); + } + + [TestMethod] + public void AuthenticatedShamirService_ReconstructWithExtraValidShares_Success() + { + // Arrange + var secret = "Secret with extra shares"; + var n = 7; + var t = 3; + var shares = _service.SplitAuthenticatedSecret(secret, n, t); + + // Act - provide more shares than needed + var reconstructed = _service.ReconstructAuthenticatedSecretString(shares.ToList(), t); + + // Assert + Assert.AreEqual(secret, reconstructed); + } + + [TestMethod] + public void HmacShareAuthenticator_CreateWithRandomKey_Success() + { + // Act + var (authenticator, key) = HmacShareAuthenticator.CreateWithRandomKey(); + + using (authenticator) + { + // Arrange + var share = new Share(1, [10, 20]); + var createdAt = DateTimeOffset.UtcNow; + + // Act + var signature = authenticator.SignShare(share, createdAt, null); + var authShare = new AuthenticatedShare(share, signature, createdAt, null); + var isValid = authenticator.VerifyShare(authShare); + + // Assert + Assert.IsNotNull(key); + Assert.AreEqual(32, key.Length); // Default key size + Assert.IsTrue(isValid); + } + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void HmacShareAuthenticator_EmptyKey_ThrowsException() + { + // Act + _ = new HmacShareAuthenticator([]); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void HmacShareAuthenticator_ShortKey_ThrowsException() + { + // Act + _ = new HmacShareAuthenticator(new byte[10]); // Less than 16 bytes + } + + [TestMethod] + public void AuthenticatedShare_ParseInvalidFormat_ThrowsException() + { + // Arrange + var invalidStrings = new[] + { + "", + "InvalidFormat", + "1:FF|NoSignature", + "1:FF|InvalidBase64!@#|2024-01-01T00:00:00Z" + }; + + // Act & Assert + foreach (var invalid in invalidStrings) + { + Assert.ThrowsException(() => AuthenticatedShare.Parse(invalid)); + } + } +} \ No newline at end of file diff --git a/docs/security-paper.md b/docs/security-paper.md index 0c6c827..485de77 100644 --- a/docs/security-paper.md +++ b/docs/security-paper.md @@ -37,13 +37,17 @@ Provided that the required number of valid shares are available, reconstruction ## Implementation Overview -The project consists of three main components: +The project consists of the following components: - **FiniteField.cs:** Implements arithmetic within GF(p), where `p` is a prime number. The default prime is 257, allowing direct splitting of byte values (0-255). - **Share.cs:** Represents an individual share, storing an X coordinate and corresponding Y values. It also includes serialization methods for easy storage and transmission. - **ShamirSecretSharingService.cs:** Provides methods to split and reconstruct secrets, using cryptographically secure random coefficients via `System.Security.Cryptography.RandomNumberGenerator`. +- **AuthenticatedShare.cs:** Extends basic shares with cryptographic signatures and temporal validity metadata. +- **IShareAuthenticator.cs:** Defines the interface for implementing different authentication strategies. +- **HmacShareAuthenticator.cs:** Provides HMAC-SHA256 based authentication for detecting tampered shares. +- **AuthenticatedShamirService.cs:** Orchestrates authenticated share creation and validation. -Unit tests (`ShamirSecretSharingTests`) verify correctness of the splitting and reconstruction logic. +Unit tests (`ShamirSecretSharingTests`) verify correctness of both basic and authenticated splitting/reconstruction logic. ## Potential Vulnerabilities @@ -51,9 +55,15 @@ Unit tests (`ShamirSecretSharingTests`) verify correctness of the splitting and If the random number generator is weak or predictable, an attacker may guess the coefficients of the secret polynomial and reconstruct the secret with fewer than `t` shares. This implementation relies on `RandomNumberGenerator`, which is suitable for cryptographic purposes. Substituting a non-cryptographic RNG would reduce security drastically. -### Malicious Shares (Lack of Verifiability) +### Malicious Shares and Authentication -SSS alone does not provide a way to verify share authenticity. An adversary could supply fabricated shares during reconstruction, leading to failures or invalid secrets. Verifiable Secret Sharing (VSS) schemes extend SSS to mitigate this, typically using commitments or signatures. Consider integrating a VSS layer or additional checks if your use case is susceptible to malicious participants. +Traditional SSS alone does not provide a way to verify share authenticity. An adversary could supply fabricated shares during reconstruction, leading to failures or invalid secrets. This implementation now includes authenticated shares to address this vulnerability: + +- **HMAC Authentication:** The `AuthenticatedShare` class wraps regular shares with HMAC-SHA256 signatures, enabling automatic detection of tampered or corrupted shares during reconstruction. +- **Temporal Validity:** Shares can include expiration timestamps, preventing the use of old shares that may have been compromised. +- **Automatic Validation:** The `AuthenticatedShamirService` validates all shares before reconstruction, rejecting any with invalid signatures or expired timestamps. + +While not a full Verifiable Secret Sharing (VSS) scheme, this authentication layer provides practical protection against most share tampering scenarios. For distributed environments where key distribution is challenging, consider implementing asymmetric authentication using the `IShareAuthenticator` interface. ### Share Leakage and Aggregation @@ -68,13 +78,15 @@ Although the implementation is straightforward, side-channel leakage could occur 1. **Use Strong Randomness:** Always rely on cryptographically secure randomness. The default RNG in this library is adequate; avoid custom or weaker RNGs. 2. **Secure Each Share:** Treat each share like sensitive data. Store and transmit over secure channels (e.g., encrypted storage, TLS). Apply least privilege to limit who can access each share. 3. **Enforce Share Diversity:** Ensure distinct shares are stored separately (different physical locations, storage systems, or administrators). Avoid keeping all shares in one place. -4. **Implement Authentication:** When reconstructing, verify the source of each share. Use digital signatures or a VSS scheme to detect tampering or malicious actors. -5. **Rotate Secrets Periodically:** For long-term secrets, periodically generate a new secret and corresponding shares. Destroy old shares securely to mitigate risk from previously leaked copies. -6. **Audit and Monitor:** Maintain logs of share access, distribution, and reconstruction attempts. Monitor for suspicious activity that might indicate an attacker attempting to gather shares. +4. **Use Authenticated Shares:** Leverage the `AuthenticatedShamirService` for applications requiring tamper detection. This provides automatic validation of share integrity and prevents reconstruction from corrupted or malicious shares. +5. **Manage Authentication Keys Securely:** When using HMAC authentication, protect the shared key with the same rigor as the shares themselves. Consider using hardware security modules (HSMs) or key management services for production deployments. +6. **Set Share Expiration:** Use temporal validity features to automatically expire shares after a defined period, reducing the window of opportunity for attackers who gradually collect shares. +7. **Rotate Secrets Periodically:** For long-term secrets, periodically generate a new secret and corresponding shares. Destroy old shares securely to mitigate risk from previously leaked copies. +8. **Audit and Monitor:** Maintain logs of share access, distribution, and reconstruction attempts. The `ValidateShares` method can be used to audit share integrity before critical operations. ## Conclusion -The "ShamirSecretSharing" project delivers a clean, minimal implementation of Shamir's Secret Sharing for .NET applications. It provides strong protection against partial data compromise when used correctly. However, security ultimately depends on how shares are managed and protected after generation. By following the best practices outlined above, developers can leverage this library to distribute trust safely and securely. +The "ShamirSecretSharing" project delivers a clean, minimal implementation of Shamir's Secret Sharing for .NET applications, now enhanced with authenticated shares for tamper detection. It provides strong protection against partial data compromise and share corruption when used correctly. The addition of HMAC-based authentication addresses a key vulnerability in traditional SSS implementations, making it suitable for production environments where share integrity is critical. However, security ultimately depends on how shares and authentication keys are managed and protected after generation. By following the best practices outlined above, developers can leverage this library to distribute trust safely and securely. ## References