feat!: genesis-pinned operator set, server identity with an encrypted control link, key-less join codes; release key continuity
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]
This commit is contained in:
1 parent
9bb6841dce
commit
a2f88a344b
89 files changed
+2145
-1195
No files matched your search
@@ -0,0 +1,114 @@
|
||||
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=***"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user