using System.Buffers.Binary; using System.Security.Cryptography; using System.Text; namespace Fleet.Identity; /// /// 预握手敲门:TCP 连接建立之后、TLS 握手之前,拨号方先发 32 字节 nonce(16) | tag(16), /// tag = HMAC-SHA256(K_pair, 时间窗 | nonce) 截断到 16 字节。监听方核对不过就什么都不写、直接断开, /// 所以扫描器拿不到证书、TLS 参数和 JARM 指纹。K_pair 只有名册里的成员算得出:双方身份密钥做静态 ECDH,再经 HKDF 加用途标签派生。 /// TCP 的 SYN-ACK 由内核发出,端口在扫描器眼里仍是「开着但不说话」,不是「没监听」。 /// public static class Knock { public const int PacketBytes = 32; public const int NonceBytes = 16; public const int TagBytes = 16; /// 时间窗长度;接受前后各一个窗,容忍时钟偏差。 public const int WindowSeconds = 30; private const string Label = "moshiro-knock-v1"; /// 两把身份密钥的配对密钥。info 里的两个公钥按字节序排列,所以两边算出的是同一个值。 public static byte[] DeriveKey(NodeIdentity own, ReadOnlySpan peerSpki) { byte[] secret = own.AgreeWith(peerSpki); try { ReadOnlySpan mine = own.PublicKeySpki; bool mineFirst = mine.SequenceCompareTo(peerSpki) <= 0; byte[] info = [.. Encoding.UTF8.GetBytes(Label), .. (mineFirst ? mine : peerSpki), .. (mineFirst ? peerSpki : mine)]; return HKDF.DeriveKey(HashAlgorithmName.SHA256, secret, 32, null, info); } finally { CryptographicOperations.ZeroMemory(secret); } } public static byte[] Build(ReadOnlySpan key, long nowMs) { byte[] packet = new byte[PacketBytes]; RandomNumberGenerator.Fill(packet.AsSpan(0, NonceBytes)); Tag(key, WindowOf(nowMs), packet.AsSpan(0, NonceBytes), packet.AsSpan(NonceBytes)); return packet; } /// tag 在前一个、当前、后一个时间窗里有一个对得上就通过。不管重放(重放由 按 nonce 去重)。 public static bool Check(ReadOnlySpan key, ReadOnlySpan packet, long nowMs) { if (packet.Length != PacketBytes) { return false; } long window = WindowOf(nowMs); Span expected = stackalloc byte[TagBytes]; bool ok = false; for (long w = window - 1; w <= window + 1; w++) { Tag(key, w, packet[..NonceBytes], expected); ok |= CryptographicOperations.FixedTimeEquals(expected, packet[NonceBytes..]); } return ok; } private static long WindowOf(long nowMs) => nowMs / 1000 / WindowSeconds; private static void Tag(ReadOnlySpan key, long window, ReadOnlySpan nonce, Span destination) { Span data = stackalloc byte[8 + NonceBytes]; BinaryPrimitives.WriteInt64BigEndian(data, window); nonce.CopyTo(data[8..]); Span full = stackalloc byte[32]; HMACSHA256.HashData(key, data, full); full[..TagBytes].CopyTo(destination); } }