feat!: genesis-pinned operator set, server identity with an encrypted control link, key-less join codes; release key continuity
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]
This commit is contained in:
1 parent
9bb6841dce
commit
a2f88a344b
89 files changed
+2145
-1195
No files matched your search
@@ -5,7 +5,7 @@ using Fleet.Identity;
|
||||
|
||||
namespace Fleet.Hosting;
|
||||
|
||||
/// <summary><c>trust.json</c>:注册时固定的创世指纹、见过的最新操作员集合链,以及(如果有)本节点自己的证明。</summary>
|
||||
/// <summary><c>trust.json</c>:安装时固定的创世指纹和属主,加见过的最新操作员集合链。</summary>
|
||||
public sealed class TrustFile
|
||||
{
|
||||
[JsonPropertyName("genesis")]
|
||||
@@ -14,19 +14,20 @@ public sealed class TrustFile
|
||||
[JsonPropertyName("chain")]
|
||||
public List<SignedStatementDto> Chain { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("proof")]
|
||||
public MemberProofDto? Proof { get; set; }
|
||||
/// <summary>属主:允许授权本节点 extension 会话(web ssh 等)的 passkey 凭证 id,安装时固定,必须是操作员集合里的凭证。</summary>
|
||||
[JsonPropertyName("controllers")]
|
||||
public List<string> Controllers { get; set; } = [];
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(TrustFile))]
|
||||
public sealed partial class TrustFileContext : JsonSerializerContext;
|
||||
|
||||
/// <summary>
|
||||
/// 节点的信任根:安装时固定下来的创世指纹,加验证过的操作员集合链。有它就是「强制」模式——
|
||||
/// 名册里的对端必须带操作员批准的证明才接纳(Backend 改不了,也签不出新的);没有 <c>trust.json</c>(旧节点、没有指纹的安装)就不强制。
|
||||
/// 链随期望状态下发,节点只接受从固定的创世出发、签名完整、版本更新的链。
|
||||
/// 节点的信任根:安装命令里带来的创世指纹和属主,在注册时固定到本地,之后 Backend 改不了。
|
||||
/// 操作员集合的新版本随期望状态下发,节点只接受从固定的创世出发、签名完整、版本更新的链。
|
||||
/// 属主只是被固定和保存:节点现在没有 extension,等做 extension 时由节点本地强制(见 docs/access.md 第 8 节)。
|
||||
/// </summary>
|
||||
public sealed class TrustAnchor
|
||||
public sealed class TrustAnchor : IServerVerifier
|
||||
{
|
||||
public const string FileName = "trust.json";
|
||||
|
||||
@@ -44,14 +45,68 @@ public sealed class TrustAnchor
|
||||
_log = log;
|
||||
}
|
||||
|
||||
/// <summary>不强制(没有固定的创世)。</summary>
|
||||
/// <summary>没有固定的创世(旧节点,或没有指纹的安装)。</summary>
|
||||
public static TrustAnchor None { get; } = new(null, null, null, NullNodeLog.Instance);
|
||||
|
||||
public bool Enforcing => _file is not null;
|
||||
public bool Pinned => _file is not null;
|
||||
|
||||
public string? Genesis => _file?.Genesis;
|
||||
|
||||
public MemberProofDto? OwnProof => _file?.Proof;
|
||||
/// <summary>当前生效的操作员集合版本;没有固定时为 0。</summary>
|
||||
public int Version
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _current?.Version ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>当前操作员集合里登记了服务器密钥:服务器必须在认证挑战里证明自己,链路用应用层加密。</summary>
|
||||
public bool Required
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _current?.Servers is { Count: > 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>服务器的签名是不是当前操作员集合里登记的某把服务器密钥签的。</summary>
|
||||
public bool Verify(string nonce, ReadOnlySpan<byte> serverEphemeralSpki, ReadOnlySpan<byte> signature)
|
||||
{
|
||||
IReadOnlyList<byte[]> servers;
|
||||
lock (_gate)
|
||||
{
|
||||
servers = _current?.Servers ?? [];
|
||||
}
|
||||
|
||||
foreach (byte[] server in servers)
|
||||
{
|
||||
if (ServerHello.Verify(server, nonce, serverEphemeralSpki, signature))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>安装时固定的属主里,仍然在当前操作员集合里的那些(从集合里去掉的凭证不再有效)。</summary>
|
||||
public IReadOnlyList<string> 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)
|
||||
{
|
||||
@@ -65,18 +120,18 @@ public sealed class TrustAnchor
|
||||
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>注册时写下:固定的创世、验证过的链、属主。</summary>
|
||||
public static void Pin(string identityDirectory, string genesis, IReadOnlyList<SignedStatementDto> chain, IReadOnlyList<string> controllers) =>
|
||||
new ConfigStore(identityDirectory).Write(FileName, JsonSerializer.SerializeToUtf8Bytes(new TrustFile { Genesis = genesis, Chain = [.. chain], Controllers = [.. controllers] }, TrustFileContext.Default.TrustFile));
|
||||
|
||||
/// <summary>期望状态里带来的链:从固定的创世验起,比现有的新才采用并保存。返回当前链的版本(没有强制时为 0)。</summary>
|
||||
/// <summary>期望状态里带来的链:从固定的创世验起,比现有的新才采用并保存。返回当前链的版本(没有固定时为 0)。</summary>
|
||||
public int Update(IReadOnlyList<SignedStatementDto>? chain)
|
||||
{
|
||||
lock (_gate)
|
||||
@@ -112,21 +167,4 @@ public sealed class TrustAnchor
|
||||
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