feat: operator trust root: passkey-signed operator set, signed join codes, nodes verify and pin the chain at enrollment [CI SKIP]
This commit is contained in:
1 parent
321cacac32
commit
fc2bbdcc6d
35 files changed
+1374
-40
No files matched your search
@@ -0,0 +1,132 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Fleet.Core;
|
||||
using Fleet.Identity;
|
||||
|
||||
namespace Fleet.Hosting;
|
||||
|
||||
/// <summary><c>trust.json</c>:注册时固定的创世指纹、见过的最新操作员集合链,以及(如果有)本节点自己的证明。</summary>
|
||||
public sealed class TrustFile
|
||||
{
|
||||
[JsonPropertyName("genesis")]
|
||||
public string Genesis { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("chain")]
|
||||
public List<SignedStatementDto> Chain { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("proof")]
|
||||
public MemberProofDto? Proof { get; set; }
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(TrustFile))]
|
||||
public sealed partial class TrustFileContext : JsonSerializerContext;
|
||||
|
||||
/// <summary>
|
||||
/// 节点的信任根:安装时固定下来的创世指纹,加验证过的操作员集合链。有它就是「强制」模式——
|
||||
/// 名册里的对端必须带操作员批准的证明才接纳(Backend 改不了,也签不出新的);没有 <c>trust.json</c>(旧节点、没有指纹的安装)就不强制。
|
||||
/// 链随期望状态下发,节点只接受从固定的创世出发、签名完整、版本更新的链。
|
||||
/// </summary>
|
||||
public sealed class TrustAnchor
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>不强制(没有固定的创世)。</summary>
|
||||
public static TrustAnchor None { get; } = new(null, null, null, NullNodeLog.Instance);
|
||||
|
||||
public bool Enforcing => _file is not null;
|
||||
|
||||
public string? Genesis => _file?.Genesis;
|
||||
|
||||
public MemberProofDto? OwnProof => _file?.Proof;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>注册时写下:固定的创世、验证过的链、本节点的证明。</summary>
|
||||
public static void Pin(string identityDirectory, string genesis, IReadOnlyList<SignedStatementDto> chain, MemberProofDto proof) =>
|
||||
new ConfigStore(identityDirectory).Write(FileName, JsonSerializer.SerializeToUtf8Bytes(new TrustFile { Genesis = genesis, Chain = [.. chain], Proof = proof }, TrustFileContext.Default.TrustFile));
|
||||
|
||||
/// <summary>期望状态里带来的链:从固定的创世验起,比现有的新才采用并保存。返回当前链的版本(没有强制时为 0)。</summary>
|
||||
public int Update(IReadOnlyList<SignedStatementDto>? 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>不强制就放行;强制时对端必须带证明,且验证通过(密钥是用来认证它的那把)。</summary>
|
||||
public TrustFailure VerifyPeer(MemberProofDto? proof, ReadOnlySpan<byte> peerPublicKey, string role)
|
||||
{
|
||||
OperatorSet? current;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_file is null)
|
||||
{
|
||||
return TrustFailure.None;
|
||||
}
|
||||
|
||||
current = _current;
|
||||
}
|
||||
|
||||
return proof is null || current is null ? TrustFailure.Malformed : OperatorTrust.VerifyPeer(current, proof.ToProof(), peerPublicKey, role);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user