feat!: genesis-pinned operator set, server identity with an encrypted control link, key-less join codes; release key continuity
ci/woodpecker/tag/gitea-release Pipeline is pending
ci/woodpecker/tag/linux-release Pipeline failed
ci/woodpecker/tag/windows-release Pipeline was canceled

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:
justme committed 2026-10-08 03:21:44 +08:00
1 parent 9bb6841dce
commit a2f88a344b
89 files changed
+2145 -1195

No files matched your search

+270 -14
View File
@@ -867,6 +867,24 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/admin/trust": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** The operator trust root: genesis fingerprint, version, whether it matches the registered passkeys, and the next version to sign */
get: operations["trust_get"];
put?: never;
/** Store the next operator set, signed with a passkey (the first one sets the genesis and needs a credential window) */
post: operations["trust_commit"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/admin/api-tokens": {
parameters: {
query?: never;
@@ -1116,7 +1134,7 @@ export interface paths {
path?: never;
cookie?: never;
};
/** One-line installer for Linux and macOS nodes (agent or probe, decided by the code) */
/** The static one-line installer for Linux and macOS nodes (same bytes for everyone; its SHA-256 is published with each release) */
get: operations["install_sh"];
put?: never;
post?: never;
@@ -1126,6 +1144,40 @@ export interface paths {
patch?: never;
trace?: never;
};
"/install/manifest": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** The signed release manifest of the component an install code is for (exact signed bytes) */
get: operations["install_manifest"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/install/manifest.sig": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Base64 DER ECDSA P-256 signature of the manifest */
get: operations["install_manifest_signature"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/install.ps1": {
parameters: {
query?: never;
@@ -1133,7 +1185,7 @@ export interface paths {
path?: never;
cookie?: never;
};
/** One-line installer for Windows nodes (agent or probe, decided by the code) */
/** The static one-line installer for Windows nodes (same bytes for everyone; its SHA-256 is published with each release) */
get: operations["install_ps1"];
put?: never;
post?: never;
@@ -1160,6 +1212,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/agent/v1/enroll/trust": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** The operator set chain and the grant of a join token, for the node to verify before it enrolls */
post: operations["fleet_enroll_trust"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
@@ -1349,6 +1418,7 @@ export interface components {
/** Format: int32 */
expires_in_hours: null | number | string;
role?: null | string;
controllers?: null | string[];
};
CreateTaskRequest: {
scope: string;
@@ -1619,6 +1689,7 @@ export interface components {
user_id: null | string;
username: null | string;
csrf_token: null | string;
auth_method?: null | string;
};
MetricDto: {
id: string;
@@ -1999,6 +2070,13 @@ export interface components {
default: string;
description: string;
};
SignedStatementDto: {
statement?: string;
credential_id?: string;
authenticator_data?: string;
client_data_json?: string;
signature?: string;
};
StartTestRequestDto: {
plan: string;
params: null | Record<string, never>;
@@ -2085,6 +2163,32 @@ export interface components {
/** Format: date-time */
finished_at: unknown;
};
TrustProposalDto: {
statement: string;
/** Format: int32 */
version: number | string;
challenge: string;
};
TrustStateDto: {
initialized: boolean;
genesis: null | string;
/** Format: int32 */
version: number | string;
in_sync: boolean;
proposal: null | components["schemas"]["TrustProposalDto"];
/** @default */
install_url: string;
operators?: null | string[];
/** @default */
install_sh_sha256: string;
/** @default */
install_ps1_sha256: string;
install_matches_release?: null | boolean;
/** @default */
server_key: string;
/** @default false */
server_key_listed: boolean;
};
UserDto: {
id: string;
username: string;
@@ -5103,6 +5207,104 @@ export interface operations {
};
};
};
trust_get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["TrustStateDto"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
};
};
trust_commit: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["SignedStatementDto"];
};
};
responses: {
/** @description Created */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["TrustStateDto"];
};
};
/** @description Bad Request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
/** @description Conflict */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
};
};
api_tokens_list: {
parameters: {
query?: never;
@@ -5899,6 +6101,53 @@ export interface operations {
};
};
install_sh: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
install_manifest: {
parameters: {
query?: {
code?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
};
};
install_manifest_signature: {
parameters: {
query?: {
code?: string;
@@ -5929,9 +6178,7 @@ export interface operations {
};
install_ps1: {
parameters: {
query?: {
code?: string;
};
query?: never;
header?: never;
path?: never;
cookie?: never;
@@ -5945,15 +6192,6 @@ export interface operations {
};
content?: never;
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
};
};
fleet_enroll: {
@@ -5974,4 +6212,22 @@ export interface operations {
};
};
};
fleet_enroll_trust: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
}
+2
View File
@@ -42,6 +42,8 @@ export type CreatedApiTokenDto = Narrow<S['CreatedApiTokenDto']>
export type CapabilityCatalogDto = Narrow<S['CapabilityCatalogDto']>
export type PasskeyDto = Narrow<S['PasskeyDto']>
export type ElevationDto = Narrow<S['ElevationDto']>
export type TrustStateDto = Narrow<S['TrustStateDto']>
export type SignedStatementDto = Narrow<S['SignedStatementDto']>
export type PasskeyCreationOptionsDto = Narrow<S['PasskeyCreationOptionsDto']>
export type PasskeyRequestOptionsDto = Narrow<S['PasskeyRequestOptionsDto']>
export type MeDto = Narrow<S['MeDto']>
+25 -1
View File
@@ -1,5 +1,5 @@
import { api } from './client'
import type { LoginResponse, PasskeyCreationOptionsDto, PasskeyDto, PasskeyRequestOptionsDto } from './types'
import type { LoginResponse, PasskeyCreationOptionsDto, PasskeyDto, PasskeyRequestOptionsDto, SignedStatementDto } from './types'
/** 浏览器是否支持在输入框的自动填充里列出 passkey(conditional UI)。 */
export async function conditionalSupported(): Promise<boolean> {
@@ -58,6 +58,30 @@ export async function signInWithPasskey(opts: { conditional?: boolean; signal?:
})
}
/**
* 用 passkey 对一段文本签名(挑战是文本的 SHA-256):操作员集合、加入码的委托都是这样签出来的,节点自己验。
* 浏览器会让用户从这个站点的 passkey 里选一个;面板再检查它是不是操作员集合里的凭证。
*/
export async function signStatement(statement: string): Promise<SignedStatementDto> {
const challenge = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(statement)))
let credential: PublicKeyCredential | null
try {
credential = (await navigator.credentials.get({ publicKey: { challenge, userVerification: 'required', timeout: 120_000 } })) as PublicKeyCredential | null
} catch (e) {
if (e instanceof DOMException && (e.name === 'NotAllowedError' || e.name === 'AbortError')) throw new Error('已取消')
throw e
}
if (!credential) throw new Error('没有选择 passkey')
const response = credential.response as AuthenticatorAssertionResponse
return {
statement,
credential_id: credential.id,
authenticator_data: toText(response.authenticatorData),
client_data_json: toText(response.clientDataJSON),
signature: toText(response.signature),
}
}
/** 提权窗口的种类:加节点、吊销和删除、改凭证(用一次就没有)。 */
export type ElevationClass = 'enroll' | 'revoke' | 'credential'