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