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