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]
95 lines
3.4 KiB
C#
95 lines
3.4 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace Fleet.Identity;
|
|
|
|
/// <summary>签名用途前缀:同一份签名不能在不同场景下复用。产品可以追加自己的用途,但不能占用 fleet 保留的这些。</summary>
|
|
public static class SignaturePurpose
|
|
{
|
|
public const string Auth = "auth";
|
|
|
|
/// <summary>安全链路的认证:签名同时覆盖服务器和节点的临时公钥,把这次认证和这条加密链路绑在一起。</summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>节点身份:ECDSA P-256 密钥对。服务端只保存公钥(SPKI)。</summary>
|
|
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<byte> pkcs8)
|
|
{
|
|
ECDsa key = ECDsa.Create();
|
|
key.ImportPkcs8PrivateKey(pkcs8, out _);
|
|
return new NodeIdentity(key);
|
|
}
|
|
|
|
public byte[] ExportPkcs8() => _key.ExportPkcs8PrivateKey();
|
|
|
|
/// <summary>与对端公钥(SPKI)做静态 ECDH,返回原始共享秘密。只用于派生对称密钥(敲门),必须再经 KDF,不能直接当密钥用。</summary>
|
|
public byte[] AgreeWith(ReadOnlySpan<byte> 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);
|
|
|
|
/// <summary>验证签名;公钥或签名格式不合法时返回 false,不抛异常。</summary>
|
|
public static bool Verify(ReadOnlySpan<byte> publicKeySpki, ReadOnlySpan<byte> 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<byte> publicKeySpki)
|
|
{
|
|
try
|
|
{
|
|
using ECDsa key = ECDsa.Create();
|
|
key.ImportSubjectPublicKeyInfo(publicKeySpki, out _);
|
|
return key.KeySize == 256;
|
|
}
|
|
catch (CryptographicException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static byte[] Fingerprint(ReadOnlySpan<byte> spki) => SHA256.HashData(spki);
|
|
|
|
public void Dispose() => _key.Dispose();
|
|
}
|