using System.Security.Cryptography; using System.Text; namespace Fleet.Identity; /// 签名用途前缀:同一份签名不能在不同场景下复用。产品可以追加自己的用途,但不能占用 fleet 保留的这些。 public static class SignaturePurpose { public const string Auth = "auth"; /// 安全链路的认证:签名同时覆盖服务器和节点的临时公钥,把这次认证和这条加密链路绑在一起。 public const string AuthSecure = "auth2"; public const string Peer = "peer"; public const string Enroll = "enroll"; } public static class SignatureString { public static string Build(string purpose, params string[] fields) => "fleet|v1|" + purpose + "|" + string.Join('|', fields); } /// 节点身份:ECDSA P-256 密钥对。服务端只保存公钥(SPKI)。 public sealed class NodeIdentity : IDisposable { private readonly ECDsa _key; private NodeIdentity(ECDsa key) { _key = key; PublicKeySpki = key.ExportSubjectPublicKeyInfo(); } public byte[] PublicKeySpki { get; } public static NodeIdentity Generate() => new(ECDsa.Create(ECCurve.NamedCurves.nistP256)); public static NodeIdentity FromPkcs8(ReadOnlySpan pkcs8) { ECDsa key = ECDsa.Create(); key.ImportPkcs8PrivateKey(pkcs8, out _); return new NodeIdentity(key); } public byte[] ExportPkcs8() => _key.ExportPkcs8PrivateKey(); /// 与对端公钥(SPKI)做静态 ECDH,返回原始共享秘密。只用于派生对称密钥(敲门),必须再经 KDF,不能直接当密钥用。 public byte[] AgreeWith(ReadOnlySpan peerSpki) { using ECDiffieHellman own = ECDiffieHellman.Create(); own.ImportPkcs8PrivateKey(_key.ExportPkcs8PrivateKey(), out _); using ECDiffieHellman peer = ECDiffieHellman.Create(); peer.ImportSubjectPublicKeyInfo(peerSpki, out _); return own.DeriveRawSecretAgreement(peer.PublicKey); } public byte[] Sign(string purpose, params string[] fields) => _key.SignData(Encoding.UTF8.GetBytes(SignatureString.Build(purpose, fields)), HashAlgorithmName.SHA256, DSASignatureFormat.IeeeP1363FixedFieldConcatenation); /// 验证签名;公钥或签名格式不合法时返回 false,不抛异常。 public static bool Verify(ReadOnlySpan publicKeySpki, ReadOnlySpan signature, string purpose, params string[] fields) { try { using ECDsa key = ECDsa.Create(); key.ImportSubjectPublicKeyInfo(publicKeySpki, out _); return key.VerifyData(Encoding.UTF8.GetBytes(SignatureString.Build(purpose, fields)), signature, HashAlgorithmName.SHA256, DSASignatureFormat.IeeeP1363FixedFieldConcatenation); } catch (CryptographicException) { return false; } } public static bool IsValidPublicKey(ReadOnlySpan publicKeySpki) { try { using ECDsa key = ECDsa.Create(); key.ImportSubjectPublicKeyInfo(publicKeySpki, out _); return key.KeySize == 256; } catch (CryptographicException) { return false; } } public static byte[] Fingerprint(ReadOnlySpan spki) => SHA256.HashData(spki); public void Dispose() => _key.Dispose(); }