using System.Security.Cryptography; using Fleet.Identity; namespace Fleet.Tests; public sealed class SecureChannelTests { private const string Nonce = "n0nce-from-the-server"; private static (LinkCipher Node, LinkCipher Server) Pair(string nodeNonce = Nonce, string serverNonce = Nonce) { using var node = new EphemeralKey(); using var server = new EphemeralKey(); return (LinkCipher.ForNode(node, server.PublicKeySpki, nodeNonce), LinkCipher.ForServer(server, node.PublicKeySpki, serverNonce)); } [Fact] public void TheServerProof_VerifiesOnlyForTheRightKey_Nonce_AndEphemeralKey() { using ECDsa serverKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); using ECDsa other = ECDsa.Create(ECCurve.NamedCurves.nistP256); using var eph = new EphemeralKey(); using var otherEph = new EphemeralKey(); byte[] spki = serverKey.ExportSubjectPublicKeyInfo(); byte[] proof = ServerHello.Sign(serverKey, Nonce, eph.PublicKeySpki); Assert.True(ServerHello.Verify(spki, Nonce, eph.PublicKeySpki, proof)); Assert.False(ServerHello.Verify(other.ExportSubjectPublicKeyInfo(), Nonce, eph.PublicKeySpki, proof)); // 别人的服务器密钥 Assert.False(ServerHello.Verify(spki, "another-nonce", eph.PublicKeySpki, proof)); // 挪到别的挑战上 Assert.False(ServerHello.Verify(spki, Nonce, otherEph.PublicKeySpki, proof)); // 中间人换了自己的临时公钥 Assert.False(ServerHello.Verify(spki, Nonce, eph.PublicKeySpki, proof.AsSpan(0, proof.Length - 1))); Assert.False(ServerHello.Verify([1, 2, 3], Nonce, eph.PublicKeySpki, proof)); // 公钥不合法也不抛异常 } [Fact] public void BothDirections_RoundTrip_AndKeepTheFrameKind() { (LinkCipher node, LinkCipher server) = Pair(); using LinkCipher n = node; using LinkCipher s = server; byte[] toServer = n.Seal(0, "hello server"u8); Assert.True(s.TryOpen(toServer, out byte kind, out byte[] data)); Assert.Equal(0, kind); Assert.Equal("hello server"u8.ToArray(), data); byte[] big = RandomNumberGenerator.GetBytes(300_000); byte[] toNode = s.Seal(1, big); Assert.True(n.TryOpen(toNode, out kind, out data)); Assert.Equal(1, kind); Assert.Equal(big, data); Assert.Equal(big.Length + LinkCipher.Overhead + 1, toNode.Length); } [Fact] public void AReplayedReorderedTamperedOrReflectedFrame_IsRefused() { (LinkCipher node, LinkCipher server) = Pair(); using LinkCipher n = node; using LinkCipher s = server; byte[] first = n.Seal(0, "one"u8); byte[] second = n.Seal(0, "two"u8); Assert.False(s.TryOpen(second, out _, out _)); // 乱序(丢了第一帧) Assert.True(s.TryOpen(first, out _, out _)); Assert.False(s.TryOpen(first, out _, out _)); // 重放 Assert.True(s.TryOpen(second, out _, out _)); byte[] tampered = n.Seal(0, "three"u8); tampered[12] ^= 0x01; Assert.False(s.TryOpen(tampered, out _, out _)); // 改了一位 byte[] fromNode = n.Seal(0, "four"u8); (LinkCipher otherNode, LinkCipher otherServer) = Pair(); using LinkCipher on = otherNode; using LinkCipher os = otherServer; Assert.False(os.TryOpen(fromNode, out _, out _)); // 另一条连接的密钥打不开 byte[] fromServer = s.Seal(0, "reflected"u8); Assert.False(s.TryOpen(fromServer, out _, out _)); // 方向不同:服务器发的东西不能原样塞回给服务器 Assert.False(n.TryOpen([1, 2, 3], out _, out _)); } [Fact] public void ADifferentNonce_GivesDifferentKeys() { (LinkCipher node, LinkCipher server) = Pair("nonce-a", "nonce-b"); // 两边对不上:没人能解开对方的东西 using LinkCipher n = node; using LinkCipher s = server; Assert.False(s.TryOpen(n.Seal(0, "x"u8), out _, out _)); } [Fact] public void AnOperatorSet_CarriesTheServerKeys_ThroughTheChain() { using var alice = new TestAuthenticator(); using ECDsa serverKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); byte[] spki = serverKey.ExportSubjectPublicKeyInfo(); var set = new OperatorSet(1, OperatorTrust.Genesis, TestAuthenticator.RpId, [TestAuthenticator.Origin], [alice.Key], [spki]); string text = OperatorTrust.Encode(set); OperatorSet parsed = OperatorTrust.ParseSet(text)!; Assert.Equal(spki, Assert.Single(parsed.Servers!)); Assert.Equal(text, OperatorTrust.Encode(parsed)); // 编码和解析互逆,指纹稳定 SignedStatement genesis = alice.Sign(text); OperatorSet verified = OperatorTrust.VerifyChain(OperatorTrust.Fingerprint(text), [genesis])!; Assert.Equal(spki, Assert.Single(verified.Servers!)); // 服务器密钥只能写在凭证后面;顺序不对、内容不是 base64 都算坏 Assert.Null(OperatorTrust.ParseSet(text + "\nkey=late:AAAA")); Assert.Null(OperatorTrust.ParseSet(text + "\nserver=***")); } }