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]
64 lines
2.8 KiB
C#
64 lines
2.8 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace Fleet.Identity;
|
|
|
|
/// <summary>点对点会话派生出的数据面凭据。<c>SessionId</c> 8 字节,<c>Key</c> 32 字节(HMAC-SHA256 的密钥)。</summary>
|
|
public sealed record SessionCredentials(byte[] SessionId, byte[] Key)
|
|
{
|
|
public long SessionIdValue => BitConverter.ToInt64(SessionId, 0);
|
|
}
|
|
|
|
/// <summary>临时 ECDH 密钥:每个会话各生成一个,用完即弃,所以会话密钥具有前向保密。</summary>
|
|
public sealed class EphemeralKey : IDisposable
|
|
{
|
|
private readonly ECDiffieHellman _key = ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256);
|
|
|
|
public byte[] PublicKeySpki => _key.ExportSubjectPublicKeyInfo();
|
|
|
|
/// <summary>与对端的临时公钥做 ECDH,返回原始共享秘密;必须再经 KDF 才能当密钥用。</summary>
|
|
public byte[] AgreeRaw(ReadOnlySpan<byte> remotePublicKeySpki)
|
|
{
|
|
using ECDiffieHellman remote = ECDiffieHellman.Create();
|
|
remote.ImportSubjectPublicKeyInfo(remotePublicKeySpki, out _);
|
|
return _key.DeriveRawSecretAgreement(remote.PublicKey);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 与对端的临时公钥协商,再经 HKDF-SHA256 派生会话凭据。
|
|
/// 盐是服务端的随机数,info 绑定双方的临时公钥,防止凭据被挪用到别的握手上。
|
|
/// </summary>
|
|
public SessionCredentials Derive(ReadOnlySpan<byte> remotePublicKeySpki, ReadOnlySpan<byte> nonce, ReadOnlySpan<byte> clientPublic, ReadOnlySpan<byte> serverPublic)
|
|
{
|
|
using ECDiffieHellman remote = ECDiffieHellman.Create();
|
|
remote.ImportSubjectPublicKeyInfo(remotePublicKeySpki, out _);
|
|
byte[] secret = _key.DeriveRawSecretAgreement(remote.PublicKey);
|
|
byte[] info = [.. Encoding.UTF8.GetBytes("fleet|v1|session|"), .. SHA256.HashData(clientPublic), .. SHA256.HashData(serverPublic)];
|
|
byte[] material = HKDF.DeriveKey(HashAlgorithmName.SHA256, secret, 40, nonce.ToArray(), info);
|
|
CryptographicOperations.ZeroMemory(secret);
|
|
return new SessionCredentials(material[..8], material[8..]);
|
|
}
|
|
|
|
public void Dispose() => _key.Dispose();
|
|
}
|
|
|
|
/// <summary>数据面的消息认证:HMAC-SHA256 截断到 16 字节。</summary>
|
|
public static class DataPlaneMac
|
|
{
|
|
public const int Bytes = 16;
|
|
|
|
public static void Compute(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, Span<byte> destination)
|
|
{
|
|
Span<byte> full = stackalloc byte[32];
|
|
HMACSHA256.HashData(key, data, full);
|
|
full[..Bytes].CopyTo(destination);
|
|
}
|
|
|
|
public static bool Verify(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, ReadOnlySpan<byte> mac)
|
|
{
|
|
Span<byte> expected = stackalloc byte[Bytes];
|
|
Compute(key, data, expected);
|
|
return CryptographicOperations.FixedTimeEquals(expected, mac);
|
|
}
|
|
}
|