feat!: probes generate their own keys and enroll with a join token; node and peer keys are separate [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
This commit is contained in:
1 parent
1ff847e45a
commit
a9f562441f
51 files changed
+505
-749
No files matched your search
@@ -29,9 +29,11 @@ public static class EnrollmentClient
|
||||
using NodeIdentity identity = NodeIdentity.Generate();
|
||||
byte[] pfx = NodeCertificate.CreateSelfSignedPfx(name, TimeSpan.FromDays(3650), time);
|
||||
byte[] spki;
|
||||
byte[] tlsPublicKey;
|
||||
using (var certificate = NodeCertificate.LoadPfx(pfx))
|
||||
{
|
||||
spki = NodeCertificate.SpkiFingerprint(certificate);
|
||||
tlsPublicKey = certificate.PublicKey.ExportSubjectPublicKeyInfo();
|
||||
}
|
||||
|
||||
string pubkey = Convert.ToBase64String(identity.PublicKeySpki);
|
||||
@@ -42,6 +44,7 @@ public static class EnrollmentClient
|
||||
["name"] = name,
|
||||
["pubkey"] = pubkey,
|
||||
["tls_spki"] = Convert.ToBase64String(spki),
|
||||
["tls_pubkey"] = Convert.ToBase64String(tlsPublicKey),
|
||||
["proof"] = Convert.ToBase64String(identity.Sign(SignaturePurpose.Enroll, tokenHash, pubkey)),
|
||||
["node_version"] = nodeVersion,
|
||||
["meta"] = productMeta,
|
||||
|
||||
@@ -331,6 +331,15 @@ public sealed class LoadedNodeIdentity : IDisposable
|
||||
|
||||
public X509Certificate2 Certificate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 对等面的签名身份:TLS 证书的那把密钥,和连服务端用的 <see cref="Identity"/> 是两把独立的密钥,
|
||||
/// 各自只用于自己的用途(一把泄露不影响另一把)。
|
||||
/// </summary>
|
||||
public NodeIdentity PeerIdentity => _peerIdentity ??= NodeIdentity.FromPkcs8(
|
||||
(Certificate.GetECDsaPrivateKey() ?? throw new InvalidDataException("the node certificate has no ECDSA private key")).ExportPkcs8PrivateKey());
|
||||
|
||||
private NodeIdentity? _peerIdentity;
|
||||
|
||||
public static LoadedNodeIdentity Load(string directory)
|
||||
{
|
||||
if (!System.IO.File.Exists(Path.Combine(directory, KeyFileName)) || !System.IO.File.Exists(Path.Combine(directory, TlsFileName)))
|
||||
@@ -347,6 +356,7 @@ public sealed class LoadedNodeIdentity : IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
Identity.Dispose();
|
||||
_peerIdentity?.Dispose();
|
||||
Certificate.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ public static class NodeCertificate
|
||||
return certificate.Export(X509ContentType.Pfx);
|
||||
}
|
||||
|
||||
public static X509Certificate2 LoadPfx(byte[] pfx) => X509CertificateLoader.LoadPkcs12(pfx, null);
|
||||
public static X509Certificate2 LoadPfx(byte[] pfx) => X509CertificateLoader.LoadPkcs12(pfx, null, X509KeyStorageFlags.Exportable);
|
||||
|
||||
/// <summary>证书公钥(SPKI)的 SHA-256:对端用它来固定这张证书。</summary>
|
||||
public static byte[] SpkiFingerprint(X509Certificate2 certificate) =>
|
||||
|
||||
@@ -5,6 +5,9 @@ public sealed class FleetServerOptions
|
||||
/// <summary>节点 ID 的类型前缀,MoShiro 使用 <c>agt</c>。</summary>
|
||||
public string NodeIdPrefix { get; set; } = "node";
|
||||
|
||||
/// <summary>加入令牌的「种类」对应的节点 ID 前缀(例如 <c>probe</c> → <c>prb</c>);没有列出的种类用 <see cref="NodeIdPrefix"/>。</summary>
|
||||
public Dictionary<string, string> NodeIdPrefixes { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
public int MaxFrameBytes { get; set; } = 16 << 20;
|
||||
|
||||
public TimeSpan ChallengeTtl { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
@@ -31,7 +31,9 @@ public sealed record NewNode(
|
||||
IReadOnlyList<string> Labels,
|
||||
string? MetaJson,
|
||||
string? NodeVersion,
|
||||
long NowMs);
|
||||
long NowMs,
|
||||
string Kind = "",
|
||||
byte[]? TlsPublicKey = null);
|
||||
|
||||
public sealed record JoinTokenRecord(
|
||||
string Id,
|
||||
@@ -43,6 +45,7 @@ public sealed record JoinTokenRecord(
|
||||
int Uses,
|
||||
long ExpiresAtMs,
|
||||
long? RevokedAtMs,
|
||||
long CreatedAtMs);
|
||||
long CreatedAtMs,
|
||||
string Kind = "");
|
||||
|
||||
public sealed record Presence(bool Online, long AtMs, string? NodeVersion, IReadOnlyList<string> Capabilities, string? HelloMetaJson);
|
||||
@@ -19,6 +19,13 @@ public sealed class EnrollRequest
|
||||
[JsonPropertyName("tls_spki")]
|
||||
public string TlsSpki { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 同一把 TLS 密钥的完整公钥(SPKI,base64),必须和 <see cref="TlsSpki"/> 对得上。节点用这把密钥做对等面的签名认证
|
||||
/// (和连服务端用的身份密钥是两把独立的密钥),对端要验签就需要完整的公钥,而不只是指纹。
|
||||
/// </summary>
|
||||
[JsonPropertyName("tls_pubkey")]
|
||||
public string TlsPublicKey { get; set; } = "";
|
||||
|
||||
/// <summary>持有证明:对 <c>fleet|v1|enroll|{sha256_hex(join_token)}|{pubkey}</c> 的签名(base64)。</summary>
|
||||
[JsonPropertyName("proof")]
|
||||
public string Proof { get; set; } = "";
|
||||
|
||||
@@ -29,9 +29,16 @@ public sealed partial class FleetEnrollmentService
|
||||
|
||||
if (!TryDecode(request.PublicKey, out byte[] publicKey) || !NodeIdentity.IsValidPublicKey(publicKey)
|
||||
|| !TryDecode(request.TlsSpki, out byte[] tlsSpki) || tlsSpki.Length != 32
|
||||
|| !TryDecode(request.TlsPublicKey, out byte[] tlsPublicKey) || !NodeIdentity.IsValidPublicKey(tlsPublicKey)
|
||||
|| !TryDecode(request.Proof, out byte[] proof))
|
||||
{
|
||||
return Result<EnrollResponse>.Fail(FleetErrors.InvalidParams, "pubkey, tls_spki or proof is malformed");
|
||||
return Result<EnrollResponse>.Fail(FleetErrors.InvalidParams, "pubkey, tls_spki, tls_pubkey or proof is malformed");
|
||||
}
|
||||
|
||||
// 指纹和完整公钥必须是同一把密钥,否则对端按指纹固定证书、按公钥验签,会各认各的
|
||||
if (!System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(NodeIdentity.Fingerprint(tlsPublicKey), tlsSpki))
|
||||
{
|
||||
return Result<EnrollResponse>.Fail(FleetErrors.InvalidParams, "tls_spki does not match tls_pubkey");
|
||||
}
|
||||
|
||||
byte[] tokenHash = TokenHash.Sha256(request.JoinToken);
|
||||
@@ -61,8 +68,9 @@ public sealed partial class FleetEnrollmentService
|
||||
bool nameTaken = await _store.FindEstablishedNodeByNameAsync(request.Name, ct) is not null;
|
||||
NodeStatus status = token.AutoApprove && !nameTaken ? NodeStatus.Active : NodeStatus.Pending;
|
||||
|
||||
string prefix = token.Kind.Length > 0 && _options.NodeIdPrefixes.TryGetValue(token.Kind, out string? kindPrefix) ? kindPrefix : _options.NodeIdPrefix;
|
||||
var node = new NewNode(
|
||||
new NodeId(Ulid.NewPrefixed(_options.NodeIdPrefix, _time)),
|
||||
new NodeId(Ulid.NewPrefixed(prefix, _time)),
|
||||
request.Name,
|
||||
status,
|
||||
publicKey,
|
||||
@@ -70,7 +78,9 @@ public sealed partial class FleetEnrollmentService
|
||||
token.Labels,
|
||||
request.Meta?.GetRawText(),
|
||||
request.NodeVersion,
|
||||
nowMs);
|
||||
nowMs,
|
||||
token.Kind,
|
||||
tlsPublicKey);
|
||||
|
||||
Result<NodeRecord> created = await _store.CreateNodeAsync(node, ct);
|
||||
if (!created.TryGet(out NodeRecord? record, out FleetError? error))
|
||||
|
||||
Reference in new issue
Block a user