using System.Security.Cryptography; using NSec.Cryptography; namespace DodoSSH.Crypto.Tests; /// /// Proves the primitives docs/crypto.md depends on are actually available and functional on /// this runtime and platform. /// /// /// Not ceremonial. Two concrete risks motivated these: /// the BCL has no X25519 or Ed25519 at all, and ChaCha20Poly1305.IsSupported is false /// on macOS, which is what disqualified the in-box AEAD for a cross-platform client. If any /// of these fail on a target platform, the specification is wrong rather than the code. /// public sealed class PrimitiveAvailabilityTests { [Fact] public void X25519_AgreesOnASharedSecret() { var algorithm = KeyAgreementAlgorithm.X25519; var creation = new KeyCreationParameters { ExportPolicy = KeyExportPolicies.AllowPlaintextExport }; using var alice = Key.Create(algorithm, creation); using var bob = Key.Create(algorithm, creation); using var aliceView = algorithm.Agree(alice, bob.PublicKey)!; using var bobView = algorithm.Agree(bob, alice.PublicKey)!; var derive = KeyDerivationAlgorithm.HkdfSha256; var fromAlice = derive.DeriveBytes(aliceView, ReadOnlySpan.Empty, "test"u8, 32); var fromBob = derive.DeriveBytes(bobView, ReadOnlySpan.Empty, "test"u8, 32); fromAlice.ShouldBe(fromBob); } [Fact] public void Ed25519_SignsAndVerifies() { var algorithm = SignatureAlgorithm.Ed25519; using var signer = Key.Create(algorithm); var message = "grant tuple"u8; var signature = algorithm.Sign(signer, message); signature.Length.ShouldBe(64); algorithm.Verify(signer.PublicKey, message, signature).ShouldBeTrue(); algorithm.Verify(signer.PublicKey, "tampered"u8, signature).ShouldBeFalse(); } [Fact] public void XChaCha20Poly1305_RoundTripsAndDetectsAadTampering() { var algorithm = AeadAlgorithm.XChaCha20Poly1305; using var key = Key.Create(algorithm); var nonce = RandomNumberGenerator.GetBytes(algorithm.NonceSize); var plaintext = "id_ed25519 private key"u8; var ciphertext = algorithm.Encrypt(key, nonce, "aad-a"u8, plaintext); algorithm.Decrypt(key, nonce, "aad-a"u8, ciphertext).ShouldBe(plaintext.ToArray()); // The whole point of binding AAD to row identity: a different AAD must not decrypt. algorithm.Decrypt(key, nonce, "aad-b"u8, ciphertext).ShouldBeNull(); } [Fact] public void XChaCha20Poly1305_NonceIs24BytesSoRandomNoncesAreSafe() { // 192-bit nonces are why we can generate one at random per message without tracking // a counter. AES-GCM's 96-bit nonce would not permit that. AeadAlgorithm.XChaCha20Poly1305.NonceSize.ShouldBe(24); AeadAlgorithm.XChaCha20Poly1305.KeySize.ShouldBe(32); AeadAlgorithm.XChaCha20Poly1305.TagSize.ShouldBe(16); } [Fact] public void Argon2id_IsAvailableAndParallelismIsPinnedToOne() { // libsodium's Argon2id implementation only supports p=1. docs/crypto.md compensates // with memory cost instead; this test pins the constraint so it is not forgotten. var algorithm = PasswordBasedKeyDerivationAlgorithm.Argon2id( new Argon2Parameters { DegreeOfParallelism = 1, MemorySize = 1 << 20, NumberOfPasses = 1 }); var salt = new byte[16]; var derived = algorithm.DeriveBytes("correct horse battery staple", salt, 32); derived.Length.ShouldBe(32); derived.ShouldNotBe(new byte[32]); } [Fact] public void Argon2id_IsDeterministicForTheSamePassphraseAndSalt() { var algorithm = PasswordBasedKeyDerivationAlgorithm.Argon2id( new Argon2Parameters { DegreeOfParallelism = 1, MemorySize = 1 << 20, NumberOfPasses = 1 }); var salt = RandomNumberGenerator.GetBytes(16); algorithm.DeriveBytes("passphrase", salt, 32) .ShouldBe(algorithm.DeriveBytes("passphrase", salt, 32)); } [Fact] public void HkdfSha512_IsAvailableInTheBcl() { // Subkey derivation from the master key uses the BCL, not NSec: HKDF is fully // supported on every platform. var info = "dsh1/kek/passphrase/v1"u8.ToArray(); var okm = HKDF.Expand(HashAlgorithmName.SHA512, new byte[64], 32, info); okm.Length.ShouldBe(32); } }