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
@@ -26,18 +26,35 @@ public static class TrustEndpoints
|
||||
.Returns<TrustStateDto>(StatusCodes.Status201Created).ProducesProblems(400, 401, 403, 409)
|
||||
.Requires(Capability.AccountsManage)
|
||||
.ElevationExempt();
|
||||
guarded.MapPost("/join-tokens/signed", CreateSignedTokenAsync)
|
||||
.Api("join_tokens_create_signed", "Create a join code from a grant the operator signed with a passkey; the browser derives the code from a seed the panel never sees", Tag)
|
||||
.Returns<JoinTokenDto>(StatusCodes.Status201Created).ProducesProblems(400, 401, 403, 409)
|
||||
.Requires(Capability.FleetEnroll)
|
||||
.ElevationExempt();
|
||||
}
|
||||
|
||||
private static async Task<IResult> StateAsync(HttpContext http, TrustService trust, BackendOptions options)
|
||||
private static async Task<IResult> StateAsync(HttpContext http, TrustService trust, ReleaseService releases, BackendOptions options)
|
||||
{
|
||||
TrustState state = await trust.StateAsync(AuthEndpoints.PasskeyContextOf(http, options), http.RequestAborted);
|
||||
http.Response.Headers.CacheControl = "no-store";
|
||||
return TypedResults.Json(Mappers.ToDto(state), ApiJsonContext.Default.TrustStateDto);
|
||||
string sh = InstallScripts.Sha256(InstallScripts.Sh);
|
||||
string ps1 = InstallScripts.Sha256(InstallScripts.Ps1);
|
||||
return TypedResults.Json(Mappers.ToDto(state, options.PublicUrl ?? $"{http.Request.Scheme}://{http.Request.Host}", sh, ps1, await MatchesReleaseAsync(releases, sh, ps1, http.RequestAborted)), ApiJsonContext.Default.TrustStateDto);
|
||||
}
|
||||
|
||||
/// <summary>面板提供的脚本和 CI 签名的发布清单里记的哈希是否一致(清单由 CI 签名,节点和浏览器之外的人改不了)。没有清单或没记哈希时返回 null。</summary>
|
||||
private static async Task<bool?> MatchesReleaseAsync(ReleaseService releases, string sh, string ps1, CancellationToken ct)
|
||||
{
|
||||
foreach (string component in new[] { JoinTokenService.AgentRole, JoinTokenService.ProbeRole })
|
||||
{
|
||||
if (await releases.GetAsync(component, ct) is not { } offer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using System.Text.Json.JsonDocument document = System.Text.Json.JsonDocument.Parse(offer.Manifest);
|
||||
if (document.RootElement.TryGetProperty("install_sh", out System.Text.Json.JsonElement recordedSh) && document.RootElement.TryGetProperty("install_ps1", out System.Text.Json.JsonElement recordedPs1))
|
||||
{
|
||||
return string.Equals(recordedSh.GetString(), sh, StringComparison.Ordinal) && string.Equals(recordedPs1.GetString(), ps1, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static async Task<IResult> CommitAsync(HttpContext http, SignedStatementDto signed, TrustService trust, AgentService agents, BackendOptions options)
|
||||
@@ -59,17 +76,4 @@ public static class TrustEndpoints
|
||||
http.SetDetail($"operator set v{state.Version}");
|
||||
return TypedResults.Json(Mappers.ToDto(state), ApiJsonContext.Default.TrustStateDto, statusCode: StatusCodes.Status201Created);
|
||||
}
|
||||
|
||||
private static async Task<IResult> CreateSignedTokenAsync(HttpContext http, CreateSignedJoinTokenRequest request, JoinTokenService tokens)
|
||||
{
|
||||
Result<JoinTokenInfo> result = await tokens.CreateSignedAsync(request.Grant, request.Note, request.Labels, request.AutoApprove ?? true, http.GetPrincipal().Label, http.RequestAborted);
|
||||
if (!result.TryGet(out JoinTokenInfo? info, out FleetError? error))
|
||||
{
|
||||
return Problems.From(http, error);
|
||||
}
|
||||
|
||||
http.SetDetail($"{JoinTokenService.RoleOf(info.Kind)}: {request.Note}");
|
||||
http.Response.Headers.CacheControl = "no-store";
|
||||
return TypedResults.Json(Mappers.ToDto(info), ApiJsonContext.Default.JoinTokenDto, statusCode: StatusCodes.Status201Created);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user