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