using System.Text.Json;
using System.Text.Json.Serialization;
using Fleet.Core;
using Fleet.Identity;
namespace Fleet.Hosting;
/// trust.json:安装时固定的创世指纹和属主,加见过的最新操作员集合链。
public sealed class TrustFile
{
[JsonPropertyName("genesis")]
public string Genesis { get; set; } = "";
[JsonPropertyName("chain")]
public List Chain { get; set; } = [];
/// 属主:允许授权本节点 extension 会话(web ssh 等)的 passkey 凭证 id,安装时固定,必须是操作员集合里的凭证。
[JsonPropertyName("controllers")]
public List Controllers { get; set; } = [];
}
[JsonSerializable(typeof(TrustFile))]
public sealed partial class TrustFileContext : JsonSerializerContext;
///
/// 节点的信任根:安装命令里带来的创世指纹和属主,在注册时固定到本地,之后 Backend 改不了。
/// 操作员集合的新版本随期望状态下发,节点只接受从固定的创世出发、签名完整、版本更新的链。
/// 属主只是被固定和保存:节点现在没有 extension,等做 extension 时由节点本地强制(见 docs/access.md 第 8 节)。
///
public sealed class TrustAnchor : IServerVerifier
{
public const string FileName = "trust.json";
private readonly ConfigStore? _store;
private readonly INodeLog _log;
private readonly object _gate = new();
private readonly TrustFile? _file;
private OperatorSet? _current;
private TrustAnchor(ConfigStore? store, TrustFile? file, OperatorSet? current, INodeLog log)
{
_store = store;
_file = file;
_current = current;
_log = log;
}
/// 没有固定的创世(旧节点,或没有指纹的安装)。
public static TrustAnchor None { get; } = new(null, null, null, NullNodeLog.Instance);
public bool Pinned => _file is not null;
public string? Genesis => _file?.Genesis;
/// 当前生效的操作员集合版本;没有固定时为 0。
public int Version
{
get
{
lock (_gate)
{
return _current?.Version ?? 0;
}
}
}
/// 当前操作员集合里登记了服务器密钥:服务器必须在认证挑战里证明自己,链路用应用层加密。
public bool Required
{
get
{
lock (_gate)
{
return _current?.Servers is { Count: > 0 };
}
}
}
/// 服务器的签名是不是当前操作员集合里登记的某把服务器密钥签的。
public bool Verify(string nonce, ReadOnlySpan serverEphemeralSpki, ReadOnlySpan signature)
{
IReadOnlyList servers;
lock (_gate)
{
servers = _current?.Servers ?? [];
}
foreach (byte[] server in servers)
{
if (ServerHello.Verify(server, nonce, serverEphemeralSpki, signature))
{
return true;
}
}
return false;
}
/// 安装时固定的属主里,仍然在当前操作员集合里的那些(从集合里去掉的凭证不再有效)。
public IReadOnlyList Controllers
{
get
{
lock (_gate)
{
return _file is null || _current is null ? [] : [.. _file.Controllers.Where(c => _current.Keys.Any(k => k.CredentialId == c))];
}
}
}
public static TrustAnchor Load(string identityDirectory, INodeLog log)
{
string path = Path.Combine(identityDirectory, FileName);
if (!File.Exists(path))
{
return new TrustAnchor(new ConfigStore(identityDirectory), null, null, log);
}
TrustFile file = NodeConfig.Load(path, TrustFileContext.Default.TrustFile);
OperatorSet? current = OperatorTrust.VerifyChain(file.Genesis, [.. file.Chain.Select(c => c.ToSigned())]);
if (current is null)
{
// 固定的创世和存下来的链对不上:不能当成「没有固定」放过,直接让节点起不来比悄悄失去保护好
throw new InvalidDataException($"{path} does not verify against its pinned genesis; remove the node's identity and enroll again");
}
return new TrustAnchor(new ConfigStore(identityDirectory), file, current, log);
}
/// 注册时写下:固定的创世、验证过的链、属主。
public static void Pin(string identityDirectory, string genesis, IReadOnlyList chain, IReadOnlyList controllers) =>
new ConfigStore(identityDirectory).Write(FileName, JsonSerializer.SerializeToUtf8Bytes(new TrustFile { Genesis = genesis, Chain = [.. chain], Controllers = [.. controllers] }, TrustFileContext.Default.TrustFile));
/// 期望状态里带来的链:从固定的创世验起,比现有的新才采用并保存。返回当前链的版本(没有固定时为 0)。
public int Update(IReadOnlyList? chain)
{
lock (_gate)
{
if (_file is null || chain is not { Count: > 0 })
{
return _current?.Version ?? 0;
}
OperatorSet? candidate = OperatorTrust.VerifyChain(_file.Genesis, [.. chain.Select(c => c.ToSigned())]);
if (candidate is null)
{
_log.Warn("the operator set from the panel does not verify against the pinned genesis; ignoring it");
return _current!.Version;
}
if (candidate.Version > _current!.Version)
{
_file.Chain = [.. chain];
_current = candidate;
try
{
_store?.Write(FileName, JsonSerializer.SerializeToUtf8Bytes(_file, TrustFileContext.Default.TrustFile));
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
_log.Warn("could not save trust.json", ("err", ex.Message));
}
_log.Info("operator set updated", ("version", candidate.Version));
}
return _current.Version;
}
}
}