Files
MoShiro/backend/fleet/src/Fleet.Identity/SessionKeys.cs
T
justme a2f88a344b
ci/woodpecker/tag/gitea-release Pipeline is pending
ci/woodpecker/tag/linux-release Pipeline failed
ci/woodpecker/tag/windows-release Pipeline was canceled
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]
2026-10-08 03:21:44 +08:00

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);
}
}