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]
83 lines
2.7 KiB
C#
83 lines
2.7 KiB
C#
using System.Security.Cryptography;
|
|
using Fleet.Identity;
|
|
using Fleet.Server;
|
|
|
|
namespace MoShiro.Backend.Application;
|
|
|
|
/// <summary>
|
|
/// 服务器的身份密钥(P-256):操作员用 passkey 把公钥签进操作员集合,之后节点只认带这把密钥签名的服务器,
|
|
/// 并在这条链路上用应用层加密(TLS 在 Cloudflare 或被冒充的证书那里终止时,中间人拿不出这把密钥)。
|
|
/// 私钥存在 Backend 的设置里:它保护的是「面板被冒充」,不是「面板被攻破」;面板被攻破时它也会泄露,那时要由 passkey 签一个换掉它的新集合版本。
|
|
/// </summary>
|
|
public sealed class ServerIdentityService : IServerIdentity, IDisposable
|
|
{
|
|
private const string Key = "server.identity_key";
|
|
|
|
private readonly ISettingsStore _settings;
|
|
private readonly object _gate = new();
|
|
private ECDsa? _key;
|
|
private byte[]? _spki;
|
|
|
|
public ServerIdentityService(ISettingsStore settings)
|
|
{
|
|
_settings = settings;
|
|
}
|
|
|
|
public byte[] PublicKeySpki
|
|
{
|
|
get
|
|
{
|
|
EnsureLoaded();
|
|
return _spki!;
|
|
}
|
|
}
|
|
|
|
/// <summary>公钥的指纹(SHA-256,小写十六进制):页面上给人核对。</summary>
|
|
public string Fingerprint => Convert.ToHexStringLower(SHA256.HashData(PublicKeySpki));
|
|
|
|
public byte[] Sign(ReadOnlySpan<byte> payload)
|
|
{
|
|
EnsureLoaded();
|
|
return _key!.SignData(payload, HashAlgorithmName.SHA256, DSASignatureFormat.IeeeP1363FixedFieldConcatenation);
|
|
}
|
|
|
|
/// <summary>启动时调用:读出或生成密钥。节点可能在启动完成之前就连上来,所以 <see cref="Sign"/> 也会按需加载。</summary>
|
|
public ValueTask EnsureAsync(CancellationToken ct)
|
|
{
|
|
EnsureLoaded(ct);
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
private void EnsureLoaded(CancellationToken ct = default)
|
|
{
|
|
if (_key is not null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (_gate)
|
|
{
|
|
if (_key is not null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string? stored = _settings.GetAsync(Key, ct).AsTask().GetAwaiter().GetResult();
|
|
var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
|
if (stored is not null)
|
|
{
|
|
key.ImportPkcs8PrivateKey(Convert.FromBase64String(stored), out _);
|
|
}
|
|
else
|
|
{
|
|
_settings.SetAsync(Key, Convert.ToBase64String(key.ExportPkcs8PrivateKey()), ct).AsTask().GetAwaiter().GetResult();
|
|
}
|
|
|
|
_spki = key.ExportSubjectPublicKeyInfo();
|
|
_key = key;
|
|
}
|
|
}
|
|
|
|
public void Dispose() => _key?.Dispose();
|
|
}
|