Join codes need no passkey touch and carry only public values (genesis fingerprint, owner credentials, installer hashes). Nodes pin the genesis and owners at enrollment and only follow passkey-signed operator set versions. The panel's server key is listed in the set; pinned nodes require its signature and run the control link under AES-256-GCM. Password sessions carry no capabilities. The updater adopts a next_key from a signed manifest. Bump version to 0.2.0. [CI SKIP]
115 lines
5.1 KiB
C#
115 lines
5.1 KiB
C#
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=***"));
|
|
}
|
|
}
|