diff --git a/AGENTS.md b/AGENTS.md index a0f4299..27d2b76 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,8 @@ woff2, strict CSP, no runtime CDN) for air-gap use. - `src/main.tsx` - entry; mounts the theme + auth providers and the router. - `src/App.tsx` - route table (Fleet `/`, Nodes `/nodes`, node detail `/nodes/:name`, Operators, - Agent keys `/agent-keys`, Audit, 404). The MCP sign-in consent page `/oauth/consent` is routed + Agent keys `/agent-keys`, Approvals `/approvals` (accepts `?id=` from the manager's + approval link), Audit, 404). The MCP sign-in consent page `/oauth/consent` is routed outside the auth gate on purpose: the manager authenticates that request with the browser's client certificate, and it talks to `/oauth2/consent/` with plain `fetch`, not Connect. - `src/components/layout/` - app shell: header, sidebar nav, wordmark, theme toggle, auth gate. @@ -49,3 +50,7 @@ woff2, strict CSP, no runtime CDN) for air-gap use. `errorCode()` from `src/lib/fleet/error-code.ts`, never on the message text, and add a code to its `ErrorCode` map when a page handles one. The Operators page does this for 1400 (no `operator_ca_node` configured) and shows a not-configured view instead of the refusal. +- The Approvals nav badge counts pending approvals through `listApprovals`. It recounts on every + navigation, every 30 seconds, and when `decideApproval` dispatches the `APPROVALS_CHANGED` + window event (`src/lib/approvals.ts`). A new surface that changes an approval should go through + `decideApproval` so the badge stays current. diff --git a/README.md b/README.md index 6a201ed..b3ea2f2 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ The Fleet Manager serves an MCP endpoint so AI agents can manage the fleet. An a - 🔑 **Sign-in consent** at `/oauth/consent?req=`. An MCP client's OAuth sign-in lands here in the operator's browser. The page shows the client's name, the loopback host the code returns to, and the operator certificate that will own the key, and lets the operator pick a level ceiling (up to their own level) and a label before choosing Approve or Deny. It sits outside the console's Log in step, because the manager authenticates the request with the certificate the browser presents. It talks to the manager's `/oauth2/consent/` endpoint as plain JSON on the same origin. - 🗝️ **Agent keys** (next to Operators) lists your keys with their client, ceiling, creation time, last use and status, and lets you revoke one. An admin can switch to every operator's keys. **Create key** mints a key for a client that cannot open the browser sign-in. The key is shown once, with a copy button, and is discarded when the dialog closes; the manager keeps only a hash of it. +- ✋ **Approvals** (next to Agent keys, with a count of pending requests) is where a person decides the step-up requests an agent raises before a tool that changes the fleet runs. It lists pending requests by default, and a status filter switches to approved, denied, expired, used or all. Each row shows the tool, a summary, who asked (CN), the agent key, the required level, when it was created and expires, and its status. **Approve** and **Deny** each ask for confirmation, repeating the approval ID and summary so they can be matched against what the agent showed you. The request digest (SHA-256 of the call's arguments) is shown as a detail; the agent never receives it, and the audit log records it. The buttons are disabled unless the request is pending and your level is at least the one it requires; the manager enforces the same rule, and an agent key can never list or decide an approval. The manager's approval link opens `/approvals?id=`, which shows every status and highlights that request. - 📜 **Audit** shows who acted (the certificate CN, and whether it acted directly or through an agent key), the surface it came through (web, mcp or api), the MCP tool and the outcome. Entries recorded before the manager captured an actor show a dash. A key is bound to the operator certificate that created it. It stops working when that certificate is revoked or renewed, and its effective level is the certificate's live level or its ceiling, whichever is lower. diff --git a/src/App.tsx b/src/App.tsx index af5d402..d213ef4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import { AppShell } from "@/components/layout/app-shell"; import { AuthGate } from "@/components/layout/auth-gate"; import { AdoptPage } from "@/pages/adopt"; import { AgentKeysPage } from "@/pages/agent-keys"; +import { ApprovalsPage } from "@/pages/approvals"; import { AuditPage } from "@/pages/audit"; import { CertificatesPage } from "@/pages/certificates"; import { DashboardPage } from "@/pages/dashboard"; @@ -71,6 +72,7 @@ export const App = () => { } path="audit" /> } path="operators" /> } path="agent-keys" /> + } path="approvals" /> } path="adopt" /> } path="certificates" /> } path="profiles" /> diff --git a/src/components/approval-decide-dialog.test.tsx b/src/components/approval-decide-dialog.test.tsx new file mode 100644 index 0000000..600ed84 --- /dev/null +++ b/src/components/approval-decide-dialog.test.tsx @@ -0,0 +1,106 @@ +/* +Copyright The CryptOS Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { ApprovalDecideDialog } from "@/components/approval-decide-dialog"; + +const decideApproval = vi.fn(); +vi.mock("@/lib/approvals", async (importOriginal) => ({ + ...(await importOriginal()), + decideApproval: (...a: unknown[]) => decideApproval(...a), +})); + +const digest = "0123456789abcdef".repeat(4); +const target = { + id: "ap-1", + requestDigest: digest, + requestedByCn: "operator@example.org", + requiredLevel: "operator", + summary: "Revoke certificate 0a1b on issuing-1", + tool: "cert_revoke", +}; + +describe("ApprovalDecideDialog", () => { + beforeEach(() => decideApproval.mockReset()); + + it("repeats the approval ID and summary to check, and the digest as an audit detail", async () => { + decideApproval.mockResolvedValue({ ...target, status: "approved" }); + const onDecided = vi.fn(); + const onClose = vi.fn(); + render( + , + ); + + const dialog = screen.getByRole("dialog"); + expect(dialog).toHaveTextContent("ap-1"); + expect(dialog).toHaveTextContent("Revoke certificate 0a1b on issuing-1"); + expect(dialog).toHaveTextContent( + "Check that the approval ID and summary match what the agent showed you.", + ); + expect(dialog).toHaveTextContent(digest); + expect(dialog).toHaveTextContent("Recorded in the audit log"); + expect(dialog).not.toHaveTextContent(/digest match/i); + expect(decideApproval).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole("button", { name: /^approve$/i })); + + await waitFor(() => expect(onDecided).toHaveBeenCalled()); + expect(decideApproval).toHaveBeenCalledWith({ approve: true, id: "ap-1" }); + expect(onClose).toHaveBeenCalled(); + }); + + it("denies through the same confirmation", async () => { + decideApproval.mockResolvedValue({ ...target, status: "denied" }); + const onDecided = vi.fn(); + render( + , + ); + + expect(screen.getByRole("dialog")).toHaveTextContent(digest); + fireEvent.click(screen.getByRole("button", { name: /^deny$/i })); + + await waitFor(() => expect(onDecided).toHaveBeenCalled()); + expect(decideApproval).toHaveBeenCalledWith({ approve: false, id: "ap-1" }); + }); + + it("cancels without deciding", () => { + const onClose = vi.fn(); + render(); + + fireEvent.click(screen.getByRole("button", { name: /cancel/i })); + + expect(onClose).toHaveBeenCalled(); + expect(decideApproval).not.toHaveBeenCalled(); + }); + + it("shows a refusal inline and stays open", async () => { + decideApproval.mockRejectedValueOnce(new Error("approval expired")); + const onClose = vi.fn(); + render(); + + fireEvent.click(screen.getByRole("button", { name: /^approve$/i })); + + expect(await screen.findByRole("alert")).toHaveTextContent("approval expired"); + expect(onClose).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/approval-decide-dialog.tsx b/src/components/approval-decide-dialog.tsx new file mode 100644 index 0000000..0ccba45 --- /dev/null +++ b/src/components/approval-decide-dialog.tsx @@ -0,0 +1,125 @@ +/* +Copyright The CryptOS Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { type ApprovalRow, decideApproval } from "@/lib/approvals"; + +// ApprovalDecideDialog confirms an approve or deny. It repeats the approval ID +// and summary, which the manager's pending_approval response also gives the +// agent, so the operator can match them against what the agent showed before +// letting the action through. The request digest is shown as a detail only: the +// agent never receives it, and the audit log records it with the decision. +export const ApprovalDecideDialog = ({ + approve, + onClose, + onDecided, + target, +}: { + approve: boolean; + onClose: () => void; + onDecided: () => void; + target: Pick< + ApprovalRow, + "id" | "requestDigest" | "requestedByCn" | "requiredLevel" | "summary" | "tool" + >; +}) => { + const [pending, setPending] = useState(false); + const [error, setError] = useState(null); + + const verb = approve ? "Approve" : "Deny"; + + const confirm = () => { + setPending(true); + setError(null); + decideApproval({ approve, id: target.id }) + .then(() => { + onDecided(); + onClose(); + }) + .catch((error_: unknown) => { + setError(error_ instanceof Error ? error_.message : `${verb} failed`); + setPending(false); + }); + }; + + return ( +
+
e.stopPropagation()} + role="dialog" + > +
+

+ {verb} request +

+

+ {target.tool} · {target.requestedByCn} · needs {target.requiredLevel} +

+
+
+

+ Approval ID +

+

{target.id}

+
+

{target.summary}

+
+

+ Request digest (SHA-256) +

+

+ {target.requestDigest} +

+

+ Recorded in the audit log with the decision and the agent's call. +

+
+

+ {approve + ? "The agent can run this exact request once. Check that the approval ID and summary match what the agent showed you." + : "The agent's request is refused and cannot be retried with this approval."} +

+ {error ? ( +

+ {error} +

+ ) : null} +
+ + +
+
+
+ ); +}; diff --git a/src/components/layout/top-nav.test.tsx b/src/components/layout/top-nav.test.tsx index 061e5b3..799831c 100644 --- a/src/components/layout/top-nav.test.tsx +++ b/src/components/layout/top-nav.test.tsx @@ -14,11 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { render, screen } from "@testing-library/react"; +import { act, render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; -import { describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { TopNav } from "@/components/layout/top-nav"; +import { APPROVALS_CHANGED } from "@/lib/approvals"; + +const listApprovals = vi.fn(); +vi.mock("@/lib/approvals", async (importOriginal) => ({ + ...(await importOriginal()), + listApprovals: (...a: unknown[]) => listApprovals(...a), +})); + +beforeEach(() => { + listApprovals.mockReset(); + listApprovals.mockResolvedValue([]); +}); describe("TopNav", () => { // Root before Nodes: that is the order the hierarchy reads, and /nodes @@ -69,4 +81,55 @@ describe("TopNav", () => { const hrefs = screen.getAllByRole("link").map((a) => a.getAttribute("href")); expect(hrefs.indexOf("/agent-keys")).toBe(hrefs.indexOf("/operators") + 1); }); + + it("places Approvals right after Agent keys", () => { + render( + + + , + ); + const hrefs = screen.getAllByRole("link").map((a) => a.getAttribute("href")); + expect(hrefs.indexOf("/approvals")).toBe(hrefs.indexOf("/agent-keys") + 1); + }); + + it("badges Approvals with the pending count", async () => { + listApprovals.mockResolvedValue([{ id: "ap-1" }, { id: "ap-2" }]); + render( + + + , + ); + + expect(await screen.findByLabelText("2 pending approvals")).toHaveTextContent("2"); + expect(listApprovals).toHaveBeenCalledWith({ status: "pending" }); + }); + + it("shows no badge when nothing is pending or the count cannot be read", async () => { + listApprovals.mockRejectedValue(new Error("manager unreachable")); + render( + + + , + ); + await act(async () => {}); + + expect(screen.queryByLabelText(/pending approvals/)).not.toBeInTheDocument(); + }); + + it("recounts after a decision", async () => { + listApprovals.mockResolvedValue([{ id: "ap-1" }, { id: "ap-2" }]); + render( + + + , + ); + await screen.findByLabelText("2 pending approvals"); + + listApprovals.mockResolvedValue([{ id: "ap-2" }]); + act(() => { + globalThis.dispatchEvent(new Event(APPROVALS_CHANGED)); + }); + + expect(await screen.findByLabelText("1 pending approval")).toHaveTextContent("1"); + }); }); diff --git a/src/components/layout/top-nav.tsx b/src/components/layout/top-nav.tsx index 61edf05..fdf3e19 100644 --- a/src/components/layout/top-nav.tsx +++ b/src/components/layout/top-nav.tsx @@ -14,10 +14,42 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { NavLink } from "react-router-dom"; +import { useCallback, useEffect, useState } from "react"; +import { NavLink, useLocation } from "react-router-dom"; +import { APPROVALS_CHANGED, listApprovals } from "@/lib/approvals"; import { cn } from "@/lib/utils"; +// New step-up requests arrive without any action in this tab, so the count is +// also refreshed on a slow timer, on every navigation, and after a decision. +const PENDING_REFRESH_MS = 30_000; + +const usePendingApprovals = (): number => { + const { pathname } = useLocation(); + const [count, setCount] = useState(0); + + const refresh = useCallback(() => { + listApprovals({ status: "pending" }) + .then((rows) => setCount(rows.length)) + .catch(() => setCount(0)); + }, []); + + useEffect(() => { + refresh(); + }, [refresh, pathname]); + + useEffect(() => { + const timer = setInterval(refresh, PENDING_REFRESH_MS); + globalThis.addEventListener(APPROVALS_CHANGED, refresh); + return () => { + clearInterval(timer); + globalThis.removeEventListener(APPROVALS_CHANGED, refresh); + }; + }, [refresh]); + + return count; +}; + const items: { end?: boolean; label: string; to: string }[] = [ { end: true, label: "Dashboard", to: "/" }, { label: "Fleet", to: "/fleet" }, @@ -33,10 +65,12 @@ const items: { end?: boolean; label: string; to: string }[] = [ { label: "Protocols", to: "/protocols" }, { label: "Operators", to: "/operators" }, { label: "Agent keys", to: "/agent-keys" }, + { label: "Approvals", to: "/approvals" }, { label: "Audit", to: "/audit" }, ]; export const TopNav = () => { + const pending = usePendingApprovals(); return ( diff --git a/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts b/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts index 3d595da..4386086 100644 --- a/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts +++ b/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts @@ -8,13 +8,15 @@ import type { CertificateProfile, MachineConfig } from "../../v1/config_pb"; import { file_cryptos_v1_config } from "../../v1/config_pb"; import type { InstallDisk } from "../../v1/node_pb"; import { file_cryptos_v1_node } from "../../v1/node_pb"; +import type { ProtocolStatus, ServiceProtocol } from "../../v1/status_pb"; +import { file_cryptos_v1_status } from "../../v1/status_pb"; import type { Message } from "@bufbuild/protobuf"; /** * Describes the file cryptos/fleet/v1/fleet.proto. */ export const file_cryptos_fleet_v1_fleet: GenFile = /*@__PURE__*/ - fileDesc("ChxjcnlwdG9zL2ZsZWV0L3YxL2ZsZWV0LnByb3RvEhBjcnlwdG9zLmZsZWV0LnYxIhIKEExpc3ROb2Rlc1JlcXVlc3QiQQoRTGlzdE5vZGVzUmVzcG9uc2USLAoFbm9kZXMYASADKAsyHS5jcnlwdG9zLmZsZWV0LnYxLk5vZGVTdW1tYXJ5Ih4KDkdldE5vZGVSZXF1ZXN0EgwKBG5hbWUYASABKAkiPQoPR2V0Tm9kZVJlc3BvbnNlEioKBG5vZGUYASABKAsyHC5jcnlwdG9zLmZsZWV0LnYxLk5vZGVEZXRhaWwiJwoXTGlzdENlcnRpZmljYXRlc1JlcXVlc3QSDAoEbm9kZRgBIAEoCSJPChhMaXN0Q2VydGlmaWNhdGVzUmVzcG9uc2USMwoMY2VydGlmaWNhdGVzGAEgAygLMh0uY3J5cHRvcy5mbGVldC52MS5DZXJ0aWZpY2F0ZSIVChNMaXN0UHJvZmlsZXNSZXF1ZXN0IkUKFExpc3RQcm9maWxlc1Jlc3BvbnNlEi0KBWl0ZW1zGAEgAygLMh4uY3J5cHRvcy52MS5DZXJ0aWZpY2F0ZVByb2ZpbGUiRwoUQ3JlYXRlUHJvZmlsZVJlcXVlc3QSLwoHcHJvZmlsZRgBIAEoCzIeLmNyeXB0b3MudjEuQ2VydGlmaWNhdGVQcm9maWxlIhcKFUNyZWF0ZVByb2ZpbGVSZXNwb25zZSJHChRVcGRhdGVQcm9maWxlUmVxdWVzdBIvCgdwcm9maWxlGAEgASgLMh4uY3J5cHRvcy52MS5DZXJ0aWZpY2F0ZVByb2ZpbGUiFwoVVXBkYXRlUHJvZmlsZVJlc3BvbnNlIiQKFERlbGV0ZVByb2ZpbGVSZXF1ZXN0EgwKBG5hbWUYASABKAkiFwoVRGVsZXRlUHJvZmlsZVJlc3BvbnNlIkQKGUFwcGx5UHJvZmlsZVRvTm9kZVJlcXVlc3QSEQoJbm9kZV9uYW1lGAEgASgJEhQKDHByb2ZpbGVfbmFtZRgCIAEoCSJJChpBcHBseVByb2ZpbGVUb05vZGVSZXNwb25zZRISCgpnZW5lcmF0aW9uGAEgASgEEhcKD3JlcXVpcmVzX3JlYm9vdBgCIAEoCCIVChNMaXN0QWRhcHRlcnNSZXF1ZXN0IkoKFExpc3RBZGFwdGVyc1Jlc3BvbnNlEjIKBWl0ZW1zGAEgAygLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50QWRhcHRlciI5ChhTZXRBZGFwdGVyRW5hYmxlZFJlcXVlc3QSDAoEbmFtZRgBIAEoCRIPCgdlbmFibGVkGAIgASgIIlEKGVNldEFkYXB0ZXJFbmFibGVkUmVzcG9uc2USNAoHYWRhcHRlchgBIAEoCzIjLmNyeXB0b3MuZmxlZXQudjEuRW5yb2xsbWVudEFkYXB0ZXIiEgoQTGlzdEF1ZGl0UmVxdWVzdCJAChFMaXN0QXVkaXRSZXNwb25zZRIrCgVpdGVtcxgBIAMoCzIcLmNyeXB0b3MuZmxlZXQudjEuQXVkaXRFdmVudCIYChZMaXN0RW5yb2xsbWVudHNSZXF1ZXN0Ik0KF0xpc3RFbnJvbGxtZW50c1Jlc3BvbnNlEjIKBWl0ZW1zGAEgAygLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50UmVxdWVzdCKvAQoLTm9kZVN1bW1hcnkSDAoEbmFtZRgBIAEoCRIPCgdhZGRyZXNzGAIgASgJEgwKBHJvbGUYAyABKAkSFgoOaWRlbnRpdHlfc3RhdGUYBCABKAkSCgoCY24YBSABKAkSDgoGaXNzdWVyGAYgASgJEigKBmhlYWx0aBgHIAEoDjIYLmNyeXB0b3MuZmxlZXQudjEuSGVhbHRoEhUKDWhlYWx0aF9kZXRhaWwYCCABKAkiSQoMTm9kZUlkZW50aXR5EhEKCWNoYWluX3BlbRgBIAEoCRIRCgljaGFpbl9kZXIYAiADKAwSEwoLbGVhZl9zaGEyNTYYAyABKAkiqQEKCk5vZGVEZXRhaWwSLgoHc3VtbWFyeRgBIAEoCzIdLmNyeXB0b3MuZmxlZXQudjEuTm9kZVN1bW1hcnkSMAoIaWRlbnRpdHkYAiABKAsyHi5jcnlwdG9zLmZsZWV0LnYxLk5vZGVJZGVudGl0eRIVCg10cG1fYXZhaWxhYmxlGAMgASgIEhIKCmJvb3RfY291bnQYBCABKAQSDgoGdXB0aW1lGAUgASgJIsABCgtDZXJ0aWZpY2F0ZRIOCgZzZXJpYWwYASABKAkSEgoKc3ViamVjdF9jbhgCIAEoCRITCgtpc3N1ZXJfbm9kZRgDIAEoCRIMCgRraW5kGAQgASgJEg4KBnN0YXR1cxgFIAEoCRISCgpub3RfYmVmb3JlGAYgASgJEhEKCW5vdF9hZnRlchgHIAEoCRIPCgdwcm9maWxlGAggASgJEhIKCnJldm9rZWRfYXQYCSABKAkSDgoGcmVhc29uGAogASgJIo0BChFFbnJvbGxtZW50QWRhcHRlchIMCgRraW5kGAEgASgJEgwKBG5hbWUYAiABKAkSEAoIZW5kcG9pbnQYAyABKAkSDwoHcHJvZmlsZRgEIAEoCRIPCgdlbmFibGVkGAUgASgIEhIKCmNoYWxsZW5nZXMYBiADKAkSFAoMZ3BvX3RlbXBsYXRlGAcgASgJIqsCCgpBdWRpdEV2ZW50EgoKAmlkGAEgASgJEgoKAmF0GAIgASgJEgwKBGtpbmQYAyABKAkSDwoHc3VtbWFyeRgEIAEoCRITCgt0YXJnZXRfa2luZBgFIAEoCRITCgt0YXJnZXRfcGF0aBgGIAEoCRISCgphY3Rvcl9raW5kGAcgASgJEhAKCGFjdG9yX2NuGAggASgJEhQKDGFjdG9yX3NlcmlhbBgJIAEoCRIOCgZrZXlfaWQYCiABKAkSCwoDdmlhGAsgASgJEgwKBHRvb2wYDCABKAkSFgoOcmVxdWVzdF9kaWdlc3QYDSABKAkSDwoHb3V0Y29tZRgOIAEoCRITCgthcHByb3ZhbF9pZBgPIAEoCRIXCg9hcHByb3Zlcl9zZXJpYWwYECABKAki1QIKEUVucm9sbG1lbnRSZXF1ZXN0EgoKAmlkGAEgASgJEhUKDXByb3Bvc2VkX25hbWUYAiABKAkSDAoEcm9sZRgDIAEoCRIRCglwYXJlbnRfY24YBCABKAkSDwoHYWRkcmVzcxgFIAEoCRIOCgZzdGF0dXMYBiABKAkSGwoTYXR0ZXN0YXRpb25fc3VtbWFyeRgHIAEoCRIbChNhdHRlc3RhdGlvbl9ub2RlX2lkGAggASgJEhQKDGNzcl9rZXlfdHlwZRgJIAEoCRIWCg5jc3Jfc3ViamVjdF9jbhgKIAEoCRIUCgxyZXF1ZXN0ZWRfYXQYCyABKAkSGAoQcmVqZWN0aW9uX3JlYXNvbhgMIAEoCRIaChJhZG1pdHRlZF9ub2RlX25hbWUYDSABKAkSDAoEa2luZBgOIAEoCRIZChFwaW5uZWRfa2V5X3NoYTI1NhgPIAEoCSK1AQoXQ3JlYXRlRW5yb2xsbWVudFJlcXVlc3QSDAoEa2luZBgBIAEoCRIVCg1ub2RlX2VuZHBvaW50GAIgASgJEhYKDmFkbWluX2NlcnRfcGVtGAMgASgJEhUKDWFkbWluX2tleV9wZW0YBCABKAkSDgoGY2FfcGVtGAUgASgJEhIKCmNoaWxkX25vZGUYBiABKAkSEQoJcGFyZW50X2NuGAcgASgJEg8KB3Byb2ZpbGUYCCABKAkiUwoYQ3JlYXRlRW5yb2xsbWVudFJlc3BvbnNlEjcKCmVucm9sbG1lbnQYASABKAsyIy5jcnlwdG9zLmZsZWV0LnYxLkVucm9sbG1lbnRSZXF1ZXN0InwKGEFwcHJvdmVFbnJvbGxtZW50UmVxdWVzdBIKCgJpZBgBIAEoCRIVCg1ub2RlX2VuZHBvaW50GAIgASgJEhYKDmFkbWluX2NlcnRfcGVtGAMgASgJEhUKDWFkbWluX2tleV9wZW0YBCABKAkSDgoGY2FfcGVtGAUgASgJIlQKGUFwcHJvdmVFbnJvbGxtZW50UmVzcG9uc2USNwoKZW5yb2xsbWVudBgBIAEoCzIjLmNyeXB0b3MuZmxlZXQudjEuRW5yb2xsbWVudFJlcXVlc3QiNQoXUmVqZWN0RW5yb2xsbWVudFJlcXVlc3QSCgoCaWQYASABKAkSDgoGcmVhc29uGAIgASgJIlMKGFJlamVjdEVucm9sbG1lbnRSZXNwb25zZRI3CgplbnJvbGxtZW50GAEgASgLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50UmVxdWVzdCIPCg1XaG9BbUlSZXF1ZXN0Ij0KEE9wZXJhdG9ySWRlbnRpdHkSCgoCY24YASABKAkSDgoGc2VyaWFsGAIgASgJEg0KBWxldmVsGAMgASgJIkYKDldob0FtSVJlc3BvbnNlEjQKCG9wZXJhdG9yGAEgASgLMiIuY3J5cHRvcy5mbGVldC52MS5PcGVyYXRvcklkZW50aXR5IlYKGFJldm9rZUNlcnRpZmljYXRlUmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSEgoKc2VyaWFsX2hleBgCIAEoCRITCgtyZWFzb25fY29kZRgDIAEoBSJYChlSZXZva2VDZXJ0aWZpY2F0ZVJlc3BvbnNlEhIKCnNlcmlhbF9oZXgYASABKAkSEgoKcmV2b2tlZF9hdBgCIAEoCRITCgtyZWFzb25fY29kZRgDIAEoBSJMChBJc3N1ZUxlYWZSZXF1ZXN0EhEKCW5vZGVfbmFtZRgBIAEoCRIPCgdjc3JfZGVyGAIgASgMEhQKDHByb2ZpbGVfbmFtZRgDIAEoCSIlChFJc3N1ZUxlYWZSZXNwb25zZRIQCghjZXJ0X2RlchgBIAEoDCI7ChBSZWtleU5vZGVSZXF1ZXN0EhEKCW5vZGVfbmFtZRgBIAEoCRIUCgxwcm9maWxlX25hbWUYAiABKAkiTQoRUmVrZXlOb2RlUmVzcG9uc2USEgoKc3ViamVjdF9jbhgBIAEoCRIRCglpc3N1ZXJfY24YAiABKAkSEQoJY2hhaW5fbGVuGAMgASgFIikKFEdldE5vZGVDb25maWdSZXF1ZXN0EhEKCW5vZGVfbmFtZRgBIAEoCSJCChVHZXROb2RlQ29uZmlnUmVzcG9uc2USKQoGY29uZmlnGAEgASgLMhkuY3J5cHRvcy52MS5NYWNoaW5lQ29uZmlnIlYKFkFwcGx5Tm9kZUNvbmZpZ1JlcXVlc3QSEQoJbm9kZV9uYW1lGAEgASgJEikKBmNvbmZpZxgCIAEoCzIZLmNyeXB0b3MudjEuTWFjaGluZUNvbmZpZyJGChdBcHBseU5vZGVDb25maWdSZXNwb25zZRISCgpnZW5lcmF0aW9uGAEgASgEEhcKD3JlcXVpcmVzX3JlYm9vdBgCIAEoCCI7ChJFeHBvcnRDQUtleVJlcXVlc3QSEQoJbm9kZV9uYW1lGAEgASgJEhIKCnBhc3NwaHJhc2UYAiABKAwiJwoTRXhwb3J0Q0FLZXlSZXNwb25zZRIQCghlbnZlbG9wZRgBIAEoDCJNChJJbXBvcnRDQUtleVJlcXVlc3QSEQoJbm9kZV9uYW1lGAEgASgJEhAKCGVudmVsb3BlGAIgASgMEhIKCnBhc3NwaHJhc2UYAyABKAwiPAoTSW1wb3J0Q0FLZXlSZXNwb25zZRISCgpzdWJqZWN0X2NuGAEgASgJEhEKCWlzc3Vlcl9jbhgCIAEoCSJVCh5Jc3N1ZU9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3QSEwoLY29tbW9uX25hbWUYASABKAkSDQoFbGV2ZWwYAiABKAkSDwoHY3NyX2RlchgDIAEoDCJHCh9Jc3N1ZU9wZXJhdG9yQ3JlZGVudGlhbFJlc3BvbnNlEhAKCGNlcnRfZGVyGAEgASgMEhIKCnNlcmlhbF9oZXgYAiABKAkiSgofUmV2b2tlT3BlcmF0b3JDcmVkZW50aWFsUmVxdWVzdBISCgpzZXJpYWxfaGV4GAEgASgJEhMKC3JlYXNvbl9jb2RlGAIgASgFIkoKIFJldm9rZU9wZXJhdG9yQ3JlZGVudGlhbFJlc3BvbnNlEhIKCnNlcmlhbF9oZXgYASABKAkSEgoKcmV2b2tlZF9hdBgCIAEoCSIgCh5MaXN0T3BlcmF0b3JDcmVkZW50aWFsc1JlcXVlc3QicAoST3BlcmF0b3JDcmVkZW50aWFsEhMKC2NvbW1vbl9uYW1lGAEgASgJEhIKCnNlcmlhbF9oZXgYAiABKAkSDQoFbGV2ZWwYAyABKAkSEQoJbm90X2FmdGVyGAQgASgJEg8KB3Jldm9rZWQYBSABKAgiVgofTGlzdE9wZXJhdG9yQ3JlZGVudGlhbHNSZXNwb25zZRIzCgVpdGVtcxgBIAMoCzIkLmNyeXB0b3MuZmxlZXQudjEuT3BlcmF0b3JDcmVkZW50aWFsIioKFlByZXZpZXdBZG9wdGlvblJlcXVlc3QSEAoIZW5kcG9pbnQYASABKAkiPwoXUHJldmlld0Fkb3B0aW9uUmVzcG9uc2USEwoLY2VydF9zaGEyNTYYASABKAkSDwoHc3ViamVjdBgCIAEoCSJHChdMaXN0SW5zdGFsbERpc2tzUmVxdWVzdBIQCghlbmRwb2ludBgBIAEoCRIaChJwaW5uZWRfY2VydF9zaGEyNTYYAiABKAkiQgoYTGlzdEluc3RhbGxEaXNrc1Jlc3BvbnNlEiYKBWRpc2tzGAEgAygLMhcuY3J5cHRvcy52MS5JbnN0YWxsRGlzayJrChBBZG9wdE5vZGVSZXF1ZXN0EhAKCGVuZHBvaW50GAEgASgJEhoKEnBpbm5lZF9jZXJ0X3NoYTI1NhgCIAEoCRIpCgZjb25maWcYAyABKAsyGS5jcnlwdG9zLnYxLk1hY2hpbmVDb25maWciQAoRQWRvcHROb2RlUmVzcG9uc2USDQoFcGhhc2UYASABKAkSDgoGZGV0YWlsGAIgASgJEgwKBGRvbmUYAyABKAgiSQoXRGVjb21taXNzaW9uTm9kZVJlcXVlc3QSEQoJbm9kZV9uYW1lGAEgASgJEhsKE2NvbmZpcm1fY29tbW9uX25hbWUYAiABKAkiLQoYRGVjb21taXNzaW9uTm9kZVJlc3BvbnNlEhEKCXJlYm9vdGluZxgBIAEoCCIhChJMaXN0TWNwS2V5c1JlcXVlc3QSCwoDYWxsGAEgASgIIj4KE0xpc3RNY3BLZXlzUmVzcG9uc2USJwoFaXRlbXMYASADKAsyGC5jcnlwdG9zLmZsZWV0LnYxLk1jcEtleSIhChNSZXZva2VNY3BLZXlSZXF1ZXN0EgoKAmlkGAEgASgJIkEKFFJldm9rZU1jcEtleVJlc3BvbnNlEikKB21jcF9rZXkYASABKAsyGC5jcnlwdG9zLmZsZWV0LnYxLk1jcEtleSI7ChNDcmVhdGVNY3BLZXlSZXF1ZXN0Eg0KBWxhYmVsGAEgASgJEhUKDWxldmVsX2NlaWxpbmcYAiABKAkiWAoUQ3JlYXRlTWNwS2V5UmVzcG9uc2USFQoNcGxhaW50ZXh0X2tleRgBIAEoCRIpCgdtY3Bfa2V5GAIgASgLMhguY3J5cHRvcy5mbGVldC52MS5NY3BLZXkiuwEKBk1jcEtleRIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRITCgtjbGllbnRfbmFtZRgDIAEoCRITCgtvcGVyYXRvcl9jbhgEIAEoCRIXCg9vcGVyYXRvcl9zZXJpYWwYBSABKAkSFQoNbGV2ZWxfY2VpbGluZxgGIAEoCRISCgpjcmVhdGVkX2F0GAcgASgJEhQKDGxhc3RfdXNlZF9hdBgIIAEoCRISCgpyZXZva2VkX2F0GAkgASgJKlIKBkhlYWx0aBIWChJIRUFMVEhfVU5TUEVDSUZJRUQQABINCglIRUFMVEhfVVAQARIPCgtIRUFMVEhfRE9XThACEhAKDEhFQUxUSF9FUlJPUhADMoEaCgxGbGVldFNlcnZpY2USVAoJTGlzdE5vZGVzEiIuY3J5cHRvcy5mbGVldC52MS5MaXN0Tm9kZXNSZXF1ZXN0GiMuY3J5cHRvcy5mbGVldC52MS5MaXN0Tm9kZXNSZXNwb25zZRJOCgdHZXROb2RlEiAuY3J5cHRvcy5mbGVldC52MS5HZXROb2RlUmVxdWVzdBohLmNyeXB0b3MuZmxlZXQudjEuR2V0Tm9kZVJlc3BvbnNlEmkKEExpc3RDZXJ0aWZpY2F0ZXMSKS5jcnlwdG9zLmZsZWV0LnYxLkxpc3RDZXJ0aWZpY2F0ZXNSZXF1ZXN0GiouY3J5cHRvcy5mbGVldC52MS5MaXN0Q2VydGlmaWNhdGVzUmVzcG9uc2USXQoMTGlzdFByb2ZpbGVzEiUuY3J5cHRvcy5mbGVldC52MS5MaXN0UHJvZmlsZXNSZXF1ZXN0GiYuY3J5cHRvcy5mbGVldC52MS5MaXN0UHJvZmlsZXNSZXNwb25zZRJgCg1DcmVhdGVQcm9maWxlEiYuY3J5cHRvcy5mbGVldC52MS5DcmVhdGVQcm9maWxlUmVxdWVzdBonLmNyeXB0b3MuZmxlZXQudjEuQ3JlYXRlUHJvZmlsZVJlc3BvbnNlEmAKDVVwZGF0ZVByb2ZpbGUSJi5jcnlwdG9zLmZsZWV0LnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0GicuY3J5cHRvcy5mbGVldC52MS5VcGRhdGVQcm9maWxlUmVzcG9uc2USYAoNRGVsZXRlUHJvZmlsZRImLmNyeXB0b3MuZmxlZXQudjEuRGVsZXRlUHJvZmlsZVJlcXVlc3QaJy5jcnlwdG9zLmZsZWV0LnYxLkRlbGV0ZVByb2ZpbGVSZXNwb25zZRJvChJBcHBseVByb2ZpbGVUb05vZGUSKy5jcnlwdG9zLmZsZWV0LnYxLkFwcGx5UHJvZmlsZVRvTm9kZVJlcXVlc3QaLC5jcnlwdG9zLmZsZWV0LnYxLkFwcGx5UHJvZmlsZVRvTm9kZVJlc3BvbnNlEl0KDExpc3RBZGFwdGVycxIlLmNyeXB0b3MuZmxlZXQudjEuTGlzdEFkYXB0ZXJzUmVxdWVzdBomLmNyeXB0b3MuZmxlZXQudjEuTGlzdEFkYXB0ZXJzUmVzcG9uc2USbAoRU2V0QWRhcHRlckVuYWJsZWQSKi5jcnlwdG9zLmZsZWV0LnYxLlNldEFkYXB0ZXJFbmFibGVkUmVxdWVzdBorLmNyeXB0b3MuZmxlZXQudjEuU2V0QWRhcHRlckVuYWJsZWRSZXNwb25zZRJUCglMaXN0QXVkaXQSIi5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBdWRpdFJlcXVlc3QaIy5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBdWRpdFJlc3BvbnNlEmYKD0xpc3RFbnJvbGxtZW50cxIoLmNyeXB0b3MuZmxlZXQudjEuTGlzdEVucm9sbG1lbnRzUmVxdWVzdBopLmNyeXB0b3MuZmxlZXQudjEuTGlzdEVucm9sbG1lbnRzUmVzcG9uc2USaQoQQ3JlYXRlRW5yb2xsbWVudBIpLmNyeXB0b3MuZmxlZXQudjEuQ3JlYXRlRW5yb2xsbWVudFJlcXVlc3QaKi5jcnlwdG9zLmZsZWV0LnYxLkNyZWF0ZUVucm9sbG1lbnRSZXNwb25zZRJsChFBcHByb3ZlRW5yb2xsbWVudBIqLmNyeXB0b3MuZmxlZXQudjEuQXBwcm92ZUVucm9sbG1lbnRSZXF1ZXN0GisuY3J5cHRvcy5mbGVldC52MS5BcHByb3ZlRW5yb2xsbWVudFJlc3BvbnNlEmkKEFJlamVjdEVucm9sbG1lbnQSKS5jcnlwdG9zLmZsZWV0LnYxLlJlamVjdEVucm9sbG1lbnRSZXF1ZXN0GiouY3J5cHRvcy5mbGVldC52MS5SZWplY3RFbnJvbGxtZW50UmVzcG9uc2USSwoGV2hvQW1JEh8uY3J5cHRvcy5mbGVldC52MS5XaG9BbUlSZXF1ZXN0GiAuY3J5cHRvcy5mbGVldC52MS5XaG9BbUlSZXNwb25zZRJsChFSZXZva2VDZXJ0aWZpY2F0ZRIqLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlQ2VydGlmaWNhdGVSZXF1ZXN0GisuY3J5cHRvcy5mbGVldC52MS5SZXZva2VDZXJ0aWZpY2F0ZVJlc3BvbnNlElQKCUlzc3VlTGVhZhIiLmNyeXB0b3MuZmxlZXQudjEuSXNzdWVMZWFmUmVxdWVzdBojLmNyeXB0b3MuZmxlZXQudjEuSXNzdWVMZWFmUmVzcG9uc2USVAoJUmVrZXlOb2RlEiIuY3J5cHRvcy5mbGVldC52MS5SZWtleU5vZGVSZXF1ZXN0GiMuY3J5cHRvcy5mbGVldC52MS5SZWtleU5vZGVSZXNwb25zZRJgCg1HZXROb2RlQ29uZmlnEiYuY3J5cHRvcy5mbGVldC52MS5HZXROb2RlQ29uZmlnUmVxdWVzdBonLmNyeXB0b3MuZmxlZXQudjEuR2V0Tm9kZUNvbmZpZ1Jlc3BvbnNlEmYKD0FwcGx5Tm9kZUNvbmZpZxIoLmNyeXB0b3MuZmxlZXQudjEuQXBwbHlOb2RlQ29uZmlnUmVxdWVzdBopLmNyeXB0b3MuZmxlZXQudjEuQXBwbHlOb2RlQ29uZmlnUmVzcG9uc2USWgoLRXhwb3J0Q0FLZXkSJC5jcnlwdG9zLmZsZWV0LnYxLkV4cG9ydENBS2V5UmVxdWVzdBolLmNyeXB0b3MuZmxlZXQudjEuRXhwb3J0Q0FLZXlSZXNwb25zZRJaCgtJbXBvcnRDQUtleRIkLmNyeXB0b3MuZmxlZXQudjEuSW1wb3J0Q0FLZXlSZXF1ZXN0GiUuY3J5cHRvcy5mbGVldC52MS5JbXBvcnRDQUtleVJlc3BvbnNlEn4KF0lzc3VlT3BlcmF0b3JDcmVkZW50aWFsEjAuY3J5cHRvcy5mbGVldC52MS5Jc3N1ZU9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3QaMS5jcnlwdG9zLmZsZWV0LnYxLklzc3VlT3BlcmF0b3JDcmVkZW50aWFsUmVzcG9uc2USgQEKGFJldm9rZU9wZXJhdG9yQ3JlZGVudGlhbBIxLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlT3BlcmF0b3JDcmVkZW50aWFsUmVxdWVzdBoyLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlT3BlcmF0b3JDcmVkZW50aWFsUmVzcG9uc2USfgoXTGlzdE9wZXJhdG9yQ3JlZGVudGlhbHMSMC5jcnlwdG9zLmZsZWV0LnYxLkxpc3RPcGVyYXRvckNyZWRlbnRpYWxzUmVxdWVzdBoxLmNyeXB0b3MuZmxlZXQudjEuTGlzdE9wZXJhdG9yQ3JlZGVudGlhbHNSZXNwb25zZRJmCg9QcmV2aWV3QWRvcHRpb24SKC5jcnlwdG9zLmZsZWV0LnYxLlByZXZpZXdBZG9wdGlvblJlcXVlc3QaKS5jcnlwdG9zLmZsZWV0LnYxLlByZXZpZXdBZG9wdGlvblJlc3BvbnNlEmkKEExpc3RJbnN0YWxsRGlza3MSKS5jcnlwdG9zLmZsZWV0LnYxLkxpc3RJbnN0YWxsRGlza3NSZXF1ZXN0GiouY3J5cHRvcy5mbGVldC52MS5MaXN0SW5zdGFsbERpc2tzUmVzcG9uc2USVgoJQWRvcHROb2RlEiIuY3J5cHRvcy5mbGVldC52MS5BZG9wdE5vZGVSZXF1ZXN0GiMuY3J5cHRvcy5mbGVldC52MS5BZG9wdE5vZGVSZXNwb25zZTABEmkKEERlY29tbWlzc2lvbk5vZGUSKS5jcnlwdG9zLmZsZWV0LnYxLkRlY29tbWlzc2lvbk5vZGVSZXF1ZXN0GiouY3J5cHRvcy5mbGVldC52MS5EZWNvbW1pc3Npb25Ob2RlUmVzcG9uc2USWgoLTGlzdE1jcEtleXMSJC5jcnlwdG9zLmZsZWV0LnYxLkxpc3RNY3BLZXlzUmVxdWVzdBolLmNyeXB0b3MuZmxlZXQudjEuTGlzdE1jcEtleXNSZXNwb25zZRJdCgxSZXZva2VNY3BLZXkSJS5jcnlwdG9zLmZsZWV0LnYxLlJldm9rZU1jcEtleVJlcXVlc3QaJi5jcnlwdG9zLmZsZWV0LnYxLlJldm9rZU1jcEtleVJlc3BvbnNlEl0KDENyZWF0ZU1jcEtleRIlLmNyeXB0b3MuZmxlZXQudjEuQ3JlYXRlTWNwS2V5UmVxdWVzdBomLmNyeXB0b3MuZmxlZXQudjEuQ3JlYXRlTWNwS2V5UmVzcG9uc2VCOFo2Z2l0aHViLmNvbS9DcnlwdE9TLVBLSS9hcGkvZ28vY3J5cHRvcy9mbGVldC92MTtmbGVldHYxYgZwcm90bzM", [file_cryptos_v1_config, file_cryptos_v1_node]); + fileDesc("ChxjcnlwdG9zL2ZsZWV0L3YxL2ZsZWV0LnByb3RvEhBjcnlwdG9zLmZsZWV0LnYxIhIKEExpc3ROb2Rlc1JlcXVlc3QiQQoRTGlzdE5vZGVzUmVzcG9uc2USLAoFbm9kZXMYASADKAsyHS5jcnlwdG9zLmZsZWV0LnYxLk5vZGVTdW1tYXJ5Ii8KDkdldE5vZGVSZXF1ZXN0EgwKBG5hbWUYASABKAkSDwoHbm9kZV9pZBgCIAEoCSI9Cg9HZXROb2RlUmVzcG9uc2USKgoEbm9kZRgBIAEoCzIcLmNyeXB0b3MuZmxlZXQudjEuTm9kZURldGFpbCI4ChdMaXN0Q2VydGlmaWNhdGVzUmVxdWVzdBIMCgRub2RlGAEgASgJEg8KB25vZGVfaWQYAiABKAkiTwoYTGlzdENlcnRpZmljYXRlc1Jlc3BvbnNlEjMKDGNlcnRpZmljYXRlcxgBIAMoCzIdLmNyeXB0b3MuZmxlZXQudjEuQ2VydGlmaWNhdGUiFQoTTGlzdFByb2ZpbGVzUmVxdWVzdCJFChRMaXN0UHJvZmlsZXNSZXNwb25zZRItCgVpdGVtcxgBIAMoCzIeLmNyeXB0b3MudjEuQ2VydGlmaWNhdGVQcm9maWxlIkcKFENyZWF0ZVByb2ZpbGVSZXF1ZXN0Ei8KB3Byb2ZpbGUYASABKAsyHi5jcnlwdG9zLnYxLkNlcnRpZmljYXRlUHJvZmlsZSIXChVDcmVhdGVQcm9maWxlUmVzcG9uc2UiRwoUVXBkYXRlUHJvZmlsZVJlcXVlc3QSLwoHcHJvZmlsZRgBIAEoCzIeLmNyeXB0b3MudjEuQ2VydGlmaWNhdGVQcm9maWxlIhcKFVVwZGF0ZVByb2ZpbGVSZXNwb25zZSIkChREZWxldGVQcm9maWxlUmVxdWVzdBIMCgRuYW1lGAEgASgJIhcKFURlbGV0ZVByb2ZpbGVSZXNwb25zZSJVChlBcHBseVByb2ZpbGVUb05vZGVSZXF1ZXN0EhEKCW5vZGVfbmFtZRgBIAEoCRIUCgxwcm9maWxlX25hbWUYAiABKAkSDwoHbm9kZV9pZBgDIAEoCSJJChpBcHBseVByb2ZpbGVUb05vZGVSZXNwb25zZRISCgpnZW5lcmF0aW9uGAEgASgEEhcKD3JlcXVpcmVzX3JlYm9vdBgCIAEoCCIVChNMaXN0QWRhcHRlcnNSZXF1ZXN0IkoKFExpc3RBZGFwdGVyc1Jlc3BvbnNlEjIKBWl0ZW1zGAEgAygLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50QWRhcHRlciI5ChhTZXRBZGFwdGVyRW5hYmxlZFJlcXVlc3QSDAoEbmFtZRgBIAEoCRIPCgdlbmFibGVkGAIgASgIIlEKGVNldEFkYXB0ZXJFbmFibGVkUmVzcG9uc2USNAoHYWRhcHRlchgBIAEoCzIjLmNyeXB0b3MuZmxlZXQudjEuRW5yb2xsbWVudEFkYXB0ZXIiEgoQTGlzdEF1ZGl0UmVxdWVzdCJAChFMaXN0QXVkaXRSZXNwb25zZRIrCgVpdGVtcxgBIAMoCzIcLmNyeXB0b3MuZmxlZXQudjEuQXVkaXRFdmVudCIYChZMaXN0RW5yb2xsbWVudHNSZXF1ZXN0Ik0KF0xpc3RFbnJvbGxtZW50c1Jlc3BvbnNlEjIKBWl0ZW1zGAEgAygLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50UmVxdWVzdCKDAgoLTm9kZVN1bW1hcnkSDAoEbmFtZRgBIAEoCRIPCgdhZGRyZXNzGAIgASgJEgwKBHJvbGUYAyABKAkSFgoOaWRlbnRpdHlfc3RhdGUYBCABKAkSCgoCY24YBSABKAkSDgoGaXNzdWVyGAYgASgJEigKBmhlYWx0aBgHIAEoDjIYLmNyeXB0b3MuZmxlZXQudjEuSGVhbHRoEhUKDWhlYWx0aF9kZXRhaWwYCCABKAkSCgoCaWQYCSABKAkSLQoJcHJvdG9jb2xzGAogAygLMhouY3J5cHRvcy52MS5Qcm90b2NvbFN0YXR1cxIXCg9yZWJvb3RfcmVxdWlyZWQYCyABKAgiSQoMTm9kZUlkZW50aXR5EhEKCWNoYWluX3BlbRgBIAEoCRIRCgljaGFpbl9kZXIYAiADKAwSEwoLbGVhZl9zaGEyNTYYAyABKAkiqQEKCk5vZGVEZXRhaWwSLgoHc3VtbWFyeRgBIAEoCzIdLmNyeXB0b3MuZmxlZXQudjEuTm9kZVN1bW1hcnkSMAoIaWRlbnRpdHkYAiABKAsyHi5jcnlwdG9zLmZsZWV0LnYxLk5vZGVJZGVudGl0eRIVCg10cG1fYXZhaWxhYmxlGAMgASgIEhIKCmJvb3RfY291bnQYBCABKAQSDgoGdXB0aW1lGAUgASgJItgBCgtDZXJ0aWZpY2F0ZRIOCgZzZXJpYWwYASABKAkSEgoKc3ViamVjdF9jbhgCIAEoCRITCgtpc3N1ZXJfbm9kZRgDIAEoCRIMCgRraW5kGAQgASgJEg4KBnN0YXR1cxgFIAEoCRISCgpub3RfYmVmb3JlGAYgASgJEhEKCW5vdF9hZnRlchgHIAEoCRIPCgdwcm9maWxlGAggASgJEhIKCnJldm9rZWRfYXQYCSABKAkSDgoGcmVhc29uGAogASgJEhYKDmlzc3Vlcl9ub2RlX2lkGAsgASgJIo0BChFFbnJvbGxtZW50QWRhcHRlchIMCgRraW5kGAEgASgJEgwKBG5hbWUYAiABKAkSEAoIZW5kcG9pbnQYAyABKAkSDwoHcHJvZmlsZRgEIAEoCRIPCgdlbmFibGVkGAUgASgIEhIKCmNoYWxsZW5nZXMYBiADKAkSFAoMZ3BvX3RlbXBsYXRlGAcgASgJIrwCCgpBdWRpdEV2ZW50EgoKAmlkGAEgASgJEgoKAmF0GAIgASgJEgwKBGtpbmQYAyABKAkSDwoHc3VtbWFyeRgEIAEoCRITCgt0YXJnZXRfa2luZBgFIAEoCRITCgt0YXJnZXRfcGF0aBgGIAEoCRISCgphY3Rvcl9raW5kGAcgASgJEhAKCGFjdG9yX2NuGAggASgJEhQKDGFjdG9yX3NlcmlhbBgJIAEoCRIOCgZrZXlfaWQYCiABKAkSCwoDdmlhGAsgASgJEgwKBHRvb2wYDCABKAkSFgoOcmVxdWVzdF9kaWdlc3QYDSABKAkSDwoHb3V0Y29tZRgOIAEoCRITCgthcHByb3ZhbF9pZBgPIAEoCRIXCg9hcHByb3Zlcl9zZXJpYWwYECABKAkSDwoHbm9kZV9pZBgRIAEoCSLvAgoRRW5yb2xsbWVudFJlcXVlc3QSCgoCaWQYASABKAkSFQoNcHJvcG9zZWRfbmFtZRgCIAEoCRIMCgRyb2xlGAMgASgJEhEKCXBhcmVudF9jbhgEIAEoCRIPCgdhZGRyZXNzGAUgASgJEg4KBnN0YXR1cxgGIAEoCRIbChNhdHRlc3RhdGlvbl9zdW1tYXJ5GAcgASgJEhsKE2F0dGVzdGF0aW9uX25vZGVfaWQYCCABKAkSFAoMY3NyX2tleV90eXBlGAkgASgJEhYKDmNzcl9zdWJqZWN0X2NuGAogASgJEhQKDHJlcXVlc3RlZF9hdBgLIAEoCRIYChByZWplY3Rpb25fcmVhc29uGAwgASgJEhoKEmFkbWl0dGVkX25vZGVfbmFtZRgNIAEoCRIMCgRraW5kGA4gASgJEhkKEXBpbm5lZF9rZXlfc2hhMjU2GA8gASgJEhgKEGFkbWl0dGVkX25vZGVfaWQYECABKAkizAEKF0NyZWF0ZUVucm9sbG1lbnRSZXF1ZXN0EgwKBGtpbmQYASABKAkSFQoNbm9kZV9lbmRwb2ludBgCIAEoCRIWCg5hZG1pbl9jZXJ0X3BlbRgDIAEoCRIVCg1hZG1pbl9rZXlfcGVtGAQgASgJEg4KBmNhX3BlbRgFIAEoCRISCgpjaGlsZF9ub2RlGAYgASgJEhEKCXBhcmVudF9jbhgHIAEoCRIPCgdwcm9maWxlGAggASgJEhUKDWNoaWxkX25vZGVfaWQYCSABKAkiUwoYQ3JlYXRlRW5yb2xsbWVudFJlc3BvbnNlEjcKCmVucm9sbG1lbnQYASABKAsyIy5jcnlwdG9zLmZsZWV0LnYxLkVucm9sbG1lbnRSZXF1ZXN0InwKGEFwcHJvdmVFbnJvbGxtZW50UmVxdWVzdBIKCgJpZBgBIAEoCRIVCg1ub2RlX2VuZHBvaW50GAIgASgJEhYKDmFkbWluX2NlcnRfcGVtGAMgASgJEhUKDWFkbWluX2tleV9wZW0YBCABKAkSDgoGY2FfcGVtGAUgASgJIlQKGUFwcHJvdmVFbnJvbGxtZW50UmVzcG9uc2USNwoKZW5yb2xsbWVudBgBIAEoCzIjLmNyeXB0b3MuZmxlZXQudjEuRW5yb2xsbWVudFJlcXVlc3QiNQoXUmVqZWN0RW5yb2xsbWVudFJlcXVlc3QSCgoCaWQYASABKAkSDgoGcmVhc29uGAIgASgJIlMKGFJlamVjdEVucm9sbG1lbnRSZXNwb25zZRI3CgplbnJvbGxtZW50GAEgASgLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50UmVxdWVzdCIPCg1XaG9BbUlSZXF1ZXN0Ij0KEE9wZXJhdG9ySWRlbnRpdHkSCgoCY24YASABKAkSDgoGc2VyaWFsGAIgASgJEg0KBWxldmVsGAMgASgJIkYKDldob0FtSVJlc3BvbnNlEjQKCG9wZXJhdG9yGAEgASgLMiIuY3J5cHRvcy5mbGVldC52MS5PcGVyYXRvcklkZW50aXR5ImcKGFJldm9rZUNlcnRpZmljYXRlUmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSEgoKc2VyaWFsX2hleBgCIAEoCRITCgtyZWFzb25fY29kZRgDIAEoBRIPCgdub2RlX2lkGAQgASgJIlgKGVJldm9rZUNlcnRpZmljYXRlUmVzcG9uc2USEgoKc2VyaWFsX2hleBgBIAEoCRISCgpyZXZva2VkX2F0GAIgASgJEhMKC3JlYXNvbl9jb2RlGAMgASgFIl0KEElzc3VlTGVhZlJlcXVlc3QSEQoJbm9kZV9uYW1lGAEgASgJEg8KB2Nzcl9kZXIYAiABKAwSFAoMcHJvZmlsZV9uYW1lGAMgASgJEg8KB25vZGVfaWQYBCABKAkiJQoRSXNzdWVMZWFmUmVzcG9uc2USEAoIY2VydF9kZXIYASABKAwiTAoQUmVrZXlOb2RlUmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSFAoMcHJvZmlsZV9uYW1lGAIgASgJEg8KB25vZGVfaWQYAyABKAkiTQoRUmVrZXlOb2RlUmVzcG9uc2USEgoKc3ViamVjdF9jbhgBIAEoCRIRCglpc3N1ZXJfY24YAiABKAkSEQoJY2hhaW5fbGVuGAMgASgFIjoKFEdldE5vZGVDb25maWdSZXF1ZXN0EhEKCW5vZGVfbmFtZRgBIAEoCRIPCgdub2RlX2lkGAIgASgJIkIKFUdldE5vZGVDb25maWdSZXNwb25zZRIpCgZjb25maWcYASABKAsyGS5jcnlwdG9zLnYxLk1hY2hpbmVDb25maWciZwoWQXBwbHlOb2RlQ29uZmlnUmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSKQoGY29uZmlnGAIgASgLMhkuY3J5cHRvcy52MS5NYWNoaW5lQ29uZmlnEg8KB25vZGVfaWQYAyABKAkiRgoXQXBwbHlOb2RlQ29uZmlnUmVzcG9uc2USEgoKZ2VuZXJhdGlvbhgBIAEoBBIXCg9yZXF1aXJlc19yZWJvb3QYAiABKAgiawoWU2V0Tm9kZVByb3RvY29sUmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSLQoIcHJvdG9jb2wYAiABKA4yGy5jcnlwdG9zLnYxLlNlcnZpY2VQcm90b2NvbBIPCgdlbmFibGVkGAMgASgIIkYKF1NldE5vZGVQcm90b2NvbFJlc3BvbnNlEhIKCmdlbmVyYXRpb24YASABKAQSFwoPcmVxdWlyZXNfcmVib290GAIgASgIIkwKEkV4cG9ydENBS2V5UmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSEgoKcGFzc3BocmFzZRgCIAEoDBIPCgdub2RlX2lkGAMgASgJIicKE0V4cG9ydENBS2V5UmVzcG9uc2USEAoIZW52ZWxvcGUYASABKAwiXgoSSW1wb3J0Q0FLZXlSZXF1ZXN0EhEKCW5vZGVfbmFtZRgBIAEoCRIQCghlbnZlbG9wZRgCIAEoDBISCgpwYXNzcGhyYXNlGAMgASgMEg8KB25vZGVfaWQYBCABKAkiPAoTSW1wb3J0Q0FLZXlSZXNwb25zZRISCgpzdWJqZWN0X2NuGAEgASgJEhEKCWlzc3Vlcl9jbhgCIAEoCSJVCh5Jc3N1ZU9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3QSEwoLY29tbW9uX25hbWUYASABKAkSDQoFbGV2ZWwYAiABKAkSDwoHY3NyX2RlchgDIAEoDCJHCh9Jc3N1ZU9wZXJhdG9yQ3JlZGVudGlhbFJlc3BvbnNlEhAKCGNlcnRfZGVyGAEgASgMEhIKCnNlcmlhbF9oZXgYAiABKAkiSgofUmV2b2tlT3BlcmF0b3JDcmVkZW50aWFsUmVxdWVzdBISCgpzZXJpYWxfaGV4GAEgASgJEhMKC3JlYXNvbl9jb2RlGAIgASgFIkoKIFJldm9rZU9wZXJhdG9yQ3JlZGVudGlhbFJlc3BvbnNlEhIKCnNlcmlhbF9oZXgYASABKAkSEgoKcmV2b2tlZF9hdBgCIAEoCSIgCh5MaXN0T3BlcmF0b3JDcmVkZW50aWFsc1JlcXVlc3QicAoST3BlcmF0b3JDcmVkZW50aWFsEhMKC2NvbW1vbl9uYW1lGAEgASgJEhIKCnNlcmlhbF9oZXgYAiABKAkSDQoFbGV2ZWwYAyABKAkSEQoJbm90X2FmdGVyGAQgASgJEg8KB3Jldm9rZWQYBSABKAgiVgofTGlzdE9wZXJhdG9yQ3JlZGVudGlhbHNSZXNwb25zZRIzCgVpdGVtcxgBIAMoCzIkLmNyeXB0b3MuZmxlZXQudjEuT3BlcmF0b3JDcmVkZW50aWFsIioKFlByZXZpZXdBZG9wdGlvblJlcXVlc3QSEAoIZW5kcG9pbnQYASABKAkiPwoXUHJldmlld0Fkb3B0aW9uUmVzcG9uc2USEwoLY2VydF9zaGEyNTYYASABKAkSDwoHc3ViamVjdBgCIAEoCSJHChdMaXN0SW5zdGFsbERpc2tzUmVxdWVzdBIQCghlbmRwb2ludBgBIAEoCRIaChJwaW5uZWRfY2VydF9zaGEyNTYYAiABKAkiQgoYTGlzdEluc3RhbGxEaXNrc1Jlc3BvbnNlEiYKBWRpc2tzGAEgAygLMhcuY3J5cHRvcy52MS5JbnN0YWxsRGlzayJrChBBZG9wdE5vZGVSZXF1ZXN0EhAKCGVuZHBvaW50GAEgASgJEhoKEnBpbm5lZF9jZXJ0X3NoYTI1NhgCIAEoCRIpCgZjb25maWcYAyABKAsyGS5jcnlwdG9zLnYxLk1hY2hpbmVDb25maWciUQoRQWRvcHROb2RlUmVzcG9uc2USDQoFcGhhc2UYASABKAkSDgoGZGV0YWlsGAIgASgJEgwKBGRvbmUYAyABKAgSDwoHbm9kZV9pZBgEIAEoCSJaChdEZWNvbW1pc3Npb25Ob2RlUmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSGwoTY29uZmlybV9jb21tb25fbmFtZRgCIAEoCRIPCgdub2RlX2lkGAMgASgJIi0KGERlY29tbWlzc2lvbk5vZGVSZXNwb25zZRIRCglyZWJvb3RpbmcYASABKAgiNgoRUmVuYW1lTm9kZVJlcXVlc3QSDwoHbm9kZV9pZBgBIAEoCRIQCghuZXdfbmFtZRgCIAEoCSJBChJSZW5hbWVOb2RlUmVzcG9uc2USKwoEbm9kZRgBIAEoCzIdLmNyeXB0b3MuZmxlZXQudjEuTm9kZVN1bW1hcnkiIQoSTGlzdE1jcEtleXNSZXF1ZXN0EgsKA2FsbBgBIAEoCCI+ChNMaXN0TWNwS2V5c1Jlc3BvbnNlEicKBWl0ZW1zGAEgAygLMhguY3J5cHRvcy5mbGVldC52MS5NY3BLZXkiIQoTUmV2b2tlTWNwS2V5UmVxdWVzdBIKCgJpZBgBIAEoCSJBChRSZXZva2VNY3BLZXlSZXNwb25zZRIpCgdtY3Bfa2V5GAEgASgLMhguY3J5cHRvcy5mbGVldC52MS5NY3BLZXkiOwoTQ3JlYXRlTWNwS2V5UmVxdWVzdBINCgVsYWJlbBgBIAEoCRIVCg1sZXZlbF9jZWlsaW5nGAIgASgJIlgKFENyZWF0ZU1jcEtleVJlc3BvbnNlEhUKDXBsYWludGV4dF9rZXkYASABKAkSKQoHbWNwX2tleRgCIAEoCzIYLmNyeXB0b3MuZmxlZXQudjEuTWNwS2V5IrsBCgZNY3BLZXkSCgoCaWQYASABKAkSDQoFbGFiZWwYAiABKAkSEwoLY2xpZW50X25hbWUYAyABKAkSEwoLb3BlcmF0b3JfY24YBCABKAkSFwoPb3BlcmF0b3Jfc2VyaWFsGAUgASgJEhUKDWxldmVsX2NlaWxpbmcYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRIUCgxsYXN0X3VzZWRfYXQYCCABKAkSEgoKcmV2b2tlZF9hdBgJIAEoCSImChRMaXN0QXBwcm92YWxzUmVxdWVzdBIOCgZzdGF0dXMYASABKAkiQgoVTGlzdEFwcHJvdmFsc1Jlc3BvbnNlEikKBWl0ZW1zGAEgAygLMhouY3J5cHRvcy5mbGVldC52MS5BcHByb3ZhbCI0ChVEZWNpZGVBcHByb3ZhbFJlcXVlc3QSCgoCaWQYASABKAkSDwoHYXBwcm92ZRgCIAEoCCJGChZEZWNpZGVBcHByb3ZhbFJlc3BvbnNlEiwKCGFwcHJvdmFsGAEgASgLMhouY3J5cHRvcy5mbGVldC52MS5BcHByb3ZhbCKpAgoIQXBwcm92YWwSCgoCaWQYASABKAkSDAoEdG9vbBgCIAEoCRIPCgdzdW1tYXJ5GAMgASgJEhYKDnJlcXVlc3RfZGlnZXN0GAQgASgJEhcKD3JlcXVlc3RlZF9ieV9jbhgFIAEoCRIbChNyZXF1ZXN0ZWRfYnlfc2VyaWFsGAYgASgJEg4KBmtleV9pZBgHIAEoCRIWCg5yZXF1aXJlZF9sZXZlbBgIIAEoCRISCgpjcmVhdGVkX2F0GAkgASgJEhIKCmV4cGlyZXNfYXQYCiABKAkSDgoGc3RhdHVzGAsgASgJEhUKDWRlY2lkZWRfYnlfY24YDCABKAkSGQoRZGVjaWRlZF9ieV9zZXJpYWwYDSABKAkSEgoKZGVjaWRlZF9hdBgOIAEoCSpSCgZIZWFsdGgSFgoSSEVBTFRIX1VOU1BFQ0lGSUVEEAASDQoJSEVBTFRIX1VQEAESDwoLSEVBTFRIX0RPV04QAhIQCgxIRUFMVEhfRVJST1IQAzKJHQoMRmxlZXRTZXJ2aWNlElQKCUxpc3ROb2RlcxIiLmNyeXB0b3MuZmxlZXQudjEuTGlzdE5vZGVzUmVxdWVzdBojLmNyeXB0b3MuZmxlZXQudjEuTGlzdE5vZGVzUmVzcG9uc2USTgoHR2V0Tm9kZRIgLmNyeXB0b3MuZmxlZXQudjEuR2V0Tm9kZVJlcXVlc3QaIS5jcnlwdG9zLmZsZWV0LnYxLkdldE5vZGVSZXNwb25zZRJpChBMaXN0Q2VydGlmaWNhdGVzEikuY3J5cHRvcy5mbGVldC52MS5MaXN0Q2VydGlmaWNhdGVzUmVxdWVzdBoqLmNyeXB0b3MuZmxlZXQudjEuTGlzdENlcnRpZmljYXRlc1Jlc3BvbnNlEl0KDExpc3RQcm9maWxlcxIlLmNyeXB0b3MuZmxlZXQudjEuTGlzdFByb2ZpbGVzUmVxdWVzdBomLmNyeXB0b3MuZmxlZXQudjEuTGlzdFByb2ZpbGVzUmVzcG9uc2USYAoNQ3JlYXRlUHJvZmlsZRImLmNyeXB0b3MuZmxlZXQudjEuQ3JlYXRlUHJvZmlsZVJlcXVlc3QaJy5jcnlwdG9zLmZsZWV0LnYxLkNyZWF0ZVByb2ZpbGVSZXNwb25zZRJgCg1VcGRhdGVQcm9maWxlEiYuY3J5cHRvcy5mbGVldC52MS5VcGRhdGVQcm9maWxlUmVxdWVzdBonLmNyeXB0b3MuZmxlZXQudjEuVXBkYXRlUHJvZmlsZVJlc3BvbnNlEmAKDURlbGV0ZVByb2ZpbGUSJi5jcnlwdG9zLmZsZWV0LnYxLkRlbGV0ZVByb2ZpbGVSZXF1ZXN0GicuY3J5cHRvcy5mbGVldC52MS5EZWxldGVQcm9maWxlUmVzcG9uc2USbwoSQXBwbHlQcm9maWxlVG9Ob2RlEisuY3J5cHRvcy5mbGVldC52MS5BcHBseVByb2ZpbGVUb05vZGVSZXF1ZXN0GiwuY3J5cHRvcy5mbGVldC52MS5BcHBseVByb2ZpbGVUb05vZGVSZXNwb25zZRJdCgxMaXN0QWRhcHRlcnMSJS5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBZGFwdGVyc1JlcXVlc3QaJi5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBZGFwdGVyc1Jlc3BvbnNlEmwKEVNldEFkYXB0ZXJFbmFibGVkEiouY3J5cHRvcy5mbGVldC52MS5TZXRBZGFwdGVyRW5hYmxlZFJlcXVlc3QaKy5jcnlwdG9zLmZsZWV0LnYxLlNldEFkYXB0ZXJFbmFibGVkUmVzcG9uc2USZgoPU2V0Tm9kZVByb3RvY29sEiguY3J5cHRvcy5mbGVldC52MS5TZXROb2RlUHJvdG9jb2xSZXF1ZXN0GikuY3J5cHRvcy5mbGVldC52MS5TZXROb2RlUHJvdG9jb2xSZXNwb25zZRJUCglMaXN0QXVkaXQSIi5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBdWRpdFJlcXVlc3QaIy5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBdWRpdFJlc3BvbnNlEmYKD0xpc3RFbnJvbGxtZW50cxIoLmNyeXB0b3MuZmxlZXQudjEuTGlzdEVucm9sbG1lbnRzUmVxdWVzdBopLmNyeXB0b3MuZmxlZXQudjEuTGlzdEVucm9sbG1lbnRzUmVzcG9uc2USaQoQQ3JlYXRlRW5yb2xsbWVudBIpLmNyeXB0b3MuZmxlZXQudjEuQ3JlYXRlRW5yb2xsbWVudFJlcXVlc3QaKi5jcnlwdG9zLmZsZWV0LnYxLkNyZWF0ZUVucm9sbG1lbnRSZXNwb25zZRJsChFBcHByb3ZlRW5yb2xsbWVudBIqLmNyeXB0b3MuZmxlZXQudjEuQXBwcm92ZUVucm9sbG1lbnRSZXF1ZXN0GisuY3J5cHRvcy5mbGVldC52MS5BcHByb3ZlRW5yb2xsbWVudFJlc3BvbnNlEmkKEFJlamVjdEVucm9sbG1lbnQSKS5jcnlwdG9zLmZsZWV0LnYxLlJlamVjdEVucm9sbG1lbnRSZXF1ZXN0GiouY3J5cHRvcy5mbGVldC52MS5SZWplY3RFbnJvbGxtZW50UmVzcG9uc2USSwoGV2hvQW1JEh8uY3J5cHRvcy5mbGVldC52MS5XaG9BbUlSZXF1ZXN0GiAuY3J5cHRvcy5mbGVldC52MS5XaG9BbUlSZXNwb25zZRJsChFSZXZva2VDZXJ0aWZpY2F0ZRIqLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlQ2VydGlmaWNhdGVSZXF1ZXN0GisuY3J5cHRvcy5mbGVldC52MS5SZXZva2VDZXJ0aWZpY2F0ZVJlc3BvbnNlElQKCUlzc3VlTGVhZhIiLmNyeXB0b3MuZmxlZXQudjEuSXNzdWVMZWFmUmVxdWVzdBojLmNyeXB0b3MuZmxlZXQudjEuSXNzdWVMZWFmUmVzcG9uc2USVAoJUmVrZXlOb2RlEiIuY3J5cHRvcy5mbGVldC52MS5SZWtleU5vZGVSZXF1ZXN0GiMuY3J5cHRvcy5mbGVldC52MS5SZWtleU5vZGVSZXNwb25zZRJgCg1HZXROb2RlQ29uZmlnEiYuY3J5cHRvcy5mbGVldC52MS5HZXROb2RlQ29uZmlnUmVxdWVzdBonLmNyeXB0b3MuZmxlZXQudjEuR2V0Tm9kZUNvbmZpZ1Jlc3BvbnNlEmYKD0FwcGx5Tm9kZUNvbmZpZxIoLmNyeXB0b3MuZmxlZXQudjEuQXBwbHlOb2RlQ29uZmlnUmVxdWVzdBopLmNyeXB0b3MuZmxlZXQudjEuQXBwbHlOb2RlQ29uZmlnUmVzcG9uc2USWgoLRXhwb3J0Q0FLZXkSJC5jcnlwdG9zLmZsZWV0LnYxLkV4cG9ydENBS2V5UmVxdWVzdBolLmNyeXB0b3MuZmxlZXQudjEuRXhwb3J0Q0FLZXlSZXNwb25zZRJaCgtJbXBvcnRDQUtleRIkLmNyeXB0b3MuZmxlZXQudjEuSW1wb3J0Q0FLZXlSZXF1ZXN0GiUuY3J5cHRvcy5mbGVldC52MS5JbXBvcnRDQUtleVJlc3BvbnNlEn4KF0lzc3VlT3BlcmF0b3JDcmVkZW50aWFsEjAuY3J5cHRvcy5mbGVldC52MS5Jc3N1ZU9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3QaMS5jcnlwdG9zLmZsZWV0LnYxLklzc3VlT3BlcmF0b3JDcmVkZW50aWFsUmVzcG9uc2USgQEKGFJldm9rZU9wZXJhdG9yQ3JlZGVudGlhbBIxLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlT3BlcmF0b3JDcmVkZW50aWFsUmVxdWVzdBoyLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlT3BlcmF0b3JDcmVkZW50aWFsUmVzcG9uc2USfgoXTGlzdE9wZXJhdG9yQ3JlZGVudGlhbHMSMC5jcnlwdG9zLmZsZWV0LnYxLkxpc3RPcGVyYXRvckNyZWRlbnRpYWxzUmVxdWVzdBoxLmNyeXB0b3MuZmxlZXQudjEuTGlzdE9wZXJhdG9yQ3JlZGVudGlhbHNSZXNwb25zZRJmCg9QcmV2aWV3QWRvcHRpb24SKC5jcnlwdG9zLmZsZWV0LnYxLlByZXZpZXdBZG9wdGlvblJlcXVlc3QaKS5jcnlwdG9zLmZsZWV0LnYxLlByZXZpZXdBZG9wdGlvblJlc3BvbnNlEmkKEExpc3RJbnN0YWxsRGlza3MSKS5jcnlwdG9zLmZsZWV0LnYxLkxpc3RJbnN0YWxsRGlza3NSZXF1ZXN0GiouY3J5cHRvcy5mbGVldC52MS5MaXN0SW5zdGFsbERpc2tzUmVzcG9uc2USVgoJQWRvcHROb2RlEiIuY3J5cHRvcy5mbGVldC52MS5BZG9wdE5vZGVSZXF1ZXN0GiMuY3J5cHRvcy5mbGVldC52MS5BZG9wdE5vZGVSZXNwb25zZTABEmkKEERlY29tbWlzc2lvbk5vZGUSKS5jcnlwdG9zLmZsZWV0LnYxLkRlY29tbWlzc2lvbk5vZGVSZXF1ZXN0GiouY3J5cHRvcy5mbGVldC52MS5EZWNvbW1pc3Npb25Ob2RlUmVzcG9uc2USVwoKUmVuYW1lTm9kZRIjLmNyeXB0b3MuZmxlZXQudjEuUmVuYW1lTm9kZVJlcXVlc3QaJC5jcnlwdG9zLmZsZWV0LnYxLlJlbmFtZU5vZGVSZXNwb25zZRJaCgtMaXN0TWNwS2V5cxIkLmNyeXB0b3MuZmxlZXQudjEuTGlzdE1jcEtleXNSZXF1ZXN0GiUuY3J5cHRvcy5mbGVldC52MS5MaXN0TWNwS2V5c1Jlc3BvbnNlEl0KDFJldm9rZU1jcEtleRIlLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlTWNwS2V5UmVxdWVzdBomLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlTWNwS2V5UmVzcG9uc2USXQoMQ3JlYXRlTWNwS2V5EiUuY3J5cHRvcy5mbGVldC52MS5DcmVhdGVNY3BLZXlSZXF1ZXN0GiYuY3J5cHRvcy5mbGVldC52MS5DcmVhdGVNY3BLZXlSZXNwb25zZRJgCg1MaXN0QXBwcm92YWxzEiYuY3J5cHRvcy5mbGVldC52MS5MaXN0QXBwcm92YWxzUmVxdWVzdBonLmNyeXB0b3MuZmxlZXQudjEuTGlzdEFwcHJvdmFsc1Jlc3BvbnNlEmMKDkRlY2lkZUFwcHJvdmFsEicuY3J5cHRvcy5mbGVldC52MS5EZWNpZGVBcHByb3ZhbFJlcXVlc3QaKC5jcnlwdG9zLmZsZWV0LnYxLkRlY2lkZUFwcHJvdmFsUmVzcG9uc2VCOFo2Z2l0aHViLmNvbS9DcnlwdE9TLVBLSS9hcGkvZ28vY3J5cHRvcy9mbGVldC92MTtmbGVldHYxYgZwcm90bzM", [file_cryptos_v1_config, file_cryptos_v1_node, file_cryptos_v1_status]); /** * @generated from message cryptos.fleet.v1.ListNodesRequest @@ -51,9 +53,23 @@ export const ListNodesResponseSchema: GenMessage = /*@__PURE_ */ export type GetNodeRequest = Message<"cryptos.fleet.v1.GetNodeRequest"> & { /** + * name is the node's display name. + * Deprecated: use node_id. Still resolves the node's current name for one + * release. Unlike other requests, a name the node held before a rename also + * resolves when no current node has it, so old links still find the node. + * * @generated from field: string name = 1; */ name: string; + + /** + * node_id is the stable ID (NodeSummary.id) of the node. Preferred over + * name; when both are set they must name the same node, or the + * manager returns InvalidArgument. + * + * @generated from field: string node_id = 2; + */ + nodeId: string; }; /** @@ -86,10 +102,22 @@ export const GetNodeResponseSchema: GenMessage = /*@__PURE__*/ export type ListCertificatesRequest = Message<"cryptos.fleet.v1.ListCertificatesRequest"> & { /** * node scopes the result to a single node by name; empty means all nodes. + * Deprecated: use node_id. Still resolves the node's current name for one + * release. * * @generated from field: string node = 1; */ node: string; + + /** + * node_id scopes the result to a single node by its stable ID + * (NodeSummary.id). Preferred over node; when both are set they must name + * the same node, or the manager returns InvalidArgument. Both empty means + * all nodes. + * + * @generated from field: string node_id = 2; + */ + nodeId: string; }; /** @@ -244,6 +272,9 @@ export const DeleteProfileResponseSchema: GenMessage = /* */ export type ApplyProfileToNodeRequest = Message<"cryptos.fleet.v1.ApplyProfileToNodeRequest"> & { /** + * Deprecated: use node_id. Still resolves the node's current name for one + * release. + * * @generated from field: string node_name = 1; */ nodeName: string; @@ -252,6 +283,15 @@ export type ApplyProfileToNodeRequest = Message<"cryptos.fleet.v1.ApplyProfileTo * @generated from field: string profile_name = 2; */ profileName: string; + + /** + * node_id is the stable ID (NodeSummary.id) of the node. Preferred over + * node_name; when both are set they must name the same node, or the + * manager returns InvalidArgument. + * + * @generated from field: string node_id = 3; + */ + nodeId: string; }; /** @@ -426,6 +466,9 @@ export const ListEnrollmentsResponseSchema: GenMessage */ export type NodeSummary = Message<"cryptos.fleet.v1.NodeSummary"> & { /** + * name is the node's display name, unique across the fleet and changed with + * RenameNode. + * * @generated from field: string name = 1; */ name: string; @@ -468,6 +511,33 @@ export type NodeSummary = Message<"cryptos.fleet.v1.NodeSummary"> & { * @generated from field: string health_detail = 8; */ healthDetail: string; + + /** + * id is the node's stable identifier: a UUIDv7 in canonical lowercase form, + * assigned by the manager when the node joins the fleet and never changed or + * reused. Address the node by this, not by name. + * + * @generated from field: string id = 9; + */ + id: string; + + /** + * protocols is the node's enrolment protocol state as it reports it: + * configured in the stored config against running this boot. Empty when the + * node is down or predates the report. + * + * @generated from field: repeated cryptos.v1.ProtocolStatus protocols = 10; + */ + protocols: ProtocolStatus[]; + + /** + * reboot_required is true when the node holds a stored change that takes + * effect only at its next boot: the node reports config_reboot_pending, or a + * protocol switch the manager applied has not yet shown up as running. + * + * @generated from field: bool reboot_required = 11; + */ + rebootRequired: boolean; }; /** @@ -607,6 +677,13 @@ export type Certificate = Message<"cryptos.fleet.v1.Certificate"> & { * @generated from field: string reason = 10; */ reason: string; + + /** + * issuer_node_id is the stable ID (NodeSummary.id) of issuer_node. + * + * @generated from field: string issuer_node_id = 11; + */ + issuerNodeId: string; }; /** @@ -689,8 +766,8 @@ export type AuditEvent = Message<"cryptos.fleet.v1.AuditEvent"> & { /** * config-applied|enroll-approved|enroll-rejected|issued|mcp-key-created| * mcp-key-first-used|mcp-key-rejected|mcp-key-revoked|profile-applied| - * profile-created|profile-deleted|profile-updated|protocol-toggled|rekeyed| - * renewed|revoked + * node-renamed|profile-created|profile-deleted|profile-updated| + * protocol-toggled|rekeyed|renewed|revoked * * @generated from field: string kind = 3; */ @@ -790,6 +867,16 @@ export type AuditEvent = Message<"cryptos.fleet.v1.AuditEvent"> & { * @generated from field: string approver_serial = 16; */ approverSerial: string; + + /** + * node_id is the stable ID (NodeSummary.id) of the node the entry concerns; + * empty when it concerns no node. Entries recorded before node IDs name the + * node in target_path only; the manager fills node_id for them from the + * name the node held at the entry's time. + * + * @generated from field: string node_id = 17; + */ + nodeId: string; }; /** @@ -876,6 +963,9 @@ export type EnrollmentRequest = Message<"cryptos.fleet.v1.EnrollmentRequest"> & rejectionReason: string; /** + * admitted_node_name is the node's name when it was admitted; it does not + * follow a later rename. Use admitted_node_id to find the node. + * * @generated from field: string admitted_node_name = 13; */ admittedNodeName: string; @@ -893,6 +983,14 @@ export type EnrollmentRequest = Message<"cryptos.fleet.v1.EnrollmentRequest"> & * @generated from field: string pinned_key_sha256 = 15; */ pinnedKeySha256: string; + + /** + * admitted_node_id is the stable ID (NodeSummary.id) of the node this request + * admitted; empty until approved. + * + * @generated from field: string admitted_node_id = 16; + */ + admittedNodeId: string; }; /** @@ -937,6 +1035,8 @@ export type CreateEnrollmentRequest = Message<"cryptos.fleet.v1.CreateEnrollment /** * SUBORDINATE: the child node, its parent CA, and the issuing profile. + * Deprecated: use child_node_id. Still resolves the node's current name for + * one release. * * @generated from field: string child_node = 6; */ @@ -951,6 +1051,15 @@ export type CreateEnrollmentRequest = Message<"cryptos.fleet.v1.CreateEnrollment * @generated from field: string profile = 8; */ profile: string; + + /** + * child_node_id is the stable ID (NodeSummary.id) of the SUBORDINATE child + * node. Preferred over child_node; when both are set they must name the + * same node, or the manager returns InvalidArgument. + * + * @generated from field: string child_node_id = 9; + */ + childNodeId: string; }; /** @@ -1144,6 +1253,8 @@ export const WhoAmIResponseSchema: GenMessage = /*@__PURE__*/ export type RevokeCertificateRequest = Message<"cryptos.fleet.v1.RevokeCertificateRequest"> & { /** * node_name is the issuing node the certificate lives on. + * Deprecated: use node_id. Still resolves the node's current name for one + * release. * * @generated from field: string node_name = 1; */ @@ -1162,6 +1273,15 @@ export type RevokeCertificateRequest = Message<"cryptos.fleet.v1.RevokeCertifica * @generated from field: int32 reason_code = 3; */ reasonCode: number; + + /** + * node_id is the stable ID (NodeSummary.id) of the node. Preferred over + * node_name; when both are set they must name the same node, or the + * manager returns InvalidArgument. + * + * @generated from field: string node_id = 4; + */ + nodeId: string; }; /** @@ -1210,6 +1330,8 @@ export const RevokeCertificateResponseSchema: GenMessage & { /** * node_name is the issuing node that signs the leaf. + * Deprecated: use node_id. Still resolves the node's current name for one + * release. * * @generated from field: string node_name = 1; */ @@ -1228,6 +1350,15 @@ export type IssueLeafRequest = Message<"cryptos.fleet.v1.IssueLeafRequest"> & { * @generated from field: string profile_name = 3; */ profileName: string; + + /** + * node_id is the stable ID (NodeSummary.id) of the node. Preferred over + * node_name; when both are set they must name the same node, or the + * manager returns InvalidArgument. + * + * @generated from field: string node_id = 4; + */ + nodeId: string; }; /** @@ -1267,6 +1398,8 @@ export const IssueLeafResponseSchema: GenMessage = /*@__PURE_ export type RekeyNodeRequest = Message<"cryptos.fleet.v1.RekeyNodeRequest"> & { /** * node_name is the subordinate CA to re-key. + * Deprecated: use node_id. Still resolves the node's current name for one + * release. * * @generated from field: string node_name = 1; */ @@ -1279,6 +1412,15 @@ export type RekeyNodeRequest = Message<"cryptos.fleet.v1.RekeyNodeRequest"> & { * @generated from field: string profile_name = 2; */ profileName: string; + + /** + * node_id is the stable ID (NodeSummary.id) of the node. Preferred over + * node_name; when both are set they must name the same node, or the + * manager returns InvalidArgument. + * + * @generated from field: string node_id = 3; + */ + nodeId: string; }; /** @@ -1332,10 +1474,21 @@ export const RekeyNodeResponseSchema: GenMessage = /*@__PURE_ export type GetNodeConfigRequest = Message<"cryptos.fleet.v1.GetNodeConfigRequest"> & { /** * node_name is the managed node to read the configuration from. + * Deprecated: use node_id. Still resolves the node's current name for one + * release. * * @generated from field: string node_name = 1; */ nodeName: string; + + /** + * node_id is the stable ID (NodeSummary.id) of the node. Preferred over + * node_name; when both are set they must name the same node, or the + * manager returns InvalidArgument. + * + * @generated from field: string node_id = 2; + */ + nodeId: string; }; /** @@ -1373,6 +1526,8 @@ export const GetNodeConfigResponseSchema: GenMessage = /* export type ApplyNodeConfigRequest = Message<"cryptos.fleet.v1.ApplyNodeConfigRequest"> & { /** * node_name is the managed node to apply the configuration to. + * Deprecated: use node_id. Still resolves the node's current name for one + * release. * * @generated from field: string node_name = 1; */ @@ -1385,6 +1540,15 @@ export type ApplyNodeConfigRequest = Message<"cryptos.fleet.v1.ApplyNodeConfigRe * @generated from field: cryptos.v1.MachineConfig config = 2; */ config?: MachineConfig | undefined; + + /** + * node_id is the stable ID (NodeSummary.id) of the node. Preferred over + * node_name; when both are set they must name the same node, or the + * manager returns InvalidArgument. + * + * @generated from field: string node_id = 3; + */ + nodeId: string; }; /** @@ -1423,6 +1587,72 @@ export type ApplyNodeConfigResponse = Message<"cryptos.fleet.v1.ApplyNodeConfigR export const ApplyNodeConfigResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_cryptos_fleet_v1_fleet, 49); +/** + * SetNodeProtocolRequest names the node, the protocol and its new state. + * + * @generated from message cryptos.fleet.v1.SetNodeProtocolRequest + */ +export type SetNodeProtocolRequest = Message<"cryptos.fleet.v1.SetNodeProtocolRequest"> & { + /** + * node_name is the managed node to switch the protocol on. + * + * @generated from field: string node_name = 1; + */ + nodeName: string; + + /** + * protocol is the enrolment protocol to switch. + * + * @generated from field: cryptos.v1.ServiceProtocol protocol = 2; + */ + protocol: ServiceProtocol; + + /** + * enabled switches the protocol on (true) or off (false) at the node's next + * boot. + * + * @generated from field: bool enabled = 3; + */ + enabled: boolean; +}; + +/** + * Describes the message cryptos.fleet.v1.SetNodeProtocolRequest. + * Use `create(SetNodeProtocolRequestSchema)` to create a new message. + */ +export const SetNodeProtocolRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_fleet_v1_fleet, 50); + +/** + * SetNodeProtocolResponse carries the node's answer to the apply. + * + * @generated from message cryptos.fleet.v1.SetNodeProtocolResponse + */ +export type SetNodeProtocolResponse = Message<"cryptos.fleet.v1.SetNodeProtocolResponse"> & { + /** + * generation is the node's config generation after the apply. + * + * @generated from field: uint64 generation = 1; + */ + generation: bigint; + + /** + * requires_reboot is true when the node stored a change that takes effect at + * its next boot. False when the protocol was already in the requested state + * and nothing was applied. + * + * @generated from field: bool requires_reboot = 2; + */ + requiresReboot: boolean; +}; + +/** + * Describes the message cryptos.fleet.v1.SetNodeProtocolResponse. + * Use `create(SetNodeProtocolResponseSchema)` to create a new message. + */ +export const SetNodeProtocolResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_fleet_v1_fleet, 51); + /** * ExportCAKeyRequest names the managed node to back up and carries the operator * passphrase the node seals the backup with. The passphrase is relayed to the @@ -1433,6 +1663,8 @@ export const ApplyNodeConfigResponseSchema: GenMessage export type ExportCAKeyRequest = Message<"cryptos.fleet.v1.ExportCAKeyRequest"> & { /** * node_name is the managed node whose CA key to export. + * Deprecated: use node_id. Still resolves the node's current name for one + * release. * * @generated from field: string node_name = 1; */ @@ -1444,6 +1676,15 @@ export type ExportCAKeyRequest = Message<"cryptos.fleet.v1.ExportCAKeyRequest"> * @generated from field: bytes passphrase = 2; */ passphrase: Uint8Array; + + /** + * node_id is the stable ID (NodeSummary.id) of the node. Preferred over + * node_name; when both are set they must name the same node, or the + * manager returns InvalidArgument. + * + * @generated from field: string node_id = 3; + */ + nodeId: string; }; /** @@ -1451,7 +1692,7 @@ export type ExportCAKeyRequest = Message<"cryptos.fleet.v1.ExportCAKeyRequest"> * Use `create(ExportCAKeyRequestSchema)` to create a new message. */ export const ExportCAKeyRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 50); + messageDesc(file_cryptos_fleet_v1_fleet, 52); /** * ExportCAKeyResponse carries the encrypted backup envelope (Argon2id + @@ -1473,7 +1714,7 @@ export type ExportCAKeyResponse = Message<"cryptos.fleet.v1.ExportCAKeyResponse" * Use `create(ExportCAKeyResponseSchema)` to create a new message. */ export const ExportCAKeyResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 51); + messageDesc(file_cryptos_fleet_v1_fleet, 53); /** * ImportCAKeyRequest names the fresh target node, the encrypted envelope to @@ -1485,6 +1726,8 @@ export const ExportCAKeyResponseSchema: GenMessage = /*@__P export type ImportCAKeyRequest = Message<"cryptos.fleet.v1.ImportCAKeyRequest"> & { /** * node_name is the fresh managed node to restore the identity onto. + * Deprecated: use node_id. Still resolves the node's current name for one + * release. * * @generated from field: string node_name = 1; */ @@ -1503,6 +1746,15 @@ export type ImportCAKeyRequest = Message<"cryptos.fleet.v1.ImportCAKeyRequest"> * @generated from field: bytes passphrase = 3; */ passphrase: Uint8Array; + + /** + * node_id is the stable ID (NodeSummary.id) of the node. Preferred over + * node_name; when both are set they must name the same node, or the + * manager returns InvalidArgument. + * + * @generated from field: string node_id = 4; + */ + nodeId: string; }; /** @@ -1510,7 +1762,7 @@ export type ImportCAKeyRequest = Message<"cryptos.fleet.v1.ImportCAKeyRequest"> * Use `create(ImportCAKeyRequestSchema)` to create a new message. */ export const ImportCAKeyRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 52); + messageDesc(file_cryptos_fleet_v1_fleet, 54); /** * ImportCAKeyResponse summarizes the restored identity so the web can confirm @@ -1539,7 +1791,7 @@ export type ImportCAKeyResponse = Message<"cryptos.fleet.v1.ImportCAKeyResponse" * Use `create(ImportCAKeyResponseSchema)` to create a new message. */ export const ImportCAKeyResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 53); + messageDesc(file_cryptos_fleet_v1_fleet, 55); /** * IssueOperatorCredentialRequest carries the operator subject CN, the access @@ -1574,7 +1826,7 @@ export type IssueOperatorCredentialRequest = Message<"cryptos.fleet.v1.IssueOper * Use `create(IssueOperatorCredentialRequestSchema)` to create a new message. */ export const IssueOperatorCredentialRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 54); + messageDesc(file_cryptos_fleet_v1_fleet, 56); /** * IssueOperatorCredentialResponse carries the signed operator certificate and @@ -1599,7 +1851,7 @@ export type IssueOperatorCredentialResponse = Message<"cryptos.fleet.v1.IssueOpe * Use `create(IssueOperatorCredentialResponseSchema)` to create a new message. */ export const IssueOperatorCredentialResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 55); + messageDesc(file_cryptos_fleet_v1_fleet, 57); /** * RevokeOperatorCredentialRequest names the operator credential to revoke by @@ -1624,7 +1876,7 @@ export type RevokeOperatorCredentialRequest = Message<"cryptos.fleet.v1.RevokeOp * Use `create(RevokeOperatorCredentialRequestSchema)` to create a new message. */ export const RevokeOperatorCredentialRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 56); + messageDesc(file_cryptos_fleet_v1_fleet, 58); /** * RevokeOperatorCredentialResponse echoes the revoked serial and the time it @@ -1649,7 +1901,7 @@ export type RevokeOperatorCredentialResponse = Message<"cryptos.fleet.v1.RevokeO * Use `create(RevokeOperatorCredentialResponseSchema)` to create a new message. */ export const RevokeOperatorCredentialResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 57); + messageDesc(file_cryptos_fleet_v1_fleet, 59); /** * @generated from message cryptos.fleet.v1.ListOperatorCredentialsRequest @@ -1662,7 +1914,7 @@ export type ListOperatorCredentialsRequest = Message<"cryptos.fleet.v1.ListOpera * Use `create(ListOperatorCredentialsRequestSchema)` to create a new message. */ export const ListOperatorCredentialsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 58); + messageDesc(file_cryptos_fleet_v1_fleet, 60); /** * OperatorCredential is one issued operator credential row. @@ -1701,7 +1953,7 @@ export type OperatorCredential = Message<"cryptos.fleet.v1.OperatorCredential"> * Use `create(OperatorCredentialSchema)` to create a new message. */ export const OperatorCredentialSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 59); + messageDesc(file_cryptos_fleet_v1_fleet, 61); /** * @generated from message cryptos.fleet.v1.ListOperatorCredentialsResponse @@ -1718,7 +1970,7 @@ export type ListOperatorCredentialsResponse = Message<"cryptos.fleet.v1.ListOper * Use `create(ListOperatorCredentialsResponseSchema)` to create a new message. */ export const ListOperatorCredentialsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 60); + messageDesc(file_cryptos_fleet_v1_fleet, 62); /** * PreviewAdoptionRequest names the maintenance endpoint to inspect. @@ -1737,7 +1989,7 @@ export type PreviewAdoptionRequest = Message<"cryptos.fleet.v1.PreviewAdoptionRe * Use `create(PreviewAdoptionRequestSchema)` to create a new message. */ export const PreviewAdoptionRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 61); + messageDesc(file_cryptos_fleet_v1_fleet, 63); /** * PreviewAdoptionResponse returns the maintenance node's certificate @@ -1762,7 +2014,7 @@ export type PreviewAdoptionResponse = Message<"cryptos.fleet.v1.PreviewAdoptionR * Use `create(PreviewAdoptionResponseSchema)` to create a new message. */ export const PreviewAdoptionResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 62); + messageDesc(file_cryptos_fleet_v1_fleet, 64); /** * @generated from message cryptos.fleet.v1.ListInstallDisksRequest @@ -1784,7 +2036,7 @@ export type ListInstallDisksRequest = Message<"cryptos.fleet.v1.ListInstallDisks * Use `create(ListInstallDisksRequestSchema)` to create a new message. */ export const ListInstallDisksRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 63); + messageDesc(file_cryptos_fleet_v1_fleet, 65); /** * @generated from message cryptos.fleet.v1.ListInstallDisksResponse @@ -1801,7 +2053,7 @@ export type ListInstallDisksResponse = Message<"cryptos.fleet.v1.ListInstallDisk * Use `create(ListInstallDisksResponseSchema)` to create a new message. */ export const ListInstallDisksResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 64); + messageDesc(file_cryptos_fleet_v1_fleet, 66); /** * AdoptNodeRequest carries the maintenance endpoint, the fingerprint the @@ -1835,7 +2087,7 @@ export type AdoptNodeRequest = Message<"cryptos.fleet.v1.AdoptNodeRequest"> & { * Use `create(AdoptNodeRequestSchema)` to create a new message. */ export const AdoptNodeRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 65); + messageDesc(file_cryptos_fleet_v1_fleet, 67); /** * AdoptNodeResponse is streamed during adoption. phase is one of @@ -1859,6 +2111,14 @@ export type AdoptNodeResponse = Message<"cryptos.fleet.v1.AdoptNodeResponse"> & * @generated from field: bool done = 3; */ done: boolean; + + /** + * node_id is the stable ID (NodeSummary.id) the manager assigned the adopted + * node; set on the final message only. + * + * @generated from field: string node_id = 4; + */ + nodeId: string; }; /** @@ -1866,7 +2126,7 @@ export type AdoptNodeResponse = Message<"cryptos.fleet.v1.AdoptNodeResponse"> & * Use `create(AdoptNodeResponseSchema)` to create a new message. */ export const AdoptNodeResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 66); + messageDesc(file_cryptos_fleet_v1_fleet, 68); /** * DecommissionNodeRequest names the managed node to wipe and echoes its current @@ -1876,6 +2136,9 @@ export const AdoptNodeResponseSchema: GenMessage = /*@__PURE_ */ export type DecommissionNodeRequest = Message<"cryptos.fleet.v1.DecommissionNodeRequest"> & { /** + * Deprecated: use node_id. Still resolves the node's current name for one + * release. + * * @generated from field: string node_name = 1; */ nodeName: string; @@ -1884,6 +2147,15 @@ export type DecommissionNodeRequest = Message<"cryptos.fleet.v1.DecommissionNode * @generated from field: string confirm_common_name = 2; */ confirmCommonName: string; + + /** + * node_id is the stable ID (NodeSummary.id) of the node. Preferred over + * node_name; when both are set they must name the same node, or the + * manager returns InvalidArgument. + * + * @generated from field: string node_id = 3; + */ + nodeId: string; }; /** @@ -1891,7 +2163,7 @@ export type DecommissionNodeRequest = Message<"cryptos.fleet.v1.DecommissionNode * Use `create(DecommissionNodeRequestSchema)` to create a new message. */ export const DecommissionNodeRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 67); + messageDesc(file_cryptos_fleet_v1_fleet, 69); /** * DecommissionNodeResponse reports that the node accepted the wipe and is @@ -1911,7 +2183,58 @@ export type DecommissionNodeResponse = Message<"cryptos.fleet.v1.DecommissionNod * Use `create(DecommissionNodeResponseSchema)` to create a new message. */ export const DecommissionNodeResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 68); + messageDesc(file_cryptos_fleet_v1_fleet, 70); + +/** + * RenameNodeRequest names the node by its stable ID and carries its new + * display name. + * + * @generated from message cryptos.fleet.v1.RenameNodeRequest + */ +export type RenameNodeRequest = Message<"cryptos.fleet.v1.RenameNodeRequest"> & { + /** + * node_id is the stable ID (NodeSummary.id) of the node to rename. + * + * @generated from field: string node_id = 1; + */ + nodeId: string; + + /** + * new_name is the new display name: an RFC 1123 label, unique across the + * fleet. + * + * @generated from field: string new_name = 2; + */ + newName: string; +}; + +/** + * Describes the message cryptos.fleet.v1.RenameNodeRequest. + * Use `create(RenameNodeRequestSchema)` to create a new message. + */ +export const RenameNodeRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_fleet_v1_fleet, 71); + +/** + * RenameNodeResponse returns the node after the rename. + * + * @generated from message cryptos.fleet.v1.RenameNodeResponse + */ +export type RenameNodeResponse = Message<"cryptos.fleet.v1.RenameNodeResponse"> & { + /** + * node is the renamed node; its id is unchanged. + * + * @generated from field: cryptos.fleet.v1.NodeSummary node = 1; + */ + node?: NodeSummary | undefined; +}; + +/** + * Describes the message cryptos.fleet.v1.RenameNodeResponse. + * Use `create(RenameNodeResponseSchema)` to create a new message. + */ +export const RenameNodeResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_fleet_v1_fleet, 72); /** * ListMcpKeysRequest scopes the MCP key listing. @@ -1932,7 +2255,7 @@ export type ListMcpKeysRequest = Message<"cryptos.fleet.v1.ListMcpKeysRequest"> * Use `create(ListMcpKeysRequestSchema)` to create a new message. */ export const ListMcpKeysRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 69); + messageDesc(file_cryptos_fleet_v1_fleet, 73); /** * ListMcpKeysResponse carries the listed MCP keys, revoked ones included. @@ -1953,7 +2276,7 @@ export type ListMcpKeysResponse = Message<"cryptos.fleet.v1.ListMcpKeysResponse" * Use `create(ListMcpKeysResponseSchema)` to create a new message. */ export const ListMcpKeysResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 70); + messageDesc(file_cryptos_fleet_v1_fleet, 74); /** * RevokeMcpKeyRequest names the MCP key to revoke. @@ -1974,7 +2297,7 @@ export type RevokeMcpKeyRequest = Message<"cryptos.fleet.v1.RevokeMcpKeyRequest" * Use `create(RevokeMcpKeyRequestSchema)` to create a new message. */ export const RevokeMcpKeyRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 71); + messageDesc(file_cryptos_fleet_v1_fleet, 75); /** * RevokeMcpKeyResponse returns the key after revocation. @@ -1995,7 +2318,7 @@ export type RevokeMcpKeyResponse = Message<"cryptos.fleet.v1.RevokeMcpKeyRespons * Use `create(RevokeMcpKeyResponseSchema)` to create a new message. */ export const RevokeMcpKeyResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 72); + messageDesc(file_cryptos_fleet_v1_fleet, 76); /** * CreateMcpKeyRequest carries the operator's label for the new key and an @@ -2026,7 +2349,7 @@ export type CreateMcpKeyRequest = Message<"cryptos.fleet.v1.CreateMcpKeyRequest" * Use `create(CreateMcpKeyRequestSchema)` to create a new message. */ export const CreateMcpKeyRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 73); + messageDesc(file_cryptos_fleet_v1_fleet, 77); /** * CreateMcpKeyResponse carries the new key. plaintext_key is returned only @@ -2056,7 +2379,7 @@ export type CreateMcpKeyResponse = Message<"cryptos.fleet.v1.CreateMcpKeyRespons * Use `create(CreateMcpKeyResponseSchema)` to create a new message. */ export const CreateMcpKeyResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 74); + messageDesc(file_cryptos_fleet_v1_fleet, 78); /** * McpKey is the metadata of one MCP agent key. It never carries the key or @@ -2140,7 +2463,221 @@ export type McpKey = Message<"cryptos.fleet.v1.McpKey"> & { * Use `create(McpKeySchema)` to create a new message. */ export const McpKeySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_fleet_v1_fleet, 75); + messageDesc(file_cryptos_fleet_v1_fleet, 79); + +/** + * ListApprovalsRequest filters the approval listing. + * + * @generated from message cryptos.fleet.v1.ListApprovalsRequest + */ +export type ListApprovalsRequest = Message<"cryptos.fleet.v1.ListApprovalsRequest"> & { + /** + * status keeps only approvals in that state: "pending" | "approved" | + * "denied" | "expired" | "used". Empty means all. + * + * @generated from field: string status = 1; + */ + status: string; +}; + +/** + * Describes the message cryptos.fleet.v1.ListApprovalsRequest. + * Use `create(ListApprovalsRequestSchema)` to create a new message. + */ +export const ListApprovalsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_fleet_v1_fleet, 80); + +/** + * ListApprovalsResponse carries the listed approvals. + * + * @generated from message cryptos.fleet.v1.ListApprovalsResponse + */ +export type ListApprovalsResponse = Message<"cryptos.fleet.v1.ListApprovalsResponse"> & { + /** + * items are the listed approvals, newest first. + * + * @generated from field: repeated cryptos.fleet.v1.Approval items = 1; + */ + items: Approval[]; +}; + +/** + * Describes the message cryptos.fleet.v1.ListApprovalsResponse. + * Use `create(ListApprovalsResponseSchema)` to create a new message. + */ +export const ListApprovalsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_fleet_v1_fleet, 81); + +/** + * DecideApprovalRequest names the approval to decide and the decision. + * + * @generated from message cryptos.fleet.v1.DecideApprovalRequest + */ +export type DecideApprovalRequest = Message<"cryptos.fleet.v1.DecideApprovalRequest"> & { + /** + * id is the Approval id to decide. + * + * @generated from field: string id = 1; + */ + id: string; + + /** + * approve approves the request when true and denies it when false. + * + * @generated from field: bool approve = 2; + */ + approve: boolean; +}; + +/** + * Describes the message cryptos.fleet.v1.DecideApprovalRequest. + * Use `create(DecideApprovalRequestSchema)` to create a new message. + */ +export const DecideApprovalRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_fleet_v1_fleet, 82); + +/** + * DecideApprovalResponse returns the approval after the decision. + * + * @generated from message cryptos.fleet.v1.DecideApprovalResponse + */ +export type DecideApprovalResponse = Message<"cryptos.fleet.v1.DecideApprovalResponse"> & { + /** + * approval is the decided approval, with status and the decided_by fields + * set. + * + * @generated from field: cryptos.fleet.v1.Approval approval = 1; + */ + approval?: Approval | undefined; +}; + +/** + * Describes the message cryptos.fleet.v1.DecideApprovalResponse. + * Use `create(DecideApprovalResponseSchema)` to create a new message. + */ +export const DecideApprovalResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_fleet_v1_fleet, 83); + +/** + * Approval is one step-up approval request for an MCP tool call. Listing and + * deciding approvals take an operator certificate; an MCP key can never list + * or decide them, and the decider's level must be at least required_level. + * Timestamps are RFC3339 strings; an unset timestamp is empty. + * + * @generated from message cryptos.fleet.v1.Approval + */ +export type Approval = Message<"cryptos.fleet.v1.Approval"> & { + /** + * id is the approval's stable identifier, used by DecideApproval and audit + * approval_id. + * + * @generated from field: string id = 1; + */ + id: string; + + /** + * tool is the MCP tool the request would run. + * + * @generated from field: string tool = 2; + */ + tool: string; + + /** + * summary is a human-readable description of the action. + * + * @generated from field: string summary = 3; + */ + summary: string; + + /** + * request_digest is the lowercase hex SHA-256 of the canonical request, so + * an approval covers exactly the request that raised it. + * + * @generated from field: string request_digest = 4; + */ + requestDigest: string; + + /** + * requested_by_cn is the subject CN of the operator certificate behind the + * requesting MCP key. + * + * @generated from field: string requested_by_cn = 5; + */ + requestedByCn: string; + + /** + * requested_by_serial is the hex serial of that operator certificate. + * + * @generated from field: string requested_by_serial = 6; + */ + requestedBySerial: string; + + /** + * key_id is the McpKey id that made the request. + * + * @generated from field: string key_id = 7; + */ + keyId: string; + + /** + * required_level is the lowest level that may decide the approval: + * "viewer" | "operator" | "admin". + * + * @generated from field: string required_level = 8; + */ + requiredLevel: string; + + /** + * created_at is when the approval was raised. + * + * @generated from field: string created_at = 9; + */ + createdAt: string; + + /** + * expires_at is when a pending approval lapses to expired. + * + * @generated from field: string expires_at = 10; + */ + expiresAt: string; + + /** + * status is "pending" | "approved" | "denied" | "expired" | "used". + * + * @generated from field: string status = 11; + */ + status: string; + + /** + * decided_by_cn is the subject CN of the deciding operator certificate; + * empty until decided. + * + * @generated from field: string decided_by_cn = 12; + */ + decidedByCn: string; + + /** + * decided_by_serial is the hex serial of that certificate; empty until + * decided. + * + * @generated from field: string decided_by_serial = 13; + */ + decidedBySerial: string; + + /** + * decided_at is when the approval was decided; empty until decided. + * + * @generated from field: string decided_at = 14; + */ + decidedAt: string; +}; + +/** + * Describes the message cryptos.fleet.v1.Approval. + * Use `create(ApprovalSchema)` to create a new message. + */ +export const ApprovalSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_fleet_v1_fleet, 84); /** * Health reports the manager's view of node reachability, independent of @@ -2299,6 +2836,27 @@ export const FleetService: GenService<{ input: typeof SetAdapterEnabledRequestSchema; output: typeof SetAdapterEnabledResponseSchema; }, + /** + * SetNodeProtocol switches one enrolment protocol on or off on one managed + * node. The manager fetches the node's config, sets that protocol block's + * enabled flag, leaves its other settings as the node returned them, and + * applies it back through the node's ApplyConfig. Write-only secrets come + * back blank from the node and are sent blank, so the node keeps what it + * stores; they never reach the manager. The other protocol blocks are left + * out of the apply, so the node keeps them unchanged. A switch is + * reboot-required: the node stores it and the listener changes at the next + * boot, and NodeSummary.reboot_required stays true until the node reports it + * running in the new state. The node's refusal (a Root refusing + * enabled=true, a block that fails validation) is returned as its error. + * Admin-gated and audited. + * + * @generated from rpc cryptos.fleet.v1.FleetService.SetNodeProtocol + */ + setNodeProtocol: { + methodKind: "unary"; + input: typeof SetNodeProtocolRequestSchema; + output: typeof SetNodeProtocolResponseSchema; + }, /** * ListAudit returns the manager's audit log. * @@ -2365,7 +2923,7 @@ export const FleetService: GenService<{ }, /** * RevokeCertificate revokes an issued certificate on the node that issued - * it, identified by node name and hex serial, with an RFC 5280 reason code. + * it, identified by node and hex serial, with an RFC 5280 reason code. * * @generated from rpc cryptos.fleet.v1.FleetService.RevokeCertificate */ @@ -2549,6 +3107,24 @@ export const FleetService: GenService<{ input: typeof DecommissionNodeRequestSchema; output: typeof DecommissionNodeResponseSchema; }, + /** + * RenameNode changes a node's display name. The node is addressed by its + * stable ID, which never changes, so URLs, audit entries and references + * recorded against the ID keep pointing at the node. Returns the updated + * node. NotFound if no node has node_id; AlreadyExists if another node + * already has new_name; InvalidArgument if new_name is not an RFC 1123 label + * (1 to 63 lowercase letters, digits and hyphens, starting and ending with a + * letter or digit). Renaming a node to its current name returns it unchanged + * and records nothing. Admin-gated and audited (node-renamed, with the old + * and new names). + * + * @generated from rpc cryptos.fleet.v1.FleetService.RenameNode + */ + renameNode: { + methodKind: "unary"; + input: typeof RenameNodeRequestSchema; + output: typeof RenameNodeResponseSchema; + }, /** * ListMcpKeys returns the MCP agent keys bound to the calling operator's * certificate. An admin may set all to list every operator's keys; a @@ -2592,6 +3168,32 @@ export const FleetService: GenService<{ input: typeof CreateMcpKeyRequestSchema; output: typeof CreateMcpKeyResponseSchema; }, + /** + * ListApprovals returns step-up approval requests, newest first, optionally + * filtered by status. An approval is raised when an MCP tool call needs a + * human decision before it runs. Operator-certificate only: an MCP key can + * never list approvals. + * + * @generated from rpc cryptos.fleet.v1.FleetService.ListApprovals + */ + listApprovals: { + methodKind: "unary"; + input: typeof ListApprovalsRequestSchema; + output: typeof ListApprovalsResponseSchema; + }, + /** + * DecideApproval approves or denies a pending approval by id and returns the + * updated approval. Operator-certificate only: an MCP key can never decide + * approvals. The deciding operator's level must be at least the approval's + * required_level. Audited. + * + * @generated from rpc cryptos.fleet.v1.FleetService.DecideApproval + */ + decideApproval: { + methodKind: "unary"; + input: typeof DecideApprovalRequestSchema; + output: typeof DecideApprovalResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_cryptos_fleet_v1_fleet, 0); diff --git a/src/gen/fleet/cryptos/v1/config_pb.ts b/src/gen/fleet/cryptos/v1/config_pb.ts index ceb5fc5..0da6f0b 100644 --- a/src/gen/fleet/cryptos/v1/config_pb.ts +++ b/src/gen/fleet/cryptos/v1/config_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file cryptos/v1/config.proto. */ export const file_cryptos_v1_config: GenFile = /*@__PURE__*/ - fileDesc("ChdjcnlwdG9zL3YxL2NvbmZpZy5wcm90bxIKY3J5cHRvcy52MSKJAwoNTWFjaGluZUNvbmZpZxITCgthcGlfdmVyc2lvbhgBIAEoCRIMCgRraW5kGAIgASgJEiYKCG1ldGFkYXRhGAMgASgLMhQuY3J5cHRvcy52MS5NZXRhZGF0YRIeCgRyb2xlGAQgASgLMhAuY3J5cHRvcy52MS5Sb2xlEiQKB25ldHdvcmsYBSABKAsyEy5jcnlwdG9zLnYxLk5ldHdvcmsSJAoHc3RvcmFnZRgGIAEoCzITLmNyeXB0b3MudjEuU3RvcmFnZRIoCglib290c3RyYXAYByABKAsyFS5jcnlwdG9zLnYxLkJvb3RzdHJhcBIcCgNwa2kYCCABKAsyDy5jcnlwdG9zLnYxLlBraRIkCgdpbnN0YWxsGAkgASgLMhMuY3J5cHRvcy52MS5JbnN0YWxsEicKCXN0YXRlX2tleRgKIAEoCzIULmNyeXB0b3MudjEuU3RhdGVLZXkSKgoKbWFuYWdlbWVudBgLIAEoCzIWLmNyeXB0b3MudjEuTWFuYWdlbWVudCJWCgpNYW5hZ2VtZW50EhIKCm1hbmFnZXJfY24YASABKAkSEQoJdHJ1c3RfcGVtGAIgASgJEiEKGW9wZXJhdG9yX3N1cmZhY2VfcmVhZG9ubHkYAyABKAgiPgoIU3RhdGVLZXkSDAoEbW9kZRgBIAEoCRIkCgNrbXMYAiABKAsyFy5jcnlwdG9zLnYxLkttc1N0YXRlS2V5IjIKC0ttc1N0YXRlS2V5EhAKCGVuZHBvaW50GAEgASgJEhEKCXRydXN0X3BlbRgCIAEoCSIYCghNZXRhZGF0YRIMCgRuYW1lGAEgASgJIhQKBFJvbGUSDAoEa2luZBgBIAEoCSJjCgdOZXR3b3JrEhEKCWludGVyZmFjZRgBIAEoCRIPCgdhZGRyZXNzGAIgASgJEg8KB2dhdGV3YXkYAyABKAkSEwoLbmFtZXNlcnZlcnMYBCADKAkSDgoGc2VhcmNoGAUgAygJIjwKB1N0b3JhZ2USHQoVc3RhdGVfcGFydGl0aW9uX2xhYmVsGAEgASgJEhIKCmZpcnN0X2Jvb3QYAiABKAgiPgoJQm9vdHN0cmFwEhYKDmFkbWluX2NlcnRfcGVtGAEgASgJEhkKEWFkbWluX2NlcnRfc2hhMjU2GAIgASgJIvUCCgNQa2kSFAoMcm9vdF9rZXlfYWxnGAEgASgJEikKDHJvb3Rfc3ViamVjdBgCIAEoCzITLmNyeXB0b3MudjEuU3ViamVjdBIbChNyb290X3ZhbGlkaXR5X3llYXJzGAMgASgNEhsKE3BhdGhfbGVuX2NvbnN0cmFpbnQYBCABKA0SMAoIcHJvZmlsZXMYBSADKAsyHi5jcnlwdG9zLnYxLkNlcnRpZmljYXRlUHJvZmlsZRIiCgZwYXJlbnQYBiABKAsyEi5jcnlwdG9zLnYxLlBhcmVudBIbChNyZXZvY2F0aW9uX2Jhc2VfdXJsGAcgASgJEicKH2FsbG93X3VudmVyaWZpZWRfcmV2b2NhdGlvbl91cmwYCCABKAgSHQoVY3JsX25leHRfdXBkYXRlX2hvdXJzGAkgASgNEhwKFHJldm9jYXRpb25faHR0cF9wb3J0GAogASgNEhoKEnJvb3RfbGVhZl9pc3N1YW5jZRgLIAEoCSI1CgZQYXJlbnQSEwoLY2FfY2VydF9wZW0YASABKAkSFgoOY2FfY2VydF9zaGEyNTYYAiABKAki6AIKEkNlcnRpZmljYXRlUHJvZmlsZRIMCgRuYW1lGAEgASgJEg8KB2tleV9hbGcYAiABKAkSJAoHc3ViamVjdBgDIAEoCzITLmNyeXB0b3MudjEuU3ViamVjdBIVCg12YWxpZGl0eV9kYXlzGAQgASgNEjcKEWJhc2ljX2NvbnN0cmFpbnRzGAUgASgLMhwuY3J5cHRvcy52MS5CYXNpY0NvbnN0cmFpbnRzEhEKCWtleV91c2FnZRgGIAMoCRIVCg1leHRfa2V5X3VzYWdlGAcgAygJEikKBHNhbnMYCCABKAsyGy5jcnlwdG9zLnYxLlN1YmplY3RBbHROYW1lcxIzChBleHRyYV9leHRlbnNpb25zGAkgAygLMhkuY3J5cHRvcy52MS5YNTA5RXh0ZW5zaW9uEhoKEmFsbG93X3JlcXVlc3Rfc2FucxgKIAEoCBIXCg92YWxpZGl0eV9wb2xpY3kYCyABKAkiRQoQQmFzaWNDb25zdHJhaW50cxINCgVpc19jYRgBIAEoCBIVCghwYXRoX2xlbhgCIAEoDUgAiAEBQgsKCV9wYXRoX2xlbiJrCg9TdWJqZWN0QWx0TmFtZXMSCwoDZG5zGAEgAygJEgoKAmlwGAIgAygJEg0KBWVtYWlsGAMgAygJEgsKA3VyaRgEIAMoCRIWCg5rcmI1X3ByaW5jaXBhbBgFIAMoCRILCgN1cG4YBiADKAkiPQoNWDUwOUV4dGVuc2lvbhILCgNvaWQYASABKAkSEAoIY3JpdGljYWwYAiABKAgSDQoFdmFsdWUYAyABKAwiFwoHSW5zdGFsbBIMCgRkaXNrGAEgASgJImkKB1N1YmplY3QSEwoLY29tbW9uX25hbWUYASABKAkSFAoMb3JnYW5pemF0aW9uGAIgASgJEg8KB2NvdW50cnkYAyABKAkSEAoIcHJvdmluY2UYBCABKAkSEAoIbG9jYWxpdHkYBSABKAlCNFoyZ2l0aHViLmNvbS9DcnlwdE9TLVBLSS9hcGkvZ28vY3J5cHRvcy92MTtjcnlwdG9zdjFiBnByb3RvMw"); + fileDesc("ChdjcnlwdG9zL3YxL2NvbmZpZy5wcm90bxIKY3J5cHRvcy52MSKJAwoNTWFjaGluZUNvbmZpZxITCgthcGlfdmVyc2lvbhgBIAEoCRIMCgRraW5kGAIgASgJEiYKCG1ldGFkYXRhGAMgASgLMhQuY3J5cHRvcy52MS5NZXRhZGF0YRIeCgRyb2xlGAQgASgLMhAuY3J5cHRvcy52MS5Sb2xlEiQKB25ldHdvcmsYBSABKAsyEy5jcnlwdG9zLnYxLk5ldHdvcmsSJAoHc3RvcmFnZRgGIAEoCzITLmNyeXB0b3MudjEuU3RvcmFnZRIoCglib290c3RyYXAYByABKAsyFS5jcnlwdG9zLnYxLkJvb3RzdHJhcBIcCgNwa2kYCCABKAsyDy5jcnlwdG9zLnYxLlBraRIkCgdpbnN0YWxsGAkgASgLMhMuY3J5cHRvcy52MS5JbnN0YWxsEicKCXN0YXRlX2tleRgKIAEoCzIULmNyeXB0b3MudjEuU3RhdGVLZXkSKgoKbWFuYWdlbWVudBgLIAEoCzIWLmNyeXB0b3MudjEuTWFuYWdlbWVudCJWCgpNYW5hZ2VtZW50EhIKCm1hbmFnZXJfY24YASABKAkSEQoJdHJ1c3RfcGVtGAIgASgJEiEKGW9wZXJhdG9yX3N1cmZhY2VfcmVhZG9ubHkYAyABKAgiPgoIU3RhdGVLZXkSDAoEbW9kZRgBIAEoCRIkCgNrbXMYAiABKAsyFy5jcnlwdG9zLnYxLkttc1N0YXRlS2V5IjIKC0ttc1N0YXRlS2V5EhAKCGVuZHBvaW50GAEgASgJEhEKCXRydXN0X3BlbRgCIAEoCSIYCghNZXRhZGF0YRIMCgRuYW1lGAEgASgJIhQKBFJvbGUSDAoEa2luZBgBIAEoCSJ4CgdOZXR3b3JrEhEKCWludGVyZmFjZRgBIAEoCRIPCgdhZGRyZXNzGAIgASgJEg8KB2dhdGV3YXkYAyABKAkSEwoLbmFtZXNlcnZlcnMYBCADKAkSDgoGc2VhcmNoGAUgAygJEhMKC250cF9zZXJ2ZXJzGAYgAygJIjwKB1N0b3JhZ2USHQoVc3RhdGVfcGFydGl0aW9uX2xhYmVsGAEgASgJEhIKCmZpcnN0X2Jvb3QYAiABKAgiPgoJQm9vdHN0cmFwEhYKDmFkbWluX2NlcnRfcGVtGAEgASgJEhkKEWFkbWluX2NlcnRfc2hhMjU2GAIgASgJIsoECgNQa2kSFAoMcm9vdF9rZXlfYWxnGAEgASgJEikKDHJvb3Rfc3ViamVjdBgCIAEoCzITLmNyeXB0b3MudjEuU3ViamVjdBIbChNyb290X3ZhbGlkaXR5X3llYXJzGAMgASgNEhsKE3BhdGhfbGVuX2NvbnN0cmFpbnQYBCABKA0SMAoIcHJvZmlsZXMYBSADKAsyHi5jcnlwdG9zLnYxLkNlcnRpZmljYXRlUHJvZmlsZRIiCgZwYXJlbnQYBiABKAsyEi5jcnlwdG9zLnYxLlBhcmVudBIbChNyZXZvY2F0aW9uX2Jhc2VfdXJsGAcgASgJEicKH2FsbG93X3VudmVyaWZpZWRfcmV2b2NhdGlvbl91cmwYCCABKAgSHQoVY3JsX25leHRfdXBkYXRlX2hvdXJzGAkgASgNEhwKFHJldm9jYXRpb25faHR0cF9wb3J0GAogASgNEhoKEnJvb3RfbGVhZl9pc3N1YW5jZRgLIAEoCRIcChRhbGxvd191bnN5bmNlZF9jbG9jaxgMIAEoCBIeCgRhY21lGA0gASgLMhAuY3J5cHRvcy52MS5BY21lEhwKA2VzdBgOIAEoCzIPLmNyeXB0b3MudjEuRXN0Eh4KBHNjZXAYDyABKAsyEC5jcnlwdG9zLnYxLlNjZXASHAoDdHNhGBAgASgLMg8uY3J5cHRvcy52MS5Uc2ESOQoSd2luZG93c19lbnJvbGxtZW50GBEgASgLMh0uY3J5cHRvcy52MS5XaW5kb3dzRW5yb2xsbWVudCKbAgoEQWNtZRIPCgdlbmFibGVkGAEgASgIEhAKCGJhc2VfdXJsGAIgASgJEhEKCWh0dHBfcG9ydBgDIAEoDRIPCgdwcm9maWxlGAQgASgJEhgKEHRlcm1zX29mX3NlcnZpY2UYBSABKAkSDwoHd2Vic2l0ZRgGIAEoCRIgChhhbGxvd19hbm9ueW1vdXNfYWNjb3VudHMYByABKAgSQQoVZXh0ZXJuYWxfYWNjb3VudF9rZXlzGAggAygLMiIuY3J5cHRvcy52MS5BY21lRXh0ZXJuYWxBY2NvdW50S2V5EiMKG2FsbG93ZWRfaWRlbnRpZmllcl9zdWZmaXhlcxgJIAMoCRIXCg9vcmRlcl90dGxfaG91cnMYCiABKA0iQQoWQWNtZUV4dGVybmFsQWNjb3VudEtleRIOCgZrZXlfaWQYASABKAkSFwoPaG1hY19rZXlfYmFzZTY0GAIgASgJIusBCgNFc3QSDwoHZW5hYmxlZBgBIAEoCBIRCglob3N0bmFtZXMYAiADKAkSEQoJaHR0cF9wb3J0GAMgASgNEg8KB3Byb2ZpbGUYBCABKAkSDQoFbGFiZWwYBSABKAkSDQoFcmVhbG0YBiABKAkSIwobYWxsb3dlZF9pZGVudGlmaWVyX3N1ZmZpeGVzGAcgAygJEhwKFGFsbG93X2FueV9pZGVudGlmaWVyGAggASgIEjsKEmVucm9sbF9jcmVkZW50aWFscxgJIAMoCzIfLmNyeXB0b3MudjEuRXN0RW5yb2xsQ3JlZGVudGlhbCJAChNFc3RFbnJvbGxDcmVkZW50aWFsEhAKCHVzZXJuYW1lGAEgASgJEhcKD3Bhc3N3b3JkX3NoYTI1NhgCIAEoCSKaAQoEU2NlcBIPCgdlbmFibGVkGAEgASgIEhEKCWh0dHBfcG9ydBgCIAEoDRIpCghwcm9maWxlcxgDIAMoCzIXLmNyeXB0b3MudjEuU2NlcFByb2ZpbGUSIwobYWxsb3dlZF9pZGVudGlmaWVyX3N1ZmZpeGVzGAQgAygJEh4KAnJhGAUgASgLMhIuY3J5cHRvcy52MS5TY2VwUmEiUgoLU2NlcFByb2ZpbGUSDwoHcHJvZmlsZRgBIAEoCRIYChBtaW5fcnNhX2tleV9iaXRzGAIgASgNEhgKEHJlcXVpcmVfYXBwcm92YWwYAyABKAgiPgoGU2NlcFJhEhUKDXZhbGlkaXR5X2RheXMYASABKA0SHQoVcm90YXRpb25fb3ZlcmxhcF9kYXlzGAIgASgNItMBCgNUc2ESDwoHZW5hYmxlZBgBIAEoCBIRCglodHRwX3BvcnQYAiABKA0SEgoKcG9saWN5X29pZBgDIAEoCRITCgthY2N1cmFjeV9tcxgEIAEoDRIsCgpyYXRlX2xpbWl0GAUgASgLMhguY3J5cHRvcy52MS5Uc2FSYXRlTGltaXQSGAoQYWxsb3dlZF9uZXR3b3JrcxgGIAMoCRI3CgtjZXJ0aWZpY2F0ZRgHIAEoCzIiLmNyeXB0b3MudjEuVHNhQ2VydGlmaWNhdGVTZXR0aW5ncyI6CgxUc2FSYXRlTGltaXQSGwoTcmVxdWVzdHNfcGVyX21pbnV0ZRgBIAEoDRINCgVidXJzdBgCIAEoDSJOChZUc2FDZXJ0aWZpY2F0ZVNldHRpbmdzEhUKDXZhbGlkaXR5X2RheXMYASABKA0SHQoVcm90YXRpb25fb3ZlcmxhcF9kYXlzGAIgASgNIrECChFXaW5kb3dzRW5yb2xsbWVudBIPCgdlbmFibGVkGAEgASgIEhEKCWhvc3RuYW1lcxgCIAMoCRISCgpodHRwc19wb3J0GAMgASgNEhMKC3BvbGljeV9wYXRoGAQgASgJEhcKD2Vucm9sbG1lbnRfcGF0aBgFIAEoCRIUCgxyZW5ld2FsX3BhdGgYBiABKAkSLQoIa2VyYmVyb3MYByABKAsyGy5jcnlwdG9zLnYxLldpbmRvd3NLZXJiZXJvcxIlCgRsZGFwGAggASgLMhcuY3J5cHRvcy52MS5XaW5kb3dzTGRhcBIuCgl0ZW1wbGF0ZXMYCSADKAsyGy5jcnlwdG9zLnYxLldpbmRvd3NUZW1wbGF0ZRIaChJvbWl0X3NpZF9leHRlbnNpb24YCiABKAgiSwoPV2luZG93c0tlcmJlcm9zEg0KBXJlYWxtGAEgASgJEhkKEXNlcnZpY2VfcHJpbmNpcGFsGAIgASgJEg4KBmtleXRhYhgDIAEoDCJrCgtXaW5kb3dzTGRhcBIMCgR1cmxzGAEgAygJEikKBGJpbmQYAiABKAsyGy5jcnlwdG9zLnYxLldpbmRvd3NMZGFwQmluZBIQCghiYXNlX2RucxgDIAMoCRIRCgl0cnVzdF9wZW0YBCABKAkiVQoPV2luZG93c0xkYXBCaW5kEgwKBG1vZGUYASABKAkSDwoHYmluZF9kbhgCIAEoCRIQCghwYXNzd29yZBgDIAEoCRIRCglwcmluY2lwYWwYBCABKAkioQEKD1dpbmRvd3NUZW1wbGF0ZRIMCgRuYW1lGAEgASgJEg8KB3Byb2ZpbGUYAiABKAkSFwoPZW5yb2xsbWVudF90eXBlGAMgASgJEiEKGWFsbG93X2NlcnRpZmljYXRlX3JlbmV3YWwYBCABKAgSFgoOYWxsb3dlZF9ncm91cHMYBSADKAkSGwoTcmVuZXdhbF9wZXJpb2RfZGF5cxgGIAEoDSI1CgZQYXJlbnQSEwoLY2FfY2VydF9wZW0YASABKAkSFgoOY2FfY2VydF9zaGEyNTYYAiABKAki6AIKEkNlcnRpZmljYXRlUHJvZmlsZRIMCgRuYW1lGAEgASgJEg8KB2tleV9hbGcYAiABKAkSJAoHc3ViamVjdBgDIAEoCzITLmNyeXB0b3MudjEuU3ViamVjdBIVCg12YWxpZGl0eV9kYXlzGAQgASgNEjcKEWJhc2ljX2NvbnN0cmFpbnRzGAUgASgLMhwuY3J5cHRvcy52MS5CYXNpY0NvbnN0cmFpbnRzEhEKCWtleV91c2FnZRgGIAMoCRIVCg1leHRfa2V5X3VzYWdlGAcgAygJEikKBHNhbnMYCCABKAsyGy5jcnlwdG9zLnYxLlN1YmplY3RBbHROYW1lcxIzChBleHRyYV9leHRlbnNpb25zGAkgAygLMhkuY3J5cHRvcy52MS5YNTA5RXh0ZW5zaW9uEhoKEmFsbG93X3JlcXVlc3Rfc2FucxgKIAEoCBIXCg92YWxpZGl0eV9wb2xpY3kYCyABKAkiRQoQQmFzaWNDb25zdHJhaW50cxINCgVpc19jYRgBIAEoCBIVCghwYXRoX2xlbhgCIAEoDUgAiAEBQgsKCV9wYXRoX2xlbiJrCg9TdWJqZWN0QWx0TmFtZXMSCwoDZG5zGAEgAygJEgoKAmlwGAIgAygJEg0KBWVtYWlsGAMgAygJEgsKA3VyaRgEIAMoCRIWCg5rcmI1X3ByaW5jaXBhbBgFIAMoCRILCgN1cG4YBiADKAkiPQoNWDUwOUV4dGVuc2lvbhILCgNvaWQYASABKAkSEAoIY3JpdGljYWwYAiABKAgSDQoFdmFsdWUYAyABKAwiFwoHSW5zdGFsbBIMCgRkaXNrGAEgASgJImkKB1N1YmplY3QSEwoLY29tbW9uX25hbWUYASABKAkSFAoMb3JnYW5pemF0aW9uGAIgASgJEg8KB2NvdW50cnkYAyABKAkSEAoIcHJvdmluY2UYBCABKAkSEAoIbG9jYWxpdHkYBSABKAlCNFoyZ2l0aHViLmNvbS9DcnlwdE9TLVBLSS9hcGkvZ28vY3J5cHRvcy92MTtjcnlwdG9zdjFiBnByb3RvMw"); /** * MachineConfig is the declarative configuration applied via ApplyConfig. @@ -262,6 +262,17 @@ export type Network = Message<"cryptos.v1.Network"> & { * @generated from field: repeated string search = 5; */ search: string[]; + + /** + * ntp_servers are the SNTP servers the node keeps its clock in sync with, at + * most three, each an IPv4 literal or a hostname. A hostname is resolved + * through the node resolver at each sync. Empty means the node uses the NTP + * servers its DHCP lease supplied (option 42), if any; with neither, the node + * runs on its hardware clock. + * + * @generated from field: repeated string ntp_servers = 6; + */ + ntpServers: string[]; }; /** @@ -323,7 +334,25 @@ export const BootstrapSchema: GenMessage = /*@__PURE__*/ */ export type Pki = Message<"cryptos.v1.Pki"> & { /** - * Phase 1: must be "ECDSA-P384" for Roots. + * root_key_alg is the algorithm of this node's own CA key, on a Root and on + * a subordinate (intermediate or issuing) alike, and so also the signature + * algorithm of every certificate the node issues. Accepted values are + * "ECDSA-P384", "RSA-3072" and "RSA-4096"; anything else is rejected when + * the config is validated. RSA is there so a platform CA that accepts only + * RSA-signed chains can be subordinated under this node. + * + * "RSA-2048" is rejected even though a 2048-bit key could sign: a node + * certifies a subject key (its own when a Root self-signs, or the key in a + * CSR it signs) only when it is ECDSA P-384 or RSA of at least 3072 bits, so + * a 2048-bit CA key would pass validation and then fail at the ceremony or + * at subordination. + * + * An RSA CA key cannot be held in the TPM: with state_key.mode "tpm" (or an + * empty mode on a build that defaults to it) the key is refused when it is + * created, at the Root ceremony or a subordinate's first boot, and the node + * does not fall back to another algorithm. Use state_key.mode "nodeid" or + * "kms" for an RSA CA, where the key is kept in software on the encrypted + * state partition. * * @generated from field: string root_key_alg = 1; */ @@ -403,6 +432,57 @@ export type Pki = Message<"cryptos.v1.Pki"> & { * @generated from field: string root_leaf_issuance = 11; */ rootLeafIssuance: string; + + /** + * allow_unsynced_clock overrides the fail-closed clock gate. While a time + * source is configured (network.ntp_servers or a DHCP lease) but the node + * has not yet synced its clock this boot, certificate signing is refused; + * true lets it proceed on the unsynced clock. A node with no time source is + * never gated, and CRL and OCSP generation are never gated. + * + * @generated from field: bool allow_unsynced_clock = 12; + */ + allowUnsyncedClock: boolean; + + /** + * acme configures the RFC 8555 enrolment endpoint. See "Protocol blocks" + * below for how an absent block, enabled=false and enabled=true apply. + * + * @generated from field: cryptos.v1.Acme acme = 13; + */ + acme?: Acme | undefined; + + /** + * est configures the RFC 7030 enrolment endpoint, applied the same way as + * acme. + * + * @generated from field: cryptos.v1.Est est = 14; + */ + est?: Est | undefined; + + /** + * scep configures the RFC 8894 enrolment endpoint, applied the same way as + * acme. + * + * @generated from field: cryptos.v1.Scep scep = 15; + */ + scep?: Scep | undefined; + + /** + * tsa configures the RFC 3161 time-stamp authority, applied the same way as + * acme. + * + * @generated from field: cryptos.v1.Tsa tsa = 16; + */ + tsa?: Tsa | undefined; + + /** + * windows_enrollment configures the Windows autoenrolment endpoints + * (MS-XCEP policy and MS-WSTEP enrolment), applied the same way as acme. + * + * @generated from field: cryptos.v1.WindowsEnrollment windows_enrollment = 17; + */ + windowsEnrollment?: WindowsEnrollment | undefined; }; /** @@ -412,6 +492,985 @@ export type Pki = Message<"cryptos.v1.Pki"> & { export const PkiSchema: GenMessage = /*@__PURE__*/ messageDesc(file_cryptos_v1_config, 9); +/** + * Acme is the node's RFC 8555 server. + * + * @generated from message cryptos.v1.Acme + */ +export type Acme = Message<"cryptos.v1.Acme"> & { + /** + * enabled switches the ACME listener on at the next boot. + * + * @generated from field: bool enabled = 1; + */ + enabled: boolean; + + /** + * base_url is the externally reachable base under which the ACME endpoints + * live, for example "https://ca.example.org/acme". Every URL handed to a + * client is built from it and every request's signed url header is checked + * against it, so it must be what clients dial rather than what the node + * binds. Required when enabled. + * + * @generated from field: string base_url = 2; + */ + baseUrl: string; + + /** + * http_port is the TCP port the ACME listener binds. Zero means the node + * default. ACME is normally fronted by TLS, so this listener is plain HTTP + * and separate from revocation_http_port. + * + * @generated from field: uint32 http_port = 3; + */ + httpPort: number; + + /** + * profile names the non-CA certificate profile in Pki.profiles that ACME + * issues under. Required when enabled; there is no default. + * + * @generated from field: string profile = 4; + */ + profile: string; + + /** + * terms_of_service, when set, is advertised in the directory and a new + * account must agree to it. + * + * @generated from field: string terms_of_service = 5; + */ + termsOfService: string; + + /** + * website is advertised in the directory meta. + * + * @generated from field: string website = 6; + */ + website: string; + + /** + * allow_anonymous_accounts drops the External Account Binding requirement + * (RFC 8555 section 7.3.4), so anyone who can answer an http-01 challenge can + * register and order. Off by default. + * + * @generated from field: bool allow_anonymous_accounts = 7; + */ + allowAnonymousAccounts: boolean; + + /** + * external_account_keys are the External Account Binding credentials. At + * least one is required when enabled, unless allow_anonymous_accounts is set. + * + * @generated from field: repeated cryptos.v1.AcmeExternalAccountKey external_account_keys = 8; + */ + externalAccountKeys: AcmeExternalAccountKey[]; + + /** + * allowed_identifier_suffixes, when non-empty, restricts the DNS names the + * node orders for: a name must equal, or be a subdomain of, one of these. + * Empty places no name restriction beyond proof of control and the account + * binding. + * + * @generated from field: repeated string allowed_identifier_suffixes = 9; + */ + allowedIdentifierSuffixes: string[]; + + /** + * order_ttl_hours is how long an order and its authorizations stay valid. + * Zero means the node default. + * + * @generated from field: uint32 order_ttl_hours = 10; + */ + orderTtlHours: number; +}; + +/** + * Describes the message cryptos.v1.Acme. + * Use `create(AcmeSchema)` to create a new message. + */ +export const AcmeSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_config, 10); + +/** + * AcmeExternalAccountKey is one External Account Binding credential. + * + * @generated from message cryptos.v1.AcmeExternalAccountKey + */ +export type AcmeExternalAccountKey = Message<"cryptos.v1.AcmeExternalAccountKey"> & { + /** + * key_id is the identifier the client sends as the binding's kid. Unique + * within the block. + * + * @generated from field: string key_id = 1; + */ + keyId: string; + + /** + * hmac_key_base64 is the shared HMAC secret, base64url-encoded without + * padding, at least 32 bytes decoded. Write-only: GetConfig returns it empty. + * + * @generated from field: string hmac_key_base64 = 2; + */ + hmacKeyBase64: string; +}; + +/** + * Describes the message cryptos.v1.AcmeExternalAccountKey. + * Use `create(AcmeExternalAccountKeySchema)` to create a new message. + */ +export const AcmeExternalAccountKeySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_config, 11); + +/** + * Est is the node's RFC 7030 server. It terminates TLS itself, with a server + * certificate the node mints from its own CA for hostnames, because + * simplereenroll authenticates with a TLS client certificate. + * + * @generated from message cryptos.v1.Est + */ +export type Est = Message<"cryptos.v1.Est"> & { + /** + * enabled switches the EST listener on at the next boot. + * + * @generated from field: bool enabled = 1; + */ + enabled: boolean; + + /** + * hostnames are the DNS names and IP literals clients reach the endpoint on, + * stamped into the listener's server certificate. At least one is required + * when enabled. + * + * @generated from field: repeated string hostnames = 2; + */ + hostnames: string[]; + + /** + * http_port is the TCP port the EST listener binds. Zero means the node + * default. + * + * @generated from field: uint32 http_port = 3; + */ + httpPort: number; + + /** + * profile names the non-CA certificate profile in Pki.profiles that EST + * issues under. Required when enabled. + * + * @generated from field: string profile = 4; + */ + profile: string; + + /** + * label is the optional path segment between /.well-known/est and the + * operation (RFC 7030 section 3.2.2), a single segment. Empty serves the + * unlabelled paths. + * + * @generated from field: string label = 5; + */ + label: string; + + /** + * realm is the HTTP Basic realm offered when simpleenroll challenges. + * + * @generated from field: string realm = 6; + */ + realm: string; + + /** + * allowed_identifier_suffixes restricts the names simpleenroll issues for: a + * name must equal, or be a subdomain of, one of these. It does not restrict + * simplereenroll, whose names are pinned to the certificate the client + * already holds. Required when enroll_credentials is set, unless + * allow_any_identifier is true. + * + * @generated from field: repeated string allowed_identifier_suffixes = 7; + */ + allowedIdentifierSuffixes: string[]; + + /** + * allow_any_identifier drops that restriction. simpleenroll proves nothing + * about control of a name, so without an allowlist one leaked credential + * mints a certificate for any name. + * + * @generated from field: bool allow_any_identifier = 8; + */ + allowAnyIdentifier: boolean; + + /** + * enroll_credentials are the HTTP Basic credentials that authorize + * simpleenroll. Empty keeps simpleenroll closed and offers + * certificate-authenticated renewal (simplereenroll) only. + * + * @generated from field: repeated cryptos.v1.EstEnrollCredential enroll_credentials = 9; + */ + enrollCredentials: EstEnrollCredential[]; +}; + +/** + * Describes the message cryptos.v1.Est. + * Use `create(EstSchema)` to create a new message. + */ +export const EstSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_config, 12); + +/** + * EstEnrollCredential is one simpleenroll credential. + * + * @generated from message cryptos.v1.EstEnrollCredential + */ +export type EstEnrollCredential = Message<"cryptos.v1.EstEnrollCredential"> & { + /** + * username is the HTTP Basic user name. Unique within the block. + * + * @generated from field: string username = 1; + */ + username: string; + + /** + * password_sha256 is the lowercase hex SHA-256 of the password, so the node + * never stores a live credential. The password must be a generated + * high-entropy value, because a digest of a chosen word falls to a + * dictionary. Write-only: GetConfig returns it empty. + * + * @generated from field: string password_sha256 = 2; + */ + passwordSha256: string; +}; + +/** + * Describes the message cryptos.v1.EstEnrollCredential. + * Use `create(EstEnrollCredentialSchema)` to create a new message. + */ +export const EstEnrollCredentialSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_config, 13); + +/** + * Scep is the node's RFC 8894 server. It is plain HTTP, as the RFC intends: + * the CMS envelope carries confidentiality and integrity. Clients encrypt + * requests to, and verify responses from, an RA certificate the node mints + * from its own CA, so the CA key only ever signs certificates. + * + * Initial enrolment (PKCSReq) is authorized by a one-time challenge minted + * with NodeService.MintScepChallenge. Renewal (RenewalReq) is authorized by + * the client's current certificate, which must chain to this node's CA and not + * be revoked, and the renewed certificate keeps that certificate's names. + * There is no static shared challenge, by design: one secret configured for + * every device would let anyone who lifts it from one device enrol as any. + * + * @generated from message cryptos.v1.Scep + */ +export type Scep = Message<"cryptos.v1.Scep"> & { + /** + * enabled switches the SCEP listener on at the next boot. + * + * @generated from field: bool enabled = 1; + */ + enabled: boolean; + + /** + * http_port is the TCP port the SCEP listener binds. Zero means the node + * default. + * + * @generated from field: uint32 http_port = 2; + */ + httpPort: number; + + /** + * profiles are the certificate profiles SCEP issues from, each with its own + * key floor and approval switch. At least one is required when enabled, and + * a profile name appears at most once. A challenge names the profile its + * enrolment issues from; a renewal issues from the profile of the + * certificate being renewed. + * + * @generated from field: repeated cryptos.v1.ScepProfile profiles = 3; + */ + profiles: ScepProfile[]; + + /** + * allowed_identifier_suffixes restricts the names an initial enrolment + * issues for: every DNS name in the request, and the subject common name, + * must equal, or be a subdomain of, one of these. Required when enabled; + * unlike EST there is no switch to drop it, because a challenge proves + * nothing about control of a name. It does not restrict renewal, whose names + * are pinned to the certificate the client already holds. + * + * @generated from field: repeated string allowed_identifier_suffixes = 4; + */ + allowedIdentifierSuffixes: string[]; + + /** + * ra configures the RA certificate that decrypts requests and signs + * responses. Unset means the defaults below. + * + * @generated from field: cryptos.v1.ScepRa ra = 5; + */ + ra?: ScepRa | undefined; +}; + +/** + * Describes the message cryptos.v1.Scep. + * Use `create(ScepSchema)` to create a new message. + */ +export const ScepSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_config, 14); + +/** + * ScepProfile is one certificate profile SCEP may issue from. + * + * @generated from message cryptos.v1.ScepProfile + */ +export type ScepProfile = Message<"cryptos.v1.ScepProfile"> & { + /** + * profile names a non-CA certificate profile in Pki.profiles. Required. + * + * @generated from field: string profile = 1; + */ + profile: string; + + /** + * min_rsa_key_bits is the smallest RSA subject key this profile accepts over + * SCEP. Zero means 3072, the node-wide floor. 2048 is the lowest value + * accepted, for devices that cannot hold a larger key (Cisco IOS and IOS-XE + * trustpoints are limited to RSA 2048), and a value below it is rejected. + * Stronger RSA keys and ECDSA P-384 keys are always accepted. The floor + * applies only to certificates this profile issues over SCEP: the node's + * own CA, RA and listener keys stay RSA 3072 or larger, or ECDSA P-384, and + * other issuance paths keep the node-wide floor. + * + * @generated from field: uint32 min_rsa_key_bits = 2; + */ + minRsaKeyBits: number; + + /** + * require_approval holds every initial enrolment for this profile in a + * queue: the client is answered PENDING and polls (CertPoll) until an admin + * approves or rejects it with ApproveScepEnrollment or RejectScepEnrollment. + * Off by default, where the one-time challenge is the approval and the node + * issues or refuses on the spot. Renewals are never queued. + * + * @generated from field: bool require_approval = 3; + */ + requireApproval: boolean; +}; + +/** + * Describes the message cryptos.v1.ScepProfile. + * Use `create(ScepProfileSchema)` to create a new message. + */ +export const ScepProfileSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_config, 15); + +/** + * ScepRa configures the node's SCEP RA certificate. The RA key is RSA 3072 + * (key transport needs an RSA recipient) with key usage digitalSignature and + * keyEncipherment, sealed on the state partition and never in the clear. + * + * @generated from message cryptos.v1.ScepRa + */ +export type ScepRa = Message<"cryptos.v1.ScepRa"> & { + /** + * validity_days is the RA certificate's lifetime. Zero means 365, which is + * also the most accepted. + * + * @generated from field: uint32 validity_days = 1; + */ + validityDays: number; + + /** + * rotation_overlap_days is how long before the RA certificate expires the + * node mints its successor. During the overlap GetCACert offers the new RA, + * and requests encrypted to either the old or the new RA decrypt, so a + * device that cached the old certificate still enrols. Zero means 30. Must + * be less than validity_days. + * + * @generated from field: uint32 rotation_overlap_days = 2; + */ + rotationOverlapDays: number; +}; + +/** + * Describes the message cryptos.v1.ScepRa. + * Use `create(ScepRaSchema)` to create a new message. + */ +export const ScepRaSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_config, 16); + +/** + * Tsa is the node's RFC 3161 time-stamp authority, for signatures that must + * keep verifying after their signing certificate expires (code signing). It + * answers HTTP POST requests of type application/timestamp-query on the root + * path of its listener with application/timestamp-reply, in plain HTTP as the + * RFC allows: the signed token carries its own integrity and there is nothing + * confidential in it. It is open to any client the rate limit and + * allowed_networks let through; RFC 3161 has no client authentication. + * + * Tokens are signed by a separate TSA certificate the node issues from its own + * CA, with a critical id-kp-timeStamping extended key usage, so the CA key + * never signs a token. The TSA key has the same algorithm as the CA key and is + * held in the TPM where the node has one, in software otherwise, like the CA + * key. Only an Intermediate or Issuing node serves a TSA. + * + * Accepted message imprint hashes are SHA-256, SHA-384 and SHA-512. SHA-1, MD5 + * and any other algorithm are refused with failInfo badAlg. + * + * The TSA fails closed on its clock: while the node's time sync + * (NodeStatus.time_sync) has not synced this boot, or its sources disagree, + * every request is refused with failInfo timeNotAvailable. The clock override + * that lets certificate signing run unsynced does not apply here, because a + * timestamp is nothing but a claim about the time. A node with no time source + * never syncs, so its TSA refuses every request. + * + * @generated from message cryptos.v1.Tsa + */ +export type Tsa = Message<"cryptos.v1.Tsa"> & { + /** + * enabled switches the TSA listener on at the next boot. It is rejected + * while policy_oid is empty, so the TSA stays off until the operator sets + * their own policy. + * + * @generated from field: bool enabled = 1; + */ + enabled: boolean; + + /** + * http_port is the TCP port the TSA listener binds. Zero means the node + * default. + * + * @generated from field: uint32 http_port = 2; + */ + httpPort: number; + + /** + * policy_oid is the TSA policy every token names (RFC 3161 section 2.4.2), + * in dotted form, for example "1.3.6.1.4.1.." under the operator's + * own IANA Private Enterprise Number. Required when enabled; there is no + * default, so no two deployments share a policy by accident. It must be a + * valid object identifier: at least two arcs, decimal with no leading zeros, + * a first arc of 0, 1 or 2, and a second arc below 40 when the first is 0 or + * 1. A request that asks for a different policy (reqPolicy) is refused with + * failInfo unacceptedPolicy. + * + * @generated from field: string policy_oid = 3; + */ + policyOid: string; + + /** + * accuracy_ms is the accuracy the TSA claims in every token (the Accuracy + * field, RFC 3161 section 2.4.2), in milliseconds either side of genTime. + * Zero means 1000. Values above 60000 are rejected. The claim is checked + * against the clock: while the offset of the latest time sync + * (NodeStatus.time_sync.last_offset) is larger than accuracy_ms, the TSA + * refuses requests with timeNotAvailable rather than claim an accuracy the + * clock does not have. + * + * @generated from field: uint32 accuracy_ms = 4; + */ + accuracyMs: number; + + /** + * rate_limit bounds how often one client may ask for a token. Unset means + * the defaults below; the limit cannot be switched off. + * + * @generated from field: cryptos.v1.TsaRateLimit rate_limit = 5; + */ + rateLimit?: TsaRateLimit | undefined; + + /** + * allowed_networks, when non-empty, are the only client networks the TSA + * answers, as IPv4 or IPv6 CIDR prefixes ("10.0.0.0/8", "2001:db8::/32"); a + * bare address means that one host. A client outside them is refused with + * HTTP 403 before its request is read. Empty answers any client. + * + * @generated from field: repeated string allowed_networks = 6; + */ + allowedNetworks: string[]; + + /** + * certificate configures the TSA certificate's lifetime and rotation. Unset + * means the defaults below. + * + * @generated from field: cryptos.v1.TsaCertificateSettings certificate = 7; + */ + certificate?: TsaCertificateSettings | undefined; +}; + +/** + * Describes the message cryptos.v1.Tsa. + * Use `create(TsaSchema)` to create a new message. + */ +export const TsaSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_config, 17); + +/** + * TsaRateLimit is a per-client token bucket. A client is its source IPv4 + * address, or its IPv6 /64, so hopping addresses inside one allocation does + * not reset the bucket. A client over its limit gets HTTP 429 with a + * Retry-After header, and the request is not read. + * + * @generated from message cryptos.v1.TsaRateLimit + */ +export type TsaRateLimit = Message<"cryptos.v1.TsaRateLimit"> & { + /** + * requests_per_minute is the steady rate one client may sustain. Zero means + * 60. + * + * @generated from field: uint32 requests_per_minute = 1; + */ + requestsPerMinute: number; + + /** + * burst is how many requests a client may make at once before the rate + * applies. Zero means requests_per_minute. + * + * @generated from field: uint32 burst = 2; + */ + burst: number; +}; + +/** + * Describes the message cryptos.v1.TsaRateLimit. + * Use `create(TsaRateLimitSchema)` to create a new message. + */ +export const TsaRateLimitSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_config, 18); + +/** + * TsaCertificateSettings configures the node's TSA certificate. Every TSA + * certificate the node has signed with stays published after it is replaced + * or expires (NodeService.ListTsaCertificates), so tokens signed before a + * rotation still verify. + * + * @generated from message cryptos.v1.TsaCertificateSettings + */ +export type TsaCertificateSettings = Message<"cryptos.v1.TsaCertificateSettings"> & { + /** + * validity_days is the TSA certificate's lifetime. Zero means 365, which is + * also the most accepted. + * + * @generated from field: uint32 validity_days = 1; + */ + validityDays: number; + + /** + * rotation_overlap_days is how long before the TSA certificate expires the + * node issues its successor with a new key and starts signing tokens with + * it. The old certificate stays valid for the rest of its life. Zero means + * 30. Must be less than validity_days. + * + * @generated from field: uint32 rotation_overlap_days = 2; + */ + rotationOverlapDays: number; +}; + +/** + * Describes the message cryptos.v1.TsaCertificateSettings. + * Use `create(TsaCertificateSettingsSchema)` to create a new message. + */ +export const TsaCertificateSettingsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_config, 19); + +/** + * WindowsEnrollment is the node's Windows autoenrolment service: the + * certificate enrolment policy endpoint of MS-XCEP and the enrolment endpoint + * of MS-WSTEP, which Group Policy ("Certificate Services Client - Certificate + * Enrollment Policy") points domain-joined machines and users at. Both speak + * SOAP 1.2 over HTTPS (MS-XCEP section 2.1, MS-WSTEP section 2.1). The node + * terminates TLS itself, with a server certificate it mints from its own CA + * for hostnames, as EST does. No agent is installed on the clients and no + * template objects are created in Active Directory: CryptOS is the source of + * truth for what is published. + * + * The listener serves three paths: + * + * - policy_path answers GetPolicies (MS-XCEP section 3.1.4.1) with one + * CertificateEnrollmentPolicy per entry in templates and one CA, this + * node, whose certificate is the node's CA certificate + * (section 3.1.4.1.3.2). It is authenticated by Kerberos only. + * - enrollment_path takes RequestSecurityToken with the Issue request type + * (MS-WSTEP section 3.1.4.2.1.1) for first enrolment, authenticated by + * Kerberos. The policy advertises it as a CAURI with clientAuthentication + * 2, Transport Kerberos, and renewalOnly false (MS-XCEP section + * 3.1.4.1.3.5). + * - renewal_path takes the same Issue request for renewal by the current + * certificate. The policy advertises it as a CAURI with clientAuthentication + * 8, X.509 certificate, and renewalOnly true, and lists it only while at + * least one template has allow_certificate_renewal set. + * + * Kerberos is transport authentication (MS-WSTEP section 3.1.1.1.1): HTTP + * Negotiate (SPNEGO, RFC 4559) inside the TLS session, checked against the + * keytab. Only a client principal in kerberos.realm is accepted. + * + * Renewal is authenticated by the certificate being renewed. A template that + * allows it is published with enrollmentFlags 0x00000040 (MS-XCEP section + * 3.1.4.1.3.1), which tells the client to sign the renewal request with the + * existing certificate's private key (MS-WCCE section 3.2.2.6.2.1.4.5.6). The + * node accepts the renewal only when that signature verifies, the certificate + * chains to this node's CA, is unexpired and not revoked, and was issued from + * the same template. The certificate may also be presented as a TLS client + * certificate (MS-WSTEP section 3.1.1.1.2); it must then be the same + * certificate that signed the request. + * + * Identity never comes from the request. For every enrolment and renewal the + * node looks the account up in Active Directory over LDAP (ldap below) and + * builds the certificate from that entry, ignoring the subject and every name + * the CSR asks for: + * + * - A machine template takes the computer account named by the Kerberos + * client principal (HOST$@REALM). The subject common name and the DNS + * name in the subject alternative name are its dNSHostName. + * - A user template takes the user account named by the Kerberos client + * principal. The subject common name is its cn and the subject alternative + * name carries its userPrincipalName as a UPN. + * - Unless omit_sid_extension is set, the certificate carries the account's + * objectSid in the szOID_NTDS_CA_SECURITY_EXT extension + * (1.3.6.1.4.1.311.25.2), the strong certificate mapping domain controllers + * check for certificate logon. + * - A renewal finds the account by the SID in the certificate being renewed + * (by its names when the certificate has no SID extension) and takes the + * names afresh, so a renamed account renews under its new name. + * - A disabled account, an account that matches more than once across + * base_dns, or one outside a template's allowed_groups is refused. + * + * The policy publishes each template with the subject name flags that say the + * CA builds the names (never "supplied in the request"), generalFlags + * 0x00000040 on machine templates so clients enrol in the machine context, a + * minimal key length of 3072 (the node-wide RSA floor), and no key archival. + * CryptOS never holds a client's private key, so the KET request type (MS-WSTEP + * section 3.1.4.2.2) is answered with a SOAP fault. Nothing is held for + * approval, so QueryTokenStatus (MS-WSTEP section 3.1.4.2.1.2) is answered + * with a SOAP fault too. The policyID (MS-XCEP section 3.1.4.1.3.23) is + * derived from the node's CA certificate, so the two nodes of an HA pair + * answer with the same policy, and nextUpdateHours is 8, the Windows Server + * default. + * + * Only an Intermediate or Issuing node serves it. It may run alongside the + * other protocol blocks on one node; a node dedicated to it is the suggested + * setup. + * + * @generated from message cryptos.v1.WindowsEnrollment + */ +export type WindowsEnrollment = Message<"cryptos.v1.WindowsEnrollment"> & { + /** + * enabled switches the Windows enrolment listener on at the next boot. + * + * @generated from field: bool enabled = 1; + */ + enabled: boolean; + + /** + * hostnames are the DNS names clients reach the endpoints on, stamped into + * the listener's server certificate. The first is the host in every URL the + * policy hands out and in kerberos.service_principal. At least one is + * required when enabled, and each must be a DNS name, not an IP literal, + * because Kerberos names the service by host. + * + * @generated from field: repeated string hostnames = 2; + */ + hostnames: string[]; + + /** + * https_port is the TCP port the listener binds. Zero means 443, the port + * Group Policy assumes when the policy URL names none. It must differ from + * every other enabled listener's port on the node. + * + * @generated from field: uint32 https_port = 3; + */ + httpsPort: number; + + /** + * policy_path is the MS-XCEP endpoint path, the one the Group Policy + * enrolment policy URL names. Empty means "/xcep". + * + * @generated from field: string policy_path = 4; + */ + policyPath: string; + + /** + * enrollment_path is the Kerberos MS-WSTEP endpoint path. Empty means + * "/wstep/kerberos". + * + * @generated from field: string enrollment_path = 5; + */ + enrollmentPath: string; + + /** + * renewal_path is the certificate-authenticated MS-WSTEP endpoint path. + * Empty means "/wstep/certificate". The three paths must each be + * absolute, contain no query or fragment, and differ from one another. + * + * @generated from field: string renewal_path = 6; + */ + renewalPath: string; + + /** + * kerberos is the service identity clients authenticate to. Required when + * enabled. + * + * @generated from field: cryptos.v1.WindowsKerberos kerberos = 7; + */ + kerberos?: WindowsKerberos | undefined; + + /** + * ldap is how the node reads the authenticated account from Active + * Directory. Required when enabled. + * + * @generated from field: cryptos.v1.WindowsLdap ldap = 8; + */ + ldap?: WindowsLdap | undefined; + + /** + * templates are the certificate templates the policy publishes, each mapped + * to a CryptOS profile. At least one is required when enabled. + * + * @generated from field: repeated cryptos.v1.WindowsTemplate templates = 9; + */ + templates: WindowsTemplate[]; + + /** + * omit_sid_extension drops the szOID_NTDS_CA_SECURITY_EXT SID extension + * from issued certificates. Off by default, so certificates carry it: once + * domain controllers enforce strong certificate mapping (Full Enforcement), + * a certificate without it, or an explicit altSecurityIdentities mapping, + * fails certificate logon. + * + * @generated from field: bool omit_sid_extension = 10; + */ + omitSidExtension: boolean; +}; + +/** + * Describes the message cryptos.v1.WindowsEnrollment. + * Use `create(WindowsEnrollmentSchema)` to create a new message. + */ +export const WindowsEnrollmentSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_config, 20); + +/** + * WindowsKerberos is the Kerberos service the Windows endpoints run as: an + * Active Directory service account with the SPN registered on it and a keytab + * exported for it. + * + * @generated from message cryptos.v1.WindowsKerberos + */ +export type WindowsKerberos = Message<"cryptos.v1.WindowsKerberos"> & { + /** + * realm is the Kerberos realm, the Active Directory domain's DNS name in + * upper case, for example "EXAMPLE.ORG". Required when enabled. Only client + * principals in this realm are accepted. + * + * @generated from field: string realm = 1; + */ + realm: string; + + /** + * service_principal is the SPN clients request tickets for, "HTTP/" followed + * by the first entry of hostnames, for example "HTTP/enroll.example.org", + * without the realm. Required when enabled. It must be registered on the + * service account and nowhere else in the forest, or clients cannot get a + * ticket for it. + * + * @generated from field: string service_principal = 2; + */ + servicePrincipal: string; + + /** + * keytab is the MIT keytab (format version 0x0502) holding the service + * account's keys for service_principal@realm. Required when enabled. Only + * AES256 and AES128 keys (aes256-cts-hmac-sha1-96, aes128-cts-hmac-sha1-96) + * are used; a keytab with no AES key for the principal is rejected, and RC4 + * and DES keys are ignored. Write-only: GetConfig returns it empty, and an + * apply that leaves it empty keeps the stored keytab while service_principal + * is unchanged. + * + * @generated from field: bytes keytab = 3; + */ + keytab: Uint8Array; +}; + +/** + * Describes the message cryptos.v1.WindowsKerberos. + * Use `create(WindowsKerberosSchema)` to create a new message. + */ +export const WindowsKerberosSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_config, 21); + +/** + * WindowsLdap is the node's read-only view of Active Directory, used to build + * every certificate from the authenticated account's entry. + * + * @generated from message cryptos.v1.WindowsLdap + */ +export type WindowsLdap = Message<"cryptos.v1.WindowsLdap"> & { + /** + * urls are the domain controllers to query, tried in order, each + * "ldaps://host" or "ldaps://host:port" (636 when the port is absent, 3269 + * for a global catalog). At least one is required when enabled. Only + * ldaps is accepted: a plain "ldap://" URL, or StartTLS, is rejected, so the + * bind credential and the directory data never cross the network in the + * clear. + * + * @generated from field: repeated string urls = 1; + */ + urls: string[]; + + /** + * bind is the account the node binds as. Required when enabled. It needs + * read access to the accounts under base_dns and nothing more. + * + * @generated from field: cryptos.v1.WindowsLdapBind bind = 2; + */ + bind?: WindowsLdapBind | undefined; + + /** + * base_dns are the subtrees searched for the authenticated account, for + * example "DC=example,DC=org". At least one is required when enabled. + * + * @generated from field: repeated string base_dns = 3; + */ + baseDns: string[]; + + /** + * trust_pem is the PEM bundle of the CA certificates that issued the domain + * controllers' LDAPS certificates. Required when enabled; the node trusts + * nothing else for these connections, and each controller's certificate must + * name the host in its URL. + * + * @generated from field: string trust_pem = 4; + */ + trustPem: string; +}; + +/** + * Describes the message cryptos.v1.WindowsLdap. + * Use `create(WindowsLdapSchema)` to create a new message. + */ +export const WindowsLdapSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_config, 22); + +/** + * WindowsLdapBind is how the node authenticates to the domain controllers. + * + * @generated from message cryptos.v1.WindowsLdapBind + */ +export type WindowsLdapBind = Message<"cryptos.v1.WindowsLdapBind"> & { + /** + * mode is "simple" (an LDAP simple bind over the TLS session) or "kerberos" + * (SASL GSSAPI with the keys in WindowsKerberos.keytab). Required when + * enabled. A Kerberos bind sends the TLS channel binding token, so it works + * where domain controllers require LDAP channel binding. + * + * @generated from field: string mode = 1; + */ + mode: string; + + /** + * bind_dn is the account a simple bind names, as a distinguished name or a + * userPrincipalName. Required for simple, rejected for kerberos. + * + * @generated from field: string bind_dn = 2; + */ + bindDn: string; + + /** + * password is the simple bind password. Required for simple, rejected for + * kerberos. Write-only: GetConfig returns it empty, and an apply that leaves + * it empty keeps the stored password while bind_dn is unchanged. + * + * @generated from field: string password = 3; + */ + password: string; + + /** + * principal is the Kerberos client principal a kerberos bind uses, without + * the realm. Empty means WindowsKerberos.service_principal. The keytab must + * hold an AES key for it. Rejected for simple. + * + * @generated from field: string principal = 4; + */ + principal: string; +}; + +/** + * Describes the message cryptos.v1.WindowsLdapBind. + * Use `create(WindowsLdapBindSchema)` to create a new message. + */ +export const WindowsLdapBindSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_config, 23); + +/** + * WindowsTemplate is one certificate template the policy publishes. + * + * @generated from message cryptos.v1.WindowsTemplate + */ +export type WindowsTemplate = Message<"cryptos.v1.WindowsTemplate"> & { + /** + * name is the template name the policy publishes (the commonName of its + * CertificateEnrollmentPolicy, MS-XCEP section 3.1.4.1.3.1), and the name + * clients and certreq use. Required, and unique within the block. + * + * @generated from field: string name = 1; + */ + name: string; + + /** + * profile names the non-CA certificate profile in Pki.profiles the template + * issues from. Required. The profile's key usage, extended key usage, + * validity and extensions apply; its subject and names are replaced by those + * read from Active Directory. + * + * @generated from field: string profile = 2; + */ + profile: string; + + /** + * enrollment_type is "machine" or "user": which kind of account may enrol + * and whose directory entry the certificate is built from. Required. A + * Kerberos client principal of the other kind is refused. + * + * @generated from field: string enrollment_type = 3; + */ + enrollmentType: string; + + /** + * allow_certificate_renewal lets a certificate issued from this template be + * renewed at renewal_path, authenticated by the certificate itself instead of + * Kerberos. Off means every renewal is a fresh Kerberos enrolment. + * + * @generated from field: bool allow_certificate_renewal = 4; + */ + allowCertificateRenewal: boolean; + + /** + * allowed_groups, when non-empty, are the distinguished names of the Active + * Directory groups whose members may enrol, membership checked transitively + * (LDAP_MATCHING_RULE_IN_CHAIN, 1.2.840.113556.1.4.1941). The policy offers + * the template, with enroll and autoEnroll permission, only to accounts in + * one of them, and a renewal re-checks it. Empty offers it to every account + * of enrollment_type under the base DNs. + * + * @generated from field: repeated string allowed_groups = 5; + */ + allowedGroups: string[]; + + /** + * renewal_period_days is how long before expiry clients start renewing + * (renewalPeriodSeconds, MS-XCEP section 3.1.4.1.3.8). Zero means one fifth + * of the profile's validity, rounded down to whole days and at least one. It + * must be less than the profile's validity. + * + * @generated from field: uint32 renewal_period_days = 6; + */ + renewalPeriodDays: number; +}; + +/** + * Describes the message cryptos.v1.WindowsTemplate. + * Use `create(WindowsTemplateSchema)` to create a new message. + */ +export const WindowsTemplateSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_config, 24); + /** * Parent is the trust anchor a subordinate CA pins for its issuer: the parent * (or root) CA certificate, by full PEM or by SHA-256 fingerprint. Exactly one @@ -436,7 +1495,7 @@ export type Parent = Message<"cryptos.v1.Parent"> & { * Use `create(ParentSchema)` to create a new message. */ export const ParentSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_v1_config, 10); + messageDesc(file_cryptos_v1_config, 25); /** * CertificateProfile drives CSR generation and certificate signing: key @@ -453,7 +1512,10 @@ export type CertificateProfile = Message<"cryptos.v1.CertificateProfile"> & { name: string; /** - * e.g. "ECDSA-P384". Covering subset; RSA sizes and other curves are additive. + * key_alg takes the same values as Pki.root_key_alg: "ECDSA-P384", + * "RSA-3072" or "RSA-4096", checked when the config is validated. Signing + * does not match a CSR against it: any CSR key that is ECDSA P-384 or RSA + * of at least 3072 bits is certified. * * @generated from field: string key_alg = 2; */ @@ -525,7 +1587,7 @@ export type CertificateProfile = Message<"cryptos.v1.CertificateProfile"> & { * Use `create(CertificateProfileSchema)` to create a new message. */ export const CertificateProfileSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_v1_config, 11); + messageDesc(file_cryptos_v1_config, 26); /** * @generated from message cryptos.v1.BasicConstraints @@ -550,7 +1612,7 @@ export type BasicConstraints = Message<"cryptos.v1.BasicConstraints"> & { * Use `create(BasicConstraintsSchema)` to create a new message. */ export const BasicConstraintsSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_v1_config, 12); + messageDesc(file_cryptos_v1_config, 27); /** * @generated from message cryptos.v1.SubjectAltNames @@ -599,7 +1661,7 @@ export type SubjectAltNames = Message<"cryptos.v1.SubjectAltNames"> & { * Use `create(SubjectAltNamesSchema)` to create a new message. */ export const SubjectAltNamesSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_v1_config, 13); + messageDesc(file_cryptos_v1_config, 28); /** * X509Extension is the raw escape hatch: a dotted OID, criticality flag, and the @@ -629,7 +1691,7 @@ export type X509Extension = Message<"cryptos.v1.X509Extension"> & { * Use `create(X509ExtensionSchema)` to create a new message. */ export const X509ExtensionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_v1_config, 14); + messageDesc(file_cryptos_v1_config, 29); /** * Install declares how the node provisions itself to persistent storage during @@ -652,7 +1714,7 @@ export type Install = Message<"cryptos.v1.Install"> & { * Use `create(InstallSchema)` to create a new message. */ export const InstallSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_v1_config, 15); + messageDesc(file_cryptos_v1_config, 30); /** * @generated from message cryptos.v1.Subject @@ -693,5 +1755,5 @@ export type Subject = Message<"cryptos.v1.Subject"> & { * Use `create(SubjectSchema)` to create a new message. */ export const SubjectSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_v1_config, 16); + messageDesc(file_cryptos_v1_config, 31); diff --git a/src/gen/fleet/cryptos/v1/node_pb.ts b/src/gen/fleet/cryptos/v1/node_pb.ts index 58e10fd..f15cf26 100644 --- a/src/gen/fleet/cryptos/v1/node_pb.ts +++ b/src/gen/fleet/cryptos/v1/node_pb.ts @@ -10,8 +10,12 @@ import type { MachineConfig, Management } from "./config_pb"; import { file_cryptos_v1_config } from "./config_pb"; import type { Identity } from "./identity_pb"; import { file_cryptos_v1_identity } from "./identity_pb"; +import type { ApproveScepEnrollmentRequestSchema, ApproveScepEnrollmentResponseSchema, ListScepChallengesRequestSchema, ListScepChallengesResponseSchema, ListScepEnrollmentsRequestSchema, ListScepEnrollmentsResponseSchema, MintScepChallengeRequestSchema, MintScepChallengeResponseSchema, RejectScepEnrollmentRequestSchema, RejectScepEnrollmentResponseSchema, RevokeScepChallengeRequestSchema, RevokeScepChallengeResponseSchema } from "./scep_pb"; +import { file_cryptos_v1_scep } from "./scep_pb"; import type { NodeStatus } from "./status_pb"; import { file_cryptos_v1_status } from "./status_pb"; +import type { ListTsaCertificatesRequestSchema, ListTsaCertificatesResponseSchema } from "./tsa_pb"; +import { file_cryptos_v1_tsa } from "./tsa_pb"; import type { Timestamp } from "@bufbuild/protobuf/wkt"; import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; import type { Message } from "@bufbuild/protobuf"; @@ -20,7 +24,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file cryptos/v1/node.proto. */ export const file_cryptos_v1_node: GenFile = /*@__PURE__*/ - fileDesc("ChVjcnlwdG9zL3YxL25vZGUucHJvdG8SCmNyeXB0b3MudjEiPwoSQXBwbHlDb25maWdSZXF1ZXN0EikKBmNvbmZpZxgBIAEoCzIZLmNyeXB0b3MudjEuTWFjaGluZUNvbmZpZyISChBHZXRDb25maWdSZXF1ZXN0Ij4KEUdldENvbmZpZ1Jlc3BvbnNlEikKBmNvbmZpZxgBIAEoCzIZLmNyeXB0b3MudjEuTWFjaGluZUNvbmZpZyJCChRTZXRNYW5hZ2VtZW50UmVxdWVzdBIqCgptYW5hZ2VtZW50GAEgASgLMhYuY3J5cHRvcy52MS5NYW5hZ2VtZW50IkQKFVNldE1hbmFnZW1lbnRSZXNwb25zZRISCgpnZW5lcmF0aW9uGAEgASgEEhcKD3JlcXVpcmVzX3JlYm9vdBgCIAEoCCJrChNBcHBseUNvbmZpZ1Jlc3BvbnNlEhIKCmdlbmVyYXRpb24YASABKAQSFwoPcmVxdWlyZXNfcmVib290GAIgASgIEhUKDWNvbmZpZ19kaWdlc3QYAyABKAwSEAoId2FybmluZ3MYBCADKAkiEgoQR2V0U3RhdHVzUmVxdWVzdCI7ChFHZXRTdGF0dXNSZXNwb25zZRImCgZzdGF0dXMYASABKAsyFi5jcnlwdG9zLnYxLk5vZGVTdGF0dXMiFAoSR2V0SWRlbnRpdHlSZXF1ZXN0Ij0KE0dldElkZW50aXR5UmVzcG9uc2USJgoIaWRlbnRpdHkYASABKAsyFC5jcnlwdG9zLnYxLklkZW50aXR5IlsKFFN0YXJ0Q2VyZW1vbnlSZXF1ZXN0EiYKBGtpbmQYASABKA4yGC5jcnlwdG9zLnYxLkNlcmVtb255S2luZBIbChNtYWNoaW5lX2NvbmZpZ195YW1sGAIgASgMIkEKFVN0YXJ0Q2VyZW1vbnlSZXNwb25zZRIoCgVldmVudBgBIAEoCzIZLmNyeXB0b3MudjEuQ2VyZW1vbnlFdmVudCIyCg5TaWduQ1NSUmVxdWVzdBIPCgdjc3JfZGVyGAEgASgMEg8KB3Byb2ZpbGUYAiABKAkiIwoPU2lnbkNTUlJlc3BvbnNlEhAKCGNlcnRfZGVyGAEgASgMIisKDFJlc2V0UmVxdWVzdBIbChNjb25maXJtX2NvbW1vbl9uYW1lGAEgASgJIg8KDVJlc2V0UmVzcG9uc2UiMQoSUmVtb3RlUmVzZXRSZXF1ZXN0EhsKE2NvbmZpcm1fY29tbW9uX25hbWUYASABKAkiKAoTUmVtb3RlUmVzZXRSZXNwb25zZRIRCglyZWJvb3RpbmcYASABKAgiHgoNQXR0ZXN0UmVxdWVzdBINCgVub25jZRgBIAEoDCJeCg5BdHRlc3RSZXNwb25zZRIRCglzaWduYXR1cmUYASABKAwSGAoQaWRlbnRpdHlfcHViX2RlchgCIAEoDBIOCgZla19wdWIYAyABKAwSDwoHZWtfY2VydBgEIAEoDCJCChlTaWduU3Vib3JkaW5hdGVDU1JSZXF1ZXN0Eg8KB2Nzcl9kZXIYASABKAwSFAoMcHJvZmlsZV9uYW1lGAIgASgJIlQKGlNpZ25TdWJvcmRpbmF0ZUNTUlJlc3BvbnNlEhEKCWNoYWluX2RlchgBIAMoDBIRCgljaGFpbl9wZW0YAiABKAkSEAoId2FybmluZ3MYAyADKAkiTAoQSXNzdWVMZWFmUmVxdWVzdBIPCgdjc3JfZGVyGAEgASgMEhQKDHByb2ZpbGVfbmFtZRgCIAEoCRIRCglkbnNfbmFtZXMYAyADKAkiNwoRSXNzdWVMZWFmUmVzcG9uc2USEAoIY2VydF9kZXIYASABKAwSEAoId2FybmluZ3MYAiADKAkiGgoYR2V0U3Vib3JkaW5hdGVDU1JSZXF1ZXN0IiwKGUdldFN1Ym9yZGluYXRlQ1NSUmVzcG9uc2USDwoHY3NyX2RlchgBIAEoDCJLCiNTdWJtaXRTdWJvcmRpbmF0ZUNlcnRpZmljYXRlUmVxdWVzdBIRCgljaGFpbl9kZXIYASADKAwSEQoJY2hhaW5fcGVtGAIgASgJIk4KJFN1Ym1pdFN1Ym9yZGluYXRlQ2VydGlmaWNhdGVSZXNwb25zZRImCghpZGVudGl0eRgBIAEoCzIULmNyeXB0b3MudjEuSWRlbnRpdHkiKAoSRXhwb3J0Q0FLZXlSZXF1ZXN0EhIKCnBhc3NwaHJhc2UYASABKAwiJwoTRXhwb3J0Q0FLZXlSZXNwb25zZRIQCghlbnZlbG9wZRgBIAEoDCI6ChJJbXBvcnRDQUtleVJlcXVlc3QSEAoIZW52ZWxvcGUYASABKAwSEgoKcGFzc3BocmFzZRgCIAEoDCI9ChNJbXBvcnRDQUtleVJlc3BvbnNlEiYKCGlkZW50aXR5GAEgASgLMhQuY3J5cHRvcy52MS5JZGVudGl0eSIZChdCZWdpbktleVJvdGF0aW9uUmVxdWVzdCIrChhCZWdpbktleVJvdGF0aW9uUmVzcG9uc2USDwoHY3NyX2RlchgBIAEoDCJCChpDb21wbGV0ZUtleVJvdGF0aW9uUmVxdWVzdBIRCgljaGFpbl9kZXIYASADKAwSEQoJY2hhaW5fcGVtGAIgASgJIkUKG0NvbXBsZXRlS2V5Um90YXRpb25SZXNwb25zZRImCghpZGVudGl0eRgBIAEoCzIULmNyeXB0b3MudjEuSWRlbnRpdHkiFgoUR2V0UmVuZXdhbENTUlJlcXVlc3QiKAoVR2V0UmVuZXdhbENTUlJlc3BvbnNlEg8KB2Nzcl9kZXIYASABKAwiRwofU3VibWl0UmVuZXdlZENlcnRpZmljYXRlUmVxdWVzdBIRCgljaGFpbl9kZXIYASADKAwSEQoJY2hhaW5fcGVtGAIgASgJIkoKIFN1Ym1pdFJlbmV3ZWRDZXJ0aWZpY2F0ZVJlc3BvbnNlEiYKCGlkZW50aXR5GAEgASgLMhQuY3J5cHRvcy52MS5JZGVudGl0eSLpAQoKSXNzdWVkQ2VydBISCgpzZXJpYWxfaGV4GAEgASgJEhIKCnN1YmplY3RfZG4YAiABKAkSLgoKbm90X2JlZm9yZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLQoJbm90X2FmdGVyGAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIPCgdza2lfaGV4GAUgASgJEhQKDHByb2ZpbGVfbmFtZRgGIAEoCRItCglpc3N1ZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wImUKClJldm9jYXRpb24SEgoKc2VyaWFsX2hleBgBIAEoCRIuCgpyZXZva2VkX2F0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBITCgtyZWFzb25fY29kZRgDIAEoBSJDChhSZXZva2VDZXJ0aWZpY2F0ZVJlcXVlc3QSEgoKc2VyaWFsX2hleBgBIAEoCRITCgtyZWFzb25fY29kZRgCIAEoBSJHChlSZXZva2VDZXJ0aWZpY2F0ZVJlc3BvbnNlEioKCnJldm9jYXRpb24YASABKAsyFi5jcnlwdG9zLnYxLlJldm9jYXRpb24iEwoRTGlzdElzc3VlZFJlcXVlc3QiPAoSTGlzdElzc3VlZFJlc3BvbnNlEiYKBmlzc3VlZBgBIAMoCzIWLmNyeXB0b3MudjEuSXNzdWVkQ2VydCIYChZMaXN0UmV2b2NhdGlvbnNSZXF1ZXN0IkYKF0xpc3RSZXZvY2F0aW9uc1Jlc3BvbnNlEisKC3Jldm9jYXRpb25zGAEgAygLMhYuY3J5cHRvcy52MS5SZXZvY2F0aW9uIhkKF0xpc3RJbnN0YWxsRGlza3NSZXF1ZXN0IkIKGExpc3RJbnN0YWxsRGlza3NSZXNwb25zZRImCgVkaXNrcxgBIAMoCzIXLmNyeXB0b3MudjEuSW5zdGFsbERpc2siZQoLSW5zdGFsbERpc2sSDAoEcGF0aBgBIAEoCRISCgpzaXplX2J5dGVzGAIgASgEEg0KBW1vZGVsGAMgASgJEhIKCnJvdGF0aW9uYWwYBCABKAgSEQoJcmVtb3ZhYmxlGAUgASgIIl0KEVN0YWdlSW1hZ2VSZXF1ZXN0EiwKBWJlZ2luGAEgASgLMhsuY3J5cHRvcy52MS5TdGFnZUltYWdlQmVnaW5IABIPCgVjaHVuaxgCIAEoDEgAQgkKB3BheWxvYWQiSAoPU3RhZ2VJbWFnZUJlZ2luEhEKCXNpZ25hdHVyZRgBIAEoDBISCgpzaXplX2J5dGVzGAIgASgEEg4KBnNoYTI1NhgDIAEoCSJWChJTdGFnZUltYWdlUmVzcG9uc2USJwoGc3RhdHVzGAEgASgLMhcuY3J5cHRvcy52MS5JbWFnZVN0YXR1cxIXCg9yZXF1aXJlc19yZWJvb3QYAiABKAgiFgoUUm9sbGJhY2tJbWFnZVJlcXVlc3QiWQoVUm9sbGJhY2tJbWFnZVJlc3BvbnNlEicKBnN0YXR1cxgBIAEoCzIXLmNyeXB0b3MudjEuSW1hZ2VTdGF0dXMSFwoPcmVxdWlyZXNfcmVib290GAIgASgIIi0KFEFjdGl2YXRlSW1hZ2VSZXF1ZXN0EhUKDWNvbmZpcm1fY2FfY24YASABKAkiKgoVQWN0aXZhdGVJbWFnZVJlc3BvbnNlEhEKCXJlYm9vdGluZxgBIAEoCCI5Cg1SZWJvb3RSZXF1ZXN0EhUKDWNvbmZpcm1fY2FfY24YASABKAkSEQoJcG93ZXJfb2ZmGAIgASgIIiMKDlJlYm9vdFJlc3BvbnNlEhEKCXJlYm9vdGluZxgBIAEoCCIXChVHZXRJbWFnZVN0YXR1c1JlcXVlc3QiQQoWR2V0SW1hZ2VTdGF0dXNSZXNwb25zZRInCgZzdGF0dXMYASABKAsyFy5jcnlwdG9zLnYxLkltYWdlU3RhdHVzIoYBCgtJbWFnZVN0YXR1cxIWCg5ydW5uaW5nX3NoYTI1NhgBIAEoCRIVCg1hY3RpdmVfc2hhMjU2GAIgASgJEhcKD3ByZXZpb3VzX3NoYTI1NhgDIAEoCRIWCg5yZWJvb3RfcGVuZGluZxgEIAEoCBIXCg9ydW5uaW5nX3ZlcnNpb24YBSABKAkyyhMKC05vZGVTZXJ2aWNlEk4KC0FwcGx5Q29uZmlnEh4uY3J5cHRvcy52MS5BcHBseUNvbmZpZ1JlcXVlc3QaHy5jcnlwdG9zLnYxLkFwcGx5Q29uZmlnUmVzcG9uc2USSAoJR2V0U3RhdHVzEhwuY3J5cHRvcy52MS5HZXRTdGF0dXNSZXF1ZXN0Gh0uY3J5cHRvcy52MS5HZXRTdGF0dXNSZXNwb25zZRJdChBMaXN0SW5zdGFsbERpc2tzEiMuY3J5cHRvcy52MS5MaXN0SW5zdGFsbERpc2tzUmVxdWVzdBokLmNyeXB0b3MudjEuTGlzdEluc3RhbGxEaXNrc1Jlc3BvbnNlEk4KC0dldElkZW50aXR5Eh4uY3J5cHRvcy52MS5HZXRJZGVudGl0eVJlcXVlc3QaHy5jcnlwdG9zLnYxLkdldElkZW50aXR5UmVzcG9uc2USVgoNU3RhcnRDZXJlbW9ueRIgLmNyeXB0b3MudjEuU3RhcnRDZXJlbW9ueVJlcXVlc3QaIS5jcnlwdG9zLnYxLlN0YXJ0Q2VyZW1vbnlSZXNwb25zZTABEkIKB1NpZ25DU1ISGi5jcnlwdG9zLnYxLlNpZ25DU1JSZXF1ZXN0GhsuY3J5cHRvcy52MS5TaWduQ1NSUmVzcG9uc2USYwoSU2lnblN1Ym9yZGluYXRlQ1NSEiUuY3J5cHRvcy52MS5TaWduU3Vib3JkaW5hdGVDU1JSZXF1ZXN0GiYuY3J5cHRvcy52MS5TaWduU3Vib3JkaW5hdGVDU1JSZXNwb25zZRJICglJc3N1ZUxlYWYSHC5jcnlwdG9zLnYxLklzc3VlTGVhZlJlcXVlc3QaHS5jcnlwdG9zLnYxLklzc3VlTGVhZlJlc3BvbnNlEmAKEUdldFN1Ym9yZGluYXRlQ1NSEiQuY3J5cHRvcy52MS5HZXRTdWJvcmRpbmF0ZUNTUlJlcXVlc3QaJS5jcnlwdG9zLnYxLkdldFN1Ym9yZGluYXRlQ1NSUmVzcG9uc2USgQEKHFN1Ym1pdFN1Ym9yZGluYXRlQ2VydGlmaWNhdGUSLy5jcnlwdG9zLnYxLlN1Ym1pdFN1Ym9yZGluYXRlQ2VydGlmaWNhdGVSZXF1ZXN0GjAuY3J5cHRvcy52MS5TdWJtaXRTdWJvcmRpbmF0ZUNlcnRpZmljYXRlUmVzcG9uc2USYAoRUmV2b2tlQ2VydGlmaWNhdGUSJC5jcnlwdG9zLnYxLlJldm9rZUNlcnRpZmljYXRlUmVxdWVzdBolLmNyeXB0b3MudjEuUmV2b2tlQ2VydGlmaWNhdGVSZXNwb25zZRJLCgpMaXN0SXNzdWVkEh0uY3J5cHRvcy52MS5MaXN0SXNzdWVkUmVxdWVzdBoeLmNyeXB0b3MudjEuTGlzdElzc3VlZFJlc3BvbnNlEloKD0xpc3RSZXZvY2F0aW9ucxIiLmNyeXB0b3MudjEuTGlzdFJldm9jYXRpb25zUmVxdWVzdBojLmNyeXB0b3MudjEuTGlzdFJldm9jYXRpb25zUmVzcG9uc2USTgoLRXhwb3J0Q0FLZXkSHi5jcnlwdG9zLnYxLkV4cG9ydENBS2V5UmVxdWVzdBofLmNyeXB0b3MudjEuRXhwb3J0Q0FLZXlSZXNwb25zZRJOCgtJbXBvcnRDQUtleRIeLmNyeXB0b3MudjEuSW1wb3J0Q0FLZXlSZXF1ZXN0Gh8uY3J5cHRvcy52MS5JbXBvcnRDQUtleVJlc3BvbnNlEl0KEEJlZ2luS2V5Um90YXRpb24SIy5jcnlwdG9zLnYxLkJlZ2luS2V5Um90YXRpb25SZXF1ZXN0GiQuY3J5cHRvcy52MS5CZWdpbktleVJvdGF0aW9uUmVzcG9uc2USZgoTQ29tcGxldGVLZXlSb3RhdGlvbhImLmNyeXB0b3MudjEuQ29tcGxldGVLZXlSb3RhdGlvblJlcXVlc3QaJy5jcnlwdG9zLnYxLkNvbXBsZXRlS2V5Um90YXRpb25SZXNwb25zZRJUCg1HZXRSZW5ld2FsQ1NSEiAuY3J5cHRvcy52MS5HZXRSZW5ld2FsQ1NSUmVxdWVzdBohLmNyeXB0b3MudjEuR2V0UmVuZXdhbENTUlJlc3BvbnNlEnUKGFN1Ym1pdFJlbmV3ZWRDZXJ0aWZpY2F0ZRIrLmNyeXB0b3MudjEuU3VibWl0UmVuZXdlZENlcnRpZmljYXRlUmVxdWVzdBosLmNyeXB0b3MudjEuU3VibWl0UmVuZXdlZENlcnRpZmljYXRlUmVzcG9uc2USPAoFUmVzZXQSGC5jcnlwdG9zLnYxLlJlc2V0UmVxdWVzdBoZLmNyeXB0b3MudjEuUmVzZXRSZXNwb25zZRJOCgtSZW1vdGVSZXNldBIeLmNyeXB0b3MudjEuUmVtb3RlUmVzZXRSZXF1ZXN0Gh8uY3J5cHRvcy52MS5SZW1vdGVSZXNldFJlc3BvbnNlEj8KBkF0dGVzdBIZLmNyeXB0b3MudjEuQXR0ZXN0UmVxdWVzdBoaLmNyeXB0b3MudjEuQXR0ZXN0UmVzcG9uc2USVAoNU2V0TWFuYWdlbWVudBIgLmNyeXB0b3MudjEuU2V0TWFuYWdlbWVudFJlcXVlc3QaIS5jcnlwdG9zLnYxLlNldE1hbmFnZW1lbnRSZXNwb25zZRJICglHZXRDb25maWcSHC5jcnlwdG9zLnYxLkdldENvbmZpZ1JlcXVlc3QaHS5jcnlwdG9zLnYxLkdldENvbmZpZ1Jlc3BvbnNlEk0KClN0YWdlSW1hZ2USHS5jcnlwdG9zLnYxLlN0YWdlSW1hZ2VSZXF1ZXN0Gh4uY3J5cHRvcy52MS5TdGFnZUltYWdlUmVzcG9uc2UoARJUCg1Sb2xsYmFja0ltYWdlEiAuY3J5cHRvcy52MS5Sb2xsYmFja0ltYWdlUmVxdWVzdBohLmNyeXB0b3MudjEuUm9sbGJhY2tJbWFnZVJlc3BvbnNlElQKDUFjdGl2YXRlSW1hZ2USIC5jcnlwdG9zLnYxLkFjdGl2YXRlSW1hZ2VSZXF1ZXN0GiEuY3J5cHRvcy52MS5BY3RpdmF0ZUltYWdlUmVzcG9uc2USVwoOR2V0SW1hZ2VTdGF0dXMSIS5jcnlwdG9zLnYxLkdldEltYWdlU3RhdHVzUmVxdWVzdBoiLmNyeXB0b3MudjEuR2V0SW1hZ2VTdGF0dXNSZXNwb25zZRI/CgZSZWJvb3QSGS5jcnlwdG9zLnYxLlJlYm9vdFJlcXVlc3QaGi5jcnlwdG9zLnYxLlJlYm9vdFJlc3BvbnNlQjRaMmdpdGh1Yi5jb20vQ3J5cHRPUy1QS0kvYXBpL2dvL2NyeXB0b3MvdjE7Y3J5cHRvc3YxYgZwcm90bzM", [file_cryptos_v1_ceremony, file_cryptos_v1_config, file_cryptos_v1_identity, file_cryptos_v1_status, file_google_protobuf_timestamp]); + fileDesc("ChVjcnlwdG9zL3YxL25vZGUucHJvdG8SCmNyeXB0b3MudjEiPwoSQXBwbHlDb25maWdSZXF1ZXN0EikKBmNvbmZpZxgBIAEoCzIZLmNyeXB0b3MudjEuTWFjaGluZUNvbmZpZyISChBHZXRDb25maWdSZXF1ZXN0Ij4KEUdldENvbmZpZ1Jlc3BvbnNlEikKBmNvbmZpZxgBIAEoCzIZLmNyeXB0b3MudjEuTWFjaGluZUNvbmZpZyJCChRTZXRNYW5hZ2VtZW50UmVxdWVzdBIqCgptYW5hZ2VtZW50GAEgASgLMhYuY3J5cHRvcy52MS5NYW5hZ2VtZW50IkQKFVNldE1hbmFnZW1lbnRSZXNwb25zZRISCgpnZW5lcmF0aW9uGAEgASgEEhcKD3JlcXVpcmVzX3JlYm9vdBgCIAEoCCJrChNBcHBseUNvbmZpZ1Jlc3BvbnNlEhIKCmdlbmVyYXRpb24YASABKAQSFwoPcmVxdWlyZXNfcmVib290GAIgASgIEhUKDWNvbmZpZ19kaWdlc3QYAyABKAwSEAoId2FybmluZ3MYBCADKAkiEgoQR2V0U3RhdHVzUmVxdWVzdCI7ChFHZXRTdGF0dXNSZXNwb25zZRImCgZzdGF0dXMYASABKAsyFi5jcnlwdG9zLnYxLk5vZGVTdGF0dXMiFAoSR2V0SWRlbnRpdHlSZXF1ZXN0Ij0KE0dldElkZW50aXR5UmVzcG9uc2USJgoIaWRlbnRpdHkYASABKAsyFC5jcnlwdG9zLnYxLklkZW50aXR5IlsKFFN0YXJ0Q2VyZW1vbnlSZXF1ZXN0EiYKBGtpbmQYASABKA4yGC5jcnlwdG9zLnYxLkNlcmVtb255S2luZBIbChNtYWNoaW5lX2NvbmZpZ195YW1sGAIgASgMIkEKFVN0YXJ0Q2VyZW1vbnlSZXNwb25zZRIoCgVldmVudBgBIAEoCzIZLmNyeXB0b3MudjEuQ2VyZW1vbnlFdmVudCIyCg5TaWduQ1NSUmVxdWVzdBIPCgdjc3JfZGVyGAEgASgMEg8KB3Byb2ZpbGUYAiABKAkiIwoPU2lnbkNTUlJlc3BvbnNlEhAKCGNlcnRfZGVyGAEgASgMIisKDFJlc2V0UmVxdWVzdBIbChNjb25maXJtX2NvbW1vbl9uYW1lGAEgASgJIg8KDVJlc2V0UmVzcG9uc2UiMQoSUmVtb3RlUmVzZXRSZXF1ZXN0EhsKE2NvbmZpcm1fY29tbW9uX25hbWUYASABKAkiKAoTUmVtb3RlUmVzZXRSZXNwb25zZRIRCglyZWJvb3RpbmcYASABKAgiHgoNQXR0ZXN0UmVxdWVzdBINCgVub25jZRgBIAEoDCJeCg5BdHRlc3RSZXNwb25zZRIRCglzaWduYXR1cmUYASABKAwSGAoQaWRlbnRpdHlfcHViX2RlchgCIAEoDBIOCgZla19wdWIYAyABKAwSDwoHZWtfY2VydBgEIAEoDCJCChlTaWduU3Vib3JkaW5hdGVDU1JSZXF1ZXN0Eg8KB2Nzcl9kZXIYASABKAwSFAoMcHJvZmlsZV9uYW1lGAIgASgJIlQKGlNpZ25TdWJvcmRpbmF0ZUNTUlJlc3BvbnNlEhEKCWNoYWluX2RlchgBIAMoDBIRCgljaGFpbl9wZW0YAiABKAkSEAoId2FybmluZ3MYAyADKAkiTAoQSXNzdWVMZWFmUmVxdWVzdBIPCgdjc3JfZGVyGAEgASgMEhQKDHByb2ZpbGVfbmFtZRgCIAEoCRIRCglkbnNfbmFtZXMYAyADKAkiNwoRSXNzdWVMZWFmUmVzcG9uc2USEAoIY2VydF9kZXIYASABKAwSEAoId2FybmluZ3MYAiADKAkiGgoYR2V0U3Vib3JkaW5hdGVDU1JSZXF1ZXN0IiwKGUdldFN1Ym9yZGluYXRlQ1NSUmVzcG9uc2USDwoHY3NyX2RlchgBIAEoDCJLCiNTdWJtaXRTdWJvcmRpbmF0ZUNlcnRpZmljYXRlUmVxdWVzdBIRCgljaGFpbl9kZXIYASADKAwSEQoJY2hhaW5fcGVtGAIgASgJIk4KJFN1Ym1pdFN1Ym9yZGluYXRlQ2VydGlmaWNhdGVSZXNwb25zZRImCghpZGVudGl0eRgBIAEoCzIULmNyeXB0b3MudjEuSWRlbnRpdHkiKAoSRXhwb3J0Q0FLZXlSZXF1ZXN0EhIKCnBhc3NwaHJhc2UYASABKAwiJwoTRXhwb3J0Q0FLZXlSZXNwb25zZRIQCghlbnZlbG9wZRgBIAEoDCI6ChJJbXBvcnRDQUtleVJlcXVlc3QSEAoIZW52ZWxvcGUYASABKAwSEgoKcGFzc3BocmFzZRgCIAEoDCI9ChNJbXBvcnRDQUtleVJlc3BvbnNlEiYKCGlkZW50aXR5GAEgASgLMhQuY3J5cHRvcy52MS5JZGVudGl0eSIZChdCZWdpbktleVJvdGF0aW9uUmVxdWVzdCIrChhCZWdpbktleVJvdGF0aW9uUmVzcG9uc2USDwoHY3NyX2RlchgBIAEoDCJCChpDb21wbGV0ZUtleVJvdGF0aW9uUmVxdWVzdBIRCgljaGFpbl9kZXIYASADKAwSEQoJY2hhaW5fcGVtGAIgASgJIkUKG0NvbXBsZXRlS2V5Um90YXRpb25SZXNwb25zZRImCghpZGVudGl0eRgBIAEoCzIULmNyeXB0b3MudjEuSWRlbnRpdHkiFgoUR2V0UmVuZXdhbENTUlJlcXVlc3QiKAoVR2V0UmVuZXdhbENTUlJlc3BvbnNlEg8KB2Nzcl9kZXIYASABKAwiRwofU3VibWl0UmVuZXdlZENlcnRpZmljYXRlUmVxdWVzdBIRCgljaGFpbl9kZXIYASADKAwSEQoJY2hhaW5fcGVtGAIgASgJIkoKIFN1Ym1pdFJlbmV3ZWRDZXJ0aWZpY2F0ZVJlc3BvbnNlEiYKCGlkZW50aXR5GAEgASgLMhQuY3J5cHRvcy52MS5JZGVudGl0eSLpAQoKSXNzdWVkQ2VydBISCgpzZXJpYWxfaGV4GAEgASgJEhIKCnN1YmplY3RfZG4YAiABKAkSLgoKbm90X2JlZm9yZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLQoJbm90X2FmdGVyGAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIPCgdza2lfaGV4GAUgASgJEhQKDHByb2ZpbGVfbmFtZRgGIAEoCRItCglpc3N1ZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wImUKClJldm9jYXRpb24SEgoKc2VyaWFsX2hleBgBIAEoCRIuCgpyZXZva2VkX2F0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBITCgtyZWFzb25fY29kZRgDIAEoBSJDChhSZXZva2VDZXJ0aWZpY2F0ZVJlcXVlc3QSEgoKc2VyaWFsX2hleBgBIAEoCRITCgtyZWFzb25fY29kZRgCIAEoBSJHChlSZXZva2VDZXJ0aWZpY2F0ZVJlc3BvbnNlEioKCnJldm9jYXRpb24YASABKAsyFi5jcnlwdG9zLnYxLlJldm9jYXRpb24iEwoRTGlzdElzc3VlZFJlcXVlc3QiPAoSTGlzdElzc3VlZFJlc3BvbnNlEiYKBmlzc3VlZBgBIAMoCzIWLmNyeXB0b3MudjEuSXNzdWVkQ2VydCIYChZMaXN0UmV2b2NhdGlvbnNSZXF1ZXN0IkYKF0xpc3RSZXZvY2F0aW9uc1Jlc3BvbnNlEisKC3Jldm9jYXRpb25zGAEgAygLMhYuY3J5cHRvcy52MS5SZXZvY2F0aW9uIhkKF0xpc3RJbnN0YWxsRGlza3NSZXF1ZXN0IkIKGExpc3RJbnN0YWxsRGlza3NSZXNwb25zZRImCgVkaXNrcxgBIAMoCzIXLmNyeXB0b3MudjEuSW5zdGFsbERpc2siZQoLSW5zdGFsbERpc2sSDAoEcGF0aBgBIAEoCRISCgpzaXplX2J5dGVzGAIgASgEEg0KBW1vZGVsGAMgASgJEhIKCnJvdGF0aW9uYWwYBCABKAgSEQoJcmVtb3ZhYmxlGAUgASgIIl0KEVN0YWdlSW1hZ2VSZXF1ZXN0EiwKBWJlZ2luGAEgASgLMhsuY3J5cHRvcy52MS5TdGFnZUltYWdlQmVnaW5IABIPCgVjaHVuaxgCIAEoDEgAQgkKB3BheWxvYWQiSAoPU3RhZ2VJbWFnZUJlZ2luEhEKCXNpZ25hdHVyZRgBIAEoDBISCgpzaXplX2J5dGVzGAIgASgEEg4KBnNoYTI1NhgDIAEoCSJWChJTdGFnZUltYWdlUmVzcG9uc2USJwoGc3RhdHVzGAEgASgLMhcuY3J5cHRvcy52MS5JbWFnZVN0YXR1cxIXCg9yZXF1aXJlc19yZWJvb3QYAiABKAgiFgoUUm9sbGJhY2tJbWFnZVJlcXVlc3QiWQoVUm9sbGJhY2tJbWFnZVJlc3BvbnNlEicKBnN0YXR1cxgBIAEoCzIXLmNyeXB0b3MudjEuSW1hZ2VTdGF0dXMSFwoPcmVxdWlyZXNfcmVib290GAIgASgIIi0KFEFjdGl2YXRlSW1hZ2VSZXF1ZXN0EhUKDWNvbmZpcm1fY2FfY24YASABKAkiKgoVQWN0aXZhdGVJbWFnZVJlc3BvbnNlEhEKCXJlYm9vdGluZxgBIAEoCCI5Cg1SZWJvb3RSZXF1ZXN0EhUKDWNvbmZpcm1fY2FfY24YASABKAkSEQoJcG93ZXJfb2ZmGAIgASgIIiMKDlJlYm9vdFJlc3BvbnNlEhEKCXJlYm9vdGluZxgBIAEoCCIXChVHZXRJbWFnZVN0YXR1c1JlcXVlc3QiQQoWR2V0SW1hZ2VTdGF0dXNSZXNwb25zZRInCgZzdGF0dXMYASABKAsyFy5jcnlwdG9zLnYxLkltYWdlU3RhdHVzIoYBCgtJbWFnZVN0YXR1cxIWCg5ydW5uaW5nX3NoYTI1NhgBIAEoCRIVCg1hY3RpdmVfc2hhMjU2GAIgASgJEhcKD3ByZXZpb3VzX3NoYTI1NhgDIAEoCRIWCg5yZWJvb3RfcGVuZGluZxgEIAEoCBIXCg9ydW5uaW5nX3ZlcnNpb24YBSABKAkyohkKC05vZGVTZXJ2aWNlEk4KC0FwcGx5Q29uZmlnEh4uY3J5cHRvcy52MS5BcHBseUNvbmZpZ1JlcXVlc3QaHy5jcnlwdG9zLnYxLkFwcGx5Q29uZmlnUmVzcG9uc2USSAoJR2V0U3RhdHVzEhwuY3J5cHRvcy52MS5HZXRTdGF0dXNSZXF1ZXN0Gh0uY3J5cHRvcy52MS5HZXRTdGF0dXNSZXNwb25zZRJdChBMaXN0SW5zdGFsbERpc2tzEiMuY3J5cHRvcy52MS5MaXN0SW5zdGFsbERpc2tzUmVxdWVzdBokLmNyeXB0b3MudjEuTGlzdEluc3RhbGxEaXNrc1Jlc3BvbnNlEk4KC0dldElkZW50aXR5Eh4uY3J5cHRvcy52MS5HZXRJZGVudGl0eVJlcXVlc3QaHy5jcnlwdG9zLnYxLkdldElkZW50aXR5UmVzcG9uc2USVgoNU3RhcnRDZXJlbW9ueRIgLmNyeXB0b3MudjEuU3RhcnRDZXJlbW9ueVJlcXVlc3QaIS5jcnlwdG9zLnYxLlN0YXJ0Q2VyZW1vbnlSZXNwb25zZTABEkIKB1NpZ25DU1ISGi5jcnlwdG9zLnYxLlNpZ25DU1JSZXF1ZXN0GhsuY3J5cHRvcy52MS5TaWduQ1NSUmVzcG9uc2USYwoSU2lnblN1Ym9yZGluYXRlQ1NSEiUuY3J5cHRvcy52MS5TaWduU3Vib3JkaW5hdGVDU1JSZXF1ZXN0GiYuY3J5cHRvcy52MS5TaWduU3Vib3JkaW5hdGVDU1JSZXNwb25zZRJICglJc3N1ZUxlYWYSHC5jcnlwdG9zLnYxLklzc3VlTGVhZlJlcXVlc3QaHS5jcnlwdG9zLnYxLklzc3VlTGVhZlJlc3BvbnNlEmAKEUdldFN1Ym9yZGluYXRlQ1NSEiQuY3J5cHRvcy52MS5HZXRTdWJvcmRpbmF0ZUNTUlJlcXVlc3QaJS5jcnlwdG9zLnYxLkdldFN1Ym9yZGluYXRlQ1NSUmVzcG9uc2USgQEKHFN1Ym1pdFN1Ym9yZGluYXRlQ2VydGlmaWNhdGUSLy5jcnlwdG9zLnYxLlN1Ym1pdFN1Ym9yZGluYXRlQ2VydGlmaWNhdGVSZXF1ZXN0GjAuY3J5cHRvcy52MS5TdWJtaXRTdWJvcmRpbmF0ZUNlcnRpZmljYXRlUmVzcG9uc2USYAoRUmV2b2tlQ2VydGlmaWNhdGUSJC5jcnlwdG9zLnYxLlJldm9rZUNlcnRpZmljYXRlUmVxdWVzdBolLmNyeXB0b3MudjEuUmV2b2tlQ2VydGlmaWNhdGVSZXNwb25zZRJLCgpMaXN0SXNzdWVkEh0uY3J5cHRvcy52MS5MaXN0SXNzdWVkUmVxdWVzdBoeLmNyeXB0b3MudjEuTGlzdElzc3VlZFJlc3BvbnNlEloKD0xpc3RSZXZvY2F0aW9ucxIiLmNyeXB0b3MudjEuTGlzdFJldm9jYXRpb25zUmVxdWVzdBojLmNyeXB0b3MudjEuTGlzdFJldm9jYXRpb25zUmVzcG9uc2USTgoLRXhwb3J0Q0FLZXkSHi5jcnlwdG9zLnYxLkV4cG9ydENBS2V5UmVxdWVzdBofLmNyeXB0b3MudjEuRXhwb3J0Q0FLZXlSZXNwb25zZRJOCgtJbXBvcnRDQUtleRIeLmNyeXB0b3MudjEuSW1wb3J0Q0FLZXlSZXF1ZXN0Gh8uY3J5cHRvcy52MS5JbXBvcnRDQUtleVJlc3BvbnNlEl0KEEJlZ2luS2V5Um90YXRpb24SIy5jcnlwdG9zLnYxLkJlZ2luS2V5Um90YXRpb25SZXF1ZXN0GiQuY3J5cHRvcy52MS5CZWdpbktleVJvdGF0aW9uUmVzcG9uc2USZgoTQ29tcGxldGVLZXlSb3RhdGlvbhImLmNyeXB0b3MudjEuQ29tcGxldGVLZXlSb3RhdGlvblJlcXVlc3QaJy5jcnlwdG9zLnYxLkNvbXBsZXRlS2V5Um90YXRpb25SZXNwb25zZRJUCg1HZXRSZW5ld2FsQ1NSEiAuY3J5cHRvcy52MS5HZXRSZW5ld2FsQ1NSUmVxdWVzdBohLmNyeXB0b3MudjEuR2V0UmVuZXdhbENTUlJlc3BvbnNlEnUKGFN1Ym1pdFJlbmV3ZWRDZXJ0aWZpY2F0ZRIrLmNyeXB0b3MudjEuU3VibWl0UmVuZXdlZENlcnRpZmljYXRlUmVxdWVzdBosLmNyeXB0b3MudjEuU3VibWl0UmVuZXdlZENlcnRpZmljYXRlUmVzcG9uc2USPAoFUmVzZXQSGC5jcnlwdG9zLnYxLlJlc2V0UmVxdWVzdBoZLmNyeXB0b3MudjEuUmVzZXRSZXNwb25zZRJOCgtSZW1vdGVSZXNldBIeLmNyeXB0b3MudjEuUmVtb3RlUmVzZXRSZXF1ZXN0Gh8uY3J5cHRvcy52MS5SZW1vdGVSZXNldFJlc3BvbnNlEj8KBkF0dGVzdBIZLmNyeXB0b3MudjEuQXR0ZXN0UmVxdWVzdBoaLmNyeXB0b3MudjEuQXR0ZXN0UmVzcG9uc2USVAoNU2V0TWFuYWdlbWVudBIgLmNyeXB0b3MudjEuU2V0TWFuYWdlbWVudFJlcXVlc3QaIS5jcnlwdG9zLnYxLlNldE1hbmFnZW1lbnRSZXNwb25zZRJICglHZXRDb25maWcSHC5jcnlwdG9zLnYxLkdldENvbmZpZ1JlcXVlc3QaHS5jcnlwdG9zLnYxLkdldENvbmZpZ1Jlc3BvbnNlEk0KClN0YWdlSW1hZ2USHS5jcnlwdG9zLnYxLlN0YWdlSW1hZ2VSZXF1ZXN0Gh4uY3J5cHRvcy52MS5TdGFnZUltYWdlUmVzcG9uc2UoARJUCg1Sb2xsYmFja0ltYWdlEiAuY3J5cHRvcy52MS5Sb2xsYmFja0ltYWdlUmVxdWVzdBohLmNyeXB0b3MudjEuUm9sbGJhY2tJbWFnZVJlc3BvbnNlElQKDUFjdGl2YXRlSW1hZ2USIC5jcnlwdG9zLnYxLkFjdGl2YXRlSW1hZ2VSZXF1ZXN0GiEuY3J5cHRvcy52MS5BY3RpdmF0ZUltYWdlUmVzcG9uc2USVwoOR2V0SW1hZ2VTdGF0dXMSIS5jcnlwdG9zLnYxLkdldEltYWdlU3RhdHVzUmVxdWVzdBoiLmNyeXB0b3MudjEuR2V0SW1hZ2VTdGF0dXNSZXNwb25zZRI/CgZSZWJvb3QSGS5jcnlwdG9zLnYxLlJlYm9vdFJlcXVlc3QaGi5jcnlwdG9zLnYxLlJlYm9vdFJlc3BvbnNlEmAKEU1pbnRTY2VwQ2hhbGxlbmdlEiQuY3J5cHRvcy52MS5NaW50U2NlcENoYWxsZW5nZVJlcXVlc3QaJS5jcnlwdG9zLnYxLk1pbnRTY2VwQ2hhbGxlbmdlUmVzcG9uc2USYwoSTGlzdFNjZXBDaGFsbGVuZ2VzEiUuY3J5cHRvcy52MS5MaXN0U2NlcENoYWxsZW5nZXNSZXF1ZXN0GiYuY3J5cHRvcy52MS5MaXN0U2NlcENoYWxsZW5nZXNSZXNwb25zZRJmChNSZXZva2VTY2VwQ2hhbGxlbmdlEiYuY3J5cHRvcy52MS5SZXZva2VTY2VwQ2hhbGxlbmdlUmVxdWVzdBonLmNyeXB0b3MudjEuUmV2b2tlU2NlcENoYWxsZW5nZVJlc3BvbnNlEmYKE0xpc3RTY2VwRW5yb2xsbWVudHMSJi5jcnlwdG9zLnYxLkxpc3RTY2VwRW5yb2xsbWVudHNSZXF1ZXN0GicuY3J5cHRvcy52MS5MaXN0U2NlcEVucm9sbG1lbnRzUmVzcG9uc2USbAoVQXBwcm92ZVNjZXBFbnJvbGxtZW50EiguY3J5cHRvcy52MS5BcHByb3ZlU2NlcEVucm9sbG1lbnRSZXF1ZXN0GikuY3J5cHRvcy52MS5BcHByb3ZlU2NlcEVucm9sbG1lbnRSZXNwb25zZRJpChRSZWplY3RTY2VwRW5yb2xsbWVudBInLmNyeXB0b3MudjEuUmVqZWN0U2NlcEVucm9sbG1lbnRSZXF1ZXN0GiguY3J5cHRvcy52MS5SZWplY3RTY2VwRW5yb2xsbWVudFJlc3BvbnNlEmYKE0xpc3RUc2FDZXJ0aWZpY2F0ZXMSJi5jcnlwdG9zLnYxLkxpc3RUc2FDZXJ0aWZpY2F0ZXNSZXF1ZXN0GicuY3J5cHRvcy52MS5MaXN0VHNhQ2VydGlmaWNhdGVzUmVzcG9uc2VCNFoyZ2l0aHViLmNvbS9DcnlwdE9TLVBLSS9hcGkvZ28vY3J5cHRvcy92MTtjcnlwdG9zdjFiBnByb3RvMw", [file_cryptos_v1_ceremony, file_cryptos_v1_config, file_cryptos_v1_identity, file_cryptos_v1_scep, file_cryptos_v1_status, file_cryptos_v1_tsa, file_google_protobuf_timestamp]); /** * @generated from message cryptos.v1.ApplyConfigRequest @@ -126,7 +130,11 @@ export type ApplyConfigResponse = Message<"cryptos.v1.ApplyConfigResponse"> & { generation: bigint; /** - * True when the apply changed a field that takes effect only on reboot. + * True when the apply changed a field that takes effect only on reboot. Any + * change to an enrolment protocol block (Pki.acme, Pki.est, Pki.scep), + * switching it on or off included, sets it: the node stores the config and + * the protocol changes at the next boot. NodeStatus.config_reboot_pending stays true until + * then. * * @generated from field: bool requires_reboot = 2; */ @@ -381,7 +389,8 @@ export const RemoteResetResponseSchema: GenMessage = /*@__P */ export type AttestRequest = Message<"cryptos.v1.AttestRequest"> & { /** - * Random challenge from the manager; the node signs it with its identity key. + * Random challenge from the manager. The node signs a domain-separated + * message built from it (see AttestResponse.signature), never the raw nonce. * * @generated from field: bytes nonce = 1; */ @@ -400,7 +409,16 @@ export const AttestRequestSchema: GenMessage = /*@__PURE__*/ */ export type AttestResponse = Message<"cryptos.v1.AttestResponse"> & { /** - * ECDSA signature (ASN.1 DER) over SHA-384 of the nonce. + * Signature by the node's CA identity key over SHA-384 of the message + * "CryptOS-PKI attestation v1" || 0x00 || uint32_be(len(nonce)) || nonce + * where the context string is its 26 ASCII bytes with no terminator, 0x00 + * is a single separator byte, and uint32_be is the nonce length as a 4-byte + * big-endian integer. The encoding follows the identity key type: an ECDSA + * key gives an ASN.1 DER SEQUENCE { r INTEGER, s INTEGER }, and an RSA key + * gives an RSASSA-PKCS1-v1_5 signature with SHA-384 (raw bytes, the length + * of the modulus). A verifier rebuilds the message from the nonce it sent, + * hashes it with SHA-384 and checks the signature against identity_pub_der + * using the scheme that key's type implies. * * @generated from field: bytes signature = 1; */ @@ -1715,7 +1733,10 @@ export const NodeService: GenService<{ /** * Attest signs the caller's nonce with the node's CA identity key so the * Fleet Manager can verify possession of the node identity (challenge- - * response). ek_pub/ek_cert are reserved for future TPM EK attestation. + * response). The nonce is domain-separated before signing (see + * AttestResponse.signature), so an Attest signature can never be replayed + * as a certificate, CRL or OCSP signature. ek_pub/ek_cert are reserved for + * future TPM EK attestation. * * @generated from rpc cryptos.v1.NodeService.Attest */ @@ -1826,6 +1847,97 @@ export const NodeService: GenService<{ input: typeof RebootRequestSchema; output: typeof RebootResponseSchema; }, + /** + * MintScepChallenge creates a single-use challenge for one initial + * enrolment and returns it. The challenge is returned by this call only: the + * node stores a digest of it and no RPC reads it back, so a lost challenge is + * revoked and minted again. It is consumed by the first PKCSReq that + * presents it, whether that request is issued, refused or queued. + * + * @generated from rpc cryptos.v1.NodeService.MintScepChallenge + */ + mintScepChallenge: { + methodKind: "unary"; + input: typeof MintScepChallengeRequestSchema; + output: typeof MintScepChallengeResponseSchema; + }, + /** + * ListScepChallenges returns the challenges that are still usable: minted, + * not yet consumed, not revoked and not expired. It never carries the + * challenge or its digest. + * + * @generated from rpc cryptos.v1.NodeService.ListScepChallenges + */ + listScepChallenges: { + methodKind: "unary"; + input: typeof ListScepChallengesRequestSchema; + output: typeof ListScepChallengesResponseSchema; + }, + /** + * RevokeScepChallenge withdraws a usable challenge by id before it is + * consumed. NotFound when the id is unknown, already consumed, revoked or + * expired. + * + * @generated from rpc cryptos.v1.NodeService.RevokeScepChallenge + */ + revokeScepChallenge: { + methodKind: "unary"; + input: typeof RevokeScepChallengeRequestSchema; + output: typeof RevokeScepChallengeResponseSchema; + }, + /** + * ListScepEnrollments returns the initial enrolments waiting for an admin + * decision on profiles with require_approval set, oldest first. + * + * @generated from rpc cryptos.v1.NodeService.ListScepEnrollments + */ + listScepEnrollments: { + methodKind: "unary"; + input: typeof ListScepEnrollmentsRequestSchema; + output: typeof ListScepEnrollmentsResponseSchema; + }, + /** + * ApproveScepEnrollment issues the certificate for a waiting enrolment under + * its profile and returns its serial. The client collects it with its next + * CertPoll. NotFound when the id is not waiting. The request is checked + * again at approval, against the config and key floor in force then, and a + * request that no longer passes is refused with FailedPrecondition and stays + * queued for rejection. + * + * @generated from rpc cryptos.v1.NodeService.ApproveScepEnrollment + */ + approveScepEnrollment: { + methodKind: "unary"; + input: typeof ApproveScepEnrollmentRequestSchema; + output: typeof ApproveScepEnrollmentResponseSchema; + }, + /** + * RejectScepEnrollment refuses a waiting enrolment. The client's next + * CertPoll is answered FAILURE. NotFound when the id is not waiting. + * + * @generated from rpc cryptos.v1.NodeService.RejectScepEnrollment + */ + rejectScepEnrollment: { + methodKind: "unary"; + input: typeof RejectScepEnrollmentRequestSchema; + output: typeof RejectScepEnrollmentResponseSchema; + }, + /** + * ListTsaCertificates returns every TSA certificate the node has signed + * timestamp tokens with, the current one and all past ones, newest first, + * so a relying party can verify a token signed before a rotation. It is a + * read, authorized like ListIssued, and it answers whether or not the TSA is + * running this boot: switching the TSA off does not unpublish the + * certificates old tokens name. Empty on a node that has never run a TSA. + * Refused with FailedPrecondition in maintenance mode. + * + * @generated from rpc cryptos.v1.NodeService.ListTsaCertificates + */ + listTsaCertificates: { + methodKind: "unary"; + input: typeof ListTsaCertificatesRequestSchema; + output: typeof ListTsaCertificatesResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_cryptos_v1_node, 0); diff --git a/src/gen/fleet/cryptos/v1/scep_pb.ts b/src/gen/fleet/cryptos/v1/scep_pb.ts new file mode 100644 index 0000000..1d4e2a2 --- /dev/null +++ b/src/gen/fleet/cryptos/v1/scep_pb.ts @@ -0,0 +1,417 @@ +// @generated by protoc-gen-es v2.12.1 with parameter "target=ts" +// @generated from file cryptos/v1/scep.proto (package cryptos.v1, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file cryptos/v1/scep.proto. + */ +export const file_cryptos_v1_scep: GenFile = /*@__PURE__*/ + fileDesc("ChVjcnlwdG9zL3YxL3NjZXAucHJvdG8SCmNyeXB0b3MudjEiuAEKDVNjZXBDaGFsbGVuZ2USCgoCaWQYASABKAkSDwoHcHJvZmlsZRgCIAEoCRITCgtib3VuZF9uYW1lcxgDIAMoCRIuCgpjcmVhdGVkX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgpleHBpcmVzX2F0GAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIVCg1jcmVhdGVkX2J5X2NuGAYgASgJIlUKGE1pbnRTY2VwQ2hhbGxlbmdlUmVxdWVzdBIPCgdwcm9maWxlGAEgASgJEhMKC3R0bF9zZWNvbmRzGAIgASgNEhMKC2JvdW5kX25hbWVzGAMgAygJImUKGU1pbnRTY2VwQ2hhbGxlbmdlUmVzcG9uc2USGgoSY2hhbGxlbmdlX3Bhc3N3b3JkGAEgASgJEiwKCWNoYWxsZW5nZRgCIAEoCzIZLmNyeXB0b3MudjEuU2NlcENoYWxsZW5nZSIbChlMaXN0U2NlcENoYWxsZW5nZXNSZXF1ZXN0IksKGkxpc3RTY2VwQ2hhbGxlbmdlc1Jlc3BvbnNlEi0KCmNoYWxsZW5nZXMYASADKAsyGS5jcnlwdG9zLnYxLlNjZXBDaGFsbGVuZ2UiKAoaUmV2b2tlU2NlcENoYWxsZW5nZVJlcXVlc3QSCgoCaWQYASABKAkiSwobUmV2b2tlU2NlcENoYWxsZW5nZVJlc3BvbnNlEiwKCWNoYWxsZW5nZRgBIAEoCzIZLmNyeXB0b3MudjEuU2NlcENoYWxsZW5nZSLrAQoOU2NlcEVucm9sbG1lbnQSCgoCaWQYASABKAkSFgoOdHJhbnNhY3Rpb25faWQYAiABKAkSDwoHcHJvZmlsZRgDIAEoCRISCgpzdWJqZWN0X2RuGAQgASgJEhEKCWRuc19uYW1lcxgFIAMoCRIUCgxpcF9hZGRyZXNzZXMYBiADKAkSDwoHa2V5X2FsZxgHIAEoCRIPCgdjc3JfcGVtGAggASgJEhQKDGNoYWxsZW5nZV9pZBgJIAEoCRIvCgtyZWNlaXZlZF9hdBgKIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiLQoaTGlzdFNjZXBFbnJvbGxtZW50c1JlcXVlc3QSDwoHcHJvZmlsZRgBIAEoCSJOChtMaXN0U2NlcEVucm9sbG1lbnRzUmVzcG9uc2USLwoLZW5yb2xsbWVudHMYASADKAsyGi5jcnlwdG9zLnYxLlNjZXBFbnJvbGxtZW50IioKHEFwcHJvdmVTY2VwRW5yb2xsbWVudFJlcXVlc3QSCgoCaWQYASABKAkiYwodQXBwcm92ZVNjZXBFbnJvbGxtZW50UmVzcG9uc2USLgoKZW5yb2xsbWVudBgBIAEoCzIaLmNyeXB0b3MudjEuU2NlcEVucm9sbG1lbnQSEgoKc2VyaWFsX2hleBgCIAEoCSI5ChtSZWplY3RTY2VwRW5yb2xsbWVudFJlcXVlc3QSCgoCaWQYASABKAkSDgoGcmVhc29uGAIgASgJIk4KHFJlamVjdFNjZXBFbnJvbGxtZW50UmVzcG9uc2USLgoKZW5yb2xsbWVudBgBIAEoCzIaLmNyeXB0b3MudjEuU2NlcEVucm9sbG1lbnRCNFoyZ2l0aHViLmNvbS9DcnlwdE9TLVBLSS9hcGkvZ28vY3J5cHRvcy92MTtjcnlwdG9zdjFiBnByb3RvMw", [file_google_protobuf_timestamp]); + +/** + * ScepChallenge describes a one-time SCEP challenge without the challenge + * itself, which only MintScepChallengeResponse carries. + * + * @generated from message cryptos.v1.ScepChallenge + */ +export type ScepChallenge = Message<"cryptos.v1.ScepChallenge"> & { + /** + * id identifies the challenge for listing and revocation. It is not secret + * and does not authorize anything. + * + * @generated from field: string id = 1; + */ + id: string; + + /** + * profile is the Scep.profiles entry the enrolment it authorizes issues + * from. + * + * @generated from field: string profile = 2; + */ + profile: string; + + /** + * bound_names, when non-empty, are the only names the enrolment may + * request: every DNS name, IP address and the subject common name in the + * request must be one of these, exactly. + * + * @generated from field: repeated string bound_names = 3; + */ + boundNames: string[]; + + /** + * @generated from field: google.protobuf.Timestamp created_at = 4; + */ + createdAt?: Timestamp | undefined; + + /** + * expires_at is when the challenge stops being accepted if still unused. + * + * @generated from field: google.protobuf.Timestamp expires_at = 5; + */ + expiresAt?: Timestamp | undefined; + + /** + * created_by_cn is the common name of the admin certificate that minted it. + * + * @generated from field: string created_by_cn = 6; + */ + createdByCn: string; +}; + +/** + * Describes the message cryptos.v1.ScepChallenge. + * Use `create(ScepChallengeSchema)` to create a new message. + */ +export const ScepChallengeSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_scep, 0); + +/** + * @generated from message cryptos.v1.MintScepChallengeRequest + */ +export type MintScepChallengeRequest = Message<"cryptos.v1.MintScepChallengeRequest"> & { + /** + * profile names the Scep.profiles entry to issue from. It may be empty only + * when exactly one profile is configured. InvalidArgument when it names no + * configured SCEP profile. + * + * @generated from field: string profile = 1; + */ + profile: string; + + /** + * ttl_seconds is how long the challenge stays usable. Zero means 3600 (one + * hour); more than 604800 (seven days) is rejected with InvalidArgument. + * + * @generated from field: uint32 ttl_seconds = 2; + */ + ttlSeconds: number; + + /** + * bound_names optionally pins the names the enrolment may request (see + * ScepChallenge.bound_names). Each must itself pass + * Scep.allowed_identifier_suffixes, or the call fails with InvalidArgument. + * + * @generated from field: repeated string bound_names = 3; + */ + boundNames: string[]; +}; + +/** + * Describes the message cryptos.v1.MintScepChallengeRequest. + * Use `create(MintScepChallengeRequestSchema)` to create a new message. + */ +export const MintScepChallengeRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_scep, 1); + +/** + * @generated from message cryptos.v1.MintScepChallengeResponse + */ +export type MintScepChallengeResponse = Message<"cryptos.v1.MintScepChallengeResponse"> & { + /** + * challenge_password is the challenge to give the device, for example as a + * Cisco trustpoint's enrolment password. It carries at least 128 bits from + * the node's random source in printable ASCII with no spaces, so it can be + * typed at a device prompt. It is returned here once and never again, and + * must not be logged. + * + * @generated from field: string challenge_password = 1; + */ + challengePassword: string; + + /** + * @generated from field: cryptos.v1.ScepChallenge challenge = 2; + */ + challenge?: ScepChallenge | undefined; +}; + +/** + * Describes the message cryptos.v1.MintScepChallengeResponse. + * Use `create(MintScepChallengeResponseSchema)` to create a new message. + */ +export const MintScepChallengeResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_scep, 2); + +/** + * @generated from message cryptos.v1.ListScepChallengesRequest + */ +export type ListScepChallengesRequest = Message<"cryptos.v1.ListScepChallengesRequest"> & { +}; + +/** + * Describes the message cryptos.v1.ListScepChallengesRequest. + * Use `create(ListScepChallengesRequestSchema)` to create a new message. + */ +export const ListScepChallengesRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_scep, 3); + +/** + * @generated from message cryptos.v1.ListScepChallengesResponse + */ +export type ListScepChallengesResponse = Message<"cryptos.v1.ListScepChallengesResponse"> & { + /** + * challenges are the usable challenges, soonest to expire first. + * + * @generated from field: repeated cryptos.v1.ScepChallenge challenges = 1; + */ + challenges: ScepChallenge[]; +}; + +/** + * Describes the message cryptos.v1.ListScepChallengesResponse. + * Use `create(ListScepChallengesResponseSchema)` to create a new message. + */ +export const ListScepChallengesResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_scep, 4); + +/** + * @generated from message cryptos.v1.RevokeScepChallengeRequest + */ +export type RevokeScepChallengeRequest = Message<"cryptos.v1.RevokeScepChallengeRequest"> & { + /** + * @generated from field: string id = 1; + */ + id: string; +}; + +/** + * Describes the message cryptos.v1.RevokeScepChallengeRequest. + * Use `create(RevokeScepChallengeRequestSchema)` to create a new message. + */ +export const RevokeScepChallengeRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_scep, 5); + +/** + * @generated from message cryptos.v1.RevokeScepChallengeResponse + */ +export type RevokeScepChallengeResponse = Message<"cryptos.v1.RevokeScepChallengeResponse"> & { + /** + * challenge is the withdrawn challenge as it was before revocation. + * + * @generated from field: cryptos.v1.ScepChallenge challenge = 1; + */ + challenge?: ScepChallenge | undefined; +}; + +/** + * Describes the message cryptos.v1.RevokeScepChallengeResponse. + * Use `create(RevokeScepChallengeResponseSchema)` to create a new message. + */ +export const RevokeScepChallengeResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_scep, 6); + +/** + * ScepEnrollment is an initial SCEP enrolment held for an admin decision. The + * challenge that authorized it was consumed on arrival and is not carried. + * + * @generated from message cryptos.v1.ScepEnrollment + */ +export type ScepEnrollment = Message<"cryptos.v1.ScepEnrollment"> & { + /** + * id identifies the enrolment for approval or rejection. + * + * @generated from field: string id = 1; + */ + id: string; + + /** + * transaction_id is the client's SCEP transactionID (RFC 8894 section + * 3.2.1.1), which its CertPoll requests carry. + * + * @generated from field: string transaction_id = 2; + */ + transactionId: string; + + /** + * profile is the Scep.profiles entry it will issue from. + * + * @generated from field: string profile = 3; + */ + profile: string; + + /** + * subject_dn is the subject the request asks for, as an RFC 4514 string. + * + * @generated from field: string subject_dn = 4; + */ + subjectDn: string; + + /** + * dns_names and ip_addresses are the subject alternative names requested. + * + * @generated from field: repeated string dns_names = 5; + */ + dnsNames: string[]; + + /** + * @generated from field: repeated string ip_addresses = 6; + */ + ipAddresses: string[]; + + /** + * key_alg is the requested subject key, for example "RSA-2048" or + * "ECDSA-P384". + * + * @generated from field: string key_alg = 7; + */ + keyAlg: string; + + /** + * csr_pem is the PKCS#10 request the client signed, PEM-encoded, for an + * admin who wants to inspect it before deciding. + * + * @generated from field: string csr_pem = 8; + */ + csrPem: string; + + /** + * challenge_id is the id of the challenge that authorized the request. + * + * @generated from field: string challenge_id = 9; + */ + challengeId: string; + + /** + * @generated from field: google.protobuf.Timestamp received_at = 10; + */ + receivedAt?: Timestamp | undefined; +}; + +/** + * Describes the message cryptos.v1.ScepEnrollment. + * Use `create(ScepEnrollmentSchema)` to create a new message. + */ +export const ScepEnrollmentSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_scep, 7); + +/** + * @generated from message cryptos.v1.ListScepEnrollmentsRequest + */ +export type ListScepEnrollmentsRequest = Message<"cryptos.v1.ListScepEnrollmentsRequest"> & { + /** + * profile, when set, keeps only enrolments for that profile. + * + * @generated from field: string profile = 1; + */ + profile: string; +}; + +/** + * Describes the message cryptos.v1.ListScepEnrollmentsRequest. + * Use `create(ListScepEnrollmentsRequestSchema)` to create a new message. + */ +export const ListScepEnrollmentsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_scep, 8); + +/** + * @generated from message cryptos.v1.ListScepEnrollmentsResponse + */ +export type ListScepEnrollmentsResponse = Message<"cryptos.v1.ListScepEnrollmentsResponse"> & { + /** + * @generated from field: repeated cryptos.v1.ScepEnrollment enrollments = 1; + */ + enrollments: ScepEnrollment[]; +}; + +/** + * Describes the message cryptos.v1.ListScepEnrollmentsResponse. + * Use `create(ListScepEnrollmentsResponseSchema)` to create a new message. + */ +export const ListScepEnrollmentsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_scep, 9); + +/** + * @generated from message cryptos.v1.ApproveScepEnrollmentRequest + */ +export type ApproveScepEnrollmentRequest = Message<"cryptos.v1.ApproveScepEnrollmentRequest"> & { + /** + * @generated from field: string id = 1; + */ + id: string; +}; + +/** + * Describes the message cryptos.v1.ApproveScepEnrollmentRequest. + * Use `create(ApproveScepEnrollmentRequestSchema)` to create a new message. + */ +export const ApproveScepEnrollmentRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_scep, 10); + +/** + * @generated from message cryptos.v1.ApproveScepEnrollmentResponse + */ +export type ApproveScepEnrollmentResponse = Message<"cryptos.v1.ApproveScepEnrollmentResponse"> & { + /** + * enrollment is the approved enrolment, now out of the queue. + * + * @generated from field: cryptos.v1.ScepEnrollment enrollment = 1; + */ + enrollment?: ScepEnrollment | undefined; + + /** + * serial_hex is the serial of the issued certificate, which ListIssued and + * RevokeCertificate know it by. + * + * @generated from field: string serial_hex = 2; + */ + serialHex: string; +}; + +/** + * Describes the message cryptos.v1.ApproveScepEnrollmentResponse. + * Use `create(ApproveScepEnrollmentResponseSchema)` to create a new message. + */ +export const ApproveScepEnrollmentResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_scep, 11); + +/** + * @generated from message cryptos.v1.RejectScepEnrollmentRequest + */ +export type RejectScepEnrollmentRequest = Message<"cryptos.v1.RejectScepEnrollmentRequest"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * reason is recorded in the audit log. It is not sent to the client, which + * RFC 8894 answers only with a failInfo code. + * + * @generated from field: string reason = 2; + */ + reason: string; +}; + +/** + * Describes the message cryptos.v1.RejectScepEnrollmentRequest. + * Use `create(RejectScepEnrollmentRequestSchema)` to create a new message. + */ +export const RejectScepEnrollmentRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_scep, 12); + +/** + * @generated from message cryptos.v1.RejectScepEnrollmentResponse + */ +export type RejectScepEnrollmentResponse = Message<"cryptos.v1.RejectScepEnrollmentResponse"> & { + /** + * enrollment is the rejected enrolment, now out of the queue. + * + * @generated from field: cryptos.v1.ScepEnrollment enrollment = 1; + */ + enrollment?: ScepEnrollment | undefined; +}; + +/** + * Describes the message cryptos.v1.RejectScepEnrollmentResponse. + * Use `create(RejectScepEnrollmentResponseSchema)` to create a new message. + */ +export const RejectScepEnrollmentResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_scep, 13); + diff --git a/src/gen/fleet/cryptos/v1/status_pb.ts b/src/gen/fleet/cryptos/v1/status_pb.ts index ed67103..159aba1 100644 --- a/src/gen/fleet/cryptos/v1/status_pb.ts +++ b/src/gen/fleet/cryptos/v1/status_pb.ts @@ -4,15 +4,15 @@ import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; -import type { Timestamp } from "@bufbuild/protobuf/wkt"; -import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import type { Duration, Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_duration, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; import type { Message } from "@bufbuild/protobuf"; /** * Describes the file cryptos/v1/status.proto. */ export const file_cryptos_v1_status: GenFile = /*@__PURE__*/ - fileDesc("ChdjcnlwdG9zL3YxL3N0YXR1cy5wcm90bxIKY3J5cHRvcy52MSKIAwoKTm9kZVN0YXR1cxIiCgRyb2xlGAEgASgOMhQuY3J5cHRvcy52MS5Ob2RlUm9sZRIxCg5pZGVudGl0eV9zdGF0ZRgCIAEoDjIZLmNyeXB0b3MudjEuSWRlbnRpdHlTdGF0ZRInCgl0cG1fc3RhdGUYAyABKA4yFC5jcnlwdG9zLnYxLlRwbVN0YXRlEikKCmV0Y2Rfc3RhdGUYBCABKA4yFS5jcnlwdG9zLnYxLkV0Y2RTdGF0ZRISCgpib290X2NvdW50GAUgASgEEhgKEHNvZnR3YXJlX3ZlcnNpb24YBiABKAkSNAoNZmxlZXRfbWFuYWdlchgHIAEoDjIdLmNyeXB0b3MudjEuRmxlZXRNYW5hZ2VyU3RhdGUSPQoUcmV2b2NhdGlvbl9wcmVmbGlnaHQYCCABKAsyHy5jcnlwdG9zLnYxLlJldm9jYXRpb25QcmVmbGlnaHQSLAoIcmVzb2x2ZXIYCSABKAsyGi5jcnlwdG9zLnYxLlJlc29sdmVyU3RhdHVzIqABChNSZXZvY2F0aW9uUHJlZmxpZ2h0EjMKBXN0YXRlGAEgASgOMiQuY3J5cHRvcy52MS5SZXZvY2F0aW9uUHJlZmxpZ2h0U3RhdGUSEAoIYmFzZV91cmwYAiABKAkSEgoKbGFzdF9lcnJvchgDIAEoCRIuCgpjaGVja2VkX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJhCg5SZXNvbHZlclN0YXR1cxIqCgZzb3VyY2UYASABKA4yGi5jcnlwdG9zLnYxLlJlc29sdmVyU291cmNlEhMKC25hbWVzZXJ2ZXJzGAIgAygJEg4KBnNlYXJjaBgDIAMoCSroAQoYUmV2b2NhdGlvblByZWZsaWdodFN0YXRlEioKJlJFVk9DQVRJT05fUFJFRkxJR0hUX1NUQVRFX1VOU1BFQ0lGSUVEEAASLQopUkVWT0NBVElPTl9QUkVGTElHSFRfU1RBVEVfTk9UX0NPTkZJR1VSRUQQARImCiJSRVZPQ0FUSU9OX1BSRUZMSUdIVF9TVEFURV9QRU5ESU5HEAISIQodUkVWT0NBVElPTl9QUkVGTElHSFRfU1RBVEVfT0sQAxImCiJSRVZPQ0FUSU9OX1BSRUZMSUdIVF9TVEFURV9GQUlMSU5HEAQqjwEKDlJlc29sdmVyU291cmNlEh8KG1JFU09MVkVSX1NPVVJDRV9VTlNQRUNJRklFRBAAEhgKFFJFU09MVkVSX1NPVVJDRV9OT05FEAESIgoeUkVTT0xWRVJfU09VUkNFX01BQ0hJTkVfQ09ORklHEAISHgoaUkVTT0xWRVJfU09VUkNFX0RIQ1BfTEVBU0UQAypsCghOb2RlUm9sZRIZChVOT0RFX1JPTEVfVU5TUEVDSUZJRUQQABISCg5OT0RFX1JPTEVfUk9PVBABEhoKFk5PREVfUk9MRV9JTlRFUk1FRElBVEUQAhIVChFOT0RFX1JPTEVfSVNTVUlORxADKrMBCg1JZGVudGl0eVN0YXRlEh4KGklERU5USVRZX1NUQVRFX1VOU1BFQ0lGSUVEEAASFwoTSURFTlRJVFlfU1RBVEVfTk9ORRABEicKI0lERU5USVRZX1NUQVRFX0NFUkVNT05ZX0lOX1BST0dSRVNTEAISHgoaSURFTlRJVFlfU1RBVEVfRVNUQUJMSVNIRUQQAxIgChxJREVOVElUWV9TVEFURV9BV0FJVElOR19DRVJUEAQqeQoIVHBtU3RhdGUSGQoVVFBNX1NUQVRFX1VOU1BFQ0lGSUVEEAASEAoMVFBNX1NUQVRFX09LEAESGQoVVFBNX1NUQVRFX1VOQVZBSUxBQkxFEAISJQohVFBNX1NUQVRFX0lOU1VGRklDSUVOVF9DQVBBQklMSVRZEAMqUwoJRXRjZFN0YXRlEhoKFkVUQ0RfU1RBVEVfVU5TUEVDSUZJRUQQABIRCg1FVENEX1NUQVRFX09LEAESFwoTRVRDRF9TVEFURV9ERUdSQURFRBACKqcBChFGbGVldE1hbmFnZXJTdGF0ZRIjCh9GTEVFVF9NQU5BR0VSX1NUQVRFX1VOU1BFQ0lGSUVEEAASJAogRkxFRVRfTUFOQUdFUl9TVEFURV9OT1RfRU5ST0xMRUQQARIhCh1GTEVFVF9NQU5BR0VSX1NUQVRFX0NPTk5FQ1RFRBACEiQKIEZMRUVUX01BTkFHRVJfU1RBVEVfRElTQ09OTkVDVEVEEANCNFoyZ2l0aHViLmNvbS9DcnlwdE9TLVBLSS9hcGkvZ28vY3J5cHRvcy92MTtjcnlwdG9zdjFiBnByb3RvMw", [file_google_protobuf_timestamp]); + fileDesc("ChdjcnlwdG9zL3YxL3N0YXR1cy5wcm90bxIKY3J5cHRvcy52MSKFBAoKTm9kZVN0YXR1cxIiCgRyb2xlGAEgASgOMhQuY3J5cHRvcy52MS5Ob2RlUm9sZRIxCg5pZGVudGl0eV9zdGF0ZRgCIAEoDjIZLmNyeXB0b3MudjEuSWRlbnRpdHlTdGF0ZRInCgl0cG1fc3RhdGUYAyABKA4yFC5jcnlwdG9zLnYxLlRwbVN0YXRlEikKCmV0Y2Rfc3RhdGUYBCABKA4yFS5jcnlwdG9zLnYxLkV0Y2RTdGF0ZRISCgpib290X2NvdW50GAUgASgEEhgKEHNvZnR3YXJlX3ZlcnNpb24YBiABKAkSNAoNZmxlZXRfbWFuYWdlchgHIAEoDjIdLmNyeXB0b3MudjEuRmxlZXRNYW5hZ2VyU3RhdGUSPQoUcmV2b2NhdGlvbl9wcmVmbGlnaHQYCCABKAsyHy5jcnlwdG9zLnYxLlJldm9jYXRpb25QcmVmbGlnaHQSLAoIcmVzb2x2ZXIYCSABKAsyGi5jcnlwdG9zLnYxLlJlc29sdmVyU3RhdHVzEi0KCXRpbWVfc3luYxgKIAEoCzIaLmNyeXB0b3MudjEuVGltZVN5bmNTdGF0dXMSLQoJcHJvdG9jb2xzGAsgAygLMhouY3J5cHRvcy52MS5Qcm90b2NvbFN0YXR1cxIdChVjb25maWdfcmVib290X3BlbmRpbmcYDCABKAgifAoOUHJvdG9jb2xTdGF0dXMSLQoIcHJvdG9jb2wYASABKA4yGy5jcnlwdG9zLnYxLlNlcnZpY2VQcm90b2NvbBISCgpjb25maWd1cmVkGAIgASgIEg8KB3J1bm5pbmcYAyABKAgSFgoOcmVib290X3BlbmRpbmcYBCABKAgioAEKE1Jldm9jYXRpb25QcmVmbGlnaHQSMwoFc3RhdGUYASABKA4yJC5jcnlwdG9zLnYxLlJldm9jYXRpb25QcmVmbGlnaHRTdGF0ZRIQCghiYXNlX3VybBgCIAEoCRISCgpsYXN0X2Vycm9yGAMgASgJEi4KCmNoZWNrZWRfYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wImEKDlJlc29sdmVyU3RhdHVzEioKBnNvdXJjZRgBIAEoDjIaLmNyeXB0b3MudjEuUmVzb2x2ZXJTb3VyY2USEwoLbmFtZXNlcnZlcnMYAiADKAkSDgoGc2VhcmNoGAMgAygJIqUCCg5UaW1lU3luY1N0YXR1cxIoCgVzdGF0ZRgBIAEoDjIZLmNyeXB0b3MudjEuVGltZVN5bmNTdGF0ZRImCgZzb3VyY2UYAiABKA4yFi5jcnlwdG9zLnYxLlRpbWVTb3VyY2USDwoHc2VydmVycxgDIAMoCRITCgtsYXN0X3NlcnZlchgEIAEoCRIuCgtsYXN0X29mZnNldBgFIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhIPCgdzdHJhdHVtGAYgASgNEi0KCWxhc3Rfc3luYxgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFwoPc3RlcHBlZF9hdF9ib290GAggASgIEhIKCmxhc3RfZXJyb3IYCSABKAkqxgEKD1NlcnZpY2VQcm90b2NvbBIgChxTRVJWSUNFX1BST1RPQ09MX1VOU1BFQ0lGSUVEEAASGQoVU0VSVklDRV9QUk9UT0NPTF9BQ01FEAESGAoUU0VSVklDRV9QUk9UT0NPTF9FU1QQAhIZChVTRVJWSUNFX1BST1RPQ09MX1NDRVAQAxIYChRTRVJWSUNFX1BST1RPQ09MX1RTQRAEEicKI1NFUlZJQ0VfUFJPVE9DT0xfV0lORE9XU19FTlJPTExNRU5UEAUq6AEKGFJldm9jYXRpb25QcmVmbGlnaHRTdGF0ZRIqCiZSRVZPQ0FUSU9OX1BSRUZMSUdIVF9TVEFURV9VTlNQRUNJRklFRBAAEi0KKVJFVk9DQVRJT05fUFJFRkxJR0hUX1NUQVRFX05PVF9DT05GSUdVUkVEEAESJgoiUkVWT0NBVElPTl9QUkVGTElHSFRfU1RBVEVfUEVORElORxACEiEKHVJFVk9DQVRJT05fUFJFRkxJR0hUX1NUQVRFX09LEAMSJgoiUkVWT0NBVElPTl9QUkVGTElHSFRfU1RBVEVfRkFJTElORxAEKo8BCg5SZXNvbHZlclNvdXJjZRIfChtSRVNPTFZFUl9TT1VSQ0VfVU5TUEVDSUZJRUQQABIYChRSRVNPTFZFUl9TT1VSQ0VfTk9ORRABEiIKHlJFU09MVkVSX1NPVVJDRV9NQUNISU5FX0NPTkZJRxACEh4KGlJFU09MVkVSX1NPVVJDRV9ESENQX0xFQVNFEAMqqwEKDVRpbWVTeW5jU3RhdGUSHwobVElNRV9TWU5DX1NUQVRFX1VOU1BFQ0lGSUVEEAASIgoeVElNRV9TWU5DX1NUQVRFX05PVF9DT05GSUdVUkVEEAESGwoXVElNRV9TWU5DX1NUQVRFX1BFTkRJTkcQAhIaChZUSU1FX1NZTkNfU1RBVEVfU1lOQ0VEEAMSHAoYVElNRV9TWU5DX1NUQVRFX1VOU1lOQ0VEEAQqewoKVGltZVNvdXJjZRIbChdUSU1FX1NPVVJDRV9VTlNQRUNJRklFRBAAEhQKEFRJTUVfU09VUkNFX05PTkUQARIeChpUSU1FX1NPVVJDRV9NQUNISU5FX0NPTkZJRxACEhoKFlRJTUVfU09VUkNFX0RIQ1BfTEVBU0UQAypsCghOb2RlUm9sZRIZChVOT0RFX1JPTEVfVU5TUEVDSUZJRUQQABISCg5OT0RFX1JPTEVfUk9PVBABEhoKFk5PREVfUk9MRV9JTlRFUk1FRElBVEUQAhIVChFOT0RFX1JPTEVfSVNTVUlORxADKrMBCg1JZGVudGl0eVN0YXRlEh4KGklERU5USVRZX1NUQVRFX1VOU1BFQ0lGSUVEEAASFwoTSURFTlRJVFlfU1RBVEVfTk9ORRABEicKI0lERU5USVRZX1NUQVRFX0NFUkVNT05ZX0lOX1BST0dSRVNTEAISHgoaSURFTlRJVFlfU1RBVEVfRVNUQUJMSVNIRUQQAxIgChxJREVOVElUWV9TVEFURV9BV0FJVElOR19DRVJUEAQqeQoIVHBtU3RhdGUSGQoVVFBNX1NUQVRFX1VOU1BFQ0lGSUVEEAASEAoMVFBNX1NUQVRFX09LEAESGQoVVFBNX1NUQVRFX1VOQVZBSUxBQkxFEAISJQohVFBNX1NUQVRFX0lOU1VGRklDSUVOVF9DQVBBQklMSVRZEAMqUwoJRXRjZFN0YXRlEhoKFkVUQ0RfU1RBVEVfVU5TUEVDSUZJRUQQABIRCg1FVENEX1NUQVRFX09LEAESFwoTRVRDRF9TVEFURV9ERUdSQURFRBACKqcBChFGbGVldE1hbmFnZXJTdGF0ZRIjCh9GTEVFVF9NQU5BR0VSX1NUQVRFX1VOU1BFQ0lGSUVEEAASJAogRkxFRVRfTUFOQUdFUl9TVEFURV9OT1RfRU5ST0xMRUQQARIhCh1GTEVFVF9NQU5BR0VSX1NUQVRFX0NPTk5FQ1RFRBACEiQKIEZMRUVUX01BTkFHRVJfU1RBVEVfRElTQ09OTkVDVEVEEANCNFoyZ2l0aHViLmNvbS9DcnlwdE9TLVBLSS9hcGkvZ28vY3J5cHRvcy92MTtjcnlwdG9zdjFiBnByb3RvMw", [file_google_protobuf_duration, file_google_protobuf_timestamp]); /** * NodeStatus is the response from GetStatus. @@ -71,6 +71,34 @@ export type NodeStatus = Message<"cryptos.v1.NodeStatus"> & { * @generated from field: cryptos.v1.ResolverStatus resolver = 9; */ resolver?: ResolverStatus | undefined; + + /** + * time_sync is the state of the node's SNTP clock synchronisation. Unset in + * maintenance mode. + * + * @generated from field: cryptos.v1.TimeSyncStatus time_sync = 10; + */ + timeSync?: TimeSyncStatus | undefined; + + /** + * protocols reports every enrolment protocol the node knows, configured in + * the stored machine config against running this boot. Unset in maintenance + * mode. + * + * @generated from field: repeated cryptos.v1.ProtocolStatus protocols = 11; + */ + protocols: ProtocolStatus[]; + + /** + * config_reboot_pending is true when the stored machine config differs from + * the one this boot started from in a field that takes effect only at boot: + * an ApplyConfig answered requires_reboot=true and the node has not rebooted + * since. It covers every reboot-required field, not only the protocol + * blocks. False in maintenance mode. + * + * @generated from field: bool config_reboot_pending = 12; + */ + configRebootPending: boolean; }; /** @@ -80,6 +108,51 @@ export type NodeStatus = Message<"cryptos.v1.NodeStatus"> & { export const NodeStatusSchema: GenMessage = /*@__PURE__*/ messageDesc(file_cryptos_v1_status, 0); +/** + * ProtocolStatus is one enrolment protocol's switch state. A protocol switch + * is reboot-required, so configured and running can differ until the next + * boot. + * + * @generated from message cryptos.v1.ProtocolStatus + */ +export type ProtocolStatus = Message<"cryptos.v1.ProtocolStatus"> & { + /** + * @generated from field: cryptos.v1.ServiceProtocol protocol = 1; + */ + protocol: ServiceProtocol; + + /** + * configured is true when the stored machine config has the protocol's block + * with enabled=true. + * + * @generated from field: bool configured = 2; + */ + configured: boolean; + + /** + * running is true when the protocol's listener started at this boot. + * + * @generated from field: bool running = 3; + */ + running: boolean; + + /** + * reboot_pending is true when the stored block differs from the one the + * running listener started from: switched on or off, or with changed + * settings. The next boot applies it. + * + * @generated from field: bool reboot_pending = 4; + */ + rebootPending: boolean; +}; + +/** + * Describes the message cryptos.v1.ProtocolStatus. + * Use `create(ProtocolStatusSchema)` to create a new message. + */ +export const ProtocolStatusSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_status, 1); + /** * RevocationPreflight reports whether the configured revocation base URL is * usable: its host resolves and every path stamped into issued certificates @@ -125,7 +198,7 @@ export type RevocationPreflight = Message<"cryptos.v1.RevocationPreflight"> & { * Use `create(RevocationPreflightSchema)` to create a new message. */ export const RevocationPreflightSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_v1_status, 1); + messageDesc(file_cryptos_v1_status, 2); /** * ResolverStatus reports where the node's DNS resolver configuration came from @@ -159,7 +232,142 @@ export type ResolverStatus = Message<"cryptos.v1.ResolverStatus"> & { * Use `create(ResolverStatusSchema)` to create a new message. */ export const ResolverStatusSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cryptos_v1_status, 2); + messageDesc(file_cryptos_v1_status, 3); + +/** + * TimeSyncStatus reports where the node's time comes from and how the latest + * synchronisation went. + * + * @generated from message cryptos.v1.TimeSyncStatus + */ +export type TimeSyncStatus = Message<"cryptos.v1.TimeSyncStatus"> & { + /** + * @generated from field: cryptos.v1.TimeSyncState state = 1; + */ + state: TimeSyncState; + + /** + * @generated from field: cryptos.v1.TimeSource source = 2; + */ + source: TimeSource; + + /** + * servers are the time servers in use, as configured or leased, in order. + * + * @generated from field: repeated string servers = 3; + */ + servers: string[]; + + /** + * last_server is the server whose reply set the clock at the latest good + * sync, as queried (a hostname or an IPv4 literal). Empty until the first + * good sync. + * + * @generated from field: string last_server = 4; + */ + lastServer: string; + + /** + * last_offset is the clock offset measured at the latest good sync: the + * server's time minus the node's. Unset until the first good sync. + * + * @generated from field: google.protobuf.Duration last_offset = 5; + */ + lastOffset?: Duration | undefined; + + /** + * stratum is the stratum of last_server at the latest good sync. Zero until + * the first good sync. + * + * @generated from field: uint32 stratum = 6; + */ + stratum: number; + + /** + * last_sync is when the latest good sync finished. Unset until the first + * good sync. + * + * @generated from field: google.protobuf.Timestamp last_sync = 7; + */ + lastSync?: Timestamp | undefined; + + /** + * stepped_at_boot is true when the boot sync stepped the clock rather than + * slewing it. + * + * @generated from field: bool stepped_at_boot = 8; + */ + steppedAtBoot: boolean; + + /** + * last_error is why the latest sync attempt did not adjust the clock. Empty + * after a good sync. + * + * @generated from field: string last_error = 9; + */ + lastError: string; +}; + +/** + * Describes the message cryptos.v1.TimeSyncStatus. + * Use `create(TimeSyncStatusSchema)` to create a new message. + */ +export const TimeSyncStatusSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_status, 4); + +/** + * ServiceProtocol names an enrolment protocol a node can serve. Values are + * added as protocols gain a Pki block. + * + * @generated from enum cryptos.v1.ServiceProtocol + */ +export enum ServiceProtocol { + /** + * @generated from enum value: SERVICE_PROTOCOL_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * ACME, RFC 8555 (Pki.acme). + * + * @generated from enum value: SERVICE_PROTOCOL_ACME = 1; + */ + ACME = 1, + + /** + * EST, RFC 7030 (Pki.est). + * + * @generated from enum value: SERVICE_PROTOCOL_EST = 2; + */ + EST = 2, + + /** + * SCEP, RFC 8894 (Pki.scep). + * + * @generated from enum value: SERVICE_PROTOCOL_SCEP = 3; + */ + SCEP = 3, + + /** + * RFC 3161 time-stamp authority (Pki.tsa). + * + * @generated from enum value: SERVICE_PROTOCOL_TSA = 4; + */ + TSA = 4, + + /** + * Windows autoenrolment, MS-XCEP and MS-WSTEP (Pki.windows_enrollment). + * + * @generated from enum value: SERVICE_PROTOCOL_WINDOWS_ENROLLMENT = 5; + */ + WINDOWS_ENROLLMENT = 5, +} + +/** + * Describes the enum cryptos.v1.ServiceProtocol. + */ +export const ServiceProtocolSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_cryptos_v1_status, 0); /** * RevocationPreflightState is the outcome of the latest revocation preflight. @@ -201,7 +409,7 @@ export enum RevocationPreflightState { * Describes the enum cryptos.v1.RevocationPreflightState. */ export const RevocationPreflightStateSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_cryptos_v1_status, 0); + enumDesc(file_cryptos_v1_status, 1); /** * ResolverSource names where the node's nameservers came from. @@ -241,7 +449,93 @@ export enum ResolverSource { * Describes the enum cryptos.v1.ResolverSource. */ export const ResolverSourceSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_cryptos_v1_status, 1); + enumDesc(file_cryptos_v1_status, 2); + +/** + * TimeSyncState is the node's clock synchronisation state. + * + * @generated from enum cryptos.v1.TimeSyncState + */ +export enum TimeSyncState { + /** + * @generated from enum value: TIME_SYNC_STATE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * No time source is configured or leased; the node runs on its hardware + * clock and signing is not gated. + * + * @generated from enum value: TIME_SYNC_STATE_NOT_CONFIGURED = 1; + */ + NOT_CONFIGURED = 1, + + /** + * A time source is configured but no sync has finished yet this boot. + * + * @generated from enum value: TIME_SYNC_STATE_PENDING = 2; + */ + PENDING = 2, + + /** + * The latest sync succeeded. + * + * @generated from enum value: TIME_SYNC_STATE_SYNCED = 3; + */ + SYNCED = 3, + + /** + * The latest sync failed or was refused; last_error says why. + * + * @generated from enum value: TIME_SYNC_STATE_UNSYNCED = 4; + */ + UNSYNCED = 4, +} + +/** + * Describes the enum cryptos.v1.TimeSyncState. + */ +export const TimeSyncStateSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_cryptos_v1_status, 3); + +/** + * TimeSource names where the node's time servers came from. + * + * @generated from enum cryptos.v1.TimeSource + */ +export enum TimeSource { + /** + * @generated from enum value: TIME_SOURCE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * Neither the machine config nor the DHCP lease named a time server. + * + * @generated from enum value: TIME_SOURCE_NONE = 1; + */ + NONE = 1, + + /** + * network.ntp_servers in the machine config. + * + * @generated from enum value: TIME_SOURCE_MACHINE_CONFIG = 2; + */ + MACHINE_CONFIG = 2, + + /** + * The kernel DHCP lease (option 42), used when network.ntp_servers is empty. + * + * @generated from enum value: TIME_SOURCE_DHCP_LEASE = 3; + */ + DHCP_LEASE = 3, +} + +/** + * Describes the enum cryptos.v1.TimeSource. + */ +export const TimeSourceSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_cryptos_v1_status, 4); /** * NodeRole is the role this node booted into, from machine config. @@ -274,7 +568,7 @@ export enum NodeRole { * Describes the enum cryptos.v1.NodeRole. */ export const NodeRoleSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_cryptos_v1_status, 2); + enumDesc(file_cryptos_v1_status, 5); /** * IdentityState reflects whether the node has a CA identity yet. @@ -315,7 +609,7 @@ export enum IdentityState { * Describes the enum cryptos.v1.IdentityState. */ export const IdentityStateSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_cryptos_v1_status, 3); + enumDesc(file_cryptos_v1_status, 6); /** * TpmState reports TPM availability and capabilities. @@ -348,7 +642,7 @@ export enum TpmState { * Describes the enum cryptos.v1.TpmState. */ export const TpmStateSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_cryptos_v1_status, 4); + enumDesc(file_cryptos_v1_status, 7); /** * EtcdState reports the embedded datastore's health. @@ -376,7 +670,7 @@ export enum EtcdState { * Describes the enum cryptos.v1.EtcdState. */ export const EtcdStateSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_cryptos_v1_status, 5); + enumDesc(file_cryptos_v1_status, 8); /** * FleetManagerState reports whether the node is connected to a Fleet Manager. @@ -411,5 +705,5 @@ export enum FleetManagerState { * Describes the enum cryptos.v1.FleetManagerState. */ export const FleetManagerStateSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_cryptos_v1_status, 6); + enumDesc(file_cryptos_v1_status, 9); diff --git a/src/gen/fleet/cryptos/v1/tsa_pb.ts b/src/gen/fleet/cryptos/v1/tsa_pb.ts new file mode 100644 index 0000000..e099e83 --- /dev/null +++ b/src/gen/fleet/cryptos/v1/tsa_pb.ts @@ -0,0 +1,106 @@ +// @generated by protoc-gen-es v2.12.1 with parameter "target=ts" +// @generated from file cryptos/v1/tsa.proto (package cryptos.v1, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file cryptos/v1/tsa.proto. + */ +export const file_cryptos_v1_tsa: GenFile = /*@__PURE__*/ + fileDesc("ChRjcnlwdG9zL3YxL3RzYS5wcm90bxIKY3J5cHRvcy52MSK6AQoOVHNhQ2VydGlmaWNhdGUSEAoIY2VydF9wZW0YASABKAkSEgoKc2VyaWFsX2hleBgCIAEoCRISCgpzaGEyNTZfaGV4GAMgASgJEi4KCm5vdF9iZWZvcmUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi0KCW5vdF9hZnRlchgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASDwoHY3VycmVudBgGIAEoCCIcChpMaXN0VHNhQ2VydGlmaWNhdGVzUmVxdWVzdCJPChtMaXN0VHNhQ2VydGlmaWNhdGVzUmVzcG9uc2USMAoMY2VydGlmaWNhdGVzGAEgAygLMhouY3J5cHRvcy52MS5Uc2FDZXJ0aWZpY2F0ZUI0WjJnaXRodWIuY29tL0NyeXB0T1MtUEtJL2FwaS9nby9jcnlwdG9zL3YxO2NyeXB0b3N2MWIGcHJvdG8z", [file_google_protobuf_timestamp]); + +/** + * TsaCertificate is one certificate the node's time-stamp authority has + * signed tokens with (see Tsa in config.proto). + * + * @generated from message cryptos.v1.TsaCertificate + */ +export type TsaCertificate = Message<"cryptos.v1.TsaCertificate"> & { + /** + * cert_pem is the TSA certificate, PEM-encoded. It chains to the node's CA + * certificate (GetIdentity). + * + * @generated from field: string cert_pem = 1; + */ + certPem: string; + + /** + * serial_hex is the certificate's serial number in hex, which a token's + * signer identifier (ESSCertIDv2) names alongside the issuer. + * + * @generated from field: string serial_hex = 2; + */ + serialHex: string; + + /** + * sha256_hex is the lowercase hex SHA-256 of the DER certificate, the hash + * a token's ESSCertIDv2 carries. + * + * @generated from field: string sha256_hex = 3; + */ + sha256Hex: string; + + /** + * @generated from field: google.protobuf.Timestamp not_before = 4; + */ + notBefore?: Timestamp | undefined; + + /** + * @generated from field: google.protobuf.Timestamp not_after = 5; + */ + notAfter?: Timestamp | undefined; + + /** + * current is true for the one certificate new tokens are signed with. + * Every other entry is a past certificate kept for verifying old tokens. + * No entry is current while the TSA is not running. + * + * @generated from field: bool current = 6; + */ + current: boolean; +}; + +/** + * Describes the message cryptos.v1.TsaCertificate. + * Use `create(TsaCertificateSchema)` to create a new message. + */ +export const TsaCertificateSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_tsa, 0); + +/** + * @generated from message cryptos.v1.ListTsaCertificatesRequest + */ +export type ListTsaCertificatesRequest = Message<"cryptos.v1.ListTsaCertificatesRequest"> & { +}; + +/** + * Describes the message cryptos.v1.ListTsaCertificatesRequest. + * Use `create(ListTsaCertificatesRequestSchema)` to create a new message. + */ +export const ListTsaCertificatesRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_tsa, 1); + +/** + * @generated from message cryptos.v1.ListTsaCertificatesResponse + */ +export type ListTsaCertificatesResponse = Message<"cryptos.v1.ListTsaCertificatesResponse"> & { + /** + * certificates are newest first. + * + * @generated from field: repeated cryptos.v1.TsaCertificate certificates = 1; + */ + certificates: TsaCertificate[]; +}; + +/** + * Describes the message cryptos.v1.ListTsaCertificatesResponse. + * Use `create(ListTsaCertificatesResponseSchema)` to create a new message. + */ +export const ListTsaCertificatesResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cryptos_v1_tsa, 2); + diff --git a/src/lib/approvals.test.ts b/src/lib/approvals.test.ts new file mode 100644 index 0000000..4ba23b4 --- /dev/null +++ b/src/lib/approvals.test.ts @@ -0,0 +1,132 @@ +/* +Copyright The CryptOS Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { + __resetApprovals, + APPROVALS_CHANGED, + canDecide, + decideApproval, + listApprovals, +} from "@/lib/approvals"; +import * as clientMod from "@/lib/fleet/client"; +import * as modeMod from "@/lib/fleet/mode"; + +afterEach(() => { + vi.restoreAllMocks(); + __resetApprovals(); +}); + +const wireApproval = { + createdAt: "2026-09-01T00:00:00Z", + decidedAt: "", + decidedByCn: "", + decidedBySerial: "", + expiresAt: "2026-09-01T00:15:00Z", + id: "ap-1", + keyId: "key-1", + requestDigest: "ab".repeat(32), + requestedByCn: "operator@example.org", + requestedBySerial: "0A:BC", + requiredLevel: "operator", + status: "pending", + summary: "Revoke certificate 0a1b on issuing-1", + tool: "cert_revoke", +}; + +describe("canDecide", () => { + it("needs a pending approval and a level at least the required one", () => { + const row = { requiredLevel: "operator", status: "pending" }; + expect(canDecide("viewer", row)).toBe(false); + expect(canDecide("operator", row)).toBe(true); + expect(canDecide("admin", row)).toBe(true); + expect(canDecide("admin", { requiredLevel: "admin", status: "pending" })).toBe(true); + expect(canDecide("operator", { requiredLevel: "admin", status: "pending" })).toBe(false); + expect(canDecide("admin", { ...row, status: "approved" })).toBe(false); + expect(canDecide("admin", { ...row, status: "expired" })).toBe(false); + }); + + it("refuses a required level it does not recognise", () => { + expect(canDecide("admin", { requiredLevel: "root", status: "pending" })).toBe(false); + }); +}); + +describe("approvals (live)", () => { + it("lists through ListApprovals with the status filter", async () => { + vi.spyOn(modeMod, "fleetMode").mockReturnValue("live"); + const listApprovalsRpc = vi.fn().mockResolvedValue({ items: [wireApproval] }); + vi.spyOn(clientMod, "fleetClient").mockReturnValue({ + listApprovals: listApprovalsRpc, + } as unknown as ReturnType); + + const rows = await listApprovals({ status: "pending" }); + + expect(listApprovalsRpc).toHaveBeenCalledWith({ status: "pending" }); + expect(rows).toEqual([wireApproval]); + }); + + it("decides through DecideApproval and announces the change", async () => { + vi.spyOn(modeMod, "fleetMode").mockReturnValue("live"); + const decided = { ...wireApproval, status: "denied" }; + const decideApprovalRpc = vi.fn().mockResolvedValue({ approval: decided }); + vi.spyOn(clientMod, "fleetClient").mockReturnValue({ + decideApproval: decideApprovalRpc, + } as unknown as ReturnType); + const changed = vi.fn(); + globalThis.addEventListener(APPROVALS_CHANGED, changed); + + const row = await decideApproval({ approve: false, id: "ap-1" }); + + globalThis.removeEventListener(APPROVALS_CHANGED, changed); + expect(decideApprovalRpc).toHaveBeenCalledWith({ approve: false, id: "ap-1" }); + expect(row.status).toBe("denied"); + expect(changed).toHaveBeenCalledTimes(1); + }); + + it("fails when the manager returns no approval", async () => { + vi.spyOn(modeMod, "fleetMode").mockReturnValue("live"); + vi.spyOn(clientMod, "fleetClient").mockReturnValue({ + decideApproval: vi.fn().mockResolvedValue({}), + } as unknown as ReturnType); + + await expect(decideApproval({ approve: true, id: "ap-1" })).rejects.toThrow(/no approval/); + }); +}); + +describe("approvals (mock)", () => { + it("filters on status, with an empty status meaning all", async () => { + vi.spyOn(modeMod, "fleetMode").mockReturnValue("mock"); + const all = await listApprovals({ status: "" }); + const pending = await listApprovals({ status: "pending" }); + + expect(pending.length).toBeGreaterThan(0); + expect(pending.every((a) => a.status === "pending")).toBe(true); + expect(all.length).toBeGreaterThan(pending.length); + }); + + it("approves a pending approval, records the decider, and refuses a second decision", async () => { + vi.spyOn(modeMod, "fleetMode").mockReturnValue("mock"); + const [first] = await listApprovals({ status: "pending" }); + + const decided = await decideApproval({ approve: true, id: first.id }); + + expect(decided).toMatchObject({ id: first.id, status: "approved" }); + expect(decided.decidedByCn).not.toBe(""); + expect(decided.decidedAt).not.toBe(""); + await expect(decideApproval({ approve: false, id: first.id })).rejects.toThrow(/not pending/); + }); +}); diff --git a/src/lib/approvals.ts b/src/lib/approvals.ts new file mode 100644 index 0000000..da63b5c --- /dev/null +++ b/src/lib/approvals.ts @@ -0,0 +1,200 @@ +/* +Copyright The CryptOS Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import type { OperatorLevel } from "@/context/auth"; +import type { Approval } from "@/gen/fleet/cryptos/fleet/v1/fleet_pb"; + +import { fleetClient } from "@/lib/fleet/client"; +import { fleetMode } from "@/lib/fleet/mode"; + +// Approvals for MCP step-up requests. An agent's step-up tool call leaves a +// pending approval that only a person presenting an operator certificate can +// decide; the agent's bearer key can neither list nor decide one. + +export interface ApprovalRow { + createdAt: string; + decidedAt: string; + decidedByCn: string; + decidedBySerial: string; + expiresAt: string; + id: string; + keyId: string; + requestDigest: string; + requestedByCn: string; + requestedBySerial: string; + requiredLevel: string; + status: string; + summary: string; + tool: string; +} + +export type ApprovalStatus = "approved" | "denied" | "expired" | "pending" | "used"; + +// The nav badge and any open Approvals page listen for this, so a decision +// made in one place recounts everywhere without polling faster. +export const APPROVALS_CHANGED = "cryptos:approvals-changed"; + +const LEVELS: string[] = ["viewer", "operator", "admin"]; + +// canDecide mirrors the manager's rule: only a pending approval, and only by +// an operator whose level is at least the one the tool requires. +export const canDecide = ( + level: OperatorLevel, + approval: Pick, +): boolean => { + const required = LEVELS.indexOf(approval.requiredLevel); + return approval.status === "pending" && required >= 0 && LEVELS.indexOf(level) >= required; +}; + +const toRow = (a: Approval): ApprovalRow => ({ + createdAt: a.createdAt, + decidedAt: a.decidedAt, + decidedByCn: a.decidedByCn, + decidedBySerial: a.decidedBySerial, + expiresAt: a.expiresAt, + id: a.id, + keyId: a.keyId, + requestDigest: a.requestDigest, + requestedByCn: a.requestedByCn, + requestedBySerial: a.requestedBySerial, + requiredLevel: a.requiredLevel, + status: a.status, + summary: a.summary, + tool: a.tool, +}); + +const MOCK_SERIAL = "3A:7F:0C:91:D2:44:8B:1E"; + +const seed = (): ApprovalRow[] => [ + { + createdAt: "2026-09-28T09:40:00Z", + decidedAt: "", + decidedByCn: "", + decidedBySerial: "", + expiresAt: "2026-09-28T09:55:00Z", + id: "ap-0004", + keyId: "mk-0003", + requestDigest: "3f6c1a9e0b7d24c58e1f09a6b3d2c47e5a8b0f1d6c3e9a27b4f08d15e6c2a9b3", + requestedByCn: "operator@example.org", + requestedBySerial: MOCK_SERIAL, + requiredLevel: "operator", + status: "pending", + summary: "Revoke certificate 4f:2a:91 on issuing-1 (keyCompromise)", + tool: "cert_revoke", + }, + { + createdAt: "2026-09-28T09:12:00Z", + decidedAt: "", + decidedByCn: "", + decidedBySerial: "", + expiresAt: "2026-09-28T09:27:00Z", + id: "ap-0003", + keyId: "mk-0002", + requestDigest: "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90", + requestedByCn: "auditor@example.org", + requestedBySerial: "11:22:33:44:55:66", + requiredLevel: "admin", + status: "pending", + summary: "Decommission node issuing-2", + tool: "node_decommission", + }, + { + createdAt: "2026-09-27T16:05:00Z", + decidedAt: "2026-09-27T16:07:00Z", + decidedByCn: "operator@example.org", + decidedBySerial: MOCK_SERIAL, + expiresAt: "2026-09-27T16:20:00Z", + id: "ap-0002", + keyId: "mk-0003", + requestDigest: "0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0", + requestedByCn: "operator@example.org", + requestedBySerial: MOCK_SERIAL, + requiredLevel: "operator", + status: "used", + summary: "Issue a leaf for svc.example.org from a CSR on issuing-1", + tool: "cert_issue_from_csr", + }, + { + createdAt: "2026-09-26T11:00:00Z", + decidedAt: "", + decidedByCn: "", + decidedBySerial: "", + expiresAt: "2026-09-26T11:15:00Z", + id: "ap-0001", + keyId: "mk-0002", + requestDigest: "9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e8d", + requestedByCn: "auditor@example.org", + requestedBySerial: "11:22:33:44:55:66", + requiredLevel: "admin", + status: "expired", + summary: "Apply config revision 12 to issuing-2", + tool: "config_apply", + }, +]; + +let mockRows = seed(); + +const announce = (): void => { + globalThis.dispatchEvent(new Event(APPROVALS_CHANGED)); +}; + +// listApprovals returns approvals newest first. An empty status means every +// status. +export const listApprovals = async ({ status }: { status: string }): Promise => { + if (fleetMode() === "mock") { + return mockRows.filter((r) => status === "" || r.status === status).map((r) => ({ ...r })); + } + const response = await fleetClient().listApprovals({ status }); + return response.items.map(toRow); +}; + +// decideApproval approves or denies one approval and returns it as the manager +// now holds it. +export const decideApproval = async ({ + approve, + id, +}: { + approve: boolean; + id: string; +}): Promise => { + if (fleetMode() === "mock") { + const current = mockRows.find((r) => r.id === id); + if (current?.status !== "pending") { + throw new Error(`Approval ${id} is not pending.`); + } + const decided: ApprovalRow = { + ...current, + decidedAt: new Date().toISOString(), + decidedByCn: "operator@example.org", + decidedBySerial: MOCK_SERIAL, + status: approve ? "approved" : "denied", + }; + mockRows = mockRows.map((r) => (r.id === id ? decided : r)); + announce(); + return { ...decided }; + } + const response = await fleetClient().decideApproval({ approve, id }); + if (!response.approval) { + throw new Error("The manager returned no approval."); + } + announce(); + return toRow(response.approval); +}; + +// Test-only: restore the seeded mock set between tests. +export const __resetApprovals = (): void => { + mockRows = seed(); +}; diff --git a/src/lib/wire-fields.test.ts b/src/lib/wire-fields.test.ts index 9cd6faf..ffe658b 100644 --- a/src/lib/wire-fields.test.ts +++ b/src/lib/wire-fields.test.ts @@ -18,6 +18,7 @@ import { type DescMessage, fromJson, type JsonValue, toJson } from "@bufbuild/pr import { describe, expect, it } from "vitest"; import { + ApprovalSchema, AuditEventSchema as FleetAuditEventSchema, McpKeySchema, } from "@/gen/fleet/cryptos/fleet/v1/fleet_pb"; @@ -107,6 +108,26 @@ describe("generated stubs keep every field the node sends", () => { expect(roundTrip(McpKeySchema, wire)).toEqual(wire); }); + it("keeps every approval field", () => { + const wire = { + createdAt: "2026-09-01T00:00:00Z", + decidedAt: "2026-09-01T00:05:00Z", + decidedByCn: "admin@example.org", + decidedBySerial: "0D:EF", + expiresAt: "2026-09-01T00:15:00Z", + id: "ap-1", + keyId: "key-1", + requestDigest: "ab".repeat(32), + requestedByCn: "operator@example.org", + requestedBySerial: "0A:BC:DE", + requiredLevel: "admin", + status: "approved", + summary: "Decommission node issuing-2", + tool: "node_decommission", + }; + expect(roundTrip(ApprovalSchema, wire)).toEqual(wire); + }); + it("keeps the revocation preflight and resolver status", () => { const wire = { resolver: { diff --git a/src/pages/approvals.test.tsx b/src/pages/approvals.test.tsx new file mode 100644 index 0000000..196ca9e --- /dev/null +++ b/src/pages/approvals.test.tsx @@ -0,0 +1,247 @@ +/* +Copyright The CryptOS Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import type { ReactNode } from "react"; + +import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { ApprovalsPage } from "@/pages/approvals"; + +const useAuth = vi.fn(); +vi.mock("@/context/auth", () => ({ useAuth: () => useAuth() })); + +const listApprovals = vi.fn(); +vi.mock("@/lib/approvals", async (importOriginal) => ({ + ...(await importOriginal()), + listApprovals: (...a: unknown[]) => listApprovals(...a), +})); + +// The dialog is covered by its own test; stub it so the page test stays on +// listing, filtering, gating and the deep link. +vi.mock("@/components/approval-decide-dialog", () => ({ + ApprovalDecideDialog: ({ + approve, + target, + }: { + approve: boolean; + target: { id: string }; + }): ReactNode => ( +
+ decide-dialog:{approve ? "approve" : "deny"}:{target.id} +
+ ), +})); + +const base = { + createdAt: "2026-09-01T10:00:00Z", + decidedAt: "", + decidedByCn: "", + decidedBySerial: "", + expiresAt: "2026-09-01T10:15:00Z", + keyId: "key-1", + requestDigest: "ab".repeat(32), + requestedByCn: "operator@example.org", + requestedBySerial: "0A:BC", +}; +const revoke = { + ...base, + id: "ap-1", + requiredLevel: "operator", + status: "pending", + summary: "Revoke certificate 0a1b on issuing-1", + tool: "cert_revoke", +}; +const decommission = { + ...base, + id: "ap-2", + keyId: "key-2", + requestedByCn: "second@example.org", + requiredLevel: "admin", + status: "pending", + summary: "Decommission node issuing-2", + tool: "node_decommission", +}; +const approved = { + ...base, + decidedAt: "2026-08-30T09:05:00Z", + decidedByCn: "admin@example.org", + id: "ap-0", + requiredLevel: "operator", + status: "approved", + summary: "Issue a leaf for svc.example.org", + tool: "cert_issue_from_csr", +}; + +const renderAt = (path = "/approvals") => + render( + + + , + ); + +const rowOf = (text: string): HTMLElement => { + const row = screen.getByText(text).closest("tr"); + if (!row) throw new Error(`no row for ${text}`); + return row; +}; + +describe("ApprovalsPage", () => { + beforeEach(() => { + listApprovals.mockReset(); + listApprovals.mockImplementation(({ status }: { status: string }) => + Promise.resolve( + [revoke, decommission, approved].filter((a) => status === "" || a.status === status), + ), + ); + }); + + it("lists pending approvals by default with every column", async () => { + useAuth.mockReturnValue({ operator: { level: "admin" } }); + renderAt(); + + expect(await screen.findByText("Revoke certificate 0a1b on issuing-1")).toBeInTheDocument(); + expect(listApprovals).toHaveBeenCalledWith({ status: "pending" }); + const row = rowOf("Revoke certificate 0a1b on issuing-1"); + expect(row).toHaveTextContent("cert_revoke"); + expect(row).toHaveTextContent("operator@example.org"); + expect(row).toHaveTextContent("key-1"); + expect(row).toHaveTextContent("operator"); + expect(row).toHaveTextContent("2026-09-01 10:00"); + expect(row).toHaveTextContent("2026-09-01 10:15"); + expect(row).toHaveTextContent("pending"); + expect(screen.getByText("Decommission node issuing-2")).toBeInTheDocument(); + expect(screen.queryByText("Issue a leaf for svc.example.org")).not.toBeInTheDocument(); + }); + + it("tells the approver to check the approval ID and summary, not the digest", async () => { + useAuth.mockReturnValue({ operator: { level: "admin" } }); + renderAt(); + + const note = await screen.findByRole("note"); + expect(note).toHaveTextContent( + "check that the approval ID and summary in the confirmation match what the agent showed you", + ); + expect(note).not.toHaveTextContent(/digest/i); + }); + + it("switches the filter to every status", async () => { + useAuth.mockReturnValue({ operator: { level: "admin" } }); + renderAt(); + await screen.findByText("Revoke certificate 0a1b on issuing-1"); + + fireEvent.change(screen.getByLabelText(/status/i), { target: { value: "" } }); + + expect(await screen.findByText("Issue a leaf for svc.example.org")).toBeInTheDocument(); + expect(listApprovals).toHaveBeenLastCalledWith({ status: "" }); + expect(rowOf("Issue a leaf for svc.example.org")).toHaveTextContent("admin@example.org"); + }); + + it("opens the approve confirmation for that approval", async () => { + useAuth.mockReturnValue({ operator: { level: "operator" } }); + renderAt(); + await screen.findByText("Revoke certificate 0a1b on issuing-1"); + + fireEvent.click( + within(rowOf("Revoke certificate 0a1b on issuing-1")).getByRole("button", { + name: /approve/i, + }), + ); + + expect(screen.getByText("decide-dialog:approve:ap-1")).toBeInTheDocument(); + }); + + it("opens the deny confirmation for that approval", async () => { + useAuth.mockReturnValue({ operator: { level: "admin" } }); + renderAt(); + await screen.findByText("Decommission node issuing-2"); + + fireEvent.click( + within(rowOf("Decommission node issuing-2")).getByRole("button", { name: /deny/i }), + ); + + expect(screen.getByText("decide-dialog:deny:ap-2")).toBeInTheDocument(); + }); + + it("disables the buttons below the required level", async () => { + useAuth.mockReturnValue({ operator: { level: "operator" } }); + renderAt(); + await screen.findByText("Decommission node issuing-2"); + + const high = within(rowOf("Decommission node issuing-2")); + expect(high.getByRole("button", { name: /approve/i })).toBeDisabled(); + expect(high.getByRole("button", { name: /deny/i })).toBeDisabled(); + const low = within(rowOf("Revoke certificate 0a1b on issuing-1")); + expect(low.getByRole("button", { name: /approve/i })).toBeEnabled(); + expect(low.getByRole("button", { name: /deny/i })).toBeEnabled(); + }); + + it("disables every decision for a viewer", async () => { + useAuth.mockReturnValue({ operator: { level: "viewer" } }); + renderAt(); + await screen.findByText("Revoke certificate 0a1b on issuing-1"); + + for (const button of screen.getAllByRole("button", { name: /approve|deny/i })) { + expect(button).toBeDisabled(); + } + }); + + it("disables the buttons once an approval is no longer pending", async () => { + useAuth.mockReturnValue({ operator: { level: "admin" } }); + renderAt(); + await screen.findByText("Revoke certificate 0a1b on issuing-1"); + fireEvent.change(screen.getByLabelText(/status/i), { target: { value: "" } }); + await screen.findByText("Issue a leaf for svc.example.org"); + + const done = within(rowOf("Issue a leaf for svc.example.org")); + expect(done.getByRole("button", { name: /approve/i })).toBeDisabled(); + expect(done.getByRole("button", { name: /deny/i })).toBeDisabled(); + }); + + it("focuses the approval a deep link names, whatever its status", async () => { + useAuth.mockReturnValue({ operator: { level: "admin" } }); + renderAt("/approvals?id=ap-0"); + + expect(await screen.findByText("Issue a leaf for svc.example.org")).toBeInTheDocument(); + expect(listApprovals).toHaveBeenCalledWith({ status: "" }); + expect(rowOf("Issue a leaf for svc.example.org")).toHaveAttribute("aria-current", "true"); + expect(rowOf("Revoke certificate 0a1b on issuing-1")).not.toHaveAttribute("aria-current"); + }); + + it("says so when the deep-linked approval is not there", async () => { + useAuth.mockReturnValue({ operator: { level: "admin" } }); + renderAt("/approvals?id=ap-404"); + + expect(await screen.findByText(/ap-404 was not found/i)).toBeInTheDocument(); + }); + + it("explains the empty state", async () => { + listApprovals.mockResolvedValue([]); + useAuth.mockReturnValue({ operator: { level: "admin" } }); + renderAt(); + + await waitFor(() => expect(screen.getByText(/no approvals/i)).toBeInTheDocument()); + }); + + it("shows a load failure", async () => { + listApprovals.mockRejectedValue(new Error("manager unreachable")); + useAuth.mockReturnValue({ operator: { level: "admin" } }); + renderAt(); + + expect(await screen.findByRole("alert")).toHaveTextContent("manager unreachable"); + }); +}); diff --git a/src/pages/approvals.tsx b/src/pages/approvals.tsx new file mode 100644 index 0000000..8091f73 --- /dev/null +++ b/src/pages/approvals.tsx @@ -0,0 +1,231 @@ +/* +Copyright The CryptOS Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useCallback, useEffect, useState } from "react"; +import { useSearchParams } from "react-router-dom"; + +import { ApprovalDecideDialog } from "@/components/approval-decide-dialog"; +import { useAuth } from "@/context/auth"; +import { type ApprovalRow, type ApprovalStatus, canDecide, listApprovals } from "@/lib/approvals"; +import { cn } from "@/lib/utils"; + +const th = + "px-3 py-2 text-left font-mono text-[11px] uppercase tracking-wider text-muted-foreground"; +const td = "px-3 py-2 font-mono text-xs"; + +const when = (ts: string): string => (ts ? ts.replace("T", " ").slice(0, 16) : "-"); + +const STATUSES: ApprovalStatus[] = ["pending", "approved", "denied", "expired", "used"]; + +const statusTone: Record = { + approved: "text-success", + denied: "text-destructive", + pending: "text-warning", +}; + +const COLUMNS = 9; + +// ApprovalsPage is where a person decides the step-up requests MCP agents +// raise. The manager's approve_url links here with ?id=, so a deep +// link shows every status: the approval may already be decided or expired by +// the time the operator opens it. +export const ApprovalsPage = () => { + const { operator } = useAuth(); + const level = operator?.level ?? "viewer"; + const [params] = useSearchParams(); + const focusId = params.get("id") ?? ""; + + const [status, setStatus] = useState(focusId ? "" : "pending"); + const [rows, setRows] = useState([]); + const [loaded, setLoaded] = useState(false); + const [loadError, setLoadError] = useState(""); + const [decision, setDecision] = useState<{ approve: boolean; target: ApprovalRow } | null>(null); + + const load = useCallback(async () => { + setLoadError(""); + try { + setRows(await listApprovals({ status })); + } catch (error_: unknown) { + setLoadError(error_ instanceof Error ? error_.message : "Failed to load approvals"); + } finally { + setLoaded(true); + } + }, [status]); + + useEffect(() => { + void load(); + }, [load]); + + const focusRef = useCallback((row: HTMLTableRowElement | null) => { + // jsdom and some older engines lack scrollIntoView. + row?.scrollIntoView?.({ block: "center" }); + }, []); + + const focusMissing = + focusId !== "" && status === "" && loaded && !loadError && !rows.some((r) => r.id === focusId); + const pendingCount = rows.filter((r) => r.status === "pending").length; + + const gateReason = (r: ApprovalRow): string => { + if (r.status !== "pending") return `Already ${r.status}`; + return canDecide(level, r) ? "" : `Needs ${r.requiredLevel} level; you are ${level}`; + }; + + return ( +
+
+
+

Approvals

+

+ {pendingCount} pending of {rows.length} shown +

+
+ +
+ +

+ An agent asks for approval before a tool that changes the fleet runs. Approving lets that + exact request through once; check that the approval ID and summary in the confirmation match + what the agent showed you. You can decide only requests at or below your own level. +

+ + {loadError ? ( +

+ {loadError} +

+ ) : null} + {focusMissing ? ( +

Approval {focusId} was not found.

+ ) : null} + +
+ + + + + + + + + + + + + + + {rows.length === 0 ? ( + + + + ) : ( + rows.map((r) => { + const focused = r.id === focusId; + const reason = gateReason(r); + return ( + + + + + + + + + + + + ); + }) + )} + +
ToolSummaryRequested byKeyRequiredCreatedExpiresStatus +
+

+ {status === "pending" ? "No approvals are waiting." : "No approvals match."} +

+

+ A request appears here when an agent calls a tool that needs a person to approve + it. +

+
{r.tool}{r.summary}{r.requestedByCn}{r.keyId || "-"}{r.requiredLevel}{when(r.createdAt)}{when(r.expiresAt)} + + {r.status} + + {r.decidedByCn ? ( + + by {r.decidedByCn} + + ) : null} + + + + + +
+
+ + {decision ? ( + setDecision(null)} + onDecided={() => void load()} + target={decision.target} + /> + ) : null} +
+ ); +};