From f41719510ddf31ae8694966436ef5d77a9cdd0c5 Mon Sep 17 00:00:00 2001
From: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
Date: Wed, 30 Sep 2026 19:59:06 -0400
Subject: [PATCH] feat(operators): credential requests, complete, deny and a
client-side request page
The manager no longer signs operator credentials (IssueOperatorCredential
is gone), so the Operators page could not issue anything. It now follows
the external operator CA model:
- Request credential: a P-384 key and CSR made in the browser with a
mandatory encrypted key backup and a passphrase shown once, or a CSR
the holder made. Shows the CSR, the extension section and the
openssl ca command the manager returns.
- Pending requests tab with cancel, and Complete: record the signed
certificate and, when the browser can open the key backup, build the
PKCS#12 with the same passphrase.
- Deny at the Fleet Manager replaces revoke: RFC 5280 reasons 0-10
except 7, a note, and the reminder to revoke at the CA as well.
- Kind, issuer, denylisted, CRL-revoked and last-seen columns.
- An anonymous /request-credential page that makes the key, backup and
CSR, and later the PKCS#12, with no network calls.
- Messages for every 1600-1611 code and sub-reason.
The TypeScript stubs are refreshed to api main.
Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
---
AGENTS.md | 13 +-
README.md | 9 +
src/App.test.tsx | 9 +
src/App.tsx | 2 +
src/components/copy-block.tsx | 66 +
.../credential-complete-dialog.test.tsx | 238 +++
src/components/credential-complete-dialog.tsx | 312 ++++
.../credential-request-wizard.test.tsx | 188 +++
src/components/credential-request-wizard.tsx | 279 ++++
src/components/dialog-frame.tsx | 47 +
src/components/key-backup-step.test.tsx | 88 ++
src/components/key-backup-step.tsx | 170 +++
src/components/layout/auth-gate.test.tsx | 9 +
src/components/layout/auth-gate.tsx | 3 +
src/components/operator-deny-dialog.test.tsx | 93 ++
src/components/operator-deny-dialog.tsx | 148 ++
src/components/operator-issue-dialog.test.tsx | 107 --
src/components/operator-issue-dialog.tsx | 241 ---
src/components/operator-revoke-dialog.tsx | 117 --
.../fleet/cryptos/fleet/v1/bootstrap_pb.ts | 450 ++++++
src/gen/fleet/cryptos/fleet/v1/errors_pb.ts | 418 ++++++
src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts | 1305 ++++++++++++++---
.../fleet/cryptos/fleet/v1/operator_ca_pb.ts | 429 ++++++
src/gen/fleet/cryptos/v1/audit_pb.ts | 203 ++-
src/gen/fleet/cryptos/v1/node_pb.ts | 135 +-
src/lib/crypto/key-backup.test.ts | 203 +++
src/lib/crypto/key-backup.ts | 126 ++
src/lib/crypto/pkcs12.test.ts | 29 +
src/lib/crypto/pkcs12.ts | 28 +-
src/lib/download.ts | 41 +
src/lib/fingerprint.ts | 20 +
src/lib/fleet/error-code.ts | 22 +
src/lib/fleet/error-copy.test.ts | 132 ++
src/lib/fleet/error-copy.ts | 192 +++
src/lib/form.ts | 21 +
src/lib/operators.test.ts | 273 +++-
src/lib/operators.ts | 483 ++++--
src/lib/wire-fields.test.ts | 69 +
src/pages/credential-request.test.tsx | 119 ++
src/pages/credential-request.tsx | 260 ++++
src/pages/operators.test.tsx | 290 ++--
src/pages/operators.tsx | 351 +++--
42 files changed, 6717 insertions(+), 1021 deletions(-)
create mode 100644 src/components/copy-block.tsx
create mode 100644 src/components/credential-complete-dialog.test.tsx
create mode 100644 src/components/credential-complete-dialog.tsx
create mode 100644 src/components/credential-request-wizard.test.tsx
create mode 100644 src/components/credential-request-wizard.tsx
create mode 100644 src/components/dialog-frame.tsx
create mode 100644 src/components/key-backup-step.test.tsx
create mode 100644 src/components/key-backup-step.tsx
create mode 100644 src/components/operator-deny-dialog.test.tsx
create mode 100644 src/components/operator-deny-dialog.tsx
delete mode 100644 src/components/operator-issue-dialog.test.tsx
delete mode 100644 src/components/operator-issue-dialog.tsx
delete mode 100644 src/components/operator-revoke-dialog.tsx
create mode 100644 src/gen/fleet/cryptos/fleet/v1/bootstrap_pb.ts
create mode 100644 src/gen/fleet/cryptos/fleet/v1/errors_pb.ts
create mode 100644 src/gen/fleet/cryptos/fleet/v1/operator_ca_pb.ts
create mode 100644 src/lib/crypto/key-backup.test.ts
create mode 100644 src/lib/crypto/key-backup.ts
create mode 100644 src/lib/download.ts
create mode 100644 src/lib/fingerprint.ts
create mode 100644 src/lib/fleet/error-copy.test.ts
create mode 100644 src/lib/fleet/error-copy.ts
create mode 100644 src/lib/form.ts
create mode 100644 src/pages/credential-request.test.tsx
create mode 100644 src/pages/credential-request.tsx
diff --git a/AGENTS.md b/AGENTS.md
index 27d2b76..91fa359 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -21,6 +21,8 @@ woff2, strict CSP, no runtime CDN) for air-gap use.
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.
+ The "Make a credential request" page `/request-credential` is outside the gate too, and makes
+ no network calls at all: it must not import the Fleet Manager client.
- `src/components/layout/` - app shell: header, sidebar nav, wordmark, theme toggle, auth gate.
- `src/components/ui/` - shadcn/ui primitives (button, card, badge, separator).
- `src/context/` - `theme.tsx` (dark/light, persisted) and `auth.tsx` (browser-mTLS gate stub).
@@ -48,8 +50,15 @@ woff2, strict CSP, no runtime CDN) for air-gap use.
- The manager puts a stable numeric code on every error it returns (metadata key
`x-cryptos-error-code`; the table is the manager's `docs/error-codes.md`). Branch on it with
`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.
+ its `ErrorCode` map when a page handles one. The Operators page does this for 1400 (no operator
+ CA configured) and shows a not-configured view instead of the refusal. The 1600-1611 block
+ (first run, operator CAs, operator credentials) also carries a sub-reason
+ (`x-cryptos-error-reason`, read with `errorReason()`); `src/lib/fleet/error-copy.ts` maps every
+ code and sub-reason to a message, and `fleetErrorMessage()` is what those screens show.
+- Operator private keys never leave the browser. `src/lib/crypto/key-backup.ts` writes the
+ encrypted key backup (PBES2 PKCS#8), reads it back, and builds the PKCS#12 with the backup's
+ own passphrase; the credential wizards hold only the encrypted backup in memory. Tests assert
+ on the request bodies that neither the key nor the passphrase is ever sent.
- 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
diff --git a/README.md b/README.md
index ce47492..e1d9b09 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,15 @@ The same bundle adapts at runtime based on the role of the node being viewed:
- š **Intermediate / Issuing nodes** ā issuance profiles, certificate inventory, CSR review, CRL / OCSP status, adapter health, audit log tail.
- šļø **All nodes** ā live status, configuration view (read-only when the node is linked to FM, which is the normal mode).
+## šŖŖ Operator credentials
+
+Operator certificates come from your own external operator CA (an offline OpenSSL CA or an enterprise CA). The Fleet Manager never signs one; it records them and can deny them.
+
+- š **Request credential** (Operators page, admin) makes a P-384 key and CSR in the browser, with a mandatory encrypted key backup whose passphrase is shown once, or takes a CSR the holder made. It returns the CSR, the OpenSSL extension section for the level and the `openssl ca` command for the CA operator.
+- ā **Complete** records the signed certificate against its request and, when this browser can open the key backup, builds the PKCS#12 locally with the same passphrase. **Record certificate** imports one made entirely at the CA.
+- ā **Deny at the Fleet Manager** puts a credential on the Fleet Manager's denylist, with an RFC 5280 reason and a note. It doesn't revoke at the CA, and says so.
+- š **Make a credential request** at `/request-credential` is an anonymous page where a future operator makes their own key, key backup and CSR, and later builds their PKCS#12. It makes no network calls.
+
## š¤ Agent access (MCP)
The Fleet Manager serves an MCP endpoint so AI agents can manage the fleet. An agent never gets more than the operator who let it in, and this UI is where that consent happens:
diff --git a/src/App.test.tsx b/src/App.test.tsx
index aa78bc4..be07b05 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -41,6 +41,15 @@ describe("App routing", () => {
expect(screen.queryByRole("button", { name: /log in/i })).not.toBeInTheDocument();
});
+ // Someone without a credential yet makes their key and CSR here, so it can't
+ // sit behind the gate that needs one.
+ it("serves the credential request page without logging in first", () => {
+ renderAt("/request-credential");
+
+ expect(screen.getByRole("heading", { name: /make a credential request/i })).toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: /log in/i })).not.toBeInTheDocument();
+ });
+
it("keeps the console behind the login gate", () => {
renderAt("/agent-keys");
diff --git a/src/App.tsx b/src/App.tsx
index d213ef4..6015c5a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -23,6 +23,7 @@ import { AgentKeysPage } from "@/pages/agent-keys";
import { ApprovalsPage } from "@/pages/approvals";
import { AuditPage } from "@/pages/audit";
import { CertificatesPage } from "@/pages/certificates";
+import { CredentialRequestPage } from "@/pages/credential-request";
import { DashboardPage } from "@/pages/dashboard";
import { EnrollmentPage } from "@/pages/enrollment";
import { EnrollmentDetailPage } from "@/pages/enrollment-detail";
@@ -49,6 +50,7 @@ export const App = () => {
return (
} path="oauth/consent" />
+ } path="request-credential" />
diff --git a/src/components/copy-block.tsx b/src/components/copy-block.tsx
new file mode 100644
index 0000000..412c316
--- /dev/null
+++ b/src/components/copy-block.tsx
@@ -0,0 +1,66 @@
+/*
+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 { Button } from "@/components/ui/button";
+import { downloadText } from "@/lib/download";
+
+// CopyBlock shows generated text (a CSR, an OpenSSL section, a command) with a
+// copy button and, when a filename is given, a download button.
+export const CopyBlock = ({
+ filename,
+ label,
+ text,
+}: {
+ filename?: string;
+ label: string;
+ text: string;
+}) => (
+
+
+
+ {label}
+
+
+
+ {filename ? (
+
+ ) : null}
+
+
+
+ {text}
+
+
+);
diff --git a/src/components/credential-complete-dialog.test.tsx b/src/components/credential-complete-dialog.test.tsx
new file mode 100644
index 0000000..3633328
--- /dev/null
+++ b/src/components/credential-complete-dialog.test.tsx
@@ -0,0 +1,238 @@
+/*
+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 "reflect-metadata";
+import { Code, ConnectError } from "@connectrpc/connect";
+import { X509CertificateGenerator } from "@peculiar/x509";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import type { CredentialRequestRow } from "@/lib/operators";
+
+import { CredentialCompleteDialog } from "@/components/credential-complete-dialog";
+import { generateBackupPassphrase, keyBackupPem } from "@/lib/crypto/key-backup";
+import { base64 } from "@/lib/crypto/leaf-key";
+import * as modeMod from "@/lib/fleet/mode";
+import * as operators from "@/lib/operators";
+
+const { buildCredentialPkcs12, downloadBytes } = vi.hoisted(() => ({
+ buildCredentialPkcs12: vi.fn(),
+ downloadBytes: vi.fn(),
+}));
+vi.mock("@/lib/download", async (importOriginal) => ({
+ ...(await importOriginal()),
+ downloadBytes: (...a: unknown[]) => downloadBytes(...a),
+}));
+
+vi.mock("@/lib/crypto/key-backup", async (importOriginal) => {
+ const actual = await importOriginal();
+ buildCredentialPkcs12.mockImplementation(actual.buildCredentialPkcs12);
+ return { ...actual, buildCredentialPkcs12: (...a: unknown[]) => buildCredentialPkcs12(...a) };
+});
+
+const EMAIL = "new-hire@example.org";
+
+const request: CredentialRequestRow = {
+ completedSerial: "",
+ createdAt: "2026-09-29T14:00:00Z",
+ createdByCn: "admin@example.org",
+ csrPem: "",
+ email: EMAIL,
+ expiresAt: "2026-10-29T14:00:00Z",
+ fullName: "New Hire",
+ id: "req-1",
+ level: "operator",
+ state: "pending",
+};
+
+const material = async () => {
+ const keys = (await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-384" }, true, [
+ "sign",
+ "verify",
+ ])) as CryptoKeyPair;
+ const passphrase = generateBackupPassphrase();
+ const backup = new TextEncoder().encode(await keyBackupPem(keys.privateKey, passphrase));
+ const cert = await X509CertificateGenerator.createSelfSigned({
+ keys,
+ name: `CN=${EMAIL}`,
+ notAfter: new Date(Date.now() + 86_400_000),
+ notBefore: new Date(Date.now() - 60_000),
+ serialNumber: "0a",
+ signingAlgorithm: { hash: "SHA-384", name: "ECDSA" },
+ });
+ return { backup, certPem: cert.toString("pem"), keys, passphrase };
+};
+
+const fill = (label: RegExp, value: string) =>
+ fireEvent.change(screen.getByLabelText(label), { target: { value } });
+
+beforeEach(() => {
+ downloadBytes.mockReset();
+ buildCredentialPkcs12.mockClear();
+ operators.__resetOperators();
+});
+afterEach(() => {
+ vi.restoreAllMocks();
+ vi.unstubAllGlobals();
+});
+
+describe("CredentialCompleteDialog", () => {
+ it("records the certificate, then builds the PKCS#12 with the key backup's passphrase", async () => {
+ const { backup, certPem, passphrase } = await material();
+ const record = vi.spyOn(operators, "recordOperatorCredential");
+ const onRecorded = vi.fn();
+ render(
+ ,
+ );
+
+ fill(/signed certificate/i, certPem);
+ fill(/backup passphrase/i, passphrase);
+ fireEvent.click(screen.getByRole("button", { name: /record/i }));
+
+ await waitFor(() => expect(downloadBytes).toHaveBeenCalled(), { timeout: 15_000 });
+ expect(record).toHaveBeenCalledWith(expect.objectContaining({ requestId: "req-1" }));
+ const [args] = buildCredentialPkcs12.mock.calls[0] as [
+ { backup: Uint8Array; friendlyName: string; passphrase: string },
+ ];
+ expect(args.backup).toBe(backup);
+ expect(args.passphrase).toBe(passphrase);
+ expect(args.friendlyName).toBe(`FleetOS operator (${EMAIL})`);
+ expect(downloadBytes.mock.calls[0][0]).toBe(`${EMAIL}.p12`);
+ expect(onRecorded).toHaveBeenCalled();
+ expect(screen.getByText(/PKCS#12 downloaded/i)).toBeInTheDocument();
+ }, 30_000);
+
+ it("stops before recording when the passphrase doesn't open the backup", async () => {
+ const { backup, certPem } = await material();
+ const record = vi.spyOn(operators, "recordOperatorCredential");
+ render(
+ ,
+ );
+ fill(/signed certificate/i, certPem);
+ fill(/backup passphrase/i, "wrong-passphrase-wrong-pass");
+ fireEvent.click(screen.getByRole("button", { name: /record/i }));
+
+ await screen.findByText(/passphrase is wrong/i, {}, { timeout: 15_000 });
+ expect(record).not.toHaveBeenCalled();
+ expect(downloadBytes).not.toHaveBeenCalled();
+ }, 30_000);
+
+ it("takes a key backup file when the browser holds none", async () => {
+ const { backup, certPem, passphrase } = await material();
+ render();
+ expect(screen.queryByLabelText(/held in this browser/i)).not.toBeInTheDocument();
+ fill(/signed certificate/i, certPem);
+ fireEvent.change(screen.getByLabelText(/^key backup file/i), {
+ target: { files: [new File([new TextDecoder().decode(backup)], "k.key.pem")] },
+ });
+ fill(/backup passphrase/i, passphrase);
+ await waitFor(() => expect(screen.getByRole("button", { name: /record/i })).toBeEnabled());
+ fireEvent.click(screen.getByRole("button", { name: /record/i }));
+ await waitFor(() => expect(downloadBytes).toHaveBeenCalled(), { timeout: 15_000 });
+ }, 30_000);
+
+ it("only records when the holder keeps the key", async () => {
+ const { certPem } = await material();
+ const record = vi.spyOn(operators, "recordOperatorCredential");
+ render();
+ fill(/signed certificate/i, certPem);
+ fireEvent.click(screen.getByLabelText(/holder has the key/i));
+ fireEvent.click(screen.getByRole("button", { name: /record/i }));
+ await screen.findByText(/recorded/i, { selector: "[role=status]" });
+ expect(record).toHaveBeenCalled();
+ expect(downloadBytes).not.toHaveBeenCalled();
+ expect(screen.getByText(/openssl pkcs12 -export/)).toBeInTheDocument();
+ }, 30_000);
+
+ it("explains a refusal by its reason code", async () => {
+ const { certPem } = await material();
+ vi.spyOn(operators, "recordOperatorCredential").mockRejectedValue(
+ new ConnectError("refused", Code.InvalidArgument, {
+ "x-cryptos-error-code": "1610",
+ "x-cryptos-error-reason": "LEVEL_EXT_CRITICAL",
+ }),
+ );
+ render();
+ fill(/signed certificate/i, certPem);
+ fireEvent.click(screen.getByLabelText(/holder has the key/i));
+ fireEvent.click(screen.getByRole("button", { name: /record/i }));
+ await screen.findByText(/marked critical/i);
+ }, 30_000);
+
+ it("asks for the holder's name when recording without a request", async () => {
+ const { certPem } = await material();
+ const record = vi.spyOn(operators, "recordOperatorCredential");
+ render();
+ fill(/signed certificate/i, certPem);
+ fill(/holder's full name/i, "Out Of Band");
+ fireEvent.click(screen.getByLabelText(/holder has the key/i));
+ fireEvent.click(screen.getByRole("button", { name: /record/i }));
+ await waitFor(() =>
+ expect(record).toHaveBeenCalledWith(
+ expect.objectContaining({ fullName: "Out Of Band", requestId: undefined }),
+ ),
+ );
+ }, 30_000);
+
+ it("never puts the key or the passphrase in a request body (live)", async () => {
+ const { backup, certPem, keys, passphrase } = await material();
+ vi.spyOn(modeMod, "fleetMode").mockReturnValue("live");
+ const bodies: string[] = [];
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
+ const raw = init?.body;
+ bodies.push(ArrayBuffer.isView(raw) ? new TextDecoder().decode(raw) : String(raw ?? ""));
+ return Response.json(
+ {
+ credential: { commonName: EMAIL, email: EMAIL, level: "operator", serialHex: "0A" },
+ },
+ { headers: { "content-type": "application/json" }, status: 200 },
+ );
+ }),
+ );
+ render(
+ ,
+ );
+ fill(/signed certificate/i, certPem);
+ fill(/backup passphrase/i, passphrase);
+ fireEvent.click(screen.getByRole("button", { name: /record/i }));
+ await waitFor(() => expect(downloadBytes).toHaveBeenCalled(), { timeout: 15_000 });
+
+ expect(bodies).toHaveLength(1);
+ const pkcs8 = base64(new Uint8Array(await crypto.subtle.exportKey("pkcs8", keys.privateKey)));
+ const jwk = await crypto.subtle.exportKey("jwk", keys.privateKey);
+ const backupB64 = base64(backup);
+ for (const secret of [pkcs8, jwk.d ?? "unset", passphrase, backupB64]) {
+ expect(bodies[0]).not.toContain(secret);
+ }
+ }, 30_000);
+});
diff --git a/src/components/credential-complete-dialog.tsx b/src/components/credential-complete-dialog.tsx
new file mode 100644
index 0000000..8aef66c
--- /dev/null
+++ b/src/components/credential-complete-dialog.tsx
@@ -0,0 +1,312 @@
+/*
+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 { DialogFrame } from "@/components/dialog-frame";
+import { Button } from "@/components/ui/button";
+import {
+ buildCredentialPkcs12,
+ keyMatchesCertificate,
+ readCertificateDer,
+ readKeyBackup,
+} from "@/lib/crypto/key-backup";
+import { base64 } from "@/lib/crypto/leaf-key";
+import { downloadBytes, readFileBytes } from "@/lib/download";
+import { fleetErrorMessage } from "@/lib/fleet/error-copy";
+import { fieldClass, labelClass } from "@/lib/form";
+import {
+ type CredentialRequestRow,
+ type OperatorCredentialRow,
+ recordOperatorCredential,
+} from "@/lib/operators";
+
+type KeySource = "file" | "held" | "holder";
+
+// CredentialCompleteDialog records a certificate the operator CA signed, for
+// a pending request or (with no request) as an out-of-band import. When this
+// browser can reach the key, through the backup it still holds or a backup
+// file, it checks the key against the certificate before recording and then
+// builds the PKCS#12, sealed with the key backup's own passphrase.
+export const CredentialCompleteDialog = ({
+ heldBackup,
+ onClose,
+ onRecorded,
+ request,
+}: {
+ heldBackup?: Uint8Array;
+ onClose: () => void;
+ onRecorded: () => void;
+ request: CredentialRequestRow | null;
+}) => {
+ const [certText, setCertText] = useState("");
+ const [fullName, setFullName] = useState("");
+ const [keySource, setKeySource] = useState(heldBackup ? "held" : "file");
+ const [backupFile, setBackupFile] = useState(null);
+ const [passphrase, setPassphrase] = useState("");
+ const [chainText, setChainText] = useState("");
+ const [pending, setPending] = useState(false);
+ const [error, setError] = useState(null);
+ const [recorded, setRecorded] = useState(null);
+ const [warnings, setWarnings] = useState([]);
+ const [p12Name, setP12Name] = useState("");
+
+ const backupFor = (source: KeySource): null | Uint8Array | undefined => {
+ if (source === "held") return heldBackup;
+ if (source === "file") return backupFile;
+ return undefined;
+ };
+ const backup = backupFor(keySource);
+ const needsKey = keySource !== "holder";
+ const ready =
+ certText.trim() !== "" &&
+ (request !== null || fullName.trim() !== "") &&
+ (!needsKey || (backup !== null && backup !== undefined && passphrase !== "")) &&
+ !pending;
+
+ const complete = async () => {
+ setPending(true);
+ setError(null);
+ try {
+ const certDer = readCertificateDer(new TextEncoder().encode(certText.trim()));
+ const chainDer =
+ chainText.trim() === ""
+ ? []
+ : [readCertificateDer(new TextEncoder().encode(chainText.trim()))];
+
+ if (needsKey && backup) {
+ const key = await readKeyBackup(backup, passphrase);
+ if (!(await keyMatchesCertificate(key, certDer))) {
+ throw new Error(
+ "The certificate does not match the key in this backup. Check that it was signed from this request's CSR.",
+ );
+ }
+ }
+
+ let result;
+ try {
+ result = await recordOperatorCredential({
+ certDer,
+ fullName: request ? undefined : fullName,
+ requestId: request?.id,
+ });
+ } catch (error_: unknown) {
+ setError(fleetErrorMessage(error_, "Recording the certificate failed."));
+ return;
+ }
+ setRecorded(result.credential);
+ setWarnings(result.warnings);
+ onRecorded();
+
+ if (needsKey && backup) {
+ const email = result.credential.email || request?.email || "operator";
+ const level = result.credential.level || request?.level || "operator";
+ const pfx = await buildCredentialPkcs12({
+ backup,
+ certDer,
+ chainDer,
+ friendlyName: `FleetOS ${level} (${email})`,
+ passphrase,
+ });
+ const name = `${email}.p12`;
+ downloadBytes(name, pfx, "application/x-pkcs12");
+ setP12Name(name);
+ }
+ setPassphrase("");
+ } catch (error_: unknown) {
+ setError(error_ instanceof Error ? error_.message : "Completing the request failed.");
+ } finally {
+ setPending(false);
+ }
+ };
+
+ let submitLabel = needsKey ? "Record and build PKCS#12" : "Record";
+ if (pending) submitLabel = "Workingā¦";
+
+ const title = request
+ ? `Complete request for ${request.email}`
+ : "Record an operator certificate";
+
+ return (
+
+
+ {title}
+
+
+ {recorded ? (
+
+
+ Recorded {recorded.commonName} ({recorded.level}), serial{" "}
+ {recorded.serialHex}.
+
+ {warnings.map((w) => (
+
+ {w}
+
+ ))}
+ {p12Name ? (
+
+ PKCS#12 downloaded as {p12Name}. It opens with the
+ key backup's passphrase. Import it into the operator's browser or OS
+ certificate store, then keep the key backup somewhere safe or delete it.
+
+ ) : (
+
+ The holder builds the PKCS#12 with their key:{" "}
+
+ openssl pkcs12 -export -inkey <key file> -in <certificate> -certfile
+ operator-ca.crt -out <name>.p12
+
+
+ >
+ )}
+
+ );
+};
diff --git a/src/components/credential-request-wizard.test.tsx b/src/components/credential-request-wizard.test.tsx
new file mode 100644
index 0000000..e97aa19
--- /dev/null
+++ b/src/components/credential-request-wizard.test.tsx
@@ -0,0 +1,188 @@
+/*
+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 "reflect-metadata";
+import { Pkcs10CertificateRequestGenerator } from "@peculiar/x509";
+import { fireEvent, render, screen } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import { CredentialRequestWizard } from "@/components/credential-request-wizard";
+import { base64 } from "@/lib/crypto/leaf-key";
+import * as modeMod from "@/lib/fleet/mode";
+
+const downloadText = vi.fn();
+vi.mock("@/lib/download", async (importOriginal) => ({
+ ...(await importOriginal()),
+ downloadText: (...a: unknown[]) => downloadText(...a),
+}));
+
+const fill = (label: RegExp, value: string) =>
+ fireEvent.change(screen.getByLabelText(label), { target: { value } });
+
+const holder = (email = "bob@example.org", level = "operator") => {
+ fill(/full name/i, "Bob Example");
+ fill(/^email/i, email);
+ fill(/access level/i, level);
+};
+
+// backUp walks the key backup step: download, tick, continue.
+const backUp = async () => {
+ await screen.findByLabelText(/key backup passphrase/i, {}, { timeout: 10_000 });
+ fireEvent.click(screen.getByRole("button", { name: /download key backup/i }));
+ fireEvent.click(screen.getByLabelText(/saved the passphrase/i));
+ fireEvent.click(screen.getByRole("button", { name: /continue/i }));
+};
+
+const csrPem = async (cn: string): Promise => {
+ const keys = (await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-384" }, true, [
+ "sign",
+ "verify",
+ ])) as CryptoKeyPair;
+ const csr = await Pkcs10CertificateRequestGenerator.create({
+ keys,
+ name: `CN=${cn}`,
+ signingAlgorithm: { hash: "SHA-384", name: "ECDSA" },
+ });
+ return csr.toString("pem");
+};
+
+afterEach(() => {
+ vi.restoreAllMocks();
+ vi.unstubAllGlobals();
+});
+beforeEach(() => downloadText.mockReset());
+
+describe("CredentialRequestWizard (mock mode)", () => {
+ it("makes the key here, forces the backup, then shows the CSR and the signing recipe", async () => {
+ const onCreated = vi.fn();
+ render();
+ holder();
+ fireEvent.click(screen.getByRole("button", { name: /next/i }));
+
+ expect(screen.queryByLabelText("CSR")).not.toBeInTheDocument();
+ await backUp();
+
+ await screen.findByLabelText("CSR", {}, { timeout: 10_000 });
+ expect(screen.getByLabelText("CSR").textContent).toMatch(/BEGIN CERTIFICATE REQUEST/);
+ expect(screen.getByLabelText(/extension section/i, { selector: "pre" }).textContent).toContain(
+ "op_operator",
+ );
+ expect(screen.getByLabelText(/signing command/i, { selector: "pre" }).textContent).toContain(
+ "openssl ca",
+ );
+ fireEvent.click(screen.getByRole("button", { name: "Download CSR" }));
+ expect(downloadText).toHaveBeenLastCalledWith(
+ "fleetos-operator-bob@example.org.csr",
+ expect.stringMatching(/BEGIN CERTIFICATE REQUEST/),
+ );
+ const [requestId, backup] = onCreated.mock.calls[0] as [string, Uint8Array];
+ expect(requestId).not.toBe("");
+ expect(new TextDecoder().decode(backup)).toMatch(/ENCRYPTED PRIVATE KEY/);
+ }, 30_000);
+
+ it("takes an uploaded CSR whose CN is the holder's email", async () => {
+ const onCreated = vi.fn();
+ render();
+ holder("carol@example.org", "viewer");
+ fireEvent.click(screen.getByLabelText(/upload a csr/i));
+ fireEvent.click(screen.getByRole("button", { name: /next/i }));
+
+ fill(/paste the csr/i, await csrPem("carol@example.org"));
+ fireEvent.click(screen.getByRole("button", { name: /file the request/i }));
+
+ await screen.findByLabelText("CSR");
+ expect(onCreated).toHaveBeenCalledWith(expect.any(String), undefined);
+ }, 30_000);
+
+ it("refuses an uploaded CSR for someone else", async () => {
+ render();
+ holder("carol@example.org");
+ fireEvent.click(screen.getByLabelText(/upload a csr/i));
+ fireEvent.click(screen.getByRole("button", { name: /next/i }));
+
+ fill(/paste the csr/i, await csrPem("mallory@example.org"));
+ fireEvent.click(screen.getByRole("button", { name: /file the request/i }));
+
+ await screen.findByText(/mallory@example.org/);
+ expect(screen.queryByLabelText("CSR")).not.toBeInTheDocument();
+ }, 30_000);
+
+ it("needs a name, a valid email and a level before Next", () => {
+ render();
+ const next = screen.getByRole("button", { name: /next/i });
+ expect(next).toBeDisabled();
+ fill(/full name/i, "Bob");
+ fill(/^email/i, "not-an-email");
+ expect(next).toBeDisabled();
+ fill(/^email/i, "bob@example.org");
+ expect(next).toBeEnabled();
+ });
+});
+
+describe("CredentialRequestWizard (live mode)", () => {
+ it("never puts the private key or the passphrase in a request body", async () => {
+ vi.spyOn(modeMod, "fleetMode").mockReturnValue("live");
+ const generated: CryptoKeyPair[] = [];
+ const realGenerate = crypto.subtle.generateKey.bind(crypto.subtle);
+ vi.spyOn(crypto.subtle, "generateKey").mockImplementation((async (
+ ...args: Parameters
+ ) => {
+ const keys = await realGenerate(...args);
+ if ("privateKey" in (keys as CryptoKeyPair)) generated.push(keys as CryptoKeyPair);
+ return keys;
+ }) as SubtleCrypto["generateKey"]);
+
+ const bodies: string[] = [];
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
+ const raw = init?.body;
+ bodies.push(ArrayBuffer.isView(raw) ? new TextDecoder().decode(raw) : String(raw ?? ""));
+ return Response.json(
+ {
+ csrPem:
+ "-----BEGIN CERTIFICATE REQUEST-----\nMAA=\n-----END CERTIFICATE REQUEST-----\n",
+ expiresAt: "2026-10-30T00:00:00Z",
+ extfileSection: "[ op_admin ]",
+ opensslCommand: "openssl ca -extensions op_admin",
+ requestId: "req-1",
+ },
+ { headers: { "content-type": "application/json" }, status: 200 },
+ );
+ }),
+ );
+
+ render();
+ holder("alice@example.org", "admin");
+ fireEvent.click(screen.getByRole("button", { name: /next/i }));
+ const shown = await screen.findByLabelText(/key backup passphrase/i, {}, { timeout: 10_000 });
+ const passphrase = shown.textContent ?? "";
+ await backUp();
+ await screen.findByLabelText("CSR", {}, { timeout: 10_000 });
+
+ expect(bodies).toHaveLength(1);
+ const body = bodies[0];
+ expect(JSON.parse(body)).toHaveProperty("csrDer");
+ expect(generated).toHaveLength(1);
+ const pkcs8 = new Uint8Array(await crypto.subtle.exportKey("pkcs8", generated[0].privateKey));
+ const jwk = await crypto.subtle.exportKey("jwk", generated[0].privateKey);
+ const b64 = base64(pkcs8);
+ const b64url = b64.replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
+ for (const secret of [b64, b64url, b64.slice(40, 100), jwk.d ?? "unset", passphrase]) {
+ expect(body).not.toContain(secret);
+ }
+ }, 30_000);
+});
diff --git a/src/components/credential-request-wizard.tsx b/src/components/credential-request-wizard.tsx
new file mode 100644
index 0000000..9a63e9b
--- /dev/null
+++ b/src/components/credential-request-wizard.tsx
@@ -0,0 +1,279 @@
+/*
+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 { CopyBlock } from "@/components/copy-block";
+import { DialogFrame } from "@/components/dialog-frame";
+import { type BackedUpKey, KeyBackupStep } from "@/components/key-backup-step";
+import { Button } from "@/components/ui/button";
+import { parseCsr } from "@/lib/crypto/csr";
+import { readFileBytes } from "@/lib/download";
+import { fleetErrorMessage } from "@/lib/fleet/error-copy";
+import { fieldClass, labelClass, looksLikeEmail } from "@/lib/form";
+import {
+ createCredentialRequest,
+ type CreatedCredentialRequest,
+ credentialFileBase,
+ OPERATOR_LEVELS,
+ type OperatorLevel,
+} from "@/lib/operators";
+
+type Step = "backup" | "done" | "holder" | "upload";
+
+// CredentialRequestWizard files an operator credential request. The CSR comes
+// from a key made here (with the mandatory key backup) or from the holder's
+// own machine. The Fleet Manager stores the request and hands back what the CA
+// operator needs: the CSR, the OpenSSL extension section for the level and the
+// signing command. The external CA signs out of band; Complete records it.
+export const CredentialRequestWizard = ({
+ onClose,
+ onCreated,
+}: {
+ onClose: () => void;
+ onCreated: (requestId: string, backup: Uint8Array | undefined) => void;
+}) => {
+ const [step, setStep] = useState("holder");
+ const [fullName, setFullName] = useState("");
+ const [email, setEmail] = useState("");
+ const [level, setLevel] = useState("operator");
+ const [source, setSource] = useState<"browser" | "upload">("browser");
+ const [csrText, setCsrText] = useState("");
+ const [pending, setPending] = useState(false);
+ const [error, setError] = useState(null);
+ const [created, setCreated] = useState(null);
+ const [heldHere, setHeldHere] = useState(false);
+
+ const cleanEmail = email.trim().toLowerCase();
+ const holderReady = fullName.trim().length > 0 && looksLikeEmail(email);
+
+ const file = async (csrDer: Uint8Array, backup?: Uint8Array) => {
+ setPending(true);
+ setError(null);
+ try {
+ const result = await createCredentialRequest({ csrDer, email: cleanEmail, fullName, level });
+ setCreated(result);
+ setHeldHere(backup !== undefined);
+ setStep("done");
+ onCreated(result.requestId, backup);
+ } catch (error_: unknown) {
+ setError(fleetErrorMessage(error_, "Filing the request failed."));
+ } finally {
+ setPending(false);
+ }
+ };
+
+ const fileUploaded = async () => {
+ setError(null);
+ try {
+ const parsed = await parseCsr(csrText);
+ if (parsed.subjectCn.toLowerCase() !== cleanEmail) {
+ setError(
+ `The CSR is for ${parsed.subjectCn}, not ${cleanEmail}. Its subject must be exactly CN=.`,
+ );
+ return;
+ }
+ await file(parsed.csrDer);
+ } catch (error_: unknown) {
+ setError(error_ instanceof Error ? error_.message : "The CSR could not be read.");
+ }
+ };
+
+ return (
+
+
+ Request operator credential
+
+
+ {step === "holder" ? (
+ <>
+
+ The Fleet Manager never signs operator credentials. This files a request; your operator
+ CA signs its CSR out of band, and you record the signed certificate with Complete.
+
+ Request filed for {cleanEmail} ({level}). It expires{" "}
+ {created.expiresAt}.
+
+
+
+
+
+ Send the CSR to whoever runs your operator CA. They sign it with the {level} section
+ above (never with copy_extensions), then send back the certificate. Record it with
+ Complete on the Pending requests tab.
+
+ {heldHere ? (
+
+ This tab keeps the encrypted key backup until you leave the page, so Complete can
+ build the PKCS#12 here. After that, Complete takes the backup file and its passphrase.
+
+ ) : null}
+
+
+
+ >
+ ) : null}
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+
+ );
+};
diff --git a/src/components/dialog-frame.tsx b/src/components/dialog-frame.tsx
new file mode 100644
index 0000000..2e59820
--- /dev/null
+++ b/src/components/dialog-frame.tsx
@@ -0,0 +1,47 @@
+/*
+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";
+
+// DialogFrame is the modal shell the operator credential dialogs share: a dim
+// backdrop that closes on click, and a labelled dialog panel.
+export const DialogFrame = ({
+ children,
+ labelId,
+ onClose,
+ wide = false,
+}: {
+ children: ReactNode;
+ labelId: string;
+ onClose: () => void;
+ wide?: boolean;
+}) => (
+
+ The private key was made in this browser and never goes to the Fleet Manager. Download the
+ encrypted key backup and store the passphrase in a password manager. The passphrase is
+ shown only here, once; the same passphrase will protect the PKCS#12. Without both, the key
+ is lost and the request has to start again.
+
+
+
+
+
+ Passphrase
+
+
+
+ {made.passphrase}
+
+
+
+
+
+
+
+ {filename}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/layout/auth-gate.test.tsx b/src/components/layout/auth-gate.test.tsx
index 8d6d946..eae340d 100644
--- a/src/components/layout/auth-gate.test.tsx
+++ b/src/components/layout/auth-gate.test.tsx
@@ -54,6 +54,15 @@ describe("AuthGate", () => {
expect(screen.queryByText("fleet console")).not.toBeInTheDocument();
});
+ it("points someone with no certificate at the credential request page", () => {
+ renderGate();
+
+ expect(screen.getByRole("link", { name: /make a credential request/i })).toHaveAttribute(
+ "href",
+ "/request-credential",
+ );
+ });
+
it("calls login when the button is clicked", () => {
renderGate();
diff --git a/src/components/layout/auth-gate.tsx b/src/components/layout/auth-gate.tsx
index b0c8a55..a3443c7 100644
--- a/src/components/layout/auth-gate.tsx
+++ b/src/components/layout/auth-gate.tsx
@@ -93,6 +93,9 @@ export const AuthGate = ({ children }: { children: ReactNode }) => {
+
+ No operator certificate yet? Make a credential request
+
);
};
diff --git a/src/components/operator-deny-dialog.test.tsx b/src/components/operator-deny-dialog.test.tsx
new file mode 100644
index 0000000..f0764f4
--- /dev/null
+++ b/src/components/operator-deny-dialog.test.tsx
@@ -0,0 +1,93 @@
+/*
+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 { Code, ConnectError } from "@connectrpc/connect";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import type { OperatorCredentialRow } from "@/lib/operators";
+
+import { OperatorDenyDialog } from "@/components/operator-deny-dialog";
+import * as operators from "@/lib/operators";
+
+const target: OperatorCredentialRow = {
+ commonName: "bob@example.org",
+ crlRevoked: false,
+ denylisted: false,
+ email: "bob@example.org",
+ firstSeenAt: "",
+ fullName: "Bob Example",
+ issuerSha256: "ab".repeat(32),
+ kind: "requested",
+ lastSeenAt: "",
+ level: "operator",
+ notAfter: "2027-01-01T00:00:00Z",
+ revoked: false,
+ serialHex: "0A:0B",
+};
+
+afterEach(() => vi.restoreAllMocks());
+
+describe("OperatorDenyDialog", () => {
+ it("offers reason codes 0 to 10 except 7", () => {
+ render();
+ const values = Array.from(
+ (screen.getByLabelText(/reason/i) as HTMLSelectElement).options,
+ (o) => Number(o.value),
+ );
+ expect(values).toHaveLength(10);
+ expect(values).toEqual(expect.arrayContaining([0, 1, 2, 3, 4, 5, 6, 8, 9, 10]));
+ });
+
+ it("warns before and after that the CA is not touched", async () => {
+ const deny = vi.spyOn(operators, "denyOperatorCredential").mockResolvedValue({
+ issuerSha256: target.issuerSha256,
+ revokedAt: "2026-09-30T12:00:00Z",
+ serialHex: target.serialHex,
+ warnings: ["Also revoke this certificate at your CA and publish a new CRL."],
+ });
+ const onDenied = vi.fn();
+ render();
+ expect(screen.getByRole("note").textContent).toMatch(/revoke .* at your CA/i);
+
+ fireEvent.change(screen.getByLabelText(/reason/i), { target: { value: "9" } });
+ fireEvent.change(screen.getByLabelText(/note/i), { target: { value: "left the team" } });
+ fireEvent.click(screen.getByRole("button", { name: /deny at the fleet manager/i }));
+
+ await waitFor(() =>
+ expect(deny).toHaveBeenCalledWith({
+ issuerSha256: target.issuerSha256,
+ note: "left the team",
+ reasonCode: 9,
+ serialHex: target.serialHex,
+ }),
+ );
+ expect(await screen.findByRole("status")).toHaveTextContent(/publish a new CRL/);
+ expect(onDenied).toHaveBeenCalled();
+ });
+
+ it("explains a refusal by its code", async () => {
+ vi.spyOn(operators, "denyOperatorCredential").mockRejectedValue(
+ new ConnectError("x", Code.FailedPrecondition, {
+ "x-cryptos-error-code": "1603",
+ "x-cryptos-error-reason": "DATABASE_REQUIRED",
+ }),
+ );
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /deny at the fleet manager/i }));
+ expect(await screen.findByRole("alert")).toHaveTextContent(/Postgres/);
+ });
+});
diff --git a/src/components/operator-deny-dialog.tsx b/src/components/operator-deny-dialog.tsx
new file mode 100644
index 0000000..3e21635
--- /dev/null
+++ b/src/components/operator-deny-dialog.tsx
@@ -0,0 +1,148 @@
+/*
+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 { DialogFrame } from "@/components/dialog-frame";
+import { Button } from "@/components/ui/button";
+import { fleetErrorMessage } from "@/lib/fleet/error-copy";
+import { fieldClass, labelClass } from "@/lib/form";
+import {
+ CA_REVOKE_REMINDER,
+ DENY_REASONS,
+ denyOperatorCredential,
+ type OperatorCredentialRow,
+} from "@/lib/operators";
+
+// OperatorDenyDialog puts an operator credential on the Fleet Manager
+// denylist. The authz middleware refuses it from its next request, on every
+// replica within seconds. It doesn't revoke at the external CA, so the dialog
+// says so before and after, including the manager's own warnings.
+export const OperatorDenyDialog = ({
+ credential,
+ onClose,
+ onDenied,
+}: {
+ credential: OperatorCredentialRow;
+ onClose: () => void;
+ onDenied: () => void;
+}) => {
+ const [reasonCode, setReasonCode] = useState(DENY_REASONS[0].code);
+ const [note, setNote] = useState("");
+ const [pending, setPending] = useState(false);
+ const [error, setError] = useState(null);
+ const [warnings, setWarnings] = useState(null);
+
+ const confirm = () => {
+ setPending(true);
+ setError(null);
+ denyOperatorCredential({
+ issuerSha256: credential.issuerSha256,
+ note: note.trim(),
+ reasonCode,
+ serialHex: credential.serialHex,
+ })
+ .then((result) => {
+ setWarnings(result.warnings.length > 0 ? result.warnings : [CA_REVOKE_REMINDER]);
+ onDenied();
+ })
+ .catch((error_: unknown) => {
+ setError(fleetErrorMessage(error_, "Deny failed."));
+ })
+ .finally(() => setPending(false));
+ };
+
+ return (
+
+
+ The Fleet Manager refuses this credential from its next request. It does not revoke at
+ your CA: other systems that trust the CA still accept it. Also revoke it at your CA and
+ publish a new CRL.
+
+
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+
+
+
+
+ >
+ )}
+
+ );
+};
diff --git a/src/components/operator-issue-dialog.test.tsx b/src/components/operator-issue-dialog.test.tsx
deleted file mode 100644
index 1969009..0000000
--- a/src/components/operator-issue-dialog.test.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
-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 { OperatorIssueDialog } from "@/components/operator-issue-dialog";
-
-const issueOperatorCredential = vi.fn();
-vi.mock("@/lib/operators", async (importOriginal) => {
- const actual = await importOriginal();
- return { ...actual, issueOperatorCredential: (...a: unknown[]) => issueOperatorCredential(...a) };
-});
-
-vi.mock("@/lib/crypto/leaf-key", async (importOriginal) => {
- const actual = await importOriginal();
- return {
- ...actual,
- generateLeafKeyAndCSR: vi.fn().mockResolvedValue({
- csrDer: new Uint8Array([1, 2, 3]),
- privateKey: {} as CryptoKey,
- }),
- generateStrongPassphrase: () => "Generated-Strong-Passphrase-1234",
- };
-});
-
-vi.mock("@/lib/crypto/pkcs12", () => ({
- assemblePkcs12: vi.fn().mockResolvedValue(new Uint8Array([0x30, 0x01])),
-}));
-
-const STRONG = "correct-horse-battery-staple";
-const issueButton = () => screen.getByRole("button", { name: /issue and download/i });
-
-describe("OperatorIssueDialog", () => {
- beforeEach(() => {
- issueOperatorCredential.mockReset();
- issueOperatorCredential.mockResolvedValue({ certDer: new Uint8Array([9]), serialHex: "0A:0B" });
- globalThis.URL.createObjectURL = vi.fn(() => "blob:stub");
- globalThis.URL.revokeObjectURL = vi.fn();
- });
-
- it("keeps Issue disabled until CN, a >= 18 passphrase, and the typed confirmation are present", () => {
- render();
- expect(issueButton()).toBeDisabled();
-
- fireEvent.change(screen.getByLabelText(/common name/i), { target: { value: "op@acme" } });
- fireEvent.change(screen.getByLabelText(/passphrase \(min/i), { target: { value: STRONG } });
- expect(issueButton()).toBeDisabled(); // still no typed confirmation
-
- fireEvent.change(screen.getByLabelText(/confirm/i), { target: { value: "ISSUE" } });
- expect(issueButton()).toBeEnabled();
- });
-
- it("rejects a short passphrase in-UI and never enables Issue", () => {
- render();
- fireEvent.change(screen.getByLabelText(/common name/i), { target: { value: "op@acme" } });
- fireEvent.change(screen.getByLabelText(/passphrase \(min/i), { target: { value: "short" } });
- fireEvent.change(screen.getByLabelText(/confirm/i), { target: { value: "ISSUE" } });
- expect(screen.getByText(/at least 18 characters/i)).toBeInTheDocument();
- expect(issueButton()).toBeDisabled();
- });
-
- it("on confirm issues with the chosen level and downloads a PKCS#12", async () => {
- const onIssued = vi.fn();
- render();
- fireEvent.change(screen.getByLabelText(/common name/i), { target: { value: "op@acme" } });
- fireEvent.change(screen.getByLabelText(/access level/i), { target: { value: "admin" } });
- fireEvent.change(screen.getByLabelText(/passphrase \(min/i), { target: { value: STRONG } });
- fireEvent.change(screen.getByLabelText(/confirm/i), { target: { value: "op@acme" } });
- fireEvent.click(issueButton());
-
- await waitFor(() =>
- expect(issueOperatorCredential).toHaveBeenCalledWith(
- "op@acme",
- "admin",
- expect.any(Uint8Array),
- ),
- );
- await waitFor(() => expect(globalThis.URL.createObjectURL).toHaveBeenCalled());
- await waitFor(() => expect(screen.getByText(/PKCS#12 downloaded/i)).toBeInTheDocument());
- expect(onIssued).toHaveBeenCalled();
- });
-
- it("surfaces an issue error inline", async () => {
- issueOperatorCredential.mockRejectedValue(new Error("operator-admin profile is absent"));
- render();
- fireEvent.change(screen.getByLabelText(/common name/i), { target: { value: "op@acme" } });
- fireEvent.change(screen.getByLabelText(/passphrase \(min/i), { target: { value: STRONG } });
- fireEvent.change(screen.getByLabelText(/confirm/i), { target: { value: "ISSUE" } });
- fireEvent.click(issueButton());
-
- await waitFor(() => expect(screen.getByText(/profile is absent/i)).toBeInTheDocument());
- });
-});
diff --git a/src/components/operator-issue-dialog.tsx b/src/components/operator-issue-dialog.tsx
deleted file mode 100644
index d4d067c..0000000
--- a/src/components/operator-issue-dialog.tsx
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
-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 {
- generateLeafKeyAndCSR,
- generateStrongPassphrase,
- MIN_PASSPHRASE_LENGTH,
-} from "@/lib/crypto/leaf-key";
-import { assemblePkcs12 } from "@/lib/crypto/pkcs12";
-import { issueOperatorCredential, type OperatorLevel } from "@/lib/operators";
-
-const field = "w-full rounded-md border bg-card px-3 py-2 font-mono text-sm";
-const label = "font-mono text-[11px] uppercase tracking-wider text-muted-foreground";
-
-const levels: OperatorLevel[] = ["viewer", "operator", "admin"];
-
-// download triggers a browser download of the .p12 bytes without leaving a URL
-// object behind. The bytes are copied into a fresh view so the Blob part type
-// is unambiguous; the PKCS#12 is opaque and never rendered on screen.
-const download = (filename: string, contents: Uint8Array): void => {
- const copy = new Uint8Array(contents.length);
- copy.set(contents);
- const url = URL.createObjectURL(new Blob([copy], { type: "application/x-pkcs12" }));
- const anchor = document.createElement("a");
- anchor.download = filename;
- anchor.href = url;
- anchor.click();
- URL.revokeObjectURL(url);
-};
-
-// OperatorIssueDialog issues an operator credential end-to-end in the browser:
-// it mints an ECDSA keypair + CSR (leaf-key), asks the manager to sign the CSR
-// under the operator- profile (issueOperatorCredential), and assembles a
-// passphrase-sealed PKCS#12 (pkcs12) from the returned cert and the browser-held
-// key, which the operator downloads. Double-guarded like the escrow export: a
-// >= 18-character passphrase (typed or generated) AND a typed confirmation are
-// both required before the download button enables. The private key and the
-// passphrase are never logged and never rendered back.
-export const OperatorIssueDialog = ({
- onClose,
- onIssued,
-}: {
- onClose: () => void;
- onIssued: () => void;
-}) => {
- const [commonName, setCommonName] = useState("");
- const [level, setLevel] = useState("operator");
- const [passphrase, setPassphrase] = useState("");
- const [reveal, setReveal] = useState(false);
- const [confirmText, setConfirmText] = useState("");
- const [pending, setPending] = useState(false);
- const [error, setError] = useState(null);
- const [done, setDone] = useState(false);
-
- const cn = commonName.trim();
- const passphraseLongEnough = passphrase.length >= MIN_PASSPHRASE_LENGTH;
- const confirmed = confirmText === "ISSUE" || (cn.length > 0 && confirmText === cn);
- const ready = cn.length > 0 && passphraseLongEnough && confirmed && !pending;
-
- const confirm = () => {
- if (!ready) return;
- setPending(true);
- setError(null);
- // The whole issue-then-pack sequence: mint key+CSR, sign via the manager,
- // assemble the sealed PKCS#12, download. Any failure surfaces inline.
- void (async () => {
- try {
- const { csrDer, privateKey } = await generateLeafKeyAndCSR({ sans: [], subjectCn: cn });
- const { certDer } = await issueOperatorCredential(cn, level, csrDer);
- const pfx = await assemblePkcs12(certDer, privateKey, passphrase);
- download(`${cn}-operator.p12`, pfx);
- // Clear the passphrase from state the moment it is no longer needed.
- setPassphrase("");
- setConfirmText("");
- setDone(true);
- onIssued();
- } catch (error_: unknown) {
- setError(error_ instanceof Error ? error_.message : "Issue failed");
- } finally {
- setPending(false);
- }
- })();
- };
-
- return (
-
-
e.stopPropagation()}
- role="dialog"
- >
-
- Issue operator credential
-
-
- {done ? (
-
- Operator PKCS#12 downloaded.
-
- ) : (
- <>
-
-
- The keypair is generated in this browser. The signed credential downloads as a
- passphrase-protected PKCS#12. Save the passphrase before issuing ā it is required to
- open the PKCS#12 and cannot be recovered afterward.
-
-
-
-
-
-
-
-
-
-
-
-
-
- {passphrase.length > 0 ? (
-
- Save this passphrase now ā it is required to open the PKCS#12 and is not
- recoverable.
-
- Passphrase must be at least {MIN_PASSPHRASE_LENGTH} characters.
-
- ) : null}
-
-
-
- {error ? (
-
- {error}
-
- ) : null}
- >
- )}
-
-
-
- {done ? null : (
-
- )}
-
-
-
- );
-};
diff --git a/src/components/operator-revoke-dialog.tsx b/src/components/operator-revoke-dialog.tsx
deleted file mode 100644
index 51c5553..0000000
--- a/src/components/operator-revoke-dialog.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
-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 { revokeOperatorCredential } from "@/lib/operators";
-
-// RFC 5280 CRL reason codes offered for an operator-credential revocation.
-const reasons: { code: number; label: string }[] = [
- { code: 1, label: "keyCompromise" },
- { code: 5, label: "cessationOfOperation" },
- { code: 4, label: "superseded" },
- { code: 3, label: "affiliationChanged" },
- { code: 0, label: "unspecified" },
-];
-
-// OperatorRevokeDialog revokes an operator credential by serial with a reason.
-// Once revoked, the manager's authz middleware denies that serial (the
-// revocation is enforced server-side); this dialog is the operator-facing
-// trigger. Errors surface inline; there is no native popup.
-export const OperatorRevokeDialog = ({
- commonName,
- onClose,
- onRevoked,
- serialHex,
-}: {
- commonName: string;
- onClose: () => void;
- onRevoked: () => void;
- serialHex: string;
-}) => {
- const [reasonCode, setReasonCode] = useState(reasons[0].code);
- const [pending, setPending] = useState(false);
- const [error, setError] = useState(null);
-
- const confirm = () => {
- setPending(true);
- setError(null);
- revokeOperatorCredential(serialHex, reasonCode)
- .then(() => {
- onRevoked();
- onClose();
- })
- .catch((error_: unknown) => {
- setError(error_ instanceof Error ? error_.message : "Revoke failed");
- setPending(false);
- });
- };
-
- return (
-
-
e.stopPropagation()}
- role="dialog"
- >
-
-
- Revoke operator credential
-
-
- {commonName} · {serialHex}
-
-
-
- {error ? (
-
- {error}
-
- ) : null}
-
-
-
-
-
-
- );
-};
diff --git a/src/gen/fleet/cryptos/fleet/v1/bootstrap_pb.ts b/src/gen/fleet/cryptos/fleet/v1/bootstrap_pb.ts
new file mode 100644
index 0000000..56cdeef
--- /dev/null
+++ b/src/gen/fleet/cryptos/fleet/v1/bootstrap_pb.ts
@@ -0,0 +1,450 @@
+// @generated by protoc-gen-es v2.12.1 with parameter "target=ts"
+// @generated from file cryptos/fleet/v1/bootstrap.proto (package cryptos.fleet.v1, syntax proto3)
+/* eslint-disable */
+
+import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
+import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
+import type { ErrorReason } from "./errors_pb";
+import { file_cryptos_fleet_v1_errors } from "./errors_pb";
+import type { OcspMode, OcspProbeResult, OperatorCA, OperatorCAAcknowledgement } from "./operator_ca_pb";
+import { file_cryptos_fleet_v1_operator_ca } from "./operator_ca_pb";
+import type { Message } from "@bufbuild/protobuf";
+
+/**
+ * Describes the file cryptos/fleet/v1/bootstrap.proto.
+ */
+export const file_cryptos_fleet_v1_bootstrap: GenFile = /*@__PURE__*/
+ fileDesc("CiBjcnlwdG9zL2ZsZWV0L3YxL2Jvb3RzdHJhcC5wcm90bxIQY3J5cHRvcy5mbGVldC52MSIaChhHZXRCb290c3RyYXBTdGF0ZVJlcXVlc3QimgEKGUdldEJvb3RzdHJhcFN0YXRlUmVzcG9uc2USLwoFc3RhdGUYASABKA4yIC5jcnlwdG9zLmZsZWV0LnYxLkJvb3RzdHJhcFN0YXRlEjIKC3JlYXNvbl9jb2RlGAIgASgOMh0uY3J5cHRvcy5mbGVldC52MS5FcnJvclJlYXNvbhIYChB0b2tlbl9leHBpcmVzX2F0GAMgASgJIi0KHFN0YXJ0Qm9vdHN0cmFwU2Vzc2lvblJlcXVlc3QSDQoFdG9rZW4YASABKAkiSwodU3RhcnRCb290c3RyYXBTZXNzaW9uUmVzcG9uc2USFgoOc2Vzc2lvbl9zZWNyZXQYASABKAkSEgoKZXhwaXJlc19hdBgCIAEoCSKgAgopQm9vdHN0cmFwU2VydmljZVJlZ2lzdGVyT3BlcmF0b3JDQVJlcXVlc3QSEwoLY2FfY2VydF9kZXIYASABKAwSDQoDdXJsGAIgASgJSAASEQoHY3JsX2RlchgDIAEoDEgAEg4KBG5vbmUYBCABKAhIABItCglvY3NwX21vZGUYBSABKA4yGi5jcnlwdG9zLmZsZWV0LnYxLk9jc3BNb2RlEhAKCG9jc3BfdXJsGAYgASgJEkUKEGFja25vd2xlZGdlbWVudHMYByADKA4yKy5jcnlwdG9zLmZsZWV0LnYxLk9wZXJhdG9yQ0FBY2tub3dsZWRnZW1lbnQSFgoOY29uZmlybV9zaGEyNTYYCCABKAlCDAoKY3JsX3NvdXJjZSLAAQoqQm9vdHN0cmFwU2VydmljZVJlZ2lzdGVyT3BlcmF0b3JDQVJlc3BvbnNlEjEKC29wZXJhdG9yX2NhGAEgASgLMhwuY3J5cHRvcy5mbGVldC52MS5PcGVyYXRvckNBEhEKCWNvbmZpcm1lZBgCIAEoCBIVCg1hZG1pbl9leHRmaWxlGAMgASgJEjUKCm9jc3BfcHJvYmUYBCABKAsyIS5jcnlwdG9zLmZsZWV0LnYxLk9jc3BQcm9iZVJlc3VsdCJaCiJTdWJtaXRGaXJzdEFkbWluQ2VydGlmaWNhdGVSZXF1ZXN0EhAKCGNlcnRfZGVyGAEgASgMEg8KB2Nzcl9kZXIYAiABKAwSEQoJZnVsbF9uYW1lGAMgASgJIoQBCiNTdWJtaXRGaXJzdEFkbWluQ2VydGlmaWNhdGVSZXNwb25zZRISCgpzZXJpYWxfaGV4GAEgASgJEhEKCW5vdF9hZnRlchgCIAEoCRINCgVlbWFpbBgDIAEoCRIVCg1pc3N1ZXJfc2hhMjU2GAQgASgJEhAKCHdhcm5pbmdzGAUgAygJKtIBCg5Cb290c3RyYXBTdGF0ZRIfChtCT09UU1RSQVBfU1RBVEVfVU5TUEVDSUZJRUQQABIiCh5CT09UU1RSQVBfU1RBVEVfTk9UX0FQUExJQ0FCTEUQARIYChRCT09UU1RSQVBfU1RBVEVfT1BFThACEiQKIEJPT1RTVFJBUF9TVEFURV9PUEVOX0lOX1BST0dSRVNTEAMSGgoWQk9PVFNUUkFQX1NUQVRFX0NMT1NFRBAEEh8KG0JPT1RTVFJBUF9TVEFURV9VTkFWQUlMQUJMRRAFMpkEChBCb290c3RyYXBTZXJ2aWNlEmwKEUdldEJvb3RzdHJhcFN0YXRlEiouY3J5cHRvcy5mbGVldC52MS5HZXRCb290c3RyYXBTdGF0ZVJlcXVlc3QaKy5jcnlwdG9zLmZsZWV0LnYxLkdldEJvb3RzdHJhcFN0YXRlUmVzcG9uc2USeAoVU3RhcnRCb290c3RyYXBTZXNzaW9uEi4uY3J5cHRvcy5mbGVldC52MS5TdGFydEJvb3RzdHJhcFNlc3Npb25SZXF1ZXN0Gi8uY3J5cHRvcy5mbGVldC52MS5TdGFydEJvb3RzdHJhcFNlc3Npb25SZXNwb25zZRKPAQoSUmVnaXN0ZXJPcGVyYXRvckNBEjsuY3J5cHRvcy5mbGVldC52MS5Cb290c3RyYXBTZXJ2aWNlUmVnaXN0ZXJPcGVyYXRvckNBUmVxdWVzdBo8LmNyeXB0b3MuZmxlZXQudjEuQm9vdHN0cmFwU2VydmljZVJlZ2lzdGVyT3BlcmF0b3JDQVJlc3BvbnNlEooBChtTdWJtaXRGaXJzdEFkbWluQ2VydGlmaWNhdGUSNC5jcnlwdG9zLmZsZWV0LnYxLlN1Ym1pdEZpcnN0QWRtaW5DZXJ0aWZpY2F0ZVJlcXVlc3QaNS5jcnlwdG9zLmZsZWV0LnYxLlN1Ym1pdEZpcnN0QWRtaW5DZXJ0aWZpY2F0ZVJlc3BvbnNlQjhaNmdpdGh1Yi5jb20vQ3J5cHRPUy1QS0kvYXBpL2dvL2NyeXB0b3MvZmxlZXQvdjE7ZmxlZXR2MWIGcHJvdG8z", [file_cryptos_fleet_v1_errors, file_cryptos_fleet_v1_operator_ca]);
+
+/**
+ * @generated from message cryptos.fleet.v1.GetBootstrapStateRequest
+ */
+export type GetBootstrapStateRequest = Message<"cryptos.fleet.v1.GetBootstrapStateRequest"> & {
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.GetBootstrapStateRequest.
+ * Use `create(GetBootstrapStateRequestSchema)` to create a new message.
+ */
+export const GetBootstrapStateRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_bootstrap, 0);
+
+/**
+ * @generated from message cryptos.fleet.v1.GetBootstrapStateResponse
+ */
+export type GetBootstrapStateResponse = Message<"cryptos.fleet.v1.GetBootstrapStateResponse"> & {
+ /**
+ * @generated from field: cryptos.fleet.v1.BootstrapState state = 1;
+ */
+ state: BootstrapState;
+
+ /**
+ * reason_code is DATABASE_REQUIRED or FIRST_RUN_DISABLED when state is
+ * unavailable, unspecified otherwise.
+ *
+ * @generated from field: cryptos.fleet.v1.ErrorReason reason_code = 2;
+ */
+ reasonCode: ErrorReason;
+
+ /**
+ * token_expires_at is when the current bootstrap token expires (RFC3339),
+ * empty unless first run is open. The token itself is only in the log.
+ *
+ * @generated from field: string token_expires_at = 3;
+ */
+ tokenExpiresAt: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.GetBootstrapStateResponse.
+ * Use `create(GetBootstrapStateResponseSchema)` to create a new message.
+ */
+export const GetBootstrapStateResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_bootstrap, 1);
+
+/**
+ * @generated from message cryptos.fleet.v1.StartBootstrapSessionRequest
+ */
+export type StartBootstrapSessionRequest = Message<"cryptos.fleet.v1.StartBootstrapSessionRequest"> & {
+ /**
+ * token is the bootstrap token printed in the manager's log
+ * (fos_boot_...).
+ *
+ * @generated from field: string token = 1;
+ */
+ token: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.StartBootstrapSessionRequest.
+ * Use `create(StartBootstrapSessionRequestSchema)` to create a new message.
+ */
+export const StartBootstrapSessionRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_bootstrap, 2);
+
+/**
+ * @generated from message cryptos.fleet.v1.StartBootstrapSessionResponse
+ */
+export type StartBootstrapSessionResponse = Message<"cryptos.fleet.v1.StartBootstrapSessionResponse"> & {
+ /**
+ * session_secret (fos_bsess_...) is returned once. Keep it in memory and
+ * send it in the Fleetos-Bootstrap-Session header.
+ *
+ * @generated from field: string session_secret = 1;
+ */
+ sessionSecret: string;
+
+ /**
+ * expires_at is the session's absolute expiry (RFC3339). It also ends after
+ * 15 minutes without a call.
+ *
+ * @generated from field: string expires_at = 2;
+ */
+ expiresAt: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.StartBootstrapSessionResponse.
+ * Use `create(StartBootstrapSessionResponseSchema)` to create a new message.
+ */
+export const StartBootstrapSessionResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_bootstrap, 3);
+
+/**
+ * BootstrapServiceRegisterOperatorCARequest has the same fields as the admin
+ * RegisterOperatorCARequest.
+ *
+ * @generated from message cryptos.fleet.v1.BootstrapServiceRegisterOperatorCARequest
+ */
+export type BootstrapServiceRegisterOperatorCARequest = Message<"cryptos.fleet.v1.BootstrapServiceRegisterOperatorCARequest"> & {
+ /**
+ * ca_cert_der is exactly one CA certificate, DER or PEM, at most 8 KiB.
+ * Empty to preview or confirm the current registration.
+ *
+ * @generated from field: bytes ca_cert_der = 1;
+ */
+ caCertDer: Uint8Array;
+
+ /**
+ * crl_source is how the manager gets the CA's CRL. A CRL given here is
+ * verified against the CA before anything is stored.
+ *
+ * @generated from oneof cryptos.fleet.v1.BootstrapServiceRegisterOperatorCARequest.crl_source
+ */
+ crlSource: {
+ /**
+ * url is an http or https URL the manager fetches the CRL from, now and
+ * on a schedule.
+ *
+ * @generated from field: string url = 2;
+ */
+ value: string;
+ case: "url";
+ } | {
+ /**
+ * crl_der is an initial CRL, DER or PEM, at most 4 MiB. Later CRLs come
+ * through UploadOperatorCRL.
+ *
+ * @generated from field: bytes crl_der = 3;
+ */
+ value: Uint8Array;
+ case: "crlDer";
+ } | {
+ /**
+ * none, set to true, means no CRL. It needs the NO_CRL acknowledgement.
+ *
+ * @generated from field: bool none = 4;
+ */
+ value: boolean;
+ case: "none";
+ } | { case: undefined; value?: undefined };
+
+ /**
+ * ocsp_mode defaults to OCSP_MODE_AIA when unspecified.
+ *
+ * @generated from field: cryptos.fleet.v1.OcspMode ocsp_mode = 5;
+ */
+ ocspMode: OcspMode;
+
+ /**
+ * ocsp_url is the responder for OCSP_MODE_URL, http or https, at most
+ * 2 KiB. The manager probes it before accepting it.
+ *
+ * @generated from field: string ocsp_url = 6;
+ */
+ ocspUrl: string;
+
+ /**
+ * @generated from field: repeated cryptos.fleet.v1.OperatorCAAcknowledgement acknowledgements = 7;
+ */
+ acknowledgements: OperatorCAAcknowledgement[];
+
+ /**
+ * confirm_sha256 is the fingerprint from the preview. Empty asks for a
+ * preview. Case and colon separators are ignored, so the output of
+ * openssl x509 -fingerprint -sha256 can be pasted.
+ *
+ * @generated from field: string confirm_sha256 = 8;
+ */
+ confirmSha256: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.BootstrapServiceRegisterOperatorCARequest.
+ * Use `create(BootstrapServiceRegisterOperatorCARequestSchema)` to create a new message.
+ */
+export const BootstrapServiceRegisterOperatorCARequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_bootstrap, 4);
+
+/**
+ * BootstrapServiceRegisterOperatorCAResponse has the same fields as the admin
+ * RegisterOperatorCAResponse.
+ *
+ * @generated from message cryptos.fleet.v1.BootstrapServiceRegisterOperatorCAResponse
+ */
+export type BootstrapServiceRegisterOperatorCAResponse = Message<"cryptos.fleet.v1.BootstrapServiceRegisterOperatorCAResponse"> & {
+ /**
+ * operator_ca holds the subject, issuer, notAfter, fingerprint, warnings,
+ * CRL status and OCSP mode.
+ *
+ * @generated from field: cryptos.fleet.v1.OperatorCA operator_ca = 1;
+ */
+ operatorCa?: OperatorCA | undefined;
+
+ /**
+ * confirmed is false for a preview and true once the CA is stored and
+ * trusted.
+ *
+ * @generated from field: bool confirmed = 2;
+ */
+ confirmed: boolean;
+
+ /**
+ * admin_extfile is the OpenSSL extension section for an admin certificate
+ * under this CA.
+ *
+ * @generated from field: string admin_extfile = 3;
+ */
+ adminExtfile: string;
+
+ /**
+ * ocsp_probe is the probe result for OCSP_MODE_URL, absent otherwise.
+ *
+ * @generated from field: cryptos.fleet.v1.OcspProbeResult ocsp_probe = 4;
+ */
+ ocspProbe?: OcspProbeResult | undefined;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.BootstrapServiceRegisterOperatorCAResponse.
+ * Use `create(BootstrapServiceRegisterOperatorCAResponseSchema)` to create a new message.
+ */
+export const BootstrapServiceRegisterOperatorCAResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_bootstrap, 5);
+
+/**
+ * @generated from message cryptos.fleet.v1.SubmitFirstAdminCertificateRequest
+ */
+export type SubmitFirstAdminCertificateRequest = Message<"cryptos.fleet.v1.SubmitFirstAdminCertificateRequest"> & {
+ /**
+ * cert_der is exactly one certificate, DER, at most 8 KiB.
+ *
+ * @generated from field: bytes cert_der = 1;
+ */
+ certDer: Uint8Array;
+
+ /**
+ * csr_der is the CSR the certificate was signed from, at most 4 KiB.
+ * Optional.
+ *
+ * @generated from field: bytes csr_der = 2;
+ */
+ csrDer: Uint8Array;
+
+ /**
+ * full_name is the admin's name, 1 to 128 characters.
+ *
+ * @generated from field: string full_name = 3;
+ */
+ fullName: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.SubmitFirstAdminCertificateRequest.
+ * Use `create(SubmitFirstAdminCertificateRequestSchema)` to create a new message.
+ */
+export const SubmitFirstAdminCertificateRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_bootstrap, 6);
+
+/**
+ * @generated from message cryptos.fleet.v1.SubmitFirstAdminCertificateResponse
+ */
+export type SubmitFirstAdminCertificateResponse = Message<"cryptos.fleet.v1.SubmitFirstAdminCertificateResponse"> & {
+ /**
+ * @generated from field: string serial_hex = 1;
+ */
+ serialHex: string;
+
+ /**
+ * @generated from field: string not_after = 2;
+ */
+ notAfter: string;
+
+ /**
+ * email is taken from the certificate's CN, lower-cased.
+ *
+ * @generated from field: string email = 3;
+ */
+ email: string;
+
+ /**
+ * issuer_sha256 is the operator CA's fingerprint.
+ *
+ * @generated from field: string issuer_sha256 = 4;
+ */
+ issuerSha256: string;
+
+ /**
+ * warnings, for example a validity over 400 days.
+ *
+ * @generated from field: repeated string warnings = 5;
+ */
+ warnings: string[];
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.SubmitFirstAdminCertificateResponse.
+ * Use `create(SubmitFirstAdminCertificateResponseSchema)` to create a new message.
+ */
+export const SubmitFirstAdminCertificateResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_bootstrap, 7);
+
+/**
+ * BootstrapState is whether first run is open.
+ *
+ * @generated from enum cryptos.fleet.v1.BootstrapState
+ */
+export enum BootstrapState {
+ /**
+ * @generated from enum value: BOOTSTRAP_STATE_UNSPECIFIED = 0;
+ */
+ UNSPECIFIED = 0,
+
+ /**
+ * The operator CA comes from the config file, so there is no first run.
+ *
+ * @generated from enum value: BOOTSTRAP_STATE_NOT_APPLICABLE = 1;
+ */
+ NOT_APPLICABLE = 1,
+
+ /**
+ * Open, with no session or registration yet.
+ *
+ * @generated from enum value: BOOTSTRAP_STATE_OPEN = 2;
+ */
+ OPEN = 2,
+
+ /**
+ * Open, and a session was started or an operator CA registered.
+ *
+ * @generated from enum value: BOOTSTRAP_STATE_OPEN_IN_PROGRESS = 3;
+ */
+ OPEN_IN_PROGRESS = 3,
+
+ /**
+ * An admin certificate has authenticated; first run is over.
+ *
+ * @generated from enum value: BOOTSTRAP_STATE_CLOSED = 4;
+ */
+ CLOSED = 4,
+
+ /**
+ * First run can't run; reason_code says why.
+ *
+ * @generated from enum value: BOOTSTRAP_STATE_UNAVAILABLE = 5;
+ */
+ UNAVAILABLE = 5,
+}
+
+/**
+ * Describes the enum cryptos.fleet.v1.BootstrapState.
+ */
+export const BootstrapStateSchema: GenEnum = /*@__PURE__*/
+ enumDesc(file_cryptos_fleet_v1_bootstrap, 0);
+
+/**
+ * BootstrapService stands a Fleet Manager up from nothing: before any
+ * operator CA is trusted, the holder of the one-time bootstrap token from the
+ * manager's log registers the external operator CA and checks the first
+ * admin certificate it signed. It is served only over HTTPS and outside the
+ * client-certificate middleware, and it closes for good the first time an
+ * admin certificate authenticates.
+ *
+ * The session procedures (RegisterOperatorCA, SubmitFirstAdminCertificate)
+ * take the secret from StartBootstrapSession in the Fleetos-Bootstrap-Session
+ * request header, never in the body. Once first run is closed, every
+ * procedure except GetBootstrapState returns 1601 before reading a token or
+ * session.
+ *
+ * @generated from service cryptos.fleet.v1.BootstrapService
+ */
+export const BootstrapService: GenService<{
+ /**
+ * GetBootstrapState reports whether first run is open. Anonymous.
+ *
+ * @generated from rpc cryptos.fleet.v1.BootstrapService.GetBootstrapState
+ */
+ getBootstrapState: {
+ methodKind: "unary";
+ input: typeof GetBootstrapStateRequestSchema;
+ output: typeof GetBootstrapStateResponseSchema;
+ },
+ /**
+ * StartBootstrapSession consumes the bootstrap token and starts the one
+ * live session, ending any earlier one. The manager prints a new token at
+ * once, so the token can't be used twice. A wrong, expired or used token
+ * returns 1600.
+ *
+ * @generated from rpc cryptos.fleet.v1.BootstrapService.StartBootstrapSession
+ */
+ startBootstrapSession: {
+ methodKind: "unary";
+ input: typeof StartBootstrapSessionRequestSchema;
+ output: typeof StartBootstrapSessionResponseSchema;
+ },
+ /**
+ * RegisterOperatorCA uploads the external operator CA's certificate and
+ * chooses its CRL source and OCSP mode. The first call checks everything
+ * and returns a preview with the fingerprint; the call is repeated with
+ * confirm_sha256 set to that fingerprint to store and trust the CA, which
+ * retires any CA registered earlier in first run. A new session keeps the
+ * current registration but must confirm it again before submitting a
+ * certificate: calling with ca_cert_der empty returns the current
+ * registration's preview, and calling again with its confirm_sha256
+ * confirms it without changing anything. Session-gated and audited.
+ *
+ * @generated from rpc cryptos.fleet.v1.BootstrapService.RegisterOperatorCA
+ */
+ registerOperatorCA: {
+ methodKind: "unary";
+ input: typeof BootstrapServiceRegisterOperatorCARequestSchema;
+ output: typeof BootstrapServiceRegisterOperatorCAResponseSchema;
+ },
+ /**
+ * SubmitFirstAdminCertificate checks and records the first admin's
+ * certificate, which the operator CA signed out of band. With csr_der, the
+ * certificate's key must match the CSR's; without it, this is a pre-flight
+ * for a certificate made entirely at the CA. A later submission with a
+ * different serial puts the earlier first-admin certificate on the
+ * denylist. Session-gated and audited. The manager signs nothing.
+ *
+ * @generated from rpc cryptos.fleet.v1.BootstrapService.SubmitFirstAdminCertificate
+ */
+ submitFirstAdminCertificate: {
+ methodKind: "unary";
+ input: typeof SubmitFirstAdminCertificateRequestSchema;
+ output: typeof SubmitFirstAdminCertificateResponseSchema;
+ },
+}> = /*@__PURE__*/
+ serviceDesc(file_cryptos_fleet_v1_bootstrap, 0);
+
diff --git a/src/gen/fleet/cryptos/fleet/v1/errors_pb.ts b/src/gen/fleet/cryptos/fleet/v1/errors_pb.ts
new file mode 100644
index 0000000..47c5ac0
--- /dev/null
+++ b/src/gen/fleet/cryptos/fleet/v1/errors_pb.ts
@@ -0,0 +1,418 @@
+// @generated by protoc-gen-es v2.12.1 with parameter "target=ts"
+// @generated from file cryptos/fleet/v1/errors.proto (package cryptos.fleet.v1, syntax proto3)
+/* eslint-disable */
+
+import type { GenEnum, GenFile } from "@bufbuild/protobuf/codegenv2";
+import { enumDesc, fileDesc } from "@bufbuild/protobuf/codegenv2";
+
+/**
+ * Describes the file cryptos/fleet/v1/errors.proto.
+ */
+export const file_cryptos_fleet_v1_errors: GenFile = /*@__PURE__*/
+ fileDesc("Ch1jcnlwdG9zL2ZsZWV0L3YxL2Vycm9ycy5wcm90bxIQY3J5cHRvcy5mbGVldC52MSq3AwoJRXJyb3JDb2RlEhoKFkVSUk9SX0NPREVfVU5TUEVDSUZJRUQQABIdChhFUlJPUl9DT0RFX1RPS0VOX0lOVkFMSUQQwAwSFgoRRVJST1JfQ09ERV9DTE9TRUQQwQwSHAoXRVJST1JfQ09ERV9SQVRFX0xJTUlURUQQwgwSGwoWRVJST1JfQ09ERV9VTkFWQUlMQUJMRRDDDBIfChpFUlJPUl9DT0RFX1NFU1NJT05fSU5WQUxJRBDEDBIkCh9FUlJPUl9DT0RFX09QRVJBVE9SX0NBX1JFSkVDVEVEEMUMEhwKF0VSUk9SX0NPREVfQ1NSX1JFSkVDVEVEEMYMEi0KKEVSUk9SX0NPREVfT1BFUkFUT1JfQ0FfTUFOQUdFRF9CWV9DT05GSUcQxwwSJAofRVJST1JfQ09ERV9OT19SRVZPQ0FUSU9OX1NPVVJDRRDIDBIiCh1FUlJPUl9DT0RFX09QRVJBVE9SX0NBX0lOX1VTRRDJDBIdChhFUlJPUl9DT0RFX0NFUlRfUkVKRUNURUQQygwSHwoaRVJST1JfQ09ERV9SRVFVRVNUX0lOVkFMSUQQywwqogkKC0Vycm9yUmVhc29uEhwKGEVSUk9SX1JFQVNPTl9VTlNQRUNJRklFRBAAEiIKHkVSUk9SX1JFQVNPTl9EQVRBQkFTRV9SRVFVSVJFRBABEiMKH0VSUk9SX1JFQVNPTl9GSVJTVF9SVU5fRElTQUJMRUQQAhIZChVFUlJPUl9SRUFTT05fTk9UX0FfQ0EQAxIZChVFUlJPUl9SRUFTT05fRVhQSVJJTkcQBBIZChVFUlJPUl9SRUFTT05fS0VZX1RZUEUQBRIbChdFUlJPUl9SRUFTT05fSVNfTk9ERV9DQRAGEiEKHUVSUk9SX1JFQVNPTl9DUkxfU0lHTl9NSVNTSU5HEAcSKAokRVJST1JfUkVBU09OX05PX0NSTF9OT1RfQUNLTk9XTEVER0VEEAgSIAocRVJST1JfUkVBU09OX0NSTF9VTlJFQUNIQUJMRRAJEhwKGEVSUk9SX1JFQVNPTl9DUkxfSU5WQUxJRBAKEh0KGUVSUk9SX1JFQVNPTl9DUkxfUk9MTEJBQ0sQCxIhCh1FUlJPUl9SRUFTT05fT0NTUF9VTlJFQUNIQUJMRRAMEh0KGUVSUk9SX1JFQVNPTl9PQ1NQX0lOVkFMSUQQDRIeChpFUlJPUl9SRUFTT05fTk9UX0NPTkZJUk1FRBAOEiUKIUVSUk9SX1JFQVNPTl9ST1RBVElPTl9JTl9QUk9HUkVTUxAPEhUKEUVSUk9SX1JFQVNPTl9TSVpFEBASGgoWRVJST1JfUkVBU09OX1NJR05BVFVSRRAREiEKHUVSUk9SX1JFQVNPTl9TVUJKRUNUX01JU01BVENIEBISGgoWRVJST1JfUkVBU09OX1NUQUxFX0NSTBATEhsKF0VSUk9SX1JFQVNPTl9TVEFMRV9PQ1NQEBQSHwobRVJST1JfUkVBU09OX1NUQUxFX0RFTllMSVNUEBUSFwoTRVJST1JfUkVBU09OX05PX0NSTBAWEhwKGEVSUk9SX1JFQVNPTl9OT1RfQ0hBSU5FRBAXEiIKHkVSUk9SX1JFQVNPTl9OT1RfQUNUSVZFX0FOQ0hPUhAYEhwKGEVSUk9SX1JFQVNPTl9XUk9OR19MRVZFTBAZEiMKH0VSUk9SX1JFQVNPTl9MRVZFTF9FWFRfQ1JJVElDQUwQGhIUChBFUlJPUl9SRUFTT05fRUtVEBsSGgoWRVJST1JfUkVBU09OX0tFWV9VU0FHRRAcEiIKHkVSUk9SX1JFQVNPTl9CQVNJQ19DT05TVFJBSU5UUxAdEh0KGUVSUk9SX1JFQVNPTl9LRVlfTUlTTUFUQ0gQHhIYChRFUlJPUl9SRUFTT05fUkVWT0tFRBAfEh0KGUVSUk9SX1JFQVNPTl9SRVZPS0VEX09DU1AQIBIdChlFUlJPUl9SRUFTT05fT0NTUF9VTktOT1dOECESGgoWRVJST1JfUkVBU09OX0RVUExJQ0FURRAiEhoKFkVSUk9SX1JFQVNPTl9OT1RfRk9VTkQQIxIYChRFUlJPUl9SRUFTT05fRVhQSVJFRBAkEhwKGEVSUk9SX1JFQVNPTl9OT1RfUEVORElORxAlQjhaNmdpdGh1Yi5jb20vQ3J5cHRPUy1QS0kvYXBpL2dvL2NyeXB0b3MvZmxlZXQvdjE7ZmxlZXR2MWIGcHJvdG8z");
+
+/**
+ * ErrorCode is the 1600-1699 block of the Fleet Manager's error codes: first
+ * run, operator CAs, operator-certificate revocation and operator credential
+ * requests. The manager owns the 1000-1999 block and defines the other ranges
+ * itself; this range is part of the contract because the web branches on it.
+ *
+ * A failure carries its code, in decimal, on the x-cryptos-error-code error
+ * metadata, and its sub-reason, when it has one, on x-cryptos-error-reason as
+ * the ErrorReason value name without the ERROR_REASON_ prefix (for example
+ * "IS_NODE_CA"). The numbers are stable.
+ *
+ * @generated from enum cryptos.fleet.v1.ErrorCode
+ */
+export enum ErrorCode {
+ /**
+ * @generated from enum value: ERROR_CODE_UNSPECIFIED = 0;
+ */
+ UNSPECIFIED = 0,
+
+ /**
+ * The bootstrap token is wrong, expired or already used. The three cases
+ * are not told apart.
+ *
+ * @generated from enum value: ERROR_CODE_TOKEN_INVALID = 1600;
+ */
+ TOKEN_INVALID = 1600,
+
+ /**
+ * First run is closed. Returned before any token or session is read.
+ *
+ * @generated from enum value: ERROR_CODE_CLOSED = 1601;
+ */
+ CLOSED = 1601,
+
+ /**
+ * Too many failures from this client; wait and try again.
+ *
+ * @generated from enum value: ERROR_CODE_RATE_LIMITED = 1602;
+ */
+ RATE_LIMITED = 1602,
+
+ /**
+ * First run can't run here. Sub-reasons: DATABASE_REQUIRED,
+ * FIRST_RUN_DISABLED. Also returned by a denylist write without Postgres
+ * (DATABASE_REQUIRED).
+ *
+ * @generated from enum value: ERROR_CODE_UNAVAILABLE = 1603;
+ */
+ UNAVAILABLE = 1603,
+
+ /**
+ * The bootstrap session is unknown, expired or ended.
+ *
+ * @generated from enum value: ERROR_CODE_SESSION_INVALID = 1604;
+ */
+ SESSION_INVALID = 1604,
+
+ /**
+ * The operator CA or its CRL or OCSP settings were refused. Sub-reasons:
+ * NOT_A_CA, EXPIRING, KEY_TYPE, IS_NODE_CA, CRL_SIGN_MISSING,
+ * NO_CRL_NOT_ACKNOWLEDGED, CRL_UNREACHABLE, CRL_INVALID, CRL_ROLLBACK,
+ * OCSP_UNREACHABLE, OCSP_INVALID, NOT_CONFIRMED, ROTATION_IN_PROGRESS.
+ *
+ * @generated from enum value: ERROR_CODE_OPERATOR_CA_REJECTED = 1605;
+ */
+ OPERATOR_CA_REJECTED = 1605,
+
+ /**
+ * The CSR was refused. Sub-reasons: SIZE, SIGNATURE, SUBJECT_MISMATCH,
+ * KEY_TYPE.
+ *
+ * @generated from enum value: ERROR_CODE_CSR_REJECTED = 1606;
+ */
+ CSR_REJECTED = 1606,
+
+ /**
+ * The operator CA comes from the manager's config file, so it can't be
+ * changed through the API.
+ *
+ * @generated from enum value: ERROR_CODE_OPERATOR_CA_MANAGED_BY_CONFIG = 1607;
+ */
+ OPERATOR_CA_MANAGED_BY_CONFIG = 1607,
+
+ /**
+ * No fresh revocation data for the certificate. Sub-reasons: STALE_CRL,
+ * STALE_OCSP, STALE_DENYLIST, NO_CRL, DATABASE_REQUIRED.
+ *
+ * @generated from enum value: ERROR_CODE_NO_REVOCATION_SOURCE = 1608;
+ */
+ NO_REVOCATION_SOURCE = 1608,
+
+ /**
+ * Retiring this operator CA would leave no trusted active CA.
+ *
+ * @generated from enum value: ERROR_CODE_OPERATOR_CA_IN_USE = 1609;
+ */
+ OPERATOR_CA_IN_USE = 1609,
+
+ /**
+ * The operator certificate was refused. Sub-reasons: NOT_CHAINED,
+ * NOT_ACTIVE_ANCHOR, WRONG_LEVEL, LEVEL_EXT_CRITICAL, EKU, KEY_USAGE,
+ * BASIC_CONSTRAINTS, SUBJECT_MISMATCH, KEY_TYPE, KEY_MISMATCH, EXPIRING,
+ * REVOKED, REVOKED_OCSP, OCSP_UNKNOWN, DUPLICATE.
+ *
+ * @generated from enum value: ERROR_CODE_CERT_REJECTED = 1610;
+ */
+ CERT_REJECTED = 1610,
+
+ /**
+ * The credential request can't be used. Sub-reasons: NOT_FOUND, EXPIRED,
+ * NOT_PENDING.
+ *
+ * @generated from enum value: ERROR_CODE_REQUEST_INVALID = 1611;
+ */
+ REQUEST_INVALID = 1611,
+}
+
+/**
+ * Describes the enum cryptos.fleet.v1.ErrorCode.
+ */
+export const ErrorCodeSchema: GenEnum = /*@__PURE__*/
+ enumDesc(file_cryptos_fleet_v1_errors, 0);
+
+/**
+ * ErrorReason names why a 16xx code was returned. A name can appear under
+ * more than one code (KEY_TYPE under 1605, 1606 and 1610), so read it with
+ * the code. GetBootstrapStateResponse.reason_code uses it too.
+ *
+ * @generated from enum cryptos.fleet.v1.ErrorReason
+ */
+export enum ErrorReason {
+ /**
+ * @generated from enum value: ERROR_REASON_UNSPECIFIED = 0;
+ */
+ UNSPECIFIED = 0,
+
+ /**
+ * 1603, 1608: the manager runs without Postgres.
+ *
+ * @generated from enum value: ERROR_REASON_DATABASE_REQUIRED = 1;
+ */
+ DATABASE_REQUIRED = 1,
+
+ /**
+ * 1603: firstRun is set to disabled.
+ *
+ * @generated from enum value: ERROR_REASON_FIRST_RUN_DISABLED = 2;
+ */
+ FIRST_RUN_DISABLED = 2,
+
+ /**
+ * 1605: not CA:TRUE, or no keyCertSign.
+ *
+ * @generated from enum value: ERROR_REASON_NOT_A_CA = 3;
+ */
+ NOT_A_CA = 3,
+
+ /**
+ * 1605: under 30 days left. 1610: under 1 day left.
+ *
+ * @generated from enum value: ERROR_REASON_EXPIRING = 4;
+ */
+ EXPIRING = 4,
+
+ /**
+ * 1605, 1606, 1610: the key isn't P-384 (P-256 is also allowed for a CA)
+ * or RSA of 3072 bits or more.
+ *
+ * @generated from enum value: ERROR_REASON_KEY_TYPE = 5;
+ */
+ KEY_TYPE = 5,
+
+ /**
+ * 1605: the certificate, or its public key, is a CryptOS node's CA.
+ *
+ * @generated from enum value: ERROR_REASON_IS_NODE_CA = 6;
+ */
+ IS_NODE_CA = 6,
+
+ /**
+ * 1605: a CRL source is set but the CA's key usage lacks cRLSign.
+ *
+ * @generated from enum value: ERROR_REASON_CRL_SIGN_MISSING = 7;
+ */
+ CRL_SIGN_MISSING = 7,
+
+ /**
+ * 1605: no CRL source without the NO_CRL acknowledgement.
+ *
+ * @generated from enum value: ERROR_REASON_NO_CRL_NOT_ACKNOWLEDGED = 8;
+ */
+ NO_CRL_NOT_ACKNOWLEDGED = 8,
+
+ /**
+ * 1605: the CRL URL could not be fetched.
+ *
+ * @generated from enum value: ERROR_REASON_CRL_UNREACHABLE = 9;
+ */
+ CRL_UNREACHABLE = 9,
+
+ /**
+ * 1605: the CRL failed verification against the CA.
+ *
+ * @generated from enum value: ERROR_REASON_CRL_INVALID = 10;
+ */
+ CRL_INVALID = 10,
+
+ /**
+ * 1605: the CRL is older than the stored one.
+ *
+ * @generated from enum value: ERROR_REASON_CRL_ROLLBACK = 11;
+ */
+ CRL_ROLLBACK = 11,
+
+ /**
+ * 1605: the OCSP responder didn't answer the probe.
+ *
+ * @generated from enum value: ERROR_REASON_OCSP_UNREACHABLE = 12;
+ */
+ OCSP_UNREACHABLE = 12,
+
+ /**
+ * 1605: the OCSP probe response failed validation.
+ *
+ * @generated from enum value: ERROR_REASON_OCSP_INVALID = 13;
+ */
+ OCSP_INVALID = 13,
+
+ /**
+ * 1605: confirm_sha256 is missing or doesn't match, or this session hasn't
+ * confirmed the registration yet.
+ *
+ * @generated from enum value: ERROR_REASON_NOT_CONFIRMED = 14;
+ */
+ NOT_CONFIRMED = 14,
+
+ /**
+ * 1605: a retiring operator CA already exists.
+ *
+ * @generated from enum value: ERROR_REASON_ROTATION_IN_PROGRESS = 15;
+ */
+ ROTATION_IN_PROGRESS = 15,
+
+ /**
+ * 1606: the CSR is over 4 KiB.
+ *
+ * @generated from enum value: ERROR_REASON_SIZE = 16;
+ */
+ SIZE = 16,
+
+ /**
+ * 1606: the CSR signature doesn't verify.
+ *
+ * @generated from enum value: ERROR_REASON_SIGNATURE = 17;
+ */
+ SIGNATURE = 17,
+
+ /**
+ * 1606, 1610: the subject isn't exactly one RDN, CN=.
+ *
+ * @generated from enum value: ERROR_REASON_SUBJECT_MISMATCH = 18;
+ */
+ SUBJECT_MISMATCH = 18,
+
+ /**
+ * 1608: the CA's CRL is past nextUpdate.
+ *
+ * @generated from enum value: ERROR_REASON_STALE_CRL = 19;
+ */
+ STALE_CRL = 19,
+
+ /**
+ * 1608: OCSP is configured for the certificate, with no fresh response and
+ * no fresh CRL.
+ *
+ * @generated from enum value: ERROR_REASON_STALE_OCSP = 20;
+ */
+ STALE_OCSP = 20,
+
+ /**
+ * 1608: this replica's last denylist poll is over 5 minutes old.
+ *
+ * @generated from enum value: ERROR_REASON_STALE_DENYLIST = 21;
+ */
+ STALE_DENYLIST = 21,
+
+ /**
+ * 1608: MCP refused because the certificate's CA has no CRL source.
+ *
+ * @generated from enum value: ERROR_REASON_NO_CRL = 22;
+ */
+ NO_CRL = 22,
+
+ /**
+ * 1610: doesn't verify against the CA for client auth.
+ *
+ * @generated from enum value: ERROR_REASON_NOT_CHAINED = 23;
+ */
+ NOT_CHAINED = 23,
+
+ /**
+ * 1610: chains to a retiring CA, not the active one.
+ *
+ * @generated from enum value: ERROR_REASON_NOT_ACTIVE_ANCHOR = 24;
+ */
+ NOT_ACTIVE_ANCHOR = 24,
+
+ /**
+ * 1610: the level extension is missing or names the wrong level.
+ *
+ * @generated from enum value: ERROR_REASON_WRONG_LEVEL = 25;
+ */
+ WRONG_LEVEL = 25,
+
+ /**
+ * 1610: the level extension is marked critical.
+ *
+ * @generated from enum value: ERROR_REASON_LEVEL_EXT_CRITICAL = 26;
+ */
+ LEVEL_EXT_CRITICAL = 26,
+
+ /**
+ * 1610: extended key usage isn't exactly clientAuth.
+ *
+ * @generated from enum value: ERROR_REASON_EKU = 27;
+ */
+ EKU = 27,
+
+ /**
+ * 1610: key usage lacks digitalSignature, or has keyCertSign or cRLSign.
+ *
+ * @generated from enum value: ERROR_REASON_KEY_USAGE = 28;
+ */
+ KEY_USAGE = 28,
+
+ /**
+ * 1610: basicConstraints is absent or CA:TRUE.
+ *
+ * @generated from enum value: ERROR_REASON_BASIC_CONSTRAINTS = 29;
+ */
+ BASIC_CONSTRAINTS = 29,
+
+ /**
+ * 1610: the public key differs from the CSR's or the request's.
+ *
+ * @generated from enum value: ERROR_REASON_KEY_MISMATCH = 30;
+ */
+ KEY_MISMATCH = 30,
+
+ /**
+ * 1610: on the manager's denylist or in the CA's CRL.
+ *
+ * @generated from enum value: ERROR_REASON_REVOKED = 31;
+ */
+ REVOKED = 31,
+
+ /**
+ * 1610: the CA's OCSP responder says revoked.
+ *
+ * @generated from enum value: ERROR_REASON_REVOKED_OCSP = 32;
+ */
+ REVOKED_OCSP = 32,
+
+ /**
+ * 1610: the CA's OCSP responder says unknown, which counts as revoked.
+ *
+ * @generated from enum value: ERROR_REASON_OCSP_UNKNOWN = 33;
+ */
+ OCSP_UNKNOWN = 33,
+
+ /**
+ * 1610: the certificate is already recorded.
+ *
+ * @generated from enum value: ERROR_REASON_DUPLICATE = 34;
+ */
+ DUPLICATE = 34,
+
+ /**
+ * 1611: no credential request has that id.
+ *
+ * @generated from enum value: ERROR_REASON_NOT_FOUND = 35;
+ */
+ NOT_FOUND = 35,
+
+ /**
+ * 1611: the credential request has expired.
+ *
+ * @generated from enum value: ERROR_REASON_EXPIRED = 36;
+ */
+ EXPIRED = 36,
+
+ /**
+ * 1611: the credential request was already completed or cancelled.
+ *
+ * @generated from enum value: ERROR_REASON_NOT_PENDING = 37;
+ */
+ NOT_PENDING = 37,
+}
+
+/**
+ * Describes the enum cryptos.fleet.v1.ErrorReason.
+ */
+export const ErrorReasonSchema: GenEnum = /*@__PURE__*/
+ enumDesc(file_cryptos_fleet_v1_errors, 1);
+
diff --git a/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts b/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts
index 4386086..96e5628 100644
--- a/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts
+++ b/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts
@@ -4,6 +4,8 @@
import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
+import type { OcspMode, OcspProbeResult, OperatorCA, OperatorCAAcknowledgement } from "./operator_ca_pb";
+import { file_cryptos_fleet_v1_operator_ca } from "./operator_ca_pb";
import type { CertificateProfile, MachineConfig } from "../../v1/config_pb";
import { file_cryptos_v1_config } from "../../v1/config_pb";
import type { InstallDisk } from "../../v1/node_pb";
@@ -16,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file cryptos/fleet/v1/fleet.proto.
*/
export const file_cryptos_fleet_v1_fleet: GenFile = /*@__PURE__*/
- fileDesc("ChxjcnlwdG9zL2ZsZWV0L3YxL2ZsZWV0LnByb3RvEhBjcnlwdG9zLmZsZWV0LnYxIhIKEExpc3ROb2Rlc1JlcXVlc3QiQQoRTGlzdE5vZGVzUmVzcG9uc2USLAoFbm9kZXMYASADKAsyHS5jcnlwdG9zLmZsZWV0LnYxLk5vZGVTdW1tYXJ5Ii8KDkdldE5vZGVSZXF1ZXN0EgwKBG5hbWUYASABKAkSDwoHbm9kZV9pZBgCIAEoCSI9Cg9HZXROb2RlUmVzcG9uc2USKgoEbm9kZRgBIAEoCzIcLmNyeXB0b3MuZmxlZXQudjEuTm9kZURldGFpbCI4ChdMaXN0Q2VydGlmaWNhdGVzUmVxdWVzdBIMCgRub2RlGAEgASgJEg8KB25vZGVfaWQYAiABKAkiTwoYTGlzdENlcnRpZmljYXRlc1Jlc3BvbnNlEjMKDGNlcnRpZmljYXRlcxgBIAMoCzIdLmNyeXB0b3MuZmxlZXQudjEuQ2VydGlmaWNhdGUiFQoTTGlzdFByb2ZpbGVzUmVxdWVzdCJFChRMaXN0UHJvZmlsZXNSZXNwb25zZRItCgVpdGVtcxgBIAMoCzIeLmNyeXB0b3MudjEuQ2VydGlmaWNhdGVQcm9maWxlIkcKFENyZWF0ZVByb2ZpbGVSZXF1ZXN0Ei8KB3Byb2ZpbGUYASABKAsyHi5jcnlwdG9zLnYxLkNlcnRpZmljYXRlUHJvZmlsZSIXChVDcmVhdGVQcm9maWxlUmVzcG9uc2UiRwoUVXBkYXRlUHJvZmlsZVJlcXVlc3QSLwoHcHJvZmlsZRgBIAEoCzIeLmNyeXB0b3MudjEuQ2VydGlmaWNhdGVQcm9maWxlIhcKFVVwZGF0ZVByb2ZpbGVSZXNwb25zZSIkChREZWxldGVQcm9maWxlUmVxdWVzdBIMCgRuYW1lGAEgASgJIhcKFURlbGV0ZVByb2ZpbGVSZXNwb25zZSJVChlBcHBseVByb2ZpbGVUb05vZGVSZXF1ZXN0EhEKCW5vZGVfbmFtZRgBIAEoCRIUCgxwcm9maWxlX25hbWUYAiABKAkSDwoHbm9kZV9pZBgDIAEoCSJJChpBcHBseVByb2ZpbGVUb05vZGVSZXNwb25zZRISCgpnZW5lcmF0aW9uGAEgASgEEhcKD3JlcXVpcmVzX3JlYm9vdBgCIAEoCCIVChNMaXN0QWRhcHRlcnNSZXF1ZXN0IkoKFExpc3RBZGFwdGVyc1Jlc3BvbnNlEjIKBWl0ZW1zGAEgAygLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50QWRhcHRlciI5ChhTZXRBZGFwdGVyRW5hYmxlZFJlcXVlc3QSDAoEbmFtZRgBIAEoCRIPCgdlbmFibGVkGAIgASgIIlEKGVNldEFkYXB0ZXJFbmFibGVkUmVzcG9uc2USNAoHYWRhcHRlchgBIAEoCzIjLmNyeXB0b3MuZmxlZXQudjEuRW5yb2xsbWVudEFkYXB0ZXIiEgoQTGlzdEF1ZGl0UmVxdWVzdCJAChFMaXN0QXVkaXRSZXNwb25zZRIrCgVpdGVtcxgBIAMoCzIcLmNyeXB0b3MuZmxlZXQudjEuQXVkaXRFdmVudCIYChZMaXN0RW5yb2xsbWVudHNSZXF1ZXN0Ik0KF0xpc3RFbnJvbGxtZW50c1Jlc3BvbnNlEjIKBWl0ZW1zGAEgAygLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50UmVxdWVzdCKDAgoLTm9kZVN1bW1hcnkSDAoEbmFtZRgBIAEoCRIPCgdhZGRyZXNzGAIgASgJEgwKBHJvbGUYAyABKAkSFgoOaWRlbnRpdHlfc3RhdGUYBCABKAkSCgoCY24YBSABKAkSDgoGaXNzdWVyGAYgASgJEigKBmhlYWx0aBgHIAEoDjIYLmNyeXB0b3MuZmxlZXQudjEuSGVhbHRoEhUKDWhlYWx0aF9kZXRhaWwYCCABKAkSCgoCaWQYCSABKAkSLQoJcHJvdG9jb2xzGAogAygLMhouY3J5cHRvcy52MS5Qcm90b2NvbFN0YXR1cxIXCg9yZWJvb3RfcmVxdWlyZWQYCyABKAgiSQoMTm9kZUlkZW50aXR5EhEKCWNoYWluX3BlbRgBIAEoCRIRCgljaGFpbl9kZXIYAiADKAwSEwoLbGVhZl9zaGEyNTYYAyABKAkiqQEKCk5vZGVEZXRhaWwSLgoHc3VtbWFyeRgBIAEoCzIdLmNyeXB0b3MuZmxlZXQudjEuTm9kZVN1bW1hcnkSMAoIaWRlbnRpdHkYAiABKAsyHi5jcnlwdG9zLmZsZWV0LnYxLk5vZGVJZGVudGl0eRIVCg10cG1fYXZhaWxhYmxlGAMgASgIEhIKCmJvb3RfY291bnQYBCABKAQSDgoGdXB0aW1lGAUgASgJItgBCgtDZXJ0aWZpY2F0ZRIOCgZzZXJpYWwYASABKAkSEgoKc3ViamVjdF9jbhgCIAEoCRITCgtpc3N1ZXJfbm9kZRgDIAEoCRIMCgRraW5kGAQgASgJEg4KBnN0YXR1cxgFIAEoCRISCgpub3RfYmVmb3JlGAYgASgJEhEKCW5vdF9hZnRlchgHIAEoCRIPCgdwcm9maWxlGAggASgJEhIKCnJldm9rZWRfYXQYCSABKAkSDgoGcmVhc29uGAogASgJEhYKDmlzc3Vlcl9ub2RlX2lkGAsgASgJIo0BChFFbnJvbGxtZW50QWRhcHRlchIMCgRraW5kGAEgASgJEgwKBG5hbWUYAiABKAkSEAoIZW5kcG9pbnQYAyABKAkSDwoHcHJvZmlsZRgEIAEoCRIPCgdlbmFibGVkGAUgASgIEhIKCmNoYWxsZW5nZXMYBiADKAkSFAoMZ3BvX3RlbXBsYXRlGAcgASgJIrwCCgpBdWRpdEV2ZW50EgoKAmlkGAEgASgJEgoKAmF0GAIgASgJEgwKBGtpbmQYAyABKAkSDwoHc3VtbWFyeRgEIAEoCRITCgt0YXJnZXRfa2luZBgFIAEoCRITCgt0YXJnZXRfcGF0aBgGIAEoCRISCgphY3Rvcl9raW5kGAcgASgJEhAKCGFjdG9yX2NuGAggASgJEhQKDGFjdG9yX3NlcmlhbBgJIAEoCRIOCgZrZXlfaWQYCiABKAkSCwoDdmlhGAsgASgJEgwKBHRvb2wYDCABKAkSFgoOcmVxdWVzdF9kaWdlc3QYDSABKAkSDwoHb3V0Y29tZRgOIAEoCRITCgthcHByb3ZhbF9pZBgPIAEoCRIXCg9hcHByb3Zlcl9zZXJpYWwYECABKAkSDwoHbm9kZV9pZBgRIAEoCSLvAgoRRW5yb2xsbWVudFJlcXVlc3QSCgoCaWQYASABKAkSFQoNcHJvcG9zZWRfbmFtZRgCIAEoCRIMCgRyb2xlGAMgASgJEhEKCXBhcmVudF9jbhgEIAEoCRIPCgdhZGRyZXNzGAUgASgJEg4KBnN0YXR1cxgGIAEoCRIbChNhdHRlc3RhdGlvbl9zdW1tYXJ5GAcgASgJEhsKE2F0dGVzdGF0aW9uX25vZGVfaWQYCCABKAkSFAoMY3NyX2tleV90eXBlGAkgASgJEhYKDmNzcl9zdWJqZWN0X2NuGAogASgJEhQKDHJlcXVlc3RlZF9hdBgLIAEoCRIYChByZWplY3Rpb25fcmVhc29uGAwgASgJEhoKEmFkbWl0dGVkX25vZGVfbmFtZRgNIAEoCRIMCgRraW5kGA4gASgJEhkKEXBpbm5lZF9rZXlfc2hhMjU2GA8gASgJEhgKEGFkbWl0dGVkX25vZGVfaWQYECABKAkizAEKF0NyZWF0ZUVucm9sbG1lbnRSZXF1ZXN0EgwKBGtpbmQYASABKAkSFQoNbm9kZV9lbmRwb2ludBgCIAEoCRIWCg5hZG1pbl9jZXJ0X3BlbRgDIAEoCRIVCg1hZG1pbl9rZXlfcGVtGAQgASgJEg4KBmNhX3BlbRgFIAEoCRISCgpjaGlsZF9ub2RlGAYgASgJEhEKCXBhcmVudF9jbhgHIAEoCRIPCgdwcm9maWxlGAggASgJEhUKDWNoaWxkX25vZGVfaWQYCSABKAkiUwoYQ3JlYXRlRW5yb2xsbWVudFJlc3BvbnNlEjcKCmVucm9sbG1lbnQYASABKAsyIy5jcnlwdG9zLmZsZWV0LnYxLkVucm9sbG1lbnRSZXF1ZXN0InwKGEFwcHJvdmVFbnJvbGxtZW50UmVxdWVzdBIKCgJpZBgBIAEoCRIVCg1ub2RlX2VuZHBvaW50GAIgASgJEhYKDmFkbWluX2NlcnRfcGVtGAMgASgJEhUKDWFkbWluX2tleV9wZW0YBCABKAkSDgoGY2FfcGVtGAUgASgJIlQKGUFwcHJvdmVFbnJvbGxtZW50UmVzcG9uc2USNwoKZW5yb2xsbWVudBgBIAEoCzIjLmNyeXB0b3MuZmxlZXQudjEuRW5yb2xsbWVudFJlcXVlc3QiNQoXUmVqZWN0RW5yb2xsbWVudFJlcXVlc3QSCgoCaWQYASABKAkSDgoGcmVhc29uGAIgASgJIlMKGFJlamVjdEVucm9sbG1lbnRSZXNwb25zZRI3CgplbnJvbGxtZW50GAEgASgLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50UmVxdWVzdCIPCg1XaG9BbUlSZXF1ZXN0Ij0KEE9wZXJhdG9ySWRlbnRpdHkSCgoCY24YASABKAkSDgoGc2VyaWFsGAIgASgJEg0KBWxldmVsGAMgASgJIkYKDldob0FtSVJlc3BvbnNlEjQKCG9wZXJhdG9yGAEgASgLMiIuY3J5cHRvcy5mbGVldC52MS5PcGVyYXRvcklkZW50aXR5ImcKGFJldm9rZUNlcnRpZmljYXRlUmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSEgoKc2VyaWFsX2hleBgCIAEoCRITCgtyZWFzb25fY29kZRgDIAEoBRIPCgdub2RlX2lkGAQgASgJIlgKGVJldm9rZUNlcnRpZmljYXRlUmVzcG9uc2USEgoKc2VyaWFsX2hleBgBIAEoCRISCgpyZXZva2VkX2F0GAIgASgJEhMKC3JlYXNvbl9jb2RlGAMgASgFIl0KEElzc3VlTGVhZlJlcXVlc3QSEQoJbm9kZV9uYW1lGAEgASgJEg8KB2Nzcl9kZXIYAiABKAwSFAoMcHJvZmlsZV9uYW1lGAMgASgJEg8KB25vZGVfaWQYBCABKAkiJQoRSXNzdWVMZWFmUmVzcG9uc2USEAoIY2VydF9kZXIYASABKAwiTAoQUmVrZXlOb2RlUmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSFAoMcHJvZmlsZV9uYW1lGAIgASgJEg8KB25vZGVfaWQYAyABKAkiTQoRUmVrZXlOb2RlUmVzcG9uc2USEgoKc3ViamVjdF9jbhgBIAEoCRIRCglpc3N1ZXJfY24YAiABKAkSEQoJY2hhaW5fbGVuGAMgASgFIjoKFEdldE5vZGVDb25maWdSZXF1ZXN0EhEKCW5vZGVfbmFtZRgBIAEoCRIPCgdub2RlX2lkGAIgASgJIkIKFUdldE5vZGVDb25maWdSZXNwb25zZRIpCgZjb25maWcYASABKAsyGS5jcnlwdG9zLnYxLk1hY2hpbmVDb25maWciZwoWQXBwbHlOb2RlQ29uZmlnUmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSKQoGY29uZmlnGAIgASgLMhkuY3J5cHRvcy52MS5NYWNoaW5lQ29uZmlnEg8KB25vZGVfaWQYAyABKAkiRgoXQXBwbHlOb2RlQ29uZmlnUmVzcG9uc2USEgoKZ2VuZXJhdGlvbhgBIAEoBBIXCg9yZXF1aXJlc19yZWJvb3QYAiABKAgiawoWU2V0Tm9kZVByb3RvY29sUmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSLQoIcHJvdG9jb2wYAiABKA4yGy5jcnlwdG9zLnYxLlNlcnZpY2VQcm90b2NvbBIPCgdlbmFibGVkGAMgASgIIkYKF1NldE5vZGVQcm90b2NvbFJlc3BvbnNlEhIKCmdlbmVyYXRpb24YASABKAQSFwoPcmVxdWlyZXNfcmVib290GAIgASgIIkwKEkV4cG9ydENBS2V5UmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSEgoKcGFzc3BocmFzZRgCIAEoDBIPCgdub2RlX2lkGAMgASgJIicKE0V4cG9ydENBS2V5UmVzcG9uc2USEAoIZW52ZWxvcGUYASABKAwiXgoSSW1wb3J0Q0FLZXlSZXF1ZXN0EhEKCW5vZGVfbmFtZRgBIAEoCRIQCghlbnZlbG9wZRgCIAEoDBISCgpwYXNzcGhyYXNlGAMgASgMEg8KB25vZGVfaWQYBCABKAkiPAoTSW1wb3J0Q0FLZXlSZXNwb25zZRISCgpzdWJqZWN0X2NuGAEgASgJEhEKCWlzc3Vlcl9jbhgCIAEoCSJVCh5Jc3N1ZU9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3QSEwoLY29tbW9uX25hbWUYASABKAkSDQoFbGV2ZWwYAiABKAkSDwoHY3NyX2RlchgDIAEoDCJHCh9Jc3N1ZU9wZXJhdG9yQ3JlZGVudGlhbFJlc3BvbnNlEhAKCGNlcnRfZGVyGAEgASgMEhIKCnNlcmlhbF9oZXgYAiABKAkiSgofUmV2b2tlT3BlcmF0b3JDcmVkZW50aWFsUmVxdWVzdBISCgpzZXJpYWxfaGV4GAEgASgJEhMKC3JlYXNvbl9jb2RlGAIgASgFIkoKIFJldm9rZU9wZXJhdG9yQ3JlZGVudGlhbFJlc3BvbnNlEhIKCnNlcmlhbF9oZXgYASABKAkSEgoKcmV2b2tlZF9hdBgCIAEoCSIgCh5MaXN0T3BlcmF0b3JDcmVkZW50aWFsc1JlcXVlc3QicAoST3BlcmF0b3JDcmVkZW50aWFsEhMKC2NvbW1vbl9uYW1lGAEgASgJEhIKCnNlcmlhbF9oZXgYAiABKAkSDQoFbGV2ZWwYAyABKAkSEQoJbm90X2FmdGVyGAQgASgJEg8KB3Jldm9rZWQYBSABKAgiVgofTGlzdE9wZXJhdG9yQ3JlZGVudGlhbHNSZXNwb25zZRIzCgVpdGVtcxgBIAMoCzIkLmNyeXB0b3MuZmxlZXQudjEuT3BlcmF0b3JDcmVkZW50aWFsIioKFlByZXZpZXdBZG9wdGlvblJlcXVlc3QSEAoIZW5kcG9pbnQYASABKAkiPwoXUHJldmlld0Fkb3B0aW9uUmVzcG9uc2USEwoLY2VydF9zaGEyNTYYASABKAkSDwoHc3ViamVjdBgCIAEoCSJHChdMaXN0SW5zdGFsbERpc2tzUmVxdWVzdBIQCghlbmRwb2ludBgBIAEoCRIaChJwaW5uZWRfY2VydF9zaGEyNTYYAiABKAkiQgoYTGlzdEluc3RhbGxEaXNrc1Jlc3BvbnNlEiYKBWRpc2tzGAEgAygLMhcuY3J5cHRvcy52MS5JbnN0YWxsRGlzayJrChBBZG9wdE5vZGVSZXF1ZXN0EhAKCGVuZHBvaW50GAEgASgJEhoKEnBpbm5lZF9jZXJ0X3NoYTI1NhgCIAEoCRIpCgZjb25maWcYAyABKAsyGS5jcnlwdG9zLnYxLk1hY2hpbmVDb25maWciUQoRQWRvcHROb2RlUmVzcG9uc2USDQoFcGhhc2UYASABKAkSDgoGZGV0YWlsGAIgASgJEgwKBGRvbmUYAyABKAgSDwoHbm9kZV9pZBgEIAEoCSJaChdEZWNvbW1pc3Npb25Ob2RlUmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSGwoTY29uZmlybV9jb21tb25fbmFtZRgCIAEoCRIPCgdub2RlX2lkGAMgASgJIi0KGERlY29tbWlzc2lvbk5vZGVSZXNwb25zZRIRCglyZWJvb3RpbmcYASABKAgiNgoRUmVuYW1lTm9kZVJlcXVlc3QSDwoHbm9kZV9pZBgBIAEoCRIQCghuZXdfbmFtZRgCIAEoCSJBChJSZW5hbWVOb2RlUmVzcG9uc2USKwoEbm9kZRgBIAEoCzIdLmNyeXB0b3MuZmxlZXQudjEuTm9kZVN1bW1hcnkiIQoSTGlzdE1jcEtleXNSZXF1ZXN0EgsKA2FsbBgBIAEoCCI+ChNMaXN0TWNwS2V5c1Jlc3BvbnNlEicKBWl0ZW1zGAEgAygLMhguY3J5cHRvcy5mbGVldC52MS5NY3BLZXkiIQoTUmV2b2tlTWNwS2V5UmVxdWVzdBIKCgJpZBgBIAEoCSJBChRSZXZva2VNY3BLZXlSZXNwb25zZRIpCgdtY3Bfa2V5GAEgASgLMhguY3J5cHRvcy5mbGVldC52MS5NY3BLZXkiOwoTQ3JlYXRlTWNwS2V5UmVxdWVzdBINCgVsYWJlbBgBIAEoCRIVCg1sZXZlbF9jZWlsaW5nGAIgASgJIlgKFENyZWF0ZU1jcEtleVJlc3BvbnNlEhUKDXBsYWludGV4dF9rZXkYASABKAkSKQoHbWNwX2tleRgCIAEoCzIYLmNyeXB0b3MuZmxlZXQudjEuTWNwS2V5IrsBCgZNY3BLZXkSCgoCaWQYASABKAkSDQoFbGFiZWwYAiABKAkSEwoLY2xpZW50X25hbWUYAyABKAkSEwoLb3BlcmF0b3JfY24YBCABKAkSFwoPb3BlcmF0b3Jfc2VyaWFsGAUgASgJEhUKDWxldmVsX2NlaWxpbmcYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRIUCgxsYXN0X3VzZWRfYXQYCCABKAkSEgoKcmV2b2tlZF9hdBgJIAEoCSImChRMaXN0QXBwcm92YWxzUmVxdWVzdBIOCgZzdGF0dXMYASABKAkiQgoVTGlzdEFwcHJvdmFsc1Jlc3BvbnNlEikKBWl0ZW1zGAEgAygLMhouY3J5cHRvcy5mbGVldC52MS5BcHByb3ZhbCI0ChVEZWNpZGVBcHByb3ZhbFJlcXVlc3QSCgoCaWQYASABKAkSDwoHYXBwcm92ZRgCIAEoCCJGChZEZWNpZGVBcHByb3ZhbFJlc3BvbnNlEiwKCGFwcHJvdmFsGAEgASgLMhouY3J5cHRvcy5mbGVldC52MS5BcHByb3ZhbCKpAgoIQXBwcm92YWwSCgoCaWQYASABKAkSDAoEdG9vbBgCIAEoCRIPCgdzdW1tYXJ5GAMgASgJEhYKDnJlcXVlc3RfZGlnZXN0GAQgASgJEhcKD3JlcXVlc3RlZF9ieV9jbhgFIAEoCRIbChNyZXF1ZXN0ZWRfYnlfc2VyaWFsGAYgASgJEg4KBmtleV9pZBgHIAEoCRIWCg5yZXF1aXJlZF9sZXZlbBgIIAEoCRISCgpjcmVhdGVkX2F0GAkgASgJEhIKCmV4cGlyZXNfYXQYCiABKAkSDgoGc3RhdHVzGAsgASgJEhUKDWRlY2lkZWRfYnlfY24YDCABKAkSGQoRZGVjaWRlZF9ieV9zZXJpYWwYDSABKAkSEgoKZGVjaWRlZF9hdBgOIAEoCSpSCgZIZWFsdGgSFgoSSEVBTFRIX1VOU1BFQ0lGSUVEEAASDQoJSEVBTFRIX1VQEAESDwoLSEVBTFRIX0RPV04QAhIQCgxIRUFMVEhfRVJST1IQAzKJHQoMRmxlZXRTZXJ2aWNlElQKCUxpc3ROb2RlcxIiLmNyeXB0b3MuZmxlZXQudjEuTGlzdE5vZGVzUmVxdWVzdBojLmNyeXB0b3MuZmxlZXQudjEuTGlzdE5vZGVzUmVzcG9uc2USTgoHR2V0Tm9kZRIgLmNyeXB0b3MuZmxlZXQudjEuR2V0Tm9kZVJlcXVlc3QaIS5jcnlwdG9zLmZsZWV0LnYxLkdldE5vZGVSZXNwb25zZRJpChBMaXN0Q2VydGlmaWNhdGVzEikuY3J5cHRvcy5mbGVldC52MS5MaXN0Q2VydGlmaWNhdGVzUmVxdWVzdBoqLmNyeXB0b3MuZmxlZXQudjEuTGlzdENlcnRpZmljYXRlc1Jlc3BvbnNlEl0KDExpc3RQcm9maWxlcxIlLmNyeXB0b3MuZmxlZXQudjEuTGlzdFByb2ZpbGVzUmVxdWVzdBomLmNyeXB0b3MuZmxlZXQudjEuTGlzdFByb2ZpbGVzUmVzcG9uc2USYAoNQ3JlYXRlUHJvZmlsZRImLmNyeXB0b3MuZmxlZXQudjEuQ3JlYXRlUHJvZmlsZVJlcXVlc3QaJy5jcnlwdG9zLmZsZWV0LnYxLkNyZWF0ZVByb2ZpbGVSZXNwb25zZRJgCg1VcGRhdGVQcm9maWxlEiYuY3J5cHRvcy5mbGVldC52MS5VcGRhdGVQcm9maWxlUmVxdWVzdBonLmNyeXB0b3MuZmxlZXQudjEuVXBkYXRlUHJvZmlsZVJlc3BvbnNlEmAKDURlbGV0ZVByb2ZpbGUSJi5jcnlwdG9zLmZsZWV0LnYxLkRlbGV0ZVByb2ZpbGVSZXF1ZXN0GicuY3J5cHRvcy5mbGVldC52MS5EZWxldGVQcm9maWxlUmVzcG9uc2USbwoSQXBwbHlQcm9maWxlVG9Ob2RlEisuY3J5cHRvcy5mbGVldC52MS5BcHBseVByb2ZpbGVUb05vZGVSZXF1ZXN0GiwuY3J5cHRvcy5mbGVldC52MS5BcHBseVByb2ZpbGVUb05vZGVSZXNwb25zZRJdCgxMaXN0QWRhcHRlcnMSJS5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBZGFwdGVyc1JlcXVlc3QaJi5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBZGFwdGVyc1Jlc3BvbnNlEmwKEVNldEFkYXB0ZXJFbmFibGVkEiouY3J5cHRvcy5mbGVldC52MS5TZXRBZGFwdGVyRW5hYmxlZFJlcXVlc3QaKy5jcnlwdG9zLmZsZWV0LnYxLlNldEFkYXB0ZXJFbmFibGVkUmVzcG9uc2USZgoPU2V0Tm9kZVByb3RvY29sEiguY3J5cHRvcy5mbGVldC52MS5TZXROb2RlUHJvdG9jb2xSZXF1ZXN0GikuY3J5cHRvcy5mbGVldC52MS5TZXROb2RlUHJvdG9jb2xSZXNwb25zZRJUCglMaXN0QXVkaXQSIi5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBdWRpdFJlcXVlc3QaIy5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBdWRpdFJlc3BvbnNlEmYKD0xpc3RFbnJvbGxtZW50cxIoLmNyeXB0b3MuZmxlZXQudjEuTGlzdEVucm9sbG1lbnRzUmVxdWVzdBopLmNyeXB0b3MuZmxlZXQudjEuTGlzdEVucm9sbG1lbnRzUmVzcG9uc2USaQoQQ3JlYXRlRW5yb2xsbWVudBIpLmNyeXB0b3MuZmxlZXQudjEuQ3JlYXRlRW5yb2xsbWVudFJlcXVlc3QaKi5jcnlwdG9zLmZsZWV0LnYxLkNyZWF0ZUVucm9sbG1lbnRSZXNwb25zZRJsChFBcHByb3ZlRW5yb2xsbWVudBIqLmNyeXB0b3MuZmxlZXQudjEuQXBwcm92ZUVucm9sbG1lbnRSZXF1ZXN0GisuY3J5cHRvcy5mbGVldC52MS5BcHByb3ZlRW5yb2xsbWVudFJlc3BvbnNlEmkKEFJlamVjdEVucm9sbG1lbnQSKS5jcnlwdG9zLmZsZWV0LnYxLlJlamVjdEVucm9sbG1lbnRSZXF1ZXN0GiouY3J5cHRvcy5mbGVldC52MS5SZWplY3RFbnJvbGxtZW50UmVzcG9uc2USSwoGV2hvQW1JEh8uY3J5cHRvcy5mbGVldC52MS5XaG9BbUlSZXF1ZXN0GiAuY3J5cHRvcy5mbGVldC52MS5XaG9BbUlSZXNwb25zZRJsChFSZXZva2VDZXJ0aWZpY2F0ZRIqLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlQ2VydGlmaWNhdGVSZXF1ZXN0GisuY3J5cHRvcy5mbGVldC52MS5SZXZva2VDZXJ0aWZpY2F0ZVJlc3BvbnNlElQKCUlzc3VlTGVhZhIiLmNyeXB0b3MuZmxlZXQudjEuSXNzdWVMZWFmUmVxdWVzdBojLmNyeXB0b3MuZmxlZXQudjEuSXNzdWVMZWFmUmVzcG9uc2USVAoJUmVrZXlOb2RlEiIuY3J5cHRvcy5mbGVldC52MS5SZWtleU5vZGVSZXF1ZXN0GiMuY3J5cHRvcy5mbGVldC52MS5SZWtleU5vZGVSZXNwb25zZRJgCg1HZXROb2RlQ29uZmlnEiYuY3J5cHRvcy5mbGVldC52MS5HZXROb2RlQ29uZmlnUmVxdWVzdBonLmNyeXB0b3MuZmxlZXQudjEuR2V0Tm9kZUNvbmZpZ1Jlc3BvbnNlEmYKD0FwcGx5Tm9kZUNvbmZpZxIoLmNyeXB0b3MuZmxlZXQudjEuQXBwbHlOb2RlQ29uZmlnUmVxdWVzdBopLmNyeXB0b3MuZmxlZXQudjEuQXBwbHlOb2RlQ29uZmlnUmVzcG9uc2USWgoLRXhwb3J0Q0FLZXkSJC5jcnlwdG9zLmZsZWV0LnYxLkV4cG9ydENBS2V5UmVxdWVzdBolLmNyeXB0b3MuZmxlZXQudjEuRXhwb3J0Q0FLZXlSZXNwb25zZRJaCgtJbXBvcnRDQUtleRIkLmNyeXB0b3MuZmxlZXQudjEuSW1wb3J0Q0FLZXlSZXF1ZXN0GiUuY3J5cHRvcy5mbGVldC52MS5JbXBvcnRDQUtleVJlc3BvbnNlEn4KF0lzc3VlT3BlcmF0b3JDcmVkZW50aWFsEjAuY3J5cHRvcy5mbGVldC52MS5Jc3N1ZU9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3QaMS5jcnlwdG9zLmZsZWV0LnYxLklzc3VlT3BlcmF0b3JDcmVkZW50aWFsUmVzcG9uc2USgQEKGFJldm9rZU9wZXJhdG9yQ3JlZGVudGlhbBIxLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlT3BlcmF0b3JDcmVkZW50aWFsUmVxdWVzdBoyLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlT3BlcmF0b3JDcmVkZW50aWFsUmVzcG9uc2USfgoXTGlzdE9wZXJhdG9yQ3JlZGVudGlhbHMSMC5jcnlwdG9zLmZsZWV0LnYxLkxpc3RPcGVyYXRvckNyZWRlbnRpYWxzUmVxdWVzdBoxLmNyeXB0b3MuZmxlZXQudjEuTGlzdE9wZXJhdG9yQ3JlZGVudGlhbHNSZXNwb25zZRJmCg9QcmV2aWV3QWRvcHRpb24SKC5jcnlwdG9zLmZsZWV0LnYxLlByZXZpZXdBZG9wdGlvblJlcXVlc3QaKS5jcnlwdG9zLmZsZWV0LnYxLlByZXZpZXdBZG9wdGlvblJlc3BvbnNlEmkKEExpc3RJbnN0YWxsRGlza3MSKS5jcnlwdG9zLmZsZWV0LnYxLkxpc3RJbnN0YWxsRGlza3NSZXF1ZXN0GiouY3J5cHRvcy5mbGVldC52MS5MaXN0SW5zdGFsbERpc2tzUmVzcG9uc2USVgoJQWRvcHROb2RlEiIuY3J5cHRvcy5mbGVldC52MS5BZG9wdE5vZGVSZXF1ZXN0GiMuY3J5cHRvcy5mbGVldC52MS5BZG9wdE5vZGVSZXNwb25zZTABEmkKEERlY29tbWlzc2lvbk5vZGUSKS5jcnlwdG9zLmZsZWV0LnYxLkRlY29tbWlzc2lvbk5vZGVSZXF1ZXN0GiouY3J5cHRvcy5mbGVldC52MS5EZWNvbW1pc3Npb25Ob2RlUmVzcG9uc2USVwoKUmVuYW1lTm9kZRIjLmNyeXB0b3MuZmxlZXQudjEuUmVuYW1lTm9kZVJlcXVlc3QaJC5jcnlwdG9zLmZsZWV0LnYxLlJlbmFtZU5vZGVSZXNwb25zZRJaCgtMaXN0TWNwS2V5cxIkLmNyeXB0b3MuZmxlZXQudjEuTGlzdE1jcEtleXNSZXF1ZXN0GiUuY3J5cHRvcy5mbGVldC52MS5MaXN0TWNwS2V5c1Jlc3BvbnNlEl0KDFJldm9rZU1jcEtleRIlLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlTWNwS2V5UmVxdWVzdBomLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlTWNwS2V5UmVzcG9uc2USXQoMQ3JlYXRlTWNwS2V5EiUuY3J5cHRvcy5mbGVldC52MS5DcmVhdGVNY3BLZXlSZXF1ZXN0GiYuY3J5cHRvcy5mbGVldC52MS5DcmVhdGVNY3BLZXlSZXNwb25zZRJgCg1MaXN0QXBwcm92YWxzEiYuY3J5cHRvcy5mbGVldC52MS5MaXN0QXBwcm92YWxzUmVxdWVzdBonLmNyeXB0b3MuZmxlZXQudjEuTGlzdEFwcHJvdmFsc1Jlc3BvbnNlEmMKDkRlY2lkZUFwcHJvdmFsEicuY3J5cHRvcy5mbGVldC52MS5EZWNpZGVBcHByb3ZhbFJlcXVlc3QaKC5jcnlwdG9zLmZsZWV0LnYxLkRlY2lkZUFwcHJvdmFsUmVzcG9uc2VCOFo2Z2l0aHViLmNvbS9DcnlwdE9TLVBLSS9hcGkvZ28vY3J5cHRvcy9mbGVldC92MTtmbGVldHYxYgZwcm90bzM", [file_cryptos_v1_config, file_cryptos_v1_node, file_cryptos_v1_status]);
+ fileDesc("ChxjcnlwdG9zL2ZsZWV0L3YxL2ZsZWV0LnByb3RvEhBjcnlwdG9zLmZsZWV0LnYxIhIKEExpc3ROb2Rlc1JlcXVlc3QiQQoRTGlzdE5vZGVzUmVzcG9uc2USLAoFbm9kZXMYASADKAsyHS5jcnlwdG9zLmZsZWV0LnYxLk5vZGVTdW1tYXJ5Ii8KDkdldE5vZGVSZXF1ZXN0EgwKBG5hbWUYASABKAkSDwoHbm9kZV9pZBgCIAEoCSI9Cg9HZXROb2RlUmVzcG9uc2USKgoEbm9kZRgBIAEoCzIcLmNyeXB0b3MuZmxlZXQudjEuTm9kZURldGFpbCI4ChdMaXN0Q2VydGlmaWNhdGVzUmVxdWVzdBIMCgRub2RlGAEgASgJEg8KB25vZGVfaWQYAiABKAkiTwoYTGlzdENlcnRpZmljYXRlc1Jlc3BvbnNlEjMKDGNlcnRpZmljYXRlcxgBIAMoCzIdLmNyeXB0b3MuZmxlZXQudjEuQ2VydGlmaWNhdGUiFQoTTGlzdFByb2ZpbGVzUmVxdWVzdCJFChRMaXN0UHJvZmlsZXNSZXNwb25zZRItCgVpdGVtcxgBIAMoCzIeLmNyeXB0b3MudjEuQ2VydGlmaWNhdGVQcm9maWxlIkcKFENyZWF0ZVByb2ZpbGVSZXF1ZXN0Ei8KB3Byb2ZpbGUYASABKAsyHi5jcnlwdG9zLnYxLkNlcnRpZmljYXRlUHJvZmlsZSIXChVDcmVhdGVQcm9maWxlUmVzcG9uc2UiRwoUVXBkYXRlUHJvZmlsZVJlcXVlc3QSLwoHcHJvZmlsZRgBIAEoCzIeLmNyeXB0b3MudjEuQ2VydGlmaWNhdGVQcm9maWxlIhcKFVVwZGF0ZVByb2ZpbGVSZXNwb25zZSIkChREZWxldGVQcm9maWxlUmVxdWVzdBIMCgRuYW1lGAEgASgJIhcKFURlbGV0ZVByb2ZpbGVSZXNwb25zZSJVChlBcHBseVByb2ZpbGVUb05vZGVSZXF1ZXN0EhEKCW5vZGVfbmFtZRgBIAEoCRIUCgxwcm9maWxlX25hbWUYAiABKAkSDwoHbm9kZV9pZBgDIAEoCSJJChpBcHBseVByb2ZpbGVUb05vZGVSZXNwb25zZRISCgpnZW5lcmF0aW9uGAEgASgEEhcKD3JlcXVpcmVzX3JlYm9vdBgCIAEoCCIVChNMaXN0QWRhcHRlcnNSZXF1ZXN0IkoKFExpc3RBZGFwdGVyc1Jlc3BvbnNlEjIKBWl0ZW1zGAEgAygLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50QWRhcHRlciI5ChhTZXRBZGFwdGVyRW5hYmxlZFJlcXVlc3QSDAoEbmFtZRgBIAEoCRIPCgdlbmFibGVkGAIgASgIIlEKGVNldEFkYXB0ZXJFbmFibGVkUmVzcG9uc2USNAoHYWRhcHRlchgBIAEoCzIjLmNyeXB0b3MuZmxlZXQudjEuRW5yb2xsbWVudEFkYXB0ZXIiEgoQTGlzdEF1ZGl0UmVxdWVzdCJAChFMaXN0QXVkaXRSZXNwb25zZRIrCgVpdGVtcxgBIAMoCzIcLmNyeXB0b3MuZmxlZXQudjEuQXVkaXRFdmVudCIYChZMaXN0RW5yb2xsbWVudHNSZXF1ZXN0Ik0KF0xpc3RFbnJvbGxtZW50c1Jlc3BvbnNlEjIKBWl0ZW1zGAEgAygLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50UmVxdWVzdCKDAgoLTm9kZVN1bW1hcnkSDAoEbmFtZRgBIAEoCRIPCgdhZGRyZXNzGAIgASgJEgwKBHJvbGUYAyABKAkSFgoOaWRlbnRpdHlfc3RhdGUYBCABKAkSCgoCY24YBSABKAkSDgoGaXNzdWVyGAYgASgJEigKBmhlYWx0aBgHIAEoDjIYLmNyeXB0b3MuZmxlZXQudjEuSGVhbHRoEhUKDWhlYWx0aF9kZXRhaWwYCCABKAkSCgoCaWQYCSABKAkSLQoJcHJvdG9jb2xzGAogAygLMhouY3J5cHRvcy52MS5Qcm90b2NvbFN0YXR1cxIXCg9yZWJvb3RfcmVxdWlyZWQYCyABKAgiSQoMTm9kZUlkZW50aXR5EhEKCWNoYWluX3BlbRgBIAEoCRIRCgljaGFpbl9kZXIYAiADKAwSEwoLbGVhZl9zaGEyNTYYAyABKAkiqQEKCk5vZGVEZXRhaWwSLgoHc3VtbWFyeRgBIAEoCzIdLmNyeXB0b3MuZmxlZXQudjEuTm9kZVN1bW1hcnkSMAoIaWRlbnRpdHkYAiABKAsyHi5jcnlwdG9zLmZsZWV0LnYxLk5vZGVJZGVudGl0eRIVCg10cG1fYXZhaWxhYmxlGAMgASgIEhIKCmJvb3RfY291bnQYBCABKAQSDgoGdXB0aW1lGAUgASgJItgBCgtDZXJ0aWZpY2F0ZRIOCgZzZXJpYWwYASABKAkSEgoKc3ViamVjdF9jbhgCIAEoCRITCgtpc3N1ZXJfbm9kZRgDIAEoCRIMCgRraW5kGAQgASgJEg4KBnN0YXR1cxgFIAEoCRISCgpub3RfYmVmb3JlGAYgASgJEhEKCW5vdF9hZnRlchgHIAEoCRIPCgdwcm9maWxlGAggASgJEhIKCnJldm9rZWRfYXQYCSABKAkSDgoGcmVhc29uGAogASgJEhYKDmlzc3Vlcl9ub2RlX2lkGAsgASgJIo0BChFFbnJvbGxtZW50QWRhcHRlchIMCgRraW5kGAEgASgJEgwKBG5hbWUYAiABKAkSEAoIZW5kcG9pbnQYAyABKAkSDwoHcHJvZmlsZRgEIAEoCRIPCgdlbmFibGVkGAUgASgIEhIKCmNoYWxsZW5nZXMYBiADKAkSFAoMZ3BvX3RlbXBsYXRlGAcgASgJIrwCCgpBdWRpdEV2ZW50EgoKAmlkGAEgASgJEgoKAmF0GAIgASgJEgwKBGtpbmQYAyABKAkSDwoHc3VtbWFyeRgEIAEoCRITCgt0YXJnZXRfa2luZBgFIAEoCRITCgt0YXJnZXRfcGF0aBgGIAEoCRISCgphY3Rvcl9raW5kGAcgASgJEhAKCGFjdG9yX2NuGAggASgJEhQKDGFjdG9yX3NlcmlhbBgJIAEoCRIOCgZrZXlfaWQYCiABKAkSCwoDdmlhGAsgASgJEgwKBHRvb2wYDCABKAkSFgoOcmVxdWVzdF9kaWdlc3QYDSABKAkSDwoHb3V0Y29tZRgOIAEoCRITCgthcHByb3ZhbF9pZBgPIAEoCRIXCg9hcHByb3Zlcl9zZXJpYWwYECABKAkSDwoHbm9kZV9pZBgRIAEoCSLvAgoRRW5yb2xsbWVudFJlcXVlc3QSCgoCaWQYASABKAkSFQoNcHJvcG9zZWRfbmFtZRgCIAEoCRIMCgRyb2xlGAMgASgJEhEKCXBhcmVudF9jbhgEIAEoCRIPCgdhZGRyZXNzGAUgASgJEg4KBnN0YXR1cxgGIAEoCRIbChNhdHRlc3RhdGlvbl9zdW1tYXJ5GAcgASgJEhsKE2F0dGVzdGF0aW9uX25vZGVfaWQYCCABKAkSFAoMY3NyX2tleV90eXBlGAkgASgJEhYKDmNzcl9zdWJqZWN0X2NuGAogASgJEhQKDHJlcXVlc3RlZF9hdBgLIAEoCRIYChByZWplY3Rpb25fcmVhc29uGAwgASgJEhoKEmFkbWl0dGVkX25vZGVfbmFtZRgNIAEoCRIMCgRraW5kGA4gASgJEhkKEXBpbm5lZF9rZXlfc2hhMjU2GA8gASgJEhgKEGFkbWl0dGVkX25vZGVfaWQYECABKAkizAEKF0NyZWF0ZUVucm9sbG1lbnRSZXF1ZXN0EgwKBGtpbmQYASABKAkSFQoNbm9kZV9lbmRwb2ludBgCIAEoCRIWCg5hZG1pbl9jZXJ0X3BlbRgDIAEoCRIVCg1hZG1pbl9rZXlfcGVtGAQgASgJEg4KBmNhX3BlbRgFIAEoCRISCgpjaGlsZF9ub2RlGAYgASgJEhEKCXBhcmVudF9jbhgHIAEoCRIPCgdwcm9maWxlGAggASgJEhUKDWNoaWxkX25vZGVfaWQYCSABKAkiUwoYQ3JlYXRlRW5yb2xsbWVudFJlc3BvbnNlEjcKCmVucm9sbG1lbnQYASABKAsyIy5jcnlwdG9zLmZsZWV0LnYxLkVucm9sbG1lbnRSZXF1ZXN0InwKGEFwcHJvdmVFbnJvbGxtZW50UmVxdWVzdBIKCgJpZBgBIAEoCRIVCg1ub2RlX2VuZHBvaW50GAIgASgJEhYKDmFkbWluX2NlcnRfcGVtGAMgASgJEhUKDWFkbWluX2tleV9wZW0YBCABKAkSDgoGY2FfcGVtGAUgASgJIlQKGUFwcHJvdmVFbnJvbGxtZW50UmVzcG9uc2USNwoKZW5yb2xsbWVudBgBIAEoCzIjLmNyeXB0b3MuZmxlZXQudjEuRW5yb2xsbWVudFJlcXVlc3QiNQoXUmVqZWN0RW5yb2xsbWVudFJlcXVlc3QSCgoCaWQYASABKAkSDgoGcmVhc29uGAIgASgJIlMKGFJlamVjdEVucm9sbG1lbnRSZXNwb25zZRI3CgplbnJvbGxtZW50GAEgASgLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50UmVxdWVzdCIPCg1XaG9BbUlSZXF1ZXN0Ij0KEE9wZXJhdG9ySWRlbnRpdHkSCgoCY24YASABKAkSDgoGc2VyaWFsGAIgASgJEg0KBWxldmVsGAMgASgJIkYKDldob0FtSVJlc3BvbnNlEjQKCG9wZXJhdG9yGAEgASgLMiIuY3J5cHRvcy5mbGVldC52MS5PcGVyYXRvcklkZW50aXR5ImcKGFJldm9rZUNlcnRpZmljYXRlUmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSEgoKc2VyaWFsX2hleBgCIAEoCRITCgtyZWFzb25fY29kZRgDIAEoBRIPCgdub2RlX2lkGAQgASgJIlgKGVJldm9rZUNlcnRpZmljYXRlUmVzcG9uc2USEgoKc2VyaWFsX2hleBgBIAEoCRISCgpyZXZva2VkX2F0GAIgASgJEhMKC3JlYXNvbl9jb2RlGAMgASgFIj4KFUdldENlcnRpZmljYXRlUmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSEgoKc2VyaWFsX2hleBgCIAEoCSJoChZHZXRDZXJ0aWZpY2F0ZVJlc3BvbnNlEhcKD2NlcnRpZmljYXRlX3BlbRgBIAEoCRIRCgljaGFpbl9wZW0YAiABKAkSDgoGc3RhdHVzGAMgASgJEhIKCnJldm9rZWRfYXQYBCABKAkiXQoQSXNzdWVMZWFmUmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSDwoHY3NyX2RlchgCIAEoDBIUCgxwcm9maWxlX25hbWUYAyABKAkSDwoHbm9kZV9pZBgEIAEoCSIlChFJc3N1ZUxlYWZSZXNwb25zZRIQCghjZXJ0X2RlchgBIAEoDCJMChBSZWtleU5vZGVSZXF1ZXN0EhEKCW5vZGVfbmFtZRgBIAEoCRIUCgxwcm9maWxlX25hbWUYAiABKAkSDwoHbm9kZV9pZBgDIAEoCSJNChFSZWtleU5vZGVSZXNwb25zZRISCgpzdWJqZWN0X2NuGAEgASgJEhEKCWlzc3Vlcl9jbhgCIAEoCRIRCgljaGFpbl9sZW4YAyABKAUiOgoUR2V0Tm9kZUNvbmZpZ1JlcXVlc3QSEQoJbm9kZV9uYW1lGAEgASgJEg8KB25vZGVfaWQYAiABKAkiQgoVR2V0Tm9kZUNvbmZpZ1Jlc3BvbnNlEikKBmNvbmZpZxgBIAEoCzIZLmNyeXB0b3MudjEuTWFjaGluZUNvbmZpZyJnChZBcHBseU5vZGVDb25maWdSZXF1ZXN0EhEKCW5vZGVfbmFtZRgBIAEoCRIpCgZjb25maWcYAiABKAsyGS5jcnlwdG9zLnYxLk1hY2hpbmVDb25maWcSDwoHbm9kZV9pZBgDIAEoCSJGChdBcHBseU5vZGVDb25maWdSZXNwb25zZRISCgpnZW5lcmF0aW9uGAEgASgEEhcKD3JlcXVpcmVzX3JlYm9vdBgCIAEoCCJrChZTZXROb2RlUHJvdG9jb2xSZXF1ZXN0EhEKCW5vZGVfbmFtZRgBIAEoCRItCghwcm90b2NvbBgCIAEoDjIbLmNyeXB0b3MudjEuU2VydmljZVByb3RvY29sEg8KB2VuYWJsZWQYAyABKAgiRgoXU2V0Tm9kZVByb3RvY29sUmVzcG9uc2USEgoKZ2VuZXJhdGlvbhgBIAEoBBIXCg9yZXF1aXJlc19yZWJvb3QYAiABKAgiTAoSRXhwb3J0Q0FLZXlSZXF1ZXN0EhEKCW5vZGVfbmFtZRgBIAEoCRISCgpwYXNzcGhyYXNlGAIgASgMEg8KB25vZGVfaWQYAyABKAkiJwoTRXhwb3J0Q0FLZXlSZXNwb25zZRIQCghlbnZlbG9wZRgBIAEoDCJeChJJbXBvcnRDQUtleVJlcXVlc3QSEQoJbm9kZV9uYW1lGAEgASgJEhAKCGVudmVsb3BlGAIgASgMEhIKCnBhc3NwaHJhc2UYAyABKAwSDwoHbm9kZV9pZBgEIAEoCSI8ChNJbXBvcnRDQUtleVJlc3BvbnNlEhIKCnN1YmplY3RfY24YASABKAkSEQoJaXNzdWVyX2NuGAIgASgJIm8KH1Jldm9rZU9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3QSEgoKc2VyaWFsX2hleBgBIAEoCRITCgtyZWFzb25fY29kZRgCIAEoBRIVCg1pc3N1ZXJfc2hhMjU2GAMgASgJEgwKBG5vdGUYBCABKAkicwogUmV2b2tlT3BlcmF0b3JDcmVkZW50aWFsUmVzcG9uc2USEgoKc2VyaWFsX2hleBgBIAEoCRISCgpyZXZva2VkX2F0GAIgASgJEhUKDWlzc3Vlcl9zaGEyNTYYAyABKAkSEAoId2FybmluZ3MYBCADKAkiIAoeTGlzdE9wZXJhdG9yQ3JlZGVudGlhbHNSZXF1ZXN0Io0CChJPcGVyYXRvckNyZWRlbnRpYWwSEwoLY29tbW9uX25hbWUYASABKAkSEgoKc2VyaWFsX2hleBgCIAEoCRINCgVsZXZlbBgDIAEoCRIRCglub3RfYWZ0ZXIYBCABKAkSDwoHcmV2b2tlZBgFIAEoCBIMCgRraW5kGAYgASgJEhUKDWlzc3Vlcl9zaGEyNTYYByABKAkSDQoFZW1haWwYCCABKAkSEQoJZnVsbF9uYW1lGAkgASgJEhIKCmRlbnlsaXN0ZWQYCiABKAgSEwoLY3JsX3Jldm9rZWQYCyABKAgSFQoNZmlyc3Rfc2Vlbl9hdBgMIAEoCRIUCgxsYXN0X3NlZW5fYXQYDSABKAkiVgofTGlzdE9wZXJhdG9yQ3JlZGVudGlhbHNSZXNwb25zZRIzCgVpdGVtcxgBIAMoCzIkLmNyeXB0b3MuZmxlZXQudjEuT3BlcmF0b3JDcmVkZW50aWFsImoKJkNyZWF0ZU9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3RSZXF1ZXN0Eg0KBWxldmVsGAEgASgJEg0KBWVtYWlsGAIgASgJEhEKCWZ1bGxfbmFtZRgDIAEoCRIPCgdjc3JfZGVyGAQgASgMIpQBCidDcmVhdGVPcGVyYXRvckNyZWRlbnRpYWxSZXF1ZXN0UmVzcG9uc2USEgoKcmVxdWVzdF9pZBgBIAEoCRIPCgdjc3JfcGVtGAIgASgJEhcKD2V4dGZpbGVfc2VjdGlvbhgDIAEoCRIXCg9vcGVuc3NsX2NvbW1hbmQYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoCSI2CiVMaXN0T3BlcmF0b3JDcmVkZW50aWFsUmVxdWVzdHNSZXF1ZXN0Eg0KBXN0YXRlGAEgASgJImQKJkxpc3RPcGVyYXRvckNyZWRlbnRpYWxSZXF1ZXN0c1Jlc3BvbnNlEjoKBWl0ZW1zGAEgAygLMisuY3J5cHRvcy5mbGVldC52MS5PcGVyYXRvckNyZWRlbnRpYWxSZXF1ZXN0IjwKJkNhbmNlbE9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3RSZXF1ZXN0EhIKCnJlcXVlc3RfaWQYASABKAkiZwonQ2FuY2VsT3BlcmF0b3JDcmVkZW50aWFsUmVxdWVzdFJlc3BvbnNlEjwKB3JlcXVlc3QYASABKAsyKy5jcnlwdG9zLmZsZWV0LnYxLk9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3Qi0QEKGU9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3QSCgoCaWQYASABKAkSDQoFbGV2ZWwYAiABKAkSDQoFZW1haWwYAyABKAkSEQoJZnVsbF9uYW1lGAQgASgJEg8KB2Nzcl9wZW0YBSABKAkSDQoFc3RhdGUYBiABKAkSFQoNY3JlYXRlZF9ieV9jbhgHIAEoCRISCgpjcmVhdGVkX2F0GAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkSGAoQY29tcGxldGVkX3NlcmlhbBgKIAEoCSJaCh9SZWNvcmRPcGVyYXRvckNyZWRlbnRpYWxSZXF1ZXN0EhAKCGNlcnRfZGVyGAEgASgMEhIKCnJlcXVlc3RfaWQYAiABKAkSEQoJZnVsbF9uYW1lGAMgASgJIm4KIFJlY29yZE9wZXJhdG9yQ3JlZGVudGlhbFJlc3BvbnNlEjgKCmNyZWRlbnRpYWwYASABKAsyJC5jcnlwdG9zLmZsZWV0LnYxLk9wZXJhdG9yQ3JlZGVudGlhbBIQCgh3YXJuaW5ncxgCIAMoCSIYChZMaXN0T3BlcmF0b3JDQXNSZXF1ZXN0IkYKF0xpc3RPcGVyYXRvckNBc1Jlc3BvbnNlEisKBWl0ZW1zGAEgAygLMhwuY3J5cHRvcy5mbGVldC52MS5PcGVyYXRvckNBIpACChlSZWdpc3Rlck9wZXJhdG9yQ0FSZXF1ZXN0EhMKC2NhX2NlcnRfZGVyGAEgASgMEg0KA3VybBgCIAEoCUgAEhEKB2NybF9kZXIYAyABKAxIABIOCgRub25lGAQgASgISAASLQoJb2NzcF9tb2RlGAUgASgOMhouY3J5cHRvcy5mbGVldC52MS5PY3NwTW9kZRIQCghvY3NwX3VybBgGIAEoCRJFChBhY2tub3dsZWRnZW1lbnRzGAcgAygOMisuY3J5cHRvcy5mbGVldC52MS5PcGVyYXRvckNBQWNrbm93bGVkZ2VtZW50EhYKDmNvbmZpcm1fc2hhMjU2GAggASgJQgwKCmNybF9zb3VyY2UisAEKGlJlZ2lzdGVyT3BlcmF0b3JDQVJlc3BvbnNlEjEKC29wZXJhdG9yX2NhGAEgASgLMhwuY3J5cHRvcy5mbGVldC52MS5PcGVyYXRvckNBEhEKCWNvbmZpcm1lZBgCIAEoCBIVCg1hZG1pbl9leHRmaWxlGAMgASgJEjUKCm9jc3BfcHJvYmUYBCABKAsyIS5jcnlwdG9zLmZsZWV0LnYxLk9jc3BQcm9iZVJlc3VsdCJMChdSZXRpcmVPcGVyYXRvckNBUmVxdWVzdBIOCgZzaGEyNTYYASABKAkSIQoZaV91bmRlcnN0YW5kX3NlbGZfbG9ja291dBgCIAEoCCJNChhSZXRpcmVPcGVyYXRvckNBUmVzcG9uc2USMQoLb3BlcmF0b3JfY2EYASABKAsyHC5jcnlwdG9zLmZsZWV0LnYxLk9wZXJhdG9yQ0EitgEKHVNldE9wZXJhdG9yQ0FDUkxTb3VyY2VSZXF1ZXN0Eg4KBnNoYTI1NhgBIAEoCRINCgN1cmwYAiABKAlIABIOCgRub25lGAMgASgISAASEQoHY3JsX2RlchgFIAEoDEgAEkUKEGFja25vd2xlZGdlbWVudHMYBCADKA4yKy5jcnlwdG9zLmZsZWV0LnYxLk9wZXJhdG9yQ0FBY2tub3dsZWRnZW1lbnRCDAoKY3JsX3NvdXJjZSJTCh5TZXRPcGVyYXRvckNBQ1JMU291cmNlUmVzcG9uc2USMQoLb3BlcmF0b3JfY2EYASABKAsyHC5jcnlwdG9zLmZsZWV0LnYxLk9wZXJhdG9yQ0EiOwoYVXBsb2FkT3BlcmF0b3JDUkxSZXF1ZXN0Eg4KBnNoYTI1NhgBIAEoCRIPCgdjcmxfZGVyGAIgASgMIk4KGVVwbG9hZE9wZXJhdG9yQ1JMUmVzcG9uc2USMQoLb3BlcmF0b3JfY2EYASABKAsyHC5jcnlwdG9zLmZsZWV0LnYxLk9wZXJhdG9yQ0EiawoYU2V0T3BlcmF0b3JDQU9DU1BSZXF1ZXN0Eg4KBnNoYTI1NhgBIAEoCRItCglvY3NwX21vZGUYAiABKA4yGi5jcnlwdG9zLmZsZWV0LnYxLk9jc3BNb2RlEhAKCG9jc3BfdXJsGAMgASgJIoUBChlTZXRPcGVyYXRvckNBT0NTUFJlc3BvbnNlEjEKC29wZXJhdG9yX2NhGAEgASgLMhwuY3J5cHRvcy5mbGVldC52MS5PcGVyYXRvckNBEjUKCm9jc3BfcHJvYmUYAiABKAsyIS5jcnlwdG9zLmZsZWV0LnYxLk9jc3BQcm9iZVJlc3VsdCIqChZQcmV2aWV3QWRvcHRpb25SZXF1ZXN0EhAKCGVuZHBvaW50GAEgASgJIj8KF1ByZXZpZXdBZG9wdGlvblJlc3BvbnNlEhMKC2NlcnRfc2hhMjU2GAEgASgJEg8KB3N1YmplY3QYAiABKAkiRwoXTGlzdEluc3RhbGxEaXNrc1JlcXVlc3QSEAoIZW5kcG9pbnQYASABKAkSGgoScGlubmVkX2NlcnRfc2hhMjU2GAIgASgJIkIKGExpc3RJbnN0YWxsRGlza3NSZXNwb25zZRImCgVkaXNrcxgBIAMoCzIXLmNyeXB0b3MudjEuSW5zdGFsbERpc2siawoQQWRvcHROb2RlUmVxdWVzdBIQCghlbmRwb2ludBgBIAEoCRIaChJwaW5uZWRfY2VydF9zaGEyNTYYAiABKAkSKQoGY29uZmlnGAMgASgLMhkuY3J5cHRvcy52MS5NYWNoaW5lQ29uZmlnIoUBChFBZG9wdE5vZGVSZXNwb25zZRINCgVwaGFzZRgBIAEoCRIOCgZkZXRhaWwYAiABKAkSDAoEZG9uZRgDIAEoCBIPCgdub2RlX2lkGAQgASgJEhMKC2Fkb3B0aW9uX2lkGAUgASgJEh0KFXByZXNlbnRlZF9jZXJ0X3NoYTI1NhgGIAEoCSJNCiFDb25maXJtQWRvcHRpb25GaW5nZXJwcmludFJlcXVlc3QSEwoLYWRvcHRpb25faWQYASABKAkSEwoLY2VydF9zaGEyNTYYAiABKAkiJAoiQ29uZmlybUFkb3B0aW9uRmluZ2VycHJpbnRSZXNwb25zZSJaChdEZWNvbW1pc3Npb25Ob2RlUmVxdWVzdBIRCglub2RlX25hbWUYASABKAkSGwoTY29uZmlybV9jb21tb25fbmFtZRgCIAEoCRIPCgdub2RlX2lkGAMgASgJIi0KGERlY29tbWlzc2lvbk5vZGVSZXNwb25zZRIRCglyZWJvb3RpbmcYASABKAgiNgoRUmVuYW1lTm9kZVJlcXVlc3QSDwoHbm9kZV9pZBgBIAEoCRIQCghuZXdfbmFtZRgCIAEoCSJBChJSZW5hbWVOb2RlUmVzcG9uc2USKwoEbm9kZRgBIAEoCzIdLmNyeXB0b3MuZmxlZXQudjEuTm9kZVN1bW1hcnkiIQoSTGlzdE1jcEtleXNSZXF1ZXN0EgsKA2FsbBgBIAEoCCI+ChNMaXN0TWNwS2V5c1Jlc3BvbnNlEicKBWl0ZW1zGAEgAygLMhguY3J5cHRvcy5mbGVldC52MS5NY3BLZXkiIQoTUmV2b2tlTWNwS2V5UmVxdWVzdBIKCgJpZBgBIAEoCSJBChRSZXZva2VNY3BLZXlSZXNwb25zZRIpCgdtY3Bfa2V5GAEgASgLMhguY3J5cHRvcy5mbGVldC52MS5NY3BLZXkiOwoTQ3JlYXRlTWNwS2V5UmVxdWVzdBINCgVsYWJlbBgBIAEoCRIVCg1sZXZlbF9jZWlsaW5nGAIgASgJIlgKFENyZWF0ZU1jcEtleVJlc3BvbnNlEhUKDXBsYWludGV4dF9rZXkYASABKAkSKQoHbWNwX2tleRgCIAEoCzIYLmNyeXB0b3MuZmxlZXQudjEuTWNwS2V5IrsBCgZNY3BLZXkSCgoCaWQYASABKAkSDQoFbGFiZWwYAiABKAkSEwoLY2xpZW50X25hbWUYAyABKAkSEwoLb3BlcmF0b3JfY24YBCABKAkSFwoPb3BlcmF0b3Jfc2VyaWFsGAUgASgJEhUKDWxldmVsX2NlaWxpbmcYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRIUCgxsYXN0X3VzZWRfYXQYCCABKAkSEgoKcmV2b2tlZF9hdBgJIAEoCSImChRMaXN0QXBwcm92YWxzUmVxdWVzdBIOCgZzdGF0dXMYASABKAkiQgoVTGlzdEFwcHJvdmFsc1Jlc3BvbnNlEikKBWl0ZW1zGAEgAygLMhouY3J5cHRvcy5mbGVldC52MS5BcHByb3ZhbCI0ChVEZWNpZGVBcHByb3ZhbFJlcXVlc3QSCgoCaWQYASABKAkSDwoHYXBwcm92ZRgCIAEoCCJGChZEZWNpZGVBcHByb3ZhbFJlc3BvbnNlEiwKCGFwcHJvdmFsGAEgASgLMhouY3J5cHRvcy5mbGVldC52MS5BcHByb3ZhbCKpAgoIQXBwcm92YWwSCgoCaWQYASABKAkSDAoEdG9vbBgCIAEoCRIPCgdzdW1tYXJ5GAMgASgJEhYKDnJlcXVlc3RfZGlnZXN0GAQgASgJEhcKD3JlcXVlc3RlZF9ieV9jbhgFIAEoCRIbChNyZXF1ZXN0ZWRfYnlfc2VyaWFsGAYgASgJEg4KBmtleV9pZBgHIAEoCRIWCg5yZXF1aXJlZF9sZXZlbBgIIAEoCRISCgpjcmVhdGVkX2F0GAkgASgJEhIKCmV4cGlyZXNfYXQYCiABKAkSDgoGc3RhdHVzGAsgASgJEhUKDWRlY2lkZWRfYnlfY24YDCABKAkSGQoRZGVjaWRlZF9ieV9zZXJpYWwYDSABKAkSEgoKZGVjaWRlZF9hdBgOIAEoCSpSCgZIZWFsdGgSFgoSSEVBTFRIX1VOU1BFQ0lGSUVEEAASDQoJSEVBTFRIX1VQEAESDwoLSEVBTFRIX0RPV04QAhIQCgxIRUFMVEhfRVJST1IQAzLhJwoMRmxlZXRTZXJ2aWNlElQKCUxpc3ROb2RlcxIiLmNyeXB0b3MuZmxlZXQudjEuTGlzdE5vZGVzUmVxdWVzdBojLmNyeXB0b3MuZmxlZXQudjEuTGlzdE5vZGVzUmVzcG9uc2USTgoHR2V0Tm9kZRIgLmNyeXB0b3MuZmxlZXQudjEuR2V0Tm9kZVJlcXVlc3QaIS5jcnlwdG9zLmZsZWV0LnYxLkdldE5vZGVSZXNwb25zZRJpChBMaXN0Q2VydGlmaWNhdGVzEikuY3J5cHRvcy5mbGVldC52MS5MaXN0Q2VydGlmaWNhdGVzUmVxdWVzdBoqLmNyeXB0b3MuZmxlZXQudjEuTGlzdENlcnRpZmljYXRlc1Jlc3BvbnNlEl0KDExpc3RQcm9maWxlcxIlLmNyeXB0b3MuZmxlZXQudjEuTGlzdFByb2ZpbGVzUmVxdWVzdBomLmNyeXB0b3MuZmxlZXQudjEuTGlzdFByb2ZpbGVzUmVzcG9uc2USYAoNQ3JlYXRlUHJvZmlsZRImLmNyeXB0b3MuZmxlZXQudjEuQ3JlYXRlUHJvZmlsZVJlcXVlc3QaJy5jcnlwdG9zLmZsZWV0LnYxLkNyZWF0ZVByb2ZpbGVSZXNwb25zZRJgCg1VcGRhdGVQcm9maWxlEiYuY3J5cHRvcy5mbGVldC52MS5VcGRhdGVQcm9maWxlUmVxdWVzdBonLmNyeXB0b3MuZmxlZXQudjEuVXBkYXRlUHJvZmlsZVJlc3BvbnNlEmAKDURlbGV0ZVByb2ZpbGUSJi5jcnlwdG9zLmZsZWV0LnYxLkRlbGV0ZVByb2ZpbGVSZXF1ZXN0GicuY3J5cHRvcy5mbGVldC52MS5EZWxldGVQcm9maWxlUmVzcG9uc2USbwoSQXBwbHlQcm9maWxlVG9Ob2RlEisuY3J5cHRvcy5mbGVldC52MS5BcHBseVByb2ZpbGVUb05vZGVSZXF1ZXN0GiwuY3J5cHRvcy5mbGVldC52MS5BcHBseVByb2ZpbGVUb05vZGVSZXNwb25zZRJdCgxMaXN0QWRhcHRlcnMSJS5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBZGFwdGVyc1JlcXVlc3QaJi5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBZGFwdGVyc1Jlc3BvbnNlEmwKEVNldEFkYXB0ZXJFbmFibGVkEiouY3J5cHRvcy5mbGVldC52MS5TZXRBZGFwdGVyRW5hYmxlZFJlcXVlc3QaKy5jcnlwdG9zLmZsZWV0LnYxLlNldEFkYXB0ZXJFbmFibGVkUmVzcG9uc2USZgoPU2V0Tm9kZVByb3RvY29sEiguY3J5cHRvcy5mbGVldC52MS5TZXROb2RlUHJvdG9jb2xSZXF1ZXN0GikuY3J5cHRvcy5mbGVldC52MS5TZXROb2RlUHJvdG9jb2xSZXNwb25zZRJUCglMaXN0QXVkaXQSIi5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBdWRpdFJlcXVlc3QaIy5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBdWRpdFJlc3BvbnNlEmYKD0xpc3RFbnJvbGxtZW50cxIoLmNyeXB0b3MuZmxlZXQudjEuTGlzdEVucm9sbG1lbnRzUmVxdWVzdBopLmNyeXB0b3MuZmxlZXQudjEuTGlzdEVucm9sbG1lbnRzUmVzcG9uc2USaQoQQ3JlYXRlRW5yb2xsbWVudBIpLmNyeXB0b3MuZmxlZXQudjEuQ3JlYXRlRW5yb2xsbWVudFJlcXVlc3QaKi5jcnlwdG9zLmZsZWV0LnYxLkNyZWF0ZUVucm9sbG1lbnRSZXNwb25zZRJsChFBcHByb3ZlRW5yb2xsbWVudBIqLmNyeXB0b3MuZmxlZXQudjEuQXBwcm92ZUVucm9sbG1lbnRSZXF1ZXN0GisuY3J5cHRvcy5mbGVldC52MS5BcHByb3ZlRW5yb2xsbWVudFJlc3BvbnNlEmkKEFJlamVjdEVucm9sbG1lbnQSKS5jcnlwdG9zLmZsZWV0LnYxLlJlamVjdEVucm9sbG1lbnRSZXF1ZXN0GiouY3J5cHRvcy5mbGVldC52MS5SZWplY3RFbnJvbGxtZW50UmVzcG9uc2USSwoGV2hvQW1JEh8uY3J5cHRvcy5mbGVldC52MS5XaG9BbUlSZXF1ZXN0GiAuY3J5cHRvcy5mbGVldC52MS5XaG9BbUlSZXNwb25zZRJsChFSZXZva2VDZXJ0aWZpY2F0ZRIqLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlQ2VydGlmaWNhdGVSZXF1ZXN0GisuY3J5cHRvcy5mbGVldC52MS5SZXZva2VDZXJ0aWZpY2F0ZVJlc3BvbnNlEmMKDkdldENlcnRpZmljYXRlEicuY3J5cHRvcy5mbGVldC52MS5HZXRDZXJ0aWZpY2F0ZVJlcXVlc3QaKC5jcnlwdG9zLmZsZWV0LnYxLkdldENlcnRpZmljYXRlUmVzcG9uc2USVAoJSXNzdWVMZWFmEiIuY3J5cHRvcy5mbGVldC52MS5Jc3N1ZUxlYWZSZXF1ZXN0GiMuY3J5cHRvcy5mbGVldC52MS5Jc3N1ZUxlYWZSZXNwb25zZRJUCglSZWtleU5vZGUSIi5jcnlwdG9zLmZsZWV0LnYxLlJla2V5Tm9kZVJlcXVlc3QaIy5jcnlwdG9zLmZsZWV0LnYxLlJla2V5Tm9kZVJlc3BvbnNlEmAKDUdldE5vZGVDb25maWcSJi5jcnlwdG9zLmZsZWV0LnYxLkdldE5vZGVDb25maWdSZXF1ZXN0GicuY3J5cHRvcy5mbGVldC52MS5HZXROb2RlQ29uZmlnUmVzcG9uc2USZgoPQXBwbHlOb2RlQ29uZmlnEiguY3J5cHRvcy5mbGVldC52MS5BcHBseU5vZGVDb25maWdSZXF1ZXN0GikuY3J5cHRvcy5mbGVldC52MS5BcHBseU5vZGVDb25maWdSZXNwb25zZRJaCgtFeHBvcnRDQUtleRIkLmNyeXB0b3MuZmxlZXQudjEuRXhwb3J0Q0FLZXlSZXF1ZXN0GiUuY3J5cHRvcy5mbGVldC52MS5FeHBvcnRDQUtleVJlc3BvbnNlEloKC0ltcG9ydENBS2V5EiQuY3J5cHRvcy5mbGVldC52MS5JbXBvcnRDQUtleVJlcXVlc3QaJS5jcnlwdG9zLmZsZWV0LnYxLkltcG9ydENBS2V5UmVzcG9uc2USlgEKH0NyZWF0ZU9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3QSOC5jcnlwdG9zLmZsZWV0LnYxLkNyZWF0ZU9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3RSZXF1ZXN0GjkuY3J5cHRvcy5mbGVldC52MS5DcmVhdGVPcGVyYXRvckNyZWRlbnRpYWxSZXF1ZXN0UmVzcG9uc2USkwEKHkxpc3RPcGVyYXRvckNyZWRlbnRpYWxSZXF1ZXN0cxI3LmNyeXB0b3MuZmxlZXQudjEuTGlzdE9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3RzUmVxdWVzdBo4LmNyeXB0b3MuZmxlZXQudjEuTGlzdE9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3RzUmVzcG9uc2USlgEKH0NhbmNlbE9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3QSOC5jcnlwdG9zLmZsZWV0LnYxLkNhbmNlbE9wZXJhdG9yQ3JlZGVudGlhbFJlcXVlc3RSZXF1ZXN0GjkuY3J5cHRvcy5mbGVldC52MS5DYW5jZWxPcGVyYXRvckNyZWRlbnRpYWxSZXF1ZXN0UmVzcG9uc2USgQEKGFJlY29yZE9wZXJhdG9yQ3JlZGVudGlhbBIxLmNyeXB0b3MuZmxlZXQudjEuUmVjb3JkT3BlcmF0b3JDcmVkZW50aWFsUmVxdWVzdBoyLmNyeXB0b3MuZmxlZXQudjEuUmVjb3JkT3BlcmF0b3JDcmVkZW50aWFsUmVzcG9uc2USgQEKGFJldm9rZU9wZXJhdG9yQ3JlZGVudGlhbBIxLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlT3BlcmF0b3JDcmVkZW50aWFsUmVxdWVzdBoyLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlT3BlcmF0b3JDcmVkZW50aWFsUmVzcG9uc2USfgoXTGlzdE9wZXJhdG9yQ3JlZGVudGlhbHMSMC5jcnlwdG9zLmZsZWV0LnYxLkxpc3RPcGVyYXRvckNyZWRlbnRpYWxzUmVxdWVzdBoxLmNyeXB0b3MuZmxlZXQudjEuTGlzdE9wZXJhdG9yQ3JlZGVudGlhbHNSZXNwb25zZRJmCg9MaXN0T3BlcmF0b3JDQXMSKC5jcnlwdG9zLmZsZWV0LnYxLkxpc3RPcGVyYXRvckNBc1JlcXVlc3QaKS5jcnlwdG9zLmZsZWV0LnYxLkxpc3RPcGVyYXRvckNBc1Jlc3BvbnNlEm8KElJlZ2lzdGVyT3BlcmF0b3JDQRIrLmNyeXB0b3MuZmxlZXQudjEuUmVnaXN0ZXJPcGVyYXRvckNBUmVxdWVzdBosLmNyeXB0b3MuZmxlZXQudjEuUmVnaXN0ZXJPcGVyYXRvckNBUmVzcG9uc2USaQoQUmV0aXJlT3BlcmF0b3JDQRIpLmNyeXB0b3MuZmxlZXQudjEuUmV0aXJlT3BlcmF0b3JDQVJlcXVlc3QaKi5jcnlwdG9zLmZsZWV0LnYxLlJldGlyZU9wZXJhdG9yQ0FSZXNwb25zZRJ7ChZTZXRPcGVyYXRvckNBQ1JMU291cmNlEi8uY3J5cHRvcy5mbGVldC52MS5TZXRPcGVyYXRvckNBQ1JMU291cmNlUmVxdWVzdBowLmNyeXB0b3MuZmxlZXQudjEuU2V0T3BlcmF0b3JDQUNSTFNvdXJjZVJlc3BvbnNlEmwKEVVwbG9hZE9wZXJhdG9yQ1JMEiouY3J5cHRvcy5mbGVldC52MS5VcGxvYWRPcGVyYXRvckNSTFJlcXVlc3QaKy5jcnlwdG9zLmZsZWV0LnYxLlVwbG9hZE9wZXJhdG9yQ1JMUmVzcG9uc2USbAoRU2V0T3BlcmF0b3JDQU9DU1ASKi5jcnlwdG9zLmZsZWV0LnYxLlNldE9wZXJhdG9yQ0FPQ1NQUmVxdWVzdBorLmNyeXB0b3MuZmxlZXQudjEuU2V0T3BlcmF0b3JDQU9DU1BSZXNwb25zZRJmCg9QcmV2aWV3QWRvcHRpb24SKC5jcnlwdG9zLmZsZWV0LnYxLlByZXZpZXdBZG9wdGlvblJlcXVlc3QaKS5jcnlwdG9zLmZsZWV0LnYxLlByZXZpZXdBZG9wdGlvblJlc3BvbnNlEmkKEExpc3RJbnN0YWxsRGlza3MSKS5jcnlwdG9zLmZsZWV0LnYxLkxpc3RJbnN0YWxsRGlza3NSZXF1ZXN0GiouY3J5cHRvcy5mbGVldC52MS5MaXN0SW5zdGFsbERpc2tzUmVzcG9uc2USVgoJQWRvcHROb2RlEiIuY3J5cHRvcy5mbGVldC52MS5BZG9wdE5vZGVSZXF1ZXN0GiMuY3J5cHRvcy5mbGVldC52MS5BZG9wdE5vZGVSZXNwb25zZTABEocBChpDb25maXJtQWRvcHRpb25GaW5nZXJwcmludBIzLmNyeXB0b3MuZmxlZXQudjEuQ29uZmlybUFkb3B0aW9uRmluZ2VycHJpbnRSZXF1ZXN0GjQuY3J5cHRvcy5mbGVldC52MS5Db25maXJtQWRvcHRpb25GaW5nZXJwcmludFJlc3BvbnNlEmkKEERlY29tbWlzc2lvbk5vZGUSKS5jcnlwdG9zLmZsZWV0LnYxLkRlY29tbWlzc2lvbk5vZGVSZXF1ZXN0GiouY3J5cHRvcy5mbGVldC52MS5EZWNvbW1pc3Npb25Ob2RlUmVzcG9uc2USVwoKUmVuYW1lTm9kZRIjLmNyeXB0b3MuZmxlZXQudjEuUmVuYW1lTm9kZVJlcXVlc3QaJC5jcnlwdG9zLmZsZWV0LnYxLlJlbmFtZU5vZGVSZXNwb25zZRJaCgtMaXN0TWNwS2V5cxIkLmNyeXB0b3MuZmxlZXQudjEuTGlzdE1jcEtleXNSZXF1ZXN0GiUuY3J5cHRvcy5mbGVldC52MS5MaXN0TWNwS2V5c1Jlc3BvbnNlEl0KDFJldm9rZU1jcEtleRIlLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlTWNwS2V5UmVxdWVzdBomLmNyeXB0b3MuZmxlZXQudjEuUmV2b2tlTWNwS2V5UmVzcG9uc2USXQoMQ3JlYXRlTWNwS2V5EiUuY3J5cHRvcy5mbGVldC52MS5DcmVhdGVNY3BLZXlSZXF1ZXN0GiYuY3J5cHRvcy5mbGVldC52MS5DcmVhdGVNY3BLZXlSZXNwb25zZRJgCg1MaXN0QXBwcm92YWxzEiYuY3J5cHRvcy5mbGVldC52MS5MaXN0QXBwcm92YWxzUmVxdWVzdBonLmNyeXB0b3MuZmxlZXQudjEuTGlzdEFwcHJvdmFsc1Jlc3BvbnNlEmMKDkRlY2lkZUFwcHJvdmFsEicuY3J5cHRvcy5mbGVldC52MS5EZWNpZGVBcHByb3ZhbFJlcXVlc3QaKC5jcnlwdG9zLmZsZWV0LnYxLkRlY2lkZUFwcHJvdmFsUmVzcG9uc2VCOFo2Z2l0aHViLmNvbS9DcnlwdE9TLVBLSS9hcGkvZ28vY3J5cHRvcy9mbGVldC92MTtmbGVldHYxYgZwcm90bzM", [file_cryptos_fleet_v1_operator_ca, file_cryptos_v1_config, file_cryptos_v1_node, file_cryptos_v1_status]);
/**
* @generated from message cryptos.fleet.v1.ListNodesRequest
@@ -1321,6 +1323,77 @@ export type RevokeCertificateResponse = Message<"cryptos.fleet.v1.RevokeCertific
export const RevokeCertificateResponseSchema: GenMessage = /*@__PURE__*/
messageDesc(file_cryptos_fleet_v1_fleet, 41);
+/**
+ * GetCertificateRequest names the issuing node and the certificate to fetch.
+ *
+ * @generated from message cryptos.fleet.v1.GetCertificateRequest
+ */
+export type GetCertificateRequest = Message<"cryptos.fleet.v1.GetCertificateRequest"> & {
+ /**
+ * node_name is the issuing node the certificate lives on.
+ *
+ * @generated from field: string node_name = 1;
+ */
+ nodeName: string;
+
+ /**
+ * serial_hex is the hex-encoded serial of the certificate.
+ *
+ * @generated from field: string serial_hex = 2;
+ */
+ serialHex: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.GetCertificateRequest.
+ * Use `create(GetCertificateRequestSchema)` to create a new message.
+ */
+export const GetCertificateRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 42);
+
+/**
+ * GetCertificateResponse carries the certificate, its chain and its status.
+ *
+ * @generated from message cryptos.fleet.v1.GetCertificateResponse
+ */
+export type GetCertificateResponse = Message<"cryptos.fleet.v1.GetCertificateResponse"> & {
+ /**
+ * certificate_pem is the PEM-encoded issued certificate.
+ *
+ * @generated from field: string certificate_pem = 1;
+ */
+ certificatePem: string;
+
+ /**
+ * chain_pem is the concatenated PEM chain, from the issuer up to the root.
+ *
+ * @generated from field: string chain_pem = 2;
+ */
+ chainPem: string;
+
+ /**
+ * status is "valid" | "revoked" | "expired".
+ *
+ * @generated from field: string status = 3;
+ */
+ status: string;
+
+ /**
+ * revoked_at is when the certificate was revoked (RFC3339); empty unless
+ * status is revoked.
+ *
+ * @generated from field: string revoked_at = 4;
+ */
+ revokedAt: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.GetCertificateResponse.
+ * Use `create(GetCertificateResponseSchema)` to create a new message.
+ */
+export const GetCertificateResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 43);
+
/**
* IssueLeafRequest carries the issuing node, the browser-generated PKCS#10
* CSR, and the issuance profile to sign the leaf under.
@@ -1366,7 +1439,7 @@ export type IssueLeafRequest = Message<"cryptos.fleet.v1.IssueLeafRequest"> & {
* Use `create(IssueLeafRequestSchema)` to create a new message.
*/
export const IssueLeafRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 42);
+ messageDesc(file_cryptos_fleet_v1_fleet, 44);
/**
* IssueLeafResponse carries the signed leaf certificate.
@@ -1387,7 +1460,7 @@ export type IssueLeafResponse = Message<"cryptos.fleet.v1.IssueLeafResponse"> &
* Use `create(IssueLeafResponseSchema)` to create a new message.
*/
export const IssueLeafResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 43);
+ messageDesc(file_cryptos_fleet_v1_fleet, 45);
/**
* RekeyNodeRequest names the subordinate to re-key and the CA profile the
@@ -1428,7 +1501,7 @@ export type RekeyNodeRequest = Message<"cryptos.fleet.v1.RekeyNodeRequest"> & {
* Use `create(RekeyNodeRequestSchema)` to create a new message.
*/
export const RekeyNodeRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 44);
+ messageDesc(file_cryptos_fleet_v1_fleet, 46);
/**
* RekeyNodeResponse reports the re-keyed identity: the node's own subject CN,
@@ -1464,7 +1537,7 @@ export type RekeyNodeResponse = Message<"cryptos.fleet.v1.RekeyNodeResponse"> &
* Use `create(RekeyNodeResponseSchema)` to create a new message.
*/
export const RekeyNodeResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 45);
+ messageDesc(file_cryptos_fleet_v1_fleet, 47);
/**
* GetNodeConfigRequest names the managed node whose current config to fetch.
@@ -1496,7 +1569,7 @@ export type GetNodeConfigRequest = Message<"cryptos.fleet.v1.GetNodeConfigReques
* Use `create(GetNodeConfigRequestSchema)` to create a new message.
*/
export const GetNodeConfigRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 46);
+ messageDesc(file_cryptos_fleet_v1_fleet, 48);
/**
* GetNodeConfigResponse carries the node's current full machine configuration.
@@ -1515,7 +1588,7 @@ export type GetNodeConfigResponse = Message<"cryptos.fleet.v1.GetNodeConfigRespo
* Use `create(GetNodeConfigResponseSchema)` to create a new message.
*/
export const GetNodeConfigResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 47);
+ messageDesc(file_cryptos_fleet_v1_fleet, 49);
/**
* ApplyNodeConfigRequest names the managed node and carries the FULL config to
@@ -1556,7 +1629,7 @@ export type ApplyNodeConfigRequest = Message<"cryptos.fleet.v1.ApplyNodeConfigRe
* Use `create(ApplyNodeConfigRequestSchema)` to create a new message.
*/
export const ApplyNodeConfigRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 48);
+ messageDesc(file_cryptos_fleet_v1_fleet, 50);
/**
* ApplyNodeConfigResponse reports the node's config generation after the apply
@@ -1585,7 +1658,7 @@ export type ApplyNodeConfigResponse = Message<"cryptos.fleet.v1.ApplyNodeConfigR
* Use `create(ApplyNodeConfigResponseSchema)` to create a new message.
*/
export const ApplyNodeConfigResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 49);
+ messageDesc(file_cryptos_fleet_v1_fleet, 51);
/**
* SetNodeProtocolRequest names the node, the protocol and its new state.
@@ -1621,7 +1694,7 @@ export type SetNodeProtocolRequest = Message<"cryptos.fleet.v1.SetNodeProtocolRe
* Use `create(SetNodeProtocolRequestSchema)` to create a new message.
*/
export const SetNodeProtocolRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 50);
+ messageDesc(file_cryptos_fleet_v1_fleet, 52);
/**
* SetNodeProtocolResponse carries the node's answer to the apply.
@@ -1651,7 +1724,7 @@ export type SetNodeProtocolResponse = Message<"cryptos.fleet.v1.SetNodeProtocolR
* Use `create(SetNodeProtocolResponseSchema)` to create a new message.
*/
export const SetNodeProtocolResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 51);
+ messageDesc(file_cryptos_fleet_v1_fleet, 53);
/**
* ExportCAKeyRequest names the managed node to back up and carries the operator
@@ -1692,7 +1765,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, 52);
+ messageDesc(file_cryptos_fleet_v1_fleet, 54);
/**
* ExportCAKeyResponse carries the encrypted backup envelope (Argon2id +
@@ -1714,7 +1787,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, 53);
+ messageDesc(file_cryptos_fleet_v1_fleet, 55);
/**
* ImportCAKeyRequest names the fresh target node, the encrypted envelope to
@@ -1738,239 +1811,881 @@ export type ImportCAKeyRequest = Message<"cryptos.fleet.v1.ImportCAKeyRequest">
*
* @generated from field: bytes envelope = 2;
*/
- envelope: Uint8Array;
+ envelope: Uint8Array;
+
+ /**
+ * passphrase unseals the envelope node-side; it is never persisted.
+ *
+ * @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;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.ImportCAKeyRequest.
+ * Use `create(ImportCAKeyRequestSchema)` to create a new message.
+ */
+export const ImportCAKeyRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 56);
+
+/**
+ * ImportCAKeyResponse summarizes the restored identity so the web can confirm
+ * what was imported, without returning the full identity chain.
+ *
+ * @generated from message cryptos.fleet.v1.ImportCAKeyResponse
+ */
+export type ImportCAKeyResponse = Message<"cryptos.fleet.v1.ImportCAKeyResponse"> & {
+ /**
+ * subject_cn is the restored identity's subject common name.
+ *
+ * @generated from field: string subject_cn = 1;
+ */
+ subjectCn: string;
+
+ /**
+ * issuer_cn is the restored identity's issuer common name.
+ *
+ * @generated from field: string issuer_cn = 2;
+ */
+ issuerCn: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.ImportCAKeyResponse.
+ * Use `create(ImportCAKeyResponseSchema)` to create a new message.
+ */
+export const ImportCAKeyResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 57);
+
+/**
+ * RevokeOperatorCredentialRequest names the operator credential to deny by
+ * hex serial, with the RFC 5280 reason code.
+ *
+ * @generated from message cryptos.fleet.v1.RevokeOperatorCredentialRequest
+ */
+export type RevokeOperatorCredentialRequest = Message<"cryptos.fleet.v1.RevokeOperatorCredentialRequest"> & {
+ /**
+ * @generated from field: string serial_hex = 1;
+ */
+ serialHex: string;
+
+ /**
+ * @generated from field: int32 reason_code = 2;
+ */
+ reasonCode: number;
+
+ /**
+ * issuer_sha256 is the operator CA's fingerprint. Empty means the recorded
+ * credential's issuer, or the active operator CA when the serial isn't
+ * recorded.
+ *
+ * @generated from field: string issuer_sha256 = 3;
+ */
+ issuerSha256: string;
+
+ /**
+ * note is a free-text reason kept with the denylist entry.
+ *
+ * @generated from field: string note = 4;
+ */
+ note: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.RevokeOperatorCredentialRequest.
+ * Use `create(RevokeOperatorCredentialRequestSchema)` to create a new message.
+ */
+export const RevokeOperatorCredentialRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 58);
+
+/**
+ * RevokeOperatorCredentialResponse echoes the denied serial, its issuer and
+ * the time it was denied (RFC3339).
+ *
+ * @generated from message cryptos.fleet.v1.RevokeOperatorCredentialResponse
+ */
+export type RevokeOperatorCredentialResponse = Message<"cryptos.fleet.v1.RevokeOperatorCredentialResponse"> & {
+ /**
+ * @generated from field: string serial_hex = 1;
+ */
+ serialHex: string;
+
+ /**
+ * @generated from field: string revoked_at = 2;
+ */
+ revokedAt: string;
+
+ /**
+ * @generated from field: string issuer_sha256 = 3;
+ */
+ issuerSha256: string;
+
+ /**
+ * warnings include the reminder that the manager's denylist doesn't revoke
+ * at the CA.
+ *
+ * @generated from field: repeated string warnings = 4;
+ */
+ warnings: string[];
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.RevokeOperatorCredentialResponse.
+ * Use `create(RevokeOperatorCredentialResponseSchema)` to create a new message.
+ */
+export const RevokeOperatorCredentialResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 59);
+
+/**
+ * @generated from message cryptos.fleet.v1.ListOperatorCredentialsRequest
+ */
+export type ListOperatorCredentialsRequest = Message<"cryptos.fleet.v1.ListOperatorCredentialsRequest"> & {
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.ListOperatorCredentialsRequest.
+ * Use `create(ListOperatorCredentialsRequestSchema)` to create a new message.
+ */
+export const ListOperatorCredentialsRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 60);
+
+/**
+ * OperatorCredential is one operator credential the manager knows.
+ * Timestamps are RFC3339 strings, empty when unset.
+ *
+ * @generated from message cryptos.fleet.v1.OperatorCredential
+ */
+export type OperatorCredential = Message<"cryptos.fleet.v1.OperatorCredential"> & {
+ /**
+ * common_name is the certificate's CN, the holder's email.
+ *
+ * @generated from field: string common_name = 1;
+ */
+ commonName: string;
+
+ /**
+ * @generated from field: string serial_hex = 2;
+ */
+ serialHex: string;
+
+ /**
+ * "viewer"|"operator"|"admin"
+ *
+ * @generated from field: string level = 3;
+ */
+ level: string;
+
+ /**
+ * @generated from field: string not_after = 4;
+ */
+ notAfter: string;
+
+ /**
+ * revoked is true when any revocation source refuses the credential.
+ *
+ * @generated from field: bool revoked = 5;
+ */
+ revoked: boolean;
+
+ /**
+ * kind is how the manager learned of it: "first_admin", "requested",
+ * "recorded", "observed" (seen in use, never recorded) or "legacy_node"
+ * (issued by a node before external operator CAs; read-only).
+ *
+ * @generated from field: string kind = 6;
+ */
+ kind: string;
+
+ /**
+ * issuer_sha256 is the operator CA's fingerprint, empty for legacy_node.
+ *
+ * @generated from field: string issuer_sha256 = 7;
+ */
+ issuerSha256: string;
+
+ /**
+ * @generated from field: string email = 8;
+ */
+ email: string;
+
+ /**
+ * @generated from field: string full_name = 9;
+ */
+ fullName: string;
+
+ /**
+ * denylisted is true when the credential is on the manager's denylist.
+ *
+ * @generated from field: bool denylisted = 10;
+ */
+ denylisted: boolean;
+
+ /**
+ * crl_revoked is true when the operator CA's CRL lists the credential.
+ *
+ * @generated from field: bool crl_revoked = 11;
+ */
+ crlRevoked: boolean;
+
+ /**
+ * @generated from field: string first_seen_at = 12;
+ */
+ firstSeenAt: string;
+
+ /**
+ * @generated from field: string last_seen_at = 13;
+ */
+ lastSeenAt: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.OperatorCredential.
+ * Use `create(OperatorCredentialSchema)` to create a new message.
+ */
+export const OperatorCredentialSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 61);
+
+/**
+ * @generated from message cryptos.fleet.v1.ListOperatorCredentialsResponse
+ */
+export type ListOperatorCredentialsResponse = Message<"cryptos.fleet.v1.ListOperatorCredentialsResponse"> & {
+ /**
+ * @generated from field: repeated cryptos.fleet.v1.OperatorCredential items = 1;
+ */
+ items: OperatorCredential[];
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.ListOperatorCredentialsResponse.
+ * Use `create(ListOperatorCredentialsResponseSchema)` to create a new message.
+ */
+export const ListOperatorCredentialsResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 62);
+
+/**
+ * CreateOperatorCredentialRequestRequest asks for a credential at a level for
+ * a holder, from a CSR made in the browser or on the holder's machine.
+ *
+ * @generated from message cryptos.fleet.v1.CreateOperatorCredentialRequestRequest
+ */
+export type CreateOperatorCredentialRequestRequest = Message<"cryptos.fleet.v1.CreateOperatorCredentialRequestRequest"> & {
+ /**
+ * "viewer"|"operator"|"admin"
+ *
+ * @generated from field: string level = 1;
+ */
+ level: string;
+
+ /**
+ * @generated from field: string email = 2;
+ */
+ email: string;
+
+ /**
+ * @generated from field: string full_name = 3;
+ */
+ fullName: string;
+
+ /**
+ * csr_der has subject exactly CN= and a P-384 or RSA 3072+ key, at
+ * most 4 KiB.
+ *
+ * @generated from field: bytes csr_der = 4;
+ */
+ csrDer: Uint8Array;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.CreateOperatorCredentialRequestRequest.
+ * Use `create(CreateOperatorCredentialRequestRequestSchema)` to create a new message.
+ */
+export const CreateOperatorCredentialRequestRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 63);
+
+/**
+ * CreateOperatorCredentialRequestResponse carries what the CA operator needs
+ * to sign the request.
+ *
+ * @generated from message cryptos.fleet.v1.CreateOperatorCredentialRequestResponse
+ */
+export type CreateOperatorCredentialRequestResponse = Message<"cryptos.fleet.v1.CreateOperatorCredentialRequestResponse"> & {
+ /**
+ * @generated from field: string request_id = 1;
+ */
+ requestId: string;
+
+ /**
+ * @generated from field: string csr_pem = 2;
+ */
+ csrPem: string;
+
+ /**
+ * extfile_section is the OpenSSL extension section for the level.
+ *
+ * @generated from field: string extfile_section = 3;
+ */
+ extfileSection: string;
+
+ /**
+ * openssl_command is the openssl ca command that signs the CSR.
+ *
+ * @generated from field: string openssl_command = 4;
+ */
+ opensslCommand: string;
+
+ /**
+ * @generated from field: string expires_at = 5;
+ */
+ expiresAt: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.CreateOperatorCredentialRequestResponse.
+ * Use `create(CreateOperatorCredentialRequestResponseSchema)` to create a new message.
+ */
+export const CreateOperatorCredentialRequestResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 64);
+
+/**
+ * @generated from message cryptos.fleet.v1.ListOperatorCredentialRequestsRequest
+ */
+export type ListOperatorCredentialRequestsRequest = Message<"cryptos.fleet.v1.ListOperatorCredentialRequestsRequest"> & {
+ /**
+ * state filters to "pending", "completed", "cancelled" or "expired"; empty
+ * lists all.
+ *
+ * @generated from field: string state = 1;
+ */
+ state: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.ListOperatorCredentialRequestsRequest.
+ * Use `create(ListOperatorCredentialRequestsRequestSchema)` to create a new message.
+ */
+export const ListOperatorCredentialRequestsRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 65);
+
+/**
+ * @generated from message cryptos.fleet.v1.ListOperatorCredentialRequestsResponse
+ */
+export type ListOperatorCredentialRequestsResponse = Message<"cryptos.fleet.v1.ListOperatorCredentialRequestsResponse"> & {
+ /**
+ * @generated from field: repeated cryptos.fleet.v1.OperatorCredentialRequest items = 1;
+ */
+ items: OperatorCredentialRequest[];
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.ListOperatorCredentialRequestsResponse.
+ * Use `create(ListOperatorCredentialRequestsResponseSchema)` to create a new message.
+ */
+export const ListOperatorCredentialRequestsResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 66);
+
+/**
+ * @generated from message cryptos.fleet.v1.CancelOperatorCredentialRequestRequest
+ */
+export type CancelOperatorCredentialRequestRequest = Message<"cryptos.fleet.v1.CancelOperatorCredentialRequestRequest"> & {
+ /**
+ * @generated from field: string request_id = 1;
+ */
+ requestId: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.CancelOperatorCredentialRequestRequest.
+ * Use `create(CancelOperatorCredentialRequestRequestSchema)` to create a new message.
+ */
+export const CancelOperatorCredentialRequestRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 67);
+
+/**
+ * @generated from message cryptos.fleet.v1.CancelOperatorCredentialRequestResponse
+ */
+export type CancelOperatorCredentialRequestResponse = Message<"cryptos.fleet.v1.CancelOperatorCredentialRequestResponse"> & {
+ /**
+ * @generated from field: cryptos.fleet.v1.OperatorCredentialRequest request = 1;
+ */
+ request?: OperatorCredentialRequest | undefined;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.CancelOperatorCredentialRequestResponse.
+ * Use `create(CancelOperatorCredentialRequestResponseSchema)` to create a new message.
+ */
+export const CancelOperatorCredentialRequestResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 68);
+
+/**
+ * OperatorCredentialRequest is one request for an operator credential.
+ * Timestamps are RFC3339 strings, empty when unset.
+ *
+ * @generated from message cryptos.fleet.v1.OperatorCredentialRequest
+ */
+export type OperatorCredentialRequest = Message<"cryptos.fleet.v1.OperatorCredentialRequest"> & {
+ /**
+ * @generated from field: string id = 1;
+ */
+ id: string;
+
+ /**
+ * @generated from field: string level = 2;
+ */
+ level: string;
+
+ /**
+ * @generated from field: string email = 3;
+ */
+ email: string;
+
+ /**
+ * @generated from field: string full_name = 4;
+ */
+ fullName: string;
+
+ /**
+ * csr_pem is empty once the request is no longer pending.
+ *
+ * @generated from field: string csr_pem = 5;
+ */
+ csrPem: string;
+
+ /**
+ * state is "pending", "completed", "cancelled" or "expired".
+ *
+ * @generated from field: string state = 6;
+ */
+ state: string;
+
+ /**
+ * @generated from field: string created_by_cn = 7;
+ */
+ createdByCn: string;
+
+ /**
+ * @generated from field: string created_at = 8;
+ */
+ createdAt: string;
+
+ /**
+ * @generated from field: string expires_at = 9;
+ */
+ expiresAt: string;
+
+ /**
+ * completed_serial is the recorded certificate's hex serial once completed.
+ *
+ * @generated from field: string completed_serial = 10;
+ */
+ completedSerial: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.OperatorCredentialRequest.
+ * Use `create(OperatorCredentialRequestSchema)` to create a new message.
+ */
+export const OperatorCredentialRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 69);
+
+/**
+ * @generated from message cryptos.fleet.v1.RecordOperatorCredentialRequest
+ */
+export type RecordOperatorCredentialRequest = Message<"cryptos.fleet.v1.RecordOperatorCredentialRequest"> & {
+ /**
+ * cert_der is exactly one certificate, DER, at most 8 KiB.
+ *
+ * @generated from field: bytes cert_der = 1;
+ */
+ certDer: Uint8Array;
+
+ /**
+ * request_id is the pending request the certificate answers. Optional.
+ *
+ * @generated from field: string request_id = 2;
+ */
+ requestId: string;
+
+ /**
+ * full_name is the holder's name when there is no request. Optional.
+ *
+ * @generated from field: string full_name = 3;
+ */
+ fullName: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.RecordOperatorCredentialRequest.
+ * Use `create(RecordOperatorCredentialRequestSchema)` to create a new message.
+ */
+export const RecordOperatorCredentialRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 70);
+
+/**
+ * @generated from message cryptos.fleet.v1.RecordOperatorCredentialResponse
+ */
+export type RecordOperatorCredentialResponse = Message<"cryptos.fleet.v1.RecordOperatorCredentialResponse"> & {
+ /**
+ * @generated from field: cryptos.fleet.v1.OperatorCredential credential = 1;
+ */
+ credential?: OperatorCredential | undefined;
+
+ /**
+ * warnings, for example a validity over 400 days.
+ *
+ * @generated from field: repeated string warnings = 2;
+ */
+ warnings: string[];
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.RecordOperatorCredentialResponse.
+ * Use `create(RecordOperatorCredentialResponseSchema)` to create a new message.
+ */
+export const RecordOperatorCredentialResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 71);
+
+/**
+ * @generated from message cryptos.fleet.v1.ListOperatorCAsRequest
+ */
+export type ListOperatorCAsRequest = Message<"cryptos.fleet.v1.ListOperatorCAsRequest"> & {
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.ListOperatorCAsRequest.
+ * Use `create(ListOperatorCAsRequestSchema)` to create a new message.
+ */
+export const ListOperatorCAsRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 72);
+
+/**
+ * @generated from message cryptos.fleet.v1.ListOperatorCAsResponse
+ */
+export type ListOperatorCAsResponse = Message<"cryptos.fleet.v1.ListOperatorCAsResponse"> & {
+ /**
+ * @generated from field: repeated cryptos.fleet.v1.OperatorCA items = 1;
+ */
+ items: OperatorCA[];
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.ListOperatorCAsResponse.
+ * Use `create(ListOperatorCAsResponseSchema)` to create a new message.
+ */
+export const ListOperatorCAsResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 73);
+
+/**
+ * RegisterOperatorCARequest has the same fields as
+ * BootstrapServiceRegisterOperatorCARequest, which documents them.
+ *
+ * @generated from message cryptos.fleet.v1.RegisterOperatorCARequest
+ */
+export type RegisterOperatorCARequest = Message<"cryptos.fleet.v1.RegisterOperatorCARequest"> & {
+ /**
+ * @generated from field: bytes ca_cert_der = 1;
+ */
+ caCertDer: Uint8Array;
+
+ /**
+ * @generated from oneof cryptos.fleet.v1.RegisterOperatorCARequest.crl_source
+ */
+ crlSource: {
+ /**
+ * @generated from field: string url = 2;
+ */
+ value: string;
+ case: "url";
+ } | {
+ /**
+ * @generated from field: bytes crl_der = 3;
+ */
+ value: Uint8Array;
+ case: "crlDer";
+ } | {
+ /**
+ * @generated from field: bool none = 4;
+ */
+ value: boolean;
+ case: "none";
+ } | { case: undefined; value?: undefined };
+
+ /**
+ * @generated from field: cryptos.fleet.v1.OcspMode ocsp_mode = 5;
+ */
+ ocspMode: OcspMode;
+
+ /**
+ * @generated from field: string ocsp_url = 6;
+ */
+ ocspUrl: string;
/**
- * passphrase unseals the envelope node-side; it is never persisted.
- *
- * @generated from field: bytes passphrase = 3;
+ * @generated from field: repeated cryptos.fleet.v1.OperatorCAAcknowledgement acknowledgements = 7;
*/
- passphrase: Uint8Array;
+ acknowledgements: OperatorCAAcknowledgement[];
/**
- * 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;
+ * @generated from field: string confirm_sha256 = 8;
*/
- nodeId: string;
+ confirmSha256: string;
};
/**
- * Describes the message cryptos.fleet.v1.ImportCAKeyRequest.
- * Use `create(ImportCAKeyRequestSchema)` to create a new message.
+ * Describes the message cryptos.fleet.v1.RegisterOperatorCARequest.
+ * Use `create(RegisterOperatorCARequestSchema)` to create a new message.
*/
-export const ImportCAKeyRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 54);
+export const RegisterOperatorCARequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 74);
/**
- * ImportCAKeyResponse summarizes the restored identity so the web can confirm
- * what was imported, without returning the full identity chain.
+ * RegisterOperatorCAResponse has the same fields as
+ * BootstrapServiceRegisterOperatorCAResponse, which documents them.
*
- * @generated from message cryptos.fleet.v1.ImportCAKeyResponse
+ * @generated from message cryptos.fleet.v1.RegisterOperatorCAResponse
*/
-export type ImportCAKeyResponse = Message<"cryptos.fleet.v1.ImportCAKeyResponse"> & {
+export type RegisterOperatorCAResponse = Message<"cryptos.fleet.v1.RegisterOperatorCAResponse"> & {
/**
- * subject_cn is the restored identity's subject common name.
- *
- * @generated from field: string subject_cn = 1;
+ * @generated from field: cryptos.fleet.v1.OperatorCA operator_ca = 1;
*/
- subjectCn: string;
+ operatorCa?: OperatorCA | undefined;
/**
- * issuer_cn is the restored identity's issuer common name.
- *
- * @generated from field: string issuer_cn = 2;
+ * @generated from field: bool confirmed = 2;
*/
- issuerCn: string;
+ confirmed: boolean;
+
+ /**
+ * @generated from field: string admin_extfile = 3;
+ */
+ adminExtfile: string;
+
+ /**
+ * @generated from field: cryptos.fleet.v1.OcspProbeResult ocsp_probe = 4;
+ */
+ ocspProbe?: OcspProbeResult | undefined;
};
/**
- * Describes the message cryptos.fleet.v1.ImportCAKeyResponse.
- * Use `create(ImportCAKeyResponseSchema)` to create a new message.
+ * Describes the message cryptos.fleet.v1.RegisterOperatorCAResponse.
+ * Use `create(RegisterOperatorCAResponseSchema)` to create a new message.
*/
-export const ImportCAKeyResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 55);
+export const RegisterOperatorCAResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 75);
/**
- * IssueOperatorCredentialRequest carries the operator subject CN, the access
- * level to grant, and the browser-generated CSR to sign under the matching
- * operator- profile.
- *
- * @generated from message cryptos.fleet.v1.IssueOperatorCredentialRequest
+ * @generated from message cryptos.fleet.v1.RetireOperatorCARequest
*/
-export type IssueOperatorCredentialRequest = Message<"cryptos.fleet.v1.IssueOperatorCredentialRequest"> & {
+export type RetireOperatorCARequest = Message<"cryptos.fleet.v1.RetireOperatorCARequest"> & {
/**
- * @generated from field: string common_name = 1;
+ * @generated from field: string sha256 = 1;
*/
- commonName: string;
+ sha256: string;
/**
- * "viewer"|"operator"|"admin"
+ * i_understand_self_lockout allows retiring the CA the caller's own
+ * certificate chains to.
*
- * @generated from field: string level = 2;
+ * @generated from field: bool i_understand_self_lockout = 2;
*/
- level: string;
+ iUnderstandSelfLockout: boolean;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.RetireOperatorCARequest.
+ * Use `create(RetireOperatorCARequestSchema)` to create a new message.
+ */
+export const RetireOperatorCARequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 76);
+/**
+ * @generated from message cryptos.fleet.v1.RetireOperatorCAResponse
+ */
+export type RetireOperatorCAResponse = Message<"cryptos.fleet.v1.RetireOperatorCAResponse"> & {
/**
- * browser-generated
- *
- * @generated from field: bytes csr_der = 3;
+ * @generated from field: cryptos.fleet.v1.OperatorCA operator_ca = 1;
*/
- csrDer: Uint8Array;
+ operatorCa?: OperatorCA | undefined;
};
/**
- * Describes the message cryptos.fleet.v1.IssueOperatorCredentialRequest.
- * Use `create(IssueOperatorCredentialRequestSchema)` to create a new message.
+ * Describes the message cryptos.fleet.v1.RetireOperatorCAResponse.
+ * Use `create(RetireOperatorCAResponseSchema)` to create a new message.
*/
-export const IssueOperatorCredentialRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 56);
+export const RetireOperatorCAResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 77);
/**
- * IssueOperatorCredentialResponse carries the signed operator certificate and
- * its hex serial. The browser assembles the PKCS#12 with its own key.
- *
- * @generated from message cryptos.fleet.v1.IssueOperatorCredentialResponse
+ * @generated from message cryptos.fleet.v1.SetOperatorCACRLSourceRequest
*/
-export type IssueOperatorCredentialResponse = Message<"cryptos.fleet.v1.IssueOperatorCredentialResponse"> & {
+export type SetOperatorCACRLSourceRequest = Message<"cryptos.fleet.v1.SetOperatorCACRLSourceRequest"> & {
/**
- * @generated from field: bytes cert_der = 1;
+ * @generated from field: string sha256 = 1;
*/
- certDer: Uint8Array;
+ sha256: string;
/**
- * @generated from field: string serial_hex = 2;
+ * @generated from oneof cryptos.fleet.v1.SetOperatorCACRLSourceRequest.crl_source
*/
- serialHex: string;
+ crlSource: {
+ /**
+ * url is an http or https URL; the CRL there must verify first.
+ *
+ * @generated from field: string url = 2;
+ */
+ value: string;
+ case: "url";
+ } | {
+ /**
+ * none, set to true, means no CRL. It needs the NO_CRL acknowledgement.
+ *
+ * @generated from field: bool none = 3;
+ */
+ value: boolean;
+ case: "none";
+ } | {
+ /**
+ * crl_der switches to the upload source with this initial CRL, DER or
+ * PEM, at most 4 MiB.
+ *
+ * @generated from field: bytes crl_der = 5;
+ */
+ value: Uint8Array;
+ case: "crlDer";
+ } | { case: undefined; value?: undefined };
+
+ /**
+ * @generated from field: repeated cryptos.fleet.v1.OperatorCAAcknowledgement acknowledgements = 4;
+ */
+ acknowledgements: OperatorCAAcknowledgement[];
};
/**
- * Describes the message cryptos.fleet.v1.IssueOperatorCredentialResponse.
- * Use `create(IssueOperatorCredentialResponseSchema)` to create a new message.
+ * Describes the message cryptos.fleet.v1.SetOperatorCACRLSourceRequest.
+ * Use `create(SetOperatorCACRLSourceRequestSchema)` to create a new message.
*/
-export const IssueOperatorCredentialResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 57);
+export const SetOperatorCACRLSourceRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 78);
/**
- * RevokeOperatorCredentialRequest names the operator credential to revoke by
- * hex serial, with the RFC 5280 reason code.
- *
- * @generated from message cryptos.fleet.v1.RevokeOperatorCredentialRequest
+ * @generated from message cryptos.fleet.v1.SetOperatorCACRLSourceResponse
*/
-export type RevokeOperatorCredentialRequest = Message<"cryptos.fleet.v1.RevokeOperatorCredentialRequest"> & {
- /**
- * @generated from field: string serial_hex = 1;
- */
- serialHex: string;
-
+export type SetOperatorCACRLSourceResponse = Message<"cryptos.fleet.v1.SetOperatorCACRLSourceResponse"> & {
/**
- * @generated from field: int32 reason_code = 2;
+ * @generated from field: cryptos.fleet.v1.OperatorCA operator_ca = 1;
*/
- reasonCode: number;
+ operatorCa?: OperatorCA | undefined;
};
/**
- * Describes the message cryptos.fleet.v1.RevokeOperatorCredentialRequest.
- * Use `create(RevokeOperatorCredentialRequestSchema)` to create a new message.
+ * Describes the message cryptos.fleet.v1.SetOperatorCACRLSourceResponse.
+ * Use `create(SetOperatorCACRLSourceResponseSchema)` to create a new message.
*/
-export const RevokeOperatorCredentialRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 58);
+export const SetOperatorCACRLSourceResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 79);
/**
- * RevokeOperatorCredentialResponse echoes the revoked serial and the time it
- * was revoked (RFC3339).
- *
- * @generated from message cryptos.fleet.v1.RevokeOperatorCredentialResponse
+ * @generated from message cryptos.fleet.v1.UploadOperatorCRLRequest
*/
-export type RevokeOperatorCredentialResponse = Message<"cryptos.fleet.v1.RevokeOperatorCredentialResponse"> & {
+export type UploadOperatorCRLRequest = Message<"cryptos.fleet.v1.UploadOperatorCRLRequest"> & {
/**
- * @generated from field: string serial_hex = 1;
+ * @generated from field: string sha256 = 1;
*/
- serialHex: string;
+ sha256: string;
/**
- * @generated from field: string revoked_at = 2;
+ * crl_der is the CRL, DER or PEM, at most 4 MiB.
+ *
+ * @generated from field: bytes crl_der = 2;
*/
- revokedAt: string;
+ crlDer: Uint8Array;
};
/**
- * Describes the message cryptos.fleet.v1.RevokeOperatorCredentialResponse.
- * Use `create(RevokeOperatorCredentialResponseSchema)` to create a new message.
+ * Describes the message cryptos.fleet.v1.UploadOperatorCRLRequest.
+ * Use `create(UploadOperatorCRLRequestSchema)` to create a new message.
*/
-export const RevokeOperatorCredentialResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 59);
+export const UploadOperatorCRLRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 80);
/**
- * @generated from message cryptos.fleet.v1.ListOperatorCredentialsRequest
+ * @generated from message cryptos.fleet.v1.UploadOperatorCRLResponse
*/
-export type ListOperatorCredentialsRequest = Message<"cryptos.fleet.v1.ListOperatorCredentialsRequest"> & {
+export type UploadOperatorCRLResponse = Message<"cryptos.fleet.v1.UploadOperatorCRLResponse"> & {
+ /**
+ * @generated from field: cryptos.fleet.v1.OperatorCA operator_ca = 1;
+ */
+ operatorCa?: OperatorCA | undefined;
};
/**
- * Describes the message cryptos.fleet.v1.ListOperatorCredentialsRequest.
- * Use `create(ListOperatorCredentialsRequestSchema)` to create a new message.
+ * Describes the message cryptos.fleet.v1.UploadOperatorCRLResponse.
+ * Use `create(UploadOperatorCRLResponseSchema)` to create a new message.
*/
-export const ListOperatorCredentialsRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 60);
+export const UploadOperatorCRLResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 81);
/**
- * OperatorCredential is one issued operator credential row.
- *
- * @generated from message cryptos.fleet.v1.OperatorCredential
+ * @generated from message cryptos.fleet.v1.SetOperatorCAOCSPRequest
*/
-export type OperatorCredential = Message<"cryptos.fleet.v1.OperatorCredential"> & {
- /**
- * @generated from field: string common_name = 1;
- */
- commonName: string;
-
+export type SetOperatorCAOCSPRequest = Message<"cryptos.fleet.v1.SetOperatorCAOCSPRequest"> & {
/**
- * @generated from field: string serial_hex = 2;
- */
- serialHex: string;
-
- /**
- * @generated from field: string level = 3;
+ * @generated from field: string sha256 = 1;
*/
- level: string;
+ sha256: string;
/**
- * @generated from field: string not_after = 4;
+ * @generated from field: cryptos.fleet.v1.OcspMode ocsp_mode = 2;
*/
- notAfter: string;
+ ocspMode: OcspMode;
/**
- * @generated from field: bool revoked = 5;
+ * ocsp_url is required for OCSP_MODE_URL, http or https, at most 2 KiB.
+ *
+ * @generated from field: string ocsp_url = 3;
*/
- revoked: boolean;
+ ocspUrl: string;
};
/**
- * Describes the message cryptos.fleet.v1.OperatorCredential.
- * Use `create(OperatorCredentialSchema)` to create a new message.
+ * Describes the message cryptos.fleet.v1.SetOperatorCAOCSPRequest.
+ * Use `create(SetOperatorCAOCSPRequestSchema)` to create a new message.
*/
-export const OperatorCredentialSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 61);
+export const SetOperatorCAOCSPRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 82);
/**
- * @generated from message cryptos.fleet.v1.ListOperatorCredentialsResponse
+ * @generated from message cryptos.fleet.v1.SetOperatorCAOCSPResponse
*/
-export type ListOperatorCredentialsResponse = Message<"cryptos.fleet.v1.ListOperatorCredentialsResponse"> & {
+export type SetOperatorCAOCSPResponse = Message<"cryptos.fleet.v1.SetOperatorCAOCSPResponse"> & {
/**
- * @generated from field: repeated cryptos.fleet.v1.OperatorCredential items = 1;
+ * @generated from field: cryptos.fleet.v1.OperatorCA operator_ca = 1;
*/
- items: OperatorCredential[];
+ operatorCa?: OperatorCA | undefined;
+
+ /**
+ * ocsp_probe is the probe result for OCSP_MODE_URL, absent otherwise.
+ *
+ * @generated from field: cryptos.fleet.v1.OcspProbeResult ocsp_probe = 2;
+ */
+ ocspProbe?: OcspProbeResult | undefined;
};
/**
- * Describes the message cryptos.fleet.v1.ListOperatorCredentialsResponse.
- * Use `create(ListOperatorCredentialsResponseSchema)` to create a new message.
+ * Describes the message cryptos.fleet.v1.SetOperatorCAOCSPResponse.
+ * Use `create(SetOperatorCAOCSPResponseSchema)` to create a new message.
*/
-export const ListOperatorCredentialsResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 62);
+export const SetOperatorCAOCSPResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 83);
/**
* PreviewAdoptionRequest names the maintenance endpoint to inspect.
@@ -1989,7 +2704,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, 63);
+ messageDesc(file_cryptos_fleet_v1_fleet, 84);
/**
* PreviewAdoptionResponse returns the maintenance node's certificate
@@ -2014,7 +2729,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, 64);
+ messageDesc(file_cryptos_fleet_v1_fleet, 85);
/**
* @generated from message cryptos.fleet.v1.ListInstallDisksRequest
@@ -2036,7 +2751,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, 65);
+ messageDesc(file_cryptos_fleet_v1_fleet, 86);
/**
* @generated from message cryptos.fleet.v1.ListInstallDisksResponse
@@ -2053,7 +2768,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, 66);
+ messageDesc(file_cryptos_fleet_v1_fleet, 87);
/**
* AdoptNodeRequest carries the maintenance endpoint, the fingerprint the
@@ -2087,12 +2802,13 @@ 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, 67);
+ messageDesc(file_cryptos_fleet_v1_fleet, 88);
/**
* AdoptNodeResponse is streamed during adoption. phase is one of
- * applying-config, installing, awaiting-reboot, ceremony, established; done is
- * set on the final message.
+ * applying-config, installing, awaiting-reboot,
+ * awaiting-fingerprint-confirmation, ceremony, established,
+ * awaiting-certificate or error; done is set on the final message.
*
* @generated from message cryptos.fleet.v1.AdoptNodeResponse
*/
@@ -2119,6 +2835,24 @@ export type AdoptNodeResponse = Message<"cryptos.fleet.v1.AdoptNodeResponse"> &
* @generated from field: string node_id = 4;
*/
nodeId: string;
+
+ /**
+ * adoption_id names this adoption run; set on every message. Pass it to
+ * ConfirmAdoptionFingerprint.
+ *
+ * @generated from field: string adoption_id = 5;
+ */
+ adoptionId: string;
+
+ /**
+ * presented_cert_sha256 is the lowercase hex SHA-256 of the management
+ * certificate the installed node presents after its reboot. Set only on the
+ * awaiting-fingerprint-confirmation phase, where the adoption waits for
+ * ConfirmAdoptionFingerprint.
+ *
+ * @generated from field: string presented_cert_sha256 = 6;
+ */
+ presentedCertSha256: string;
};
/**
@@ -2126,7 +2860,53 @@ 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, 68);
+ messageDesc(file_cryptos_fleet_v1_fleet, 89);
+
+/**
+ * ConfirmAdoptionFingerprintRequest names the paused adoption and the
+ * fingerprint the operator read on the node's console.
+ *
+ * @generated from message cryptos.fleet.v1.ConfirmAdoptionFingerprintRequest
+ */
+export type ConfirmAdoptionFingerprintRequest = Message<"cryptos.fleet.v1.ConfirmAdoptionFingerprintRequest"> & {
+ /**
+ * adoption_id is AdoptNodeResponse.adoption_id.
+ *
+ * @generated from field: string adoption_id = 1;
+ */
+ adoptionId: string;
+
+ /**
+ * cert_sha256 is the SHA-256 the operator confirmed, hex with or without
+ * colons, in either case.
+ *
+ * @generated from field: string cert_sha256 = 2;
+ */
+ certSha256: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.ConfirmAdoptionFingerprintRequest.
+ * Use `create(ConfirmAdoptionFingerprintRequestSchema)` to create a new message.
+ */
+export const ConfirmAdoptionFingerprintRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 90);
+
+/**
+ * ConfirmAdoptionFingerprintResponse is empty: the adoption's progress
+ * continues on the AdoptNode stream.
+ *
+ * @generated from message cryptos.fleet.v1.ConfirmAdoptionFingerprintResponse
+ */
+export type ConfirmAdoptionFingerprintResponse = Message<"cryptos.fleet.v1.ConfirmAdoptionFingerprintResponse"> & {
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.ConfirmAdoptionFingerprintResponse.
+ * Use `create(ConfirmAdoptionFingerprintResponseSchema)` to create a new message.
+ */
+export const ConfirmAdoptionFingerprintResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 91);
/**
* DecommissionNodeRequest names the managed node to wipe and echoes its current
@@ -2163,7 +2943,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, 69);
+ messageDesc(file_cryptos_fleet_v1_fleet, 92);
/**
* DecommissionNodeResponse reports that the node accepted the wipe and is
@@ -2183,7 +2963,7 @@ 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, 70);
+ messageDesc(file_cryptos_fleet_v1_fleet, 93);
/**
* RenameNodeRequest names the node by its stable ID and carries its new
@@ -2213,7 +2993,7 @@ export type RenameNodeRequest = 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);
+ messageDesc(file_cryptos_fleet_v1_fleet, 94);
/**
* RenameNodeResponse returns the node after the rename.
@@ -2234,7 +3014,7 @@ export type RenameNodeResponse = 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);
+ messageDesc(file_cryptos_fleet_v1_fleet, 95);
/**
* ListMcpKeysRequest scopes the MCP key listing.
@@ -2255,7 +3035,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, 73);
+ messageDesc(file_cryptos_fleet_v1_fleet, 96);
/**
* ListMcpKeysResponse carries the listed MCP keys, revoked ones included.
@@ -2276,7 +3056,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, 74);
+ messageDesc(file_cryptos_fleet_v1_fleet, 97);
/**
* RevokeMcpKeyRequest names the MCP key to revoke.
@@ -2297,7 +3077,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, 75);
+ messageDesc(file_cryptos_fleet_v1_fleet, 98);
/**
* RevokeMcpKeyResponse returns the key after revocation.
@@ -2318,7 +3098,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, 76);
+ messageDesc(file_cryptos_fleet_v1_fleet, 99);
/**
* CreateMcpKeyRequest carries the operator's label for the new key and an
@@ -2349,7 +3129,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, 77);
+ messageDesc(file_cryptos_fleet_v1_fleet, 100);
/**
* CreateMcpKeyResponse carries the new key. plaintext_key is returned only
@@ -2379,7 +3159,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, 78);
+ messageDesc(file_cryptos_fleet_v1_fleet, 101);
/**
* McpKey is the metadata of one MCP agent key. It never carries the key or
@@ -2463,7 +3243,7 @@ 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, 79);
+ messageDesc(file_cryptos_fleet_v1_fleet, 102);
/**
* ListApprovalsRequest filters the approval listing.
@@ -2485,7 +3265,7 @@ export type ListApprovalsRequest = Message<"cryptos.fleet.v1.ListApprovalsReques
* Use `create(ListApprovalsRequestSchema)` to create a new message.
*/
export const ListApprovalsRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 80);
+ messageDesc(file_cryptos_fleet_v1_fleet, 103);
/**
* ListApprovalsResponse carries the listed approvals.
@@ -2506,7 +3286,7 @@ export type ListApprovalsResponse = Message<"cryptos.fleet.v1.ListApprovalsRespo
* Use `create(ListApprovalsResponseSchema)` to create a new message.
*/
export const ListApprovalsResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 81);
+ messageDesc(file_cryptos_fleet_v1_fleet, 104);
/**
* DecideApprovalRequest names the approval to decide and the decision.
@@ -2534,7 +3314,7 @@ export type DecideApprovalRequest = Message<"cryptos.fleet.v1.DecideApprovalRequ
* Use `create(DecideApprovalRequestSchema)` to create a new message.
*/
export const DecideApprovalRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 82);
+ messageDesc(file_cryptos_fleet_v1_fleet, 105);
/**
* DecideApprovalResponse returns the approval after the decision.
@@ -2556,7 +3336,7 @@ export type DecideApprovalResponse = Message<"cryptos.fleet.v1.DecideApprovalRes
* Use `create(DecideApprovalResponseSchema)` to create a new message.
*/
export const DecideApprovalResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_fleet_v1_fleet, 83);
+ messageDesc(file_cryptos_fleet_v1_fleet, 106);
/**
* Approval is one step-up approval request for an MCP tool call. Listing and
@@ -2677,7 +3457,7 @@ export type Approval = 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);
+ messageDesc(file_cryptos_fleet_v1_fleet, 107);
/**
* Health reports the manager's view of node reachability, independent of
@@ -2932,6 +3712,19 @@ export const FleetService: GenService<{
input: typeof RevokeCertificateRequestSchema;
output: typeof RevokeCertificateResponseSchema;
},
+ /**
+ * GetCertificate fetches an issued certificate and its chain, PEM encoded,
+ * from the node that issued it, identified by node name and hex serial.
+ * Readable at viewer level and above; a read, so it is not audited. NotFound
+ * if the node has no certificate with that serial.
+ *
+ * @generated from rpc cryptos.fleet.v1.FleetService.GetCertificate
+ */
+ getCertificate: {
+ methodKind: "unary";
+ input: typeof GetCertificateRequestSchema;
+ output: typeof GetCertificateResponseSchema;
+ },
/**
* IssueLeaf signs a leaf certificate on the named issuing node from a
* browser-generated PKCS#10 CSR under a named issuance profile. Only the
@@ -3014,25 +3807,63 @@ export const FleetService: GenService<{
output: typeof ImportCAKeyResponseSchema;
},
/**
- * IssueOperatorCredential issues an operator client certificate. The browser
- * generates the key and CSR; the manager routes signing to the node acting as
- * the operator CA under an operator- profile that carries the
- * access-level extension. Only the CSR crosses the wire; the operator key
- * stays in the browser. Admin-gated and audited (the audit names the subject
- * and serial only, never the CSR or certificate bytes).
+ * CreateOperatorCredentialRequest stores a pending request for an operator
+ * credential from a CSR with subject CN=, for the external operator
+ * CA to sign out of band. It returns the CSR, the OpenSSL extension section
+ * for the level, and the signing command. The request expires after 30
+ * days. Admin-gated and audited.
+ *
+ * @generated from rpc cryptos.fleet.v1.FleetService.CreateOperatorCredentialRequest
+ */
+ createOperatorCredentialRequest: {
+ methodKind: "unary";
+ input: typeof CreateOperatorCredentialRequestRequestSchema;
+ output: typeof CreateOperatorCredentialRequestResponseSchema;
+ },
+ /**
+ * ListOperatorCredentialRequests returns credential requests, newest first,
+ * optionally filtered by state. Operator-readable.
+ *
+ * @generated from rpc cryptos.fleet.v1.FleetService.ListOperatorCredentialRequests
+ */
+ listOperatorCredentialRequests: {
+ methodKind: "unary";
+ input: typeof ListOperatorCredentialRequestsRequestSchema;
+ output: typeof ListOperatorCredentialRequestsResponseSchema;
+ },
+ /**
+ * CancelOperatorCredentialRequest cancels a pending request. Admin-gated and
+ * audited.
+ *
+ * @generated from rpc cryptos.fleet.v1.FleetService.CancelOperatorCredentialRequest
+ */
+ cancelOperatorCredentialRequest: {
+ methodKind: "unary";
+ input: typeof CancelOperatorCredentialRequestRequestSchema;
+ output: typeof CancelOperatorCredentialRequestResponseSchema;
+ },
+ /**
+ * RecordOperatorCredential checks and records an operator certificate the
+ * external operator CA signed. With request_id, the level, CN and public key
+ * must match the pending request, which is then completed; without it, a
+ * certificate made entirely out of band is imported. The certificate must
+ * chain to the active operator CA and not be recorded already. Admin-gated
+ * and audited.
*
- * @generated from rpc cryptos.fleet.v1.FleetService.IssueOperatorCredential
+ * @generated from rpc cryptos.fleet.v1.FleetService.RecordOperatorCredential
*/
- issueOperatorCredential: {
+ recordOperatorCredential: {
methodKind: "unary";
- input: typeof IssueOperatorCredentialRequestSchema;
- output: typeof IssueOperatorCredentialResponseSchema;
+ input: typeof RecordOperatorCredentialRequestSchema;
+ output: typeof RecordOperatorCredentialResponseSchema;
},
/**
- * RevokeOperatorCredential revokes an operator credential on the operator-CA
- * node by hex serial, with an RFC 5280 reason code. The manager enforces the
- * revocation in the authz middleware, so a revoked operator can no longer
- * authenticate. Admin-gated and audited.
+ * RevokeOperatorCredential puts an operator credential on the manager's
+ * denylist, keyed by issuer and hex serial, with an RFC 5280 reason code.
+ * The authz middleware refuses the credential from its next request. A
+ * serial the manager never recorded can be denied too. It calls no node and
+ * doesn't revoke at the CA: the response warns to revoke there too and
+ * publish a new CRL. Admin-gated and audited.
*
* @generated from rpc cryptos.fleet.v1.FleetService.RevokeOperatorCredential
*/
@@ -3042,8 +3873,9 @@ export const FleetService: GenService<{
output: typeof RevokeOperatorCredentialResponseSchema;
},
/**
- * ListOperatorCredentials returns the operator credentials the manager has
- * issued, with their level, expiry, and revocation state. Operator-readable.
+ * ListOperatorCredentials returns the operator credentials the manager
+ * knows: first-admin, requested, recorded and observed in use, with their
+ * issuer and revocation state. Operator-readable.
*
* @generated from rpc cryptos.fleet.v1.FleetService.ListOperatorCredentials
*/
@@ -3052,6 +3884,82 @@ export const FleetService: GenService<{
input: typeof ListOperatorCredentialsRequestSchema;
output: typeof ListOperatorCredentialsResponseSchema;
},
+ /**
+ * ListOperatorCAs returns the operator CAs the manager trusts or has
+ * trusted, with their CRL and OCSP state. Operator-readable.
+ *
+ * @generated from rpc cryptos.fleet.v1.FleetService.ListOperatorCAs
+ */
+ listOperatorCAs: {
+ methodKind: "unary";
+ input: typeof ListOperatorCAsRequestSchema;
+ output: typeof ListOperatorCAsResponseSchema;
+ },
+ /**
+ * RegisterOperatorCA registers a new operator CA after first run, with the
+ * same checks, preview and confirm_sha256 step as
+ * BootstrapService.RegisterOperatorCA. The new CA becomes active and the
+ * previous active CA becomes retiring, still trusted until it is retired.
+ * Refused with 1605 ROTATION_IN_PROGRESS while a retiring CA exists.
+ * Admin-gated and audited. Every operator-CA write returns 1607 while the
+ * operator CA comes from the manager's config file.
+ *
+ * @generated from rpc cryptos.fleet.v1.FleetService.RegisterOperatorCA
+ */
+ registerOperatorCA: {
+ methodKind: "unary";
+ input: typeof RegisterOperatorCARequestSchema;
+ output: typeof RegisterOperatorCAResponseSchema;
+ },
+ /**
+ * RetireOperatorCA stops trusting an operator CA; every certificate under
+ * it is refused from its next request. Refused with 1609 for the last
+ * active CA, and refused when the caller's own certificate chains only to
+ * it unless i_understand_self_lockout is set. Admin-gated and audited.
+ *
+ * @generated from rpc cryptos.fleet.v1.FleetService.RetireOperatorCA
+ */
+ retireOperatorCA: {
+ methodKind: "unary";
+ input: typeof RetireOperatorCARequestSchema;
+ output: typeof RetireOperatorCAResponseSchema;
+ },
+ /**
+ * SetOperatorCACRLSource changes an operator CA's CRL source. A new URL or
+ * CRL must verify against the CA first; switching to none needs the NO_CRL
+ * acknowledgement and cuts off MCP for certificates under the CA.
+ * Admin-gated and audited.
+ *
+ * @generated from rpc cryptos.fleet.v1.FleetService.SetOperatorCACRLSource
+ */
+ setOperatorCACRLSource: {
+ methodKind: "unary";
+ input: typeof SetOperatorCACRLSourceRequestSchema;
+ output: typeof SetOperatorCACRLSourceResponseSchema;
+ },
+ /**
+ * UploadOperatorCRL stores a new CRL for an operator CA whose CRL source is
+ * upload. It must verify against the CA and be newer than the stored CRL.
+ * Admin-gated and audited.
+ *
+ * @generated from rpc cryptos.fleet.v1.FleetService.UploadOperatorCRL
+ */
+ uploadOperatorCRL: {
+ methodKind: "unary";
+ input: typeof UploadOperatorCRLRequestSchema;
+ output: typeof UploadOperatorCRLResponseSchema;
+ },
+ /**
+ * SetOperatorCAOCSP changes an operator CA's OCSP mode. OCSP_MODE_URL must
+ * pass a probe of the responder first. Admin-gated and audited.
+ *
+ * @generated from rpc cryptos.fleet.v1.FleetService.SetOperatorCAOCSP
+ */
+ setOperatorCAOCSP: {
+ methodKind: "unary";
+ input: typeof SetOperatorCAOCSPRequestSchema;
+ output: typeof SetOperatorCAOCSPResponseSchema;
+ },
/**
* PreviewAdoption performs the trust-on-first-use step of adopting a new
* node: it dials the maintenance endpoint (no pin yet), captures the
@@ -3082,10 +3990,12 @@ export const FleetService: GenService<{
/**
* AdoptNode provisions a new maintenance node end to end and streams progress.
* Pinned to the fingerprint the operator confirmed via PreviewAdoption, the
- * manager applies the initial config, awaits the reboot, drives the first-boot
- * ceremony, and registers the node. Each streamed message carries a phase
- * (applying-config, installing, awaiting-reboot, ceremony, established) with
- * human-readable detail; the final message sets done. Admin-gated and audited.
+ * manager applies the initial config, awaits the reboot, waits for the
+ * operator to confirm the installed node's certificate fingerprint
+ * (ConfirmAdoptionFingerprint), drives the first-boot ceremony, and
+ * registers the node. Each streamed message carries a phase (see
+ * AdoptNodeResponse) with human-readable detail; the final message sets
+ * done. Admin-gated and audited.
*
* @generated from rpc cryptos.fleet.v1.FleetService.AdoptNode
*/
@@ -3094,6 +4004,27 @@ export const FleetService: GenService<{
input: typeof AdoptNodeRequestSchema;
output: typeof AdoptNodeResponseSchema;
},
+ /**
+ * ConfirmAdoptionFingerprint lets a paused adoption continue. After the node
+ * installs and reboots it presents a new management certificate that
+ * nothing links to the maintenance fingerprint confirmed via
+ * PreviewAdoption, so AdoptNode streams the
+ * awaiting-fingerprint-confirmation phase with that certificate's SHA-256
+ * and waits. The operator compares it with the Mgmt SHA-256 line on the
+ * node's console and sends it back here. On a match (case and colons
+ * ignored) the adoption resumes and the presented certificate is what the
+ * manager trusts and records; on a mismatch the manager returns
+ * InvalidArgument, the adoption stream ends with the error phase, and
+ * nothing is recorded. NotFound means no adoption with that ID is waiting.
+ * Admin-gated and audited.
+ *
+ * @generated from rpc cryptos.fleet.v1.FleetService.ConfirmAdoptionFingerprint
+ */
+ confirmAdoptionFingerprint: {
+ methodKind: "unary";
+ input: typeof ConfirmAdoptionFingerprintRequestSchema;
+ output: typeof ConfirmAdoptionFingerprintResponseSchema;
+ },
/**
* DecommissionNode remotely wipes a managed node's identity and data via the
* node's mTLS-served RemoteReset, then reboots it into maintenance. The caller
diff --git a/src/gen/fleet/cryptos/fleet/v1/operator_ca_pb.ts b/src/gen/fleet/cryptos/fleet/v1/operator_ca_pb.ts
new file mode 100644
index 0000000..d4d66bb
--- /dev/null
+++ b/src/gen/fleet/cryptos/fleet/v1/operator_ca_pb.ts
@@ -0,0 +1,429 @@
+// @generated by protoc-gen-es v2.12.1 with parameter "target=ts"
+// @generated from file cryptos/fleet/v1/operator_ca.proto (package cryptos.fleet.v1, syntax proto3)
+/* eslint-disable */
+
+import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2";
+import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2";
+import type { Message } from "@bufbuild/protobuf";
+
+/**
+ * Describes the file cryptos/fleet/v1/operator_ca.proto.
+ */
+export const file_cryptos_fleet_v1_operator_ca: GenFile = /*@__PURE__*/
+ fileDesc("CiJjcnlwdG9zL2ZsZWV0L3YxL29wZXJhdG9yX2NhLnByb3RvEhBjcnlwdG9zLmZsZWV0LnYxIoQECgpPcGVyYXRvckNBEg4KBnNoYTI1NhgBIAEoCRIPCgdzdWJqZWN0GAIgASgJEg4KBmlzc3VlchgDIAEoCRIRCglub3RfYWZ0ZXIYBCABKAkSMAoFc3RhdGUYBSABKA4yIS5jcnlwdG9zLmZsZWV0LnYxLk9wZXJhdG9yQ0FTdGF0ZRIvCgpjcmxfc291cmNlGAYgASgOMhsuY3J5cHRvcy5mbGVldC52MS5DcmxTb3VyY2USFAoMY3JsX2xvY2F0aW9uGAcgASgJEigKA2NybBgIIAEoCzIbLmNyeXB0b3MuZmxlZXQudjEuQ3JsU3RhdHVzEi0KCW9jc3BfbW9kZRgJIAEoDjIaLmNyeXB0b3MuZmxlZXQudjEuT2NzcE1vZGUSEAoIb2NzcF91cmwYCiABKAkSFwoPb2NzcF9sYXN0X2Vycm9yGAsgASgJEhAKCHdhcm5pbmdzGAwgAygJEkUKEGFja25vd2xlZGdlbWVudHMYDSADKA4yKy5jcnlwdG9zLmZsZWV0LnYxLk9wZXJhdG9yQ0FBY2tub3dsZWRnZW1lbnQSFQoNcmVnaXN0ZXJlZF9hdBgOIAEoCRISCgpyZXRpcmVkX2F0GA8gASgJEhYKDnJldGlyZWRfcmVhc29uGBAgASgJEhkKEW1hbmFnZWRfYnlfY29uZmlnGBEgASgIIpcBCglDcmxTdGF0dXMSEwoLdGhpc191cGRhdGUYASABKAkSEwoLbmV4dF91cGRhdGUYAiABKAkSFQoNcmV2b2tlZF9jb3VudBgDIAEoAxISCgpjcmxfbnVtYmVyGAQgASgJEhIKCmZldGNoZWRfYXQYBSABKAkSEgoKbGFzdF9lcnJvchgGIAEoCRINCgVzdGFsZRgHIAEoCCKGAQoPT2NzcFByb2JlUmVzdWx0EiwKBnNpZ25lchgBIAEoDjIcLmNyeXB0b3MuZmxlZXQudjEuT2NzcFNpZ25lchIWCg5zaWduZXJfc3ViamVjdBgCIAEoCRIYChBzaWduZXJfbm90X2FmdGVyGAMgASgJEhMKC2NlcnRfc3RhdHVzGAQgASgJKpEBCg9PcGVyYXRvckNBU3RhdGUSIQodT1BFUkFUT1JfQ0FfU1RBVEVfVU5TUEVDSUZJRUQQABIcChhPUEVSQVRPUl9DQV9TVEFURV9BQ1RJVkUQARIeChpPUEVSQVRPUl9DQV9TVEFURV9SRVRJUklORxACEh0KGU9QRVJBVE9SX0NBX1NUQVRFX1JFVElSRUQQAyp8CglDcmxTb3VyY2USGgoWQ1JMX1NPVVJDRV9VTlNQRUNJRklFRBAAEhMKD0NSTF9TT1VSQ0VfTk9ORRABEhIKDkNSTF9TT1VSQ0VfVVJMEAISFQoRQ1JMX1NPVVJDRV9VUExPQUQQAxITCg9DUkxfU09VUkNFX1BBVEgQBCpeCghPY3NwTW9kZRIZChVPQ1NQX01PREVfVU5TUEVDSUZJRUQQABIRCg1PQ1NQX01PREVfT0ZGEAESEQoNT0NTUF9NT0RFX0FJQRACEhEKDU9DU1BfTU9ERV9VUkwQAypcCgpPY3NwU2lnbmVyEhsKF09DU1BfU0lHTkVSX1VOU1BFQ0lGSUVEEAASFgoST0NTUF9TSUdORVJfQU5DSE9SEAESGQoVT0NTUF9TSUdORVJfREVMRUdBVEVEEAIqcAoZT3BlcmF0b3JDQUFja25vd2xlZGdlbWVudBIrCidPUEVSQVRPUl9DQV9BQ0tOT1dMRURHRU1FTlRfVU5TUEVDSUZJRUQQABImCiJPUEVSQVRPUl9DQV9BQ0tOT1dMRURHRU1FTlRfTk9fQ1JMEAFCOFo2Z2l0aHViLmNvbS9DcnlwdE9TLVBLSS9hcGkvZ28vY3J5cHRvcy9mbGVldC92MTtmbGVldHYxYgZwcm90bzM");
+
+/**
+ * OperatorCA is one trusted, or formerly trusted, operator CA. Timestamps are
+ * RFC3339 strings, empty when unset.
+ *
+ * @generated from message cryptos.fleet.v1.OperatorCA
+ */
+export type OperatorCA = Message<"cryptos.fleet.v1.OperatorCA"> & {
+ /**
+ * sha256 is the lowercase hex SHA-256 of the DER certificate, with no
+ * separators. It identifies the CA in every other request.
+ *
+ * @generated from field: string sha256 = 1;
+ */
+ sha256: string;
+
+ /**
+ * subject is the certificate subject as an RFC 4514 string.
+ *
+ * @generated from field: string subject = 2;
+ */
+ subject: string;
+
+ /**
+ * issuer is the certificate issuer as an RFC 4514 string.
+ *
+ * @generated from field: string issuer = 3;
+ */
+ issuer: string;
+
+ /**
+ * @generated from field: string not_after = 4;
+ */
+ notAfter: string;
+
+ /**
+ * state is unspecified on a registration preview that isn't stored yet.
+ *
+ * @generated from field: cryptos.fleet.v1.OperatorCAState state = 5;
+ */
+ state: OperatorCAState;
+
+ /**
+ * @generated from field: cryptos.fleet.v1.CrlSource crl_source = 6;
+ */
+ crlSource: CrlSource;
+
+ /**
+ * crl_location is the CRL URL, or the file path for CRL_SOURCE_PATH.
+ *
+ * @generated from field: string crl_location = 7;
+ */
+ crlLocation: string;
+
+ /**
+ * crl describes the CRL the manager holds; absent when it holds none.
+ *
+ * @generated from field: cryptos.fleet.v1.CrlStatus crl = 8;
+ */
+ crl?: CrlStatus | undefined;
+
+ /**
+ * @generated from field: cryptos.fleet.v1.OcspMode ocsp_mode = 9;
+ */
+ ocspMode: OcspMode;
+
+ /**
+ * ocsp_url is the responder for OCSP_MODE_URL, empty otherwise.
+ *
+ * @generated from field: string ocsp_url = 10;
+ */
+ ocspUrl: string;
+
+ /**
+ * ocsp_last_error is the last OCSP failure this replica saw for a
+ * certificate under the CA, empty when none.
+ *
+ * @generated from field: string ocsp_last_error = 11;
+ */
+ ocspLastError: string;
+
+ /**
+ * warnings the caller should see, for example that the CA was issued by a
+ * CryptOS node's CA.
+ *
+ * @generated from field: repeated string warnings = 12;
+ */
+ warnings: string[];
+
+ /**
+ * @generated from field: repeated cryptos.fleet.v1.OperatorCAAcknowledgement acknowledgements = 13;
+ */
+ acknowledgements: OperatorCAAcknowledgement[];
+
+ /**
+ * @generated from field: string registered_at = 14;
+ */
+ registeredAt: string;
+
+ /**
+ * @generated from field: string retired_at = 15;
+ */
+ retiredAt: string;
+
+ /**
+ * retired_reason is "superseded" or "retired", empty unless retired.
+ *
+ * @generated from field: string retired_reason = 16;
+ */
+ retiredReason: string;
+
+ /**
+ * managed_by_config is true for a CA from the manager's config file. It
+ * can't be changed through the API.
+ *
+ * @generated from field: bool managed_by_config = 17;
+ */
+ managedByConfig: boolean;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.OperatorCA.
+ * Use `create(OperatorCASchema)` to create a new message.
+ */
+export const OperatorCASchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_operator_ca, 0);
+
+/**
+ * CrlStatus describes the CRL the manager holds for an operator CA. It is
+ * verified against the CA before it is stored or used.
+ *
+ * @generated from message cryptos.fleet.v1.CrlStatus
+ */
+export type CrlStatus = Message<"cryptos.fleet.v1.CrlStatus"> & {
+ /**
+ * @generated from field: string this_update = 1;
+ */
+ thisUpdate: string;
+
+ /**
+ * @generated from field: string next_update = 2;
+ */
+ nextUpdate: string;
+
+ /**
+ * @generated from field: int64 revoked_count = 3;
+ */
+ revokedCount: bigint;
+
+ /**
+ * crl_number is the cRLNumber in decimal, empty when the CRL has none.
+ *
+ * @generated from field: string crl_number = 4;
+ */
+ crlNumber: string;
+
+ /**
+ * fetched_at is when the manager last stored a new CRL.
+ *
+ * @generated from field: string fetched_at = 5;
+ */
+ fetchedAt: string;
+
+ /**
+ * last_error is the last fetch or verification failure, empty when the
+ * last attempt succeeded.
+ *
+ * @generated from field: string last_error = 6;
+ */
+ lastError: string;
+
+ /**
+ * stale is true once the CRL is past next_update.
+ *
+ * @generated from field: bool stale = 7;
+ */
+ stale: boolean;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.CrlStatus.
+ * Use `create(CrlStatusSchema)` to create a new message.
+ */
+export const CrlStatusSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_operator_ca, 1);
+
+/**
+ * OcspProbeResult is the answer the manager got when it probed an OCSP
+ * responder with a random serial. Any certificate status passes the probe;
+ * only a validly signed response is required.
+ *
+ * @generated from message cryptos.fleet.v1.OcspProbeResult
+ */
+export type OcspProbeResult = Message<"cryptos.fleet.v1.OcspProbeResult"> & {
+ /**
+ * @generated from field: cryptos.fleet.v1.OcspSigner signer = 1;
+ */
+ signer: OcspSigner;
+
+ /**
+ * signer_subject is the delegated responder certificate's subject, or the
+ * CA's subject when the CA signed.
+ *
+ * @generated from field: string signer_subject = 2;
+ */
+ signerSubject: string;
+
+ /**
+ * @generated from field: string signer_not_after = 3;
+ */
+ signerNotAfter: string;
+
+ /**
+ * cert_status is "good", "revoked" or "unknown" for the probed serial.
+ *
+ * @generated from field: string cert_status = 4;
+ */
+ certStatus: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.OcspProbeResult.
+ * Use `create(OcspProbeResultSchema)` to create a new message.
+ */
+export const OcspProbeResultSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_operator_ca, 2);
+
+/**
+ * OperatorCAState is where a registered operator CA is in its life.
+ *
+ * @generated from enum cryptos.fleet.v1.OperatorCAState
+ */
+export enum OperatorCAState {
+ /**
+ * @generated from enum value: OPERATOR_CA_STATE_UNSPECIFIED = 0;
+ */
+ OPERATOR_CA_STATE_UNSPECIFIED = 0,
+
+ /**
+ * Trusted, and the only CA new credentials are recorded under.
+ *
+ * @generated from enum value: OPERATOR_CA_STATE_ACTIVE = 1;
+ */
+ OPERATOR_CA_STATE_ACTIVE = 1,
+
+ /**
+ * Still trusted during a rotation, but no new credentials are recorded
+ * under it.
+ *
+ * @generated from enum value: OPERATOR_CA_STATE_RETIRING = 2;
+ */
+ OPERATOR_CA_STATE_RETIRING = 2,
+
+ /**
+ * No longer trusted. Kept for the record.
+ *
+ * @generated from enum value: OPERATOR_CA_STATE_RETIRED = 3;
+ */
+ OPERATOR_CA_STATE_RETIRED = 3,
+}
+
+/**
+ * Describes the enum cryptos.fleet.v1.OperatorCAState.
+ */
+export const OperatorCAStateSchema: GenEnum = /*@__PURE__*/
+ enumDesc(file_cryptos_fleet_v1_operator_ca, 0);
+
+/**
+ * CrlSource is where the manager gets an operator CA's CRL.
+ *
+ * @generated from enum cryptos.fleet.v1.CrlSource
+ */
+export enum CrlSource {
+ /**
+ * @generated from enum value: CRL_SOURCE_UNSPECIFIED = 0;
+ */
+ UNSPECIFIED = 0,
+
+ /**
+ * No CRL. Revocations made at the CA aren't seen, and MCP is refused for
+ * certificates under the CA.
+ *
+ * @generated from enum value: CRL_SOURCE_NONE = 1;
+ */
+ NONE = 1,
+
+ /**
+ * Fetched from an http or https URL.
+ *
+ * @generated from enum value: CRL_SOURCE_URL = 2;
+ */
+ URL = 2,
+
+ /**
+ * Uploaded with UploadOperatorCRL.
+ *
+ * @generated from enum value: CRL_SOURCE_UPLOAD = 3;
+ */
+ UPLOAD = 3,
+
+ /**
+ * A file the manager reads (config-file operator CAs only).
+ *
+ * @generated from enum value: CRL_SOURCE_PATH = 4;
+ */
+ PATH = 4,
+}
+
+/**
+ * Describes the enum cryptos.fleet.v1.CrlSource.
+ */
+export const CrlSourceSchema: GenEnum = /*@__PURE__*/
+ enumDesc(file_cryptos_fleet_v1_operator_ca, 1);
+
+/**
+ * OcspMode is how the manager finds an operator CA's OCSP responder.
+ *
+ * @generated from enum cryptos.fleet.v1.OcspMode
+ */
+export enum OcspMode {
+ /**
+ * Treated as OCSP_MODE_AIA when registering.
+ *
+ * @generated from enum value: OCSP_MODE_UNSPECIFIED = 0;
+ */
+ UNSPECIFIED = 0,
+
+ /**
+ * No OCSP.
+ *
+ * @generated from enum value: OCSP_MODE_OFF = 1;
+ */
+ OFF = 1,
+
+ /**
+ * The OCSP URI in each leaf's authorityInfoAccess. A leaf without one gets
+ * no OCSP.
+ *
+ * @generated from enum value: OCSP_MODE_AIA = 2;
+ */
+ AIA = 2,
+
+ /**
+ * The responder at ocsp_url.
+ *
+ * @generated from enum value: OCSP_MODE_URL = 3;
+ */
+ URL = 3,
+}
+
+/**
+ * Describes the enum cryptos.fleet.v1.OcspMode.
+ */
+export const OcspModeSchema: GenEnum = /*@__PURE__*/
+ enumDesc(file_cryptos_fleet_v1_operator_ca, 2);
+
+/**
+ * OcspSigner is who signed an OCSP response.
+ *
+ * @generated from enum cryptos.fleet.v1.OcspSigner
+ */
+export enum OcspSigner {
+ /**
+ * @generated from enum value: OCSP_SIGNER_UNSPECIFIED = 0;
+ */
+ UNSPECIFIED = 0,
+
+ /**
+ * The operator CA itself.
+ *
+ * @generated from enum value: OCSP_SIGNER_ANCHOR = 1;
+ */
+ ANCHOR = 1,
+
+ /**
+ * A delegated responder certificate the operator CA issued, with EKU
+ * OCSPSigning.
+ *
+ * @generated from enum value: OCSP_SIGNER_DELEGATED = 2;
+ */
+ DELEGATED = 2,
+}
+
+/**
+ * Describes the enum cryptos.fleet.v1.OcspSigner.
+ */
+export const OcspSignerSchema: GenEnum = /*@__PURE__*/
+ enumDesc(file_cryptos_fleet_v1_operator_ca, 3);
+
+/**
+ * OperatorCAAcknowledgement is a risk the caller accepts explicitly when
+ * registering an operator CA or changing its CRL source.
+ *
+ * @generated from enum cryptos.fleet.v1.OperatorCAAcknowledgement
+ */
+export enum OperatorCAAcknowledgement {
+ /**
+ * @generated from enum value: OPERATOR_CA_ACKNOWLEDGEMENT_UNSPECIFIED = 0;
+ */
+ OPERATOR_CA_ACKNOWLEDGEMENT_UNSPECIFIED = 0,
+
+ /**
+ * No CRL: revocations made at the CA won't be seen by the manager, and MCP
+ * is refused under this CA. Required with crl_source none.
+ *
+ * @generated from enum value: OPERATOR_CA_ACKNOWLEDGEMENT_NO_CRL = 1;
+ */
+ OPERATOR_CA_ACKNOWLEDGEMENT_NO_CRL = 1,
+}
+
+/**
+ * Describes the enum cryptos.fleet.v1.OperatorCAAcknowledgement.
+ */
+export const OperatorCAAcknowledgementSchema: GenEnum = /*@__PURE__*/
+ enumDesc(file_cryptos_fleet_v1_operator_ca, 4);
+
diff --git a/src/gen/fleet/cryptos/v1/audit_pb.ts b/src/gen/fleet/cryptos/v1/audit_pb.ts
index 0d839b6..ed5a987 100644
--- a/src/gen/fleet/cryptos/v1/audit_pb.ts
+++ b/src/gen/fleet/cryptos/v1/audit_pb.ts
@@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file cryptos/v1/audit.proto.
*/
export const file_cryptos_v1_audit: GenFile = /*@__PURE__*/
- fileDesc("ChZjcnlwdG9zL3YxL2F1ZGl0LnByb3RvEgpjcnlwdG9zLnYxIrICCgpBdWRpdEV2ZW50EgsKA3NlcRgBIAEoBBImCgJ0cxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFQoNYWN0b3Jfc3ViamVjdBgDIAEoCRISCgpycGNfbWV0aG9kGAQgASgJEh0KFXJlcXVlc3RfZGlnZXN0X3NoYTI1NhgFIAEoDBIkCgdvdXRjb21lGAYgASgOMhMuY3J5cHRvcy52MS5PdXRjb21lEhkKEXByZXZfZW50cnlfc2hhMjU2GAcgASgMEjQKB2RldGFpbHMYCCADKAsyIy5jcnlwdG9zLnYxLkF1ZGl0RXZlbnQuRGV0YWlsc0VudHJ5Gi4KDERldGFpbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBKlkKB091dGNvbWUSFwoTT1VUQ09NRV9VTlNQRUNJRklFRBAAEg4KCk9VVENPTUVfT0sQARISCg5PVVRDT01FX0RFTklFRBACEhEKDU9VVENPTUVfRVJST1IQA0I0WjJnaXRodWIuY29tL0NyeXB0T1MtUEtJL2FwaS9nby9jcnlwdG9zL3YxO2NyeXB0b3N2MWIGcHJvdG8z", [file_google_protobuf_timestamp]);
+ fileDesc("ChZjcnlwdG9zL3YxL2F1ZGl0LnByb3RvEgpjcnlwdG9zLnYxIrICCgpBdWRpdEV2ZW50EgsKA3NlcRgBIAEoBBImCgJ0cxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFQoNYWN0b3Jfc3ViamVjdBgDIAEoCRISCgpycGNfbWV0aG9kGAQgASgJEh0KFXJlcXVlc3RfZGlnZXN0X3NoYTI1NhgFIAEoDBIkCgdvdXRjb21lGAYgASgOMhMuY3J5cHRvcy52MS5PdXRjb21lEhkKEXByZXZfZW50cnlfc2hhMjU2GAcgASgMEjQKB2RldGFpbHMYCCADKAsyIy5jcnlwdG9zLnYxLkF1ZGl0RXZlbnQuRGV0YWlsc0VudHJ5Gi4KDERldGFpbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIm0KDUF1ZGl0TG9nRW50cnkSJQoFZXZlbnQYASABKAsyFi5jcnlwdG9zLnYxLkF1ZGl0RXZlbnQSFAoMZW50cnlfc2hhMjU2GAIgASgMEg4KBnRhcmdldBgDIAEoCRIPCgdzdW1tYXJ5GAQgASgJIoYBChZMaXN0QXVkaXRFdmVudHNSZXF1ZXN0EhEKCXBhZ2Vfc2l6ZRgBIAEoBRISCgpwYWdlX3Rva2VuGAIgASgJEhEKCWZyb21fdGltZRgDIAEoCRIPCgd0b190aW1lGAQgASgJEhIKCmV2ZW50X3R5cGUYBSABKAkSDQoFYWN0b3IYBiABKAkiXgoXTGlzdEF1ZGl0RXZlbnRzUmVzcG9uc2USKgoHZW50cmllcxgBIAMoCzIZLmNyeXB0b3MudjEuQXVkaXRMb2dFbnRyeRIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkiGQoXVmVyaWZ5QXVkaXRDaGFpblJlcXVlc3QibgoYVmVyaWZ5QXVkaXRDaGFpblJlc3BvbnNlEhMKC2VudHJ5X2NvdW50GAEgASgEEg4KBmludGFjdBgCIAEoCBIdChVmaXJzdF9icm9rZW5fc2VxdWVuY2UYAyABKAQSDgoGcmVhc29uGAQgASgJKlkKB091dGNvbWUSFwoTT1VUQ09NRV9VTlNQRUNJRklFRBAAEg4KCk9VVENPTUVfT0sQARISCg5PVVRDT01FX0RFTklFRBACEhEKDU9VVENPTUVfRVJST1IQA0I0WjJnaXRodWIuY29tL0NyeXB0T1MtUEtJL2FwaS9nby9jcnlwdG9zL3YxO2NyeXB0b3N2MWIGcHJvdG8z", [file_google_protobuf_timestamp]);
/**
* AuditEvent is one entry in the hash-chained audit log. Stored as
@@ -84,6 +84,207 @@ export type AuditEvent = Message<"cryptos.v1.AuditEvent"> & {
export const AuditEventSchema: GenMessage = /*@__PURE__*/
messageDesc(file_cryptos_v1_audit, 0);
+/**
+ * AuditLogEntry is one audit log entry as ListAuditEvents returns it.
+ *
+ * @generated from message cryptos.v1.AuditLogEntry
+ */
+export type AuditLogEntry = Message<"cryptos.v1.AuditLogEntry"> & {
+ /**
+ * event is the entry exactly as the node stored, signed and chained it.
+ *
+ * @generated from field: cryptos.v1.AuditEvent event = 1;
+ */
+ event?: AuditEvent | undefined;
+
+ /**
+ * entry_sha256 is the SHA-256 of the entry's stored encoding: the value the
+ * next entry's prev_entry_sha256 must equal. The node computes it from the
+ * bytes on disk, since re-encoding event would not reproduce them.
+ *
+ * @generated from field: bytes entry_sha256 = 2;
+ */
+ entrySha256: Uint8Array;
+
+ /**
+ * target names what the call acted on when the entry records it, for
+ * example a certificate serial; empty when the entry names no target. The
+ * node derives it from event for display; it is not part of the chain.
+ *
+ * @generated from field: string target = 3;
+ */
+ target: string;
+
+ /**
+ * summary is a one-line, human-readable description of the entry, derived
+ * by the node from event for display; it is not part of the chain.
+ *
+ * @generated from field: string summary = 4;
+ */
+ summary: string;
+};
+
+/**
+ * Describes the message cryptos.v1.AuditLogEntry.
+ * Use `create(AuditLogEntrySchema)` to create a new message.
+ */
+export const AuditLogEntrySchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_v1_audit, 1);
+
+/**
+ * ListAuditEventsRequest pages through the node's audit log, oldest entry
+ * first (ascending seq). Every filter is optional and they combine with AND.
+ *
+ * @generated from message cryptos.v1.ListAuditEventsRequest
+ */
+export type ListAuditEventsRequest = Message<"cryptos.v1.ListAuditEventsRequest"> & {
+ /**
+ * page_size is the most entries to return. Zero means the node's default;
+ * the node caps larger values. Negative is InvalidArgument.
+ *
+ * @generated from field: int32 page_size = 1;
+ */
+ pageSize: number;
+
+ /**
+ * page_token is the next_page_token of the previous call, made with the
+ * same filters; empty starts at the oldest matching entry. A token the node
+ * did not issue, or one reused with different filters, is InvalidArgument.
+ *
+ * @generated from field: string page_token = 2;
+ */
+ pageToken: string;
+
+ /**
+ * from_time keeps entries whose ts is at or after this RFC3339 time; empty
+ * is unbounded. Not RFC3339 is InvalidArgument.
+ *
+ * @generated from field: string from_time = 3;
+ */
+ fromTime: string;
+
+ /**
+ * to_time keeps entries whose ts is before this RFC3339 time; empty is
+ * unbounded. Not RFC3339, or earlier than from_time, is InvalidArgument.
+ *
+ * @generated from field: string to_time = 4;
+ */
+ toTime: string;
+
+ /**
+ * event_type keeps entries whose rpc_method matches: either the full
+ * method, e.g. "/cryptos.v1.NodeService/RevokeCertificate", or its method
+ * name alone, e.g. "RevokeCertificate". Empty matches every entry.
+ *
+ * @generated from field: string event_type = 5;
+ */
+ eventType: string;
+
+ /**
+ * actor keeps entries whose actor_subject contains this text (case
+ * sensitive). Empty matches every entry.
+ *
+ * @generated from field: string actor = 6;
+ */
+ actor: string;
+};
+
+/**
+ * Describes the message cryptos.v1.ListAuditEventsRequest.
+ * Use `create(ListAuditEventsRequestSchema)` to create a new message.
+ */
+export const ListAuditEventsRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_v1_audit, 2);
+
+/**
+ * @generated from message cryptos.v1.ListAuditEventsResponse
+ */
+export type ListAuditEventsResponse = Message<"cryptos.v1.ListAuditEventsResponse"> & {
+ /**
+ * entries are the matching entries on this page, in ascending seq order.
+ *
+ * @generated from field: repeated cryptos.v1.AuditLogEntry entries = 1;
+ */
+ entries: AuditLogEntry[];
+
+ /**
+ * next_page_token fetches the next page; empty when this is the last page.
+ *
+ * @generated from field: string next_page_token = 2;
+ */
+ nextPageToken: string;
+};
+
+/**
+ * Describes the message cryptos.v1.ListAuditEventsResponse.
+ * Use `create(ListAuditEventsResponseSchema)` to create a new message.
+ */
+export const ListAuditEventsResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_v1_audit, 3);
+
+/**
+ * @generated from message cryptos.v1.VerifyAuditChainRequest
+ */
+export type VerifyAuditChainRequest = Message<"cryptos.v1.VerifyAuditChainRequest"> & {
+};
+
+/**
+ * Describes the message cryptos.v1.VerifyAuditChainRequest.
+ * Use `create(VerifyAuditChainRequestSchema)` to create a new message.
+ */
+export const VerifyAuditChainRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_v1_audit, 4);
+
+/**
+ * VerifyAuditChainResponse reports the result of walking the whole stored
+ * log: every entry's signature, a gap-free seq starting at 1, and each
+ * entry's prev_entry_sha256 against the prior entry's hash. A broken chain
+ * is a result, not an RPC error; the RPC fails only when the log cannot be
+ * read at all.
+ *
+ * @generated from message cryptos.v1.VerifyAuditChainResponse
+ */
+export type VerifyAuditChainResponse = Message<"cryptos.v1.VerifyAuditChainResponse"> & {
+ /**
+ * entry_count is the number of entries in the stored log.
+ *
+ * @generated from field: uint64 entry_count = 1;
+ */
+ entryCount: bigint;
+
+ /**
+ * intact is true when every entry verified.
+ *
+ * @generated from field: bool intact = 2;
+ */
+ intact: boolean;
+
+ /**
+ * first_broken_sequence is the seq at which verification first failed:
+ * the seq the failing entry held, or the one expected at that position when
+ * the entry could not be read. 0 when intact.
+ *
+ * @generated from field: uint64 first_broken_sequence = 3;
+ */
+ firstBrokenSequence: bigint;
+
+ /**
+ * reason says why verification failed at first_broken_sequence, e.g. a
+ * signature mismatch, a seq gap or a prev_entry_sha256 mismatch; empty when
+ * intact.
+ *
+ * @generated from field: string reason = 4;
+ */
+ reason: string;
+};
+
+/**
+ * Describes the message cryptos.v1.VerifyAuditChainResponse.
+ * Use `create(VerifyAuditChainResponseSchema)` to create a new message.
+ */
+export const VerifyAuditChainResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_v1_audit, 5);
+
/**
* @generated from enum cryptos.v1.Outcome
*/
diff --git a/src/gen/fleet/cryptos/v1/node_pb.ts b/src/gen/fleet/cryptos/v1/node_pb.ts
index f15cf26..b4ca288 100644
--- a/src/gen/fleet/cryptos/v1/node_pb.ts
+++ b/src/gen/fleet/cryptos/v1/node_pb.ts
@@ -4,6 +4,8 @@
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
+import type { ListAuditEventsRequestSchema, ListAuditEventsResponseSchema, VerifyAuditChainRequestSchema, VerifyAuditChainResponseSchema } from "./audit_pb";
+import { file_cryptos_v1_audit } from "./audit_pb";
import type { CeremonyEvent, CeremonyKind } from "./ceremony_pb";
import { file_cryptos_v1_ceremony } from "./ceremony_pb";
import type { MachineConfig, Management } from "./config_pb";
@@ -24,7 +26,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file cryptos/v1/node.proto.
*/
export const file_cryptos_v1_node: GenFile = /*@__PURE__*/
- 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]);
+ fileDesc("ChVjcnlwdG9zL3YxL25vZGUucHJvdG8SCmNyeXB0b3MudjEiPwoSQXBwbHlDb25maWdSZXF1ZXN0EikKBmNvbmZpZxgBIAEoCzIZLmNyeXB0b3MudjEuTWFjaGluZUNvbmZpZyISChBHZXRDb25maWdSZXF1ZXN0Ij4KEUdldENvbmZpZ1Jlc3BvbnNlEikKBmNvbmZpZxgBIAEoCzIZLmNyeXB0b3MudjEuTWFjaGluZUNvbmZpZyJCChRTZXRNYW5hZ2VtZW50UmVxdWVzdBIqCgptYW5hZ2VtZW50GAEgASgLMhYuY3J5cHRvcy52MS5NYW5hZ2VtZW50IkQKFVNldE1hbmFnZW1lbnRSZXNwb25zZRISCgpnZW5lcmF0aW9uGAEgASgEEhcKD3JlcXVpcmVzX3JlYm9vdBgCIAEoCCJrChNBcHBseUNvbmZpZ1Jlc3BvbnNlEhIKCmdlbmVyYXRpb24YASABKAQSFwoPcmVxdWlyZXNfcmVib290GAIgASgIEhUKDWNvbmZpZ19kaWdlc3QYAyABKAwSEAoId2FybmluZ3MYBCADKAkiEgoQR2V0U3RhdHVzUmVxdWVzdCI7ChFHZXRTdGF0dXNSZXNwb25zZRImCgZzdGF0dXMYASABKAsyFi5jcnlwdG9zLnYxLk5vZGVTdGF0dXMiFAoSR2V0SWRlbnRpdHlSZXF1ZXN0Ij0KE0dldElkZW50aXR5UmVzcG9uc2USJgoIaWRlbnRpdHkYASABKAsyFC5jcnlwdG9zLnYxLklkZW50aXR5IlsKFFN0YXJ0Q2VyZW1vbnlSZXF1ZXN0EiYKBGtpbmQYASABKA4yGC5jcnlwdG9zLnYxLkNlcmVtb255S2luZBIbChNtYWNoaW5lX2NvbmZpZ195YW1sGAIgASgMIkEKFVN0YXJ0Q2VyZW1vbnlSZXNwb25zZRIoCgVldmVudBgBIAEoCzIZLmNyeXB0b3MudjEuQ2VyZW1vbnlFdmVudCIyCg5TaWduQ1NSUmVxdWVzdBIPCgdjc3JfZGVyGAEgASgMEg8KB3Byb2ZpbGUYAiABKAkiIwoPU2lnbkNTUlJlc3BvbnNlEhAKCGNlcnRfZGVyGAEgASgMIisKDFJlc2V0UmVxdWVzdBIbChNjb25maXJtX2NvbW1vbl9uYW1lGAEgASgJIg8KDVJlc2V0UmVzcG9uc2UiMQoSUmVtb3RlUmVzZXRSZXF1ZXN0EhsKE2NvbmZpcm1fY29tbW9uX25hbWUYASABKAkiKAoTUmVtb3RlUmVzZXRSZXNwb25zZRIRCglyZWJvb3RpbmcYASABKAgiHgoNQXR0ZXN0UmVxdWVzdBINCgVub25jZRgBIAEoDCJeCg5BdHRlc3RSZXNwb25zZRIRCglzaWduYXR1cmUYASABKAwSGAoQaWRlbnRpdHlfcHViX2RlchgCIAEoDBIOCgZla19wdWIYAyABKAwSDwoHZWtfY2VydBgEIAEoDCJCChlTaWduU3Vib3JkaW5hdGVDU1JSZXF1ZXN0Eg8KB2Nzcl9kZXIYASABKAwSFAoMcHJvZmlsZV9uYW1lGAIgASgJIlQKGlNpZ25TdWJvcmRpbmF0ZUNTUlJlc3BvbnNlEhEKCWNoYWluX2RlchgBIAMoDBIRCgljaGFpbl9wZW0YAiABKAkSEAoId2FybmluZ3MYAyADKAkiTAoQSXNzdWVMZWFmUmVxdWVzdBIPCgdjc3JfZGVyGAEgASgMEhQKDHByb2ZpbGVfbmFtZRgCIAEoCRIRCglkbnNfbmFtZXMYAyADKAkiNwoRSXNzdWVMZWFmUmVzcG9uc2USEAoIY2VydF9kZXIYASABKAwSEAoId2FybmluZ3MYAiADKAkiGgoYR2V0U3Vib3JkaW5hdGVDU1JSZXF1ZXN0IiwKGUdldFN1Ym9yZGluYXRlQ1NSUmVzcG9uc2USDwoHY3NyX2RlchgBIAEoDCJLCiNTdWJtaXRTdWJvcmRpbmF0ZUNlcnRpZmljYXRlUmVxdWVzdBIRCgljaGFpbl9kZXIYASADKAwSEQoJY2hhaW5fcGVtGAIgASgJIk4KJFN1Ym1pdFN1Ym9yZGluYXRlQ2VydGlmaWNhdGVSZXNwb25zZRImCghpZGVudGl0eRgBIAEoCzIULmNyeXB0b3MudjEuSWRlbnRpdHkiKAoSRXhwb3J0Q0FLZXlSZXF1ZXN0EhIKCnBhc3NwaHJhc2UYASABKAwiJwoTRXhwb3J0Q0FLZXlSZXNwb25zZRIQCghlbnZlbG9wZRgBIAEoDCI6ChJJbXBvcnRDQUtleVJlcXVlc3QSEAoIZW52ZWxvcGUYASABKAwSEgoKcGFzc3BocmFzZRgCIAEoDCI9ChNJbXBvcnRDQUtleVJlc3BvbnNlEiYKCGlkZW50aXR5GAEgASgLMhQuY3J5cHRvcy52MS5JZGVudGl0eSIZChdCZWdpbktleVJvdGF0aW9uUmVxdWVzdCIrChhCZWdpbktleVJvdGF0aW9uUmVzcG9uc2USDwoHY3NyX2RlchgBIAEoDCJCChpDb21wbGV0ZUtleVJvdGF0aW9uUmVxdWVzdBIRCgljaGFpbl9kZXIYASADKAwSEQoJY2hhaW5fcGVtGAIgASgJIkUKG0NvbXBsZXRlS2V5Um90YXRpb25SZXNwb25zZRImCghpZGVudGl0eRgBIAEoCzIULmNyeXB0b3MudjEuSWRlbnRpdHkiFgoUR2V0UmVuZXdhbENTUlJlcXVlc3QiKAoVR2V0UmVuZXdhbENTUlJlc3BvbnNlEg8KB2Nzcl9kZXIYASABKAwiRwofU3VibWl0UmVuZXdlZENlcnRpZmljYXRlUmVxdWVzdBIRCgljaGFpbl9kZXIYASADKAwSEQoJY2hhaW5fcGVtGAIgASgJIkoKIFN1Ym1pdFJlbmV3ZWRDZXJ0aWZpY2F0ZVJlc3BvbnNlEiYKCGlkZW50aXR5GAEgASgLMhQuY3J5cHRvcy52MS5JZGVudGl0eSLpAQoKSXNzdWVkQ2VydBISCgpzZXJpYWxfaGV4GAEgASgJEhIKCnN1YmplY3RfZG4YAiABKAkSLgoKbm90X2JlZm9yZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLQoJbm90X2FmdGVyGAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIPCgdza2lfaGV4GAUgASgJEhQKDHByb2ZpbGVfbmFtZRgGIAEoCRItCglpc3N1ZWRfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wImUKClJldm9jYXRpb24SEgoKc2VyaWFsX2hleBgBIAEoCRIuCgpyZXZva2VkX2F0GAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBITCgtyZWFzb25fY29kZRgDIAEoBSJDChhSZXZva2VDZXJ0aWZpY2F0ZVJlcXVlc3QSEgoKc2VyaWFsX2hleBgBIAEoCRITCgtyZWFzb25fY29kZRgCIAEoBSJHChlSZXZva2VDZXJ0aWZpY2F0ZVJlc3BvbnNlEioKCnJldm9jYXRpb24YASABKAsyFi5jcnlwdG9zLnYxLlJldm9jYXRpb24iEwoRTGlzdElzc3VlZFJlcXVlc3QiPAoSTGlzdElzc3VlZFJlc3BvbnNlEiYKBmlzc3VlZBgBIAMoCzIWLmNyeXB0b3MudjEuSXNzdWVkQ2VydCIYChZMaXN0UmV2b2NhdGlvbnNSZXF1ZXN0IkYKF0xpc3RSZXZvY2F0aW9uc1Jlc3BvbnNlEisKC3Jldm9jYXRpb25zGAEgAygLMhYuY3J5cHRvcy52MS5SZXZvY2F0aW9uIjEKG0dldElzc3VlZENlcnRpZmljYXRlUmVxdWVzdBISCgpzZXJpYWxfaGV4GAEgASgJIm4KHEdldElzc3VlZENlcnRpZmljYXRlUmVzcG9uc2USFwoPY2VydGlmaWNhdGVfZGVyGAEgASgMEhEKCWNoYWluX2RlchgCIAMoDBIOCgZzdGF0dXMYAyABKAkSEgoKcmV2b2tlZF9hdBgEIAEoCSIZChdMaXN0SW5zdGFsbERpc2tzUmVxdWVzdCJCChhMaXN0SW5zdGFsbERpc2tzUmVzcG9uc2USJgoFZGlza3MYASADKAsyFy5jcnlwdG9zLnYxLkluc3RhbGxEaXNrImUKC0luc3RhbGxEaXNrEgwKBHBhdGgYASABKAkSEgoKc2l6ZV9ieXRlcxgCIAEoBBINCgVtb2RlbBgDIAEoCRISCgpyb3RhdGlvbmFsGAQgASgIEhEKCXJlbW92YWJsZRgFIAEoCCJdChFTdGFnZUltYWdlUmVxdWVzdBIsCgViZWdpbhgBIAEoCzIbLmNyeXB0b3MudjEuU3RhZ2VJbWFnZUJlZ2luSAASDwoFY2h1bmsYAiABKAxIAEIJCgdwYXlsb2FkIkgKD1N0YWdlSW1hZ2VCZWdpbhIRCglzaWduYXR1cmUYASABKAwSEgoKc2l6ZV9ieXRlcxgCIAEoBBIOCgZzaGEyNTYYAyABKAkiVgoSU3RhZ2VJbWFnZVJlc3BvbnNlEicKBnN0YXR1cxgBIAEoCzIXLmNyeXB0b3MudjEuSW1hZ2VTdGF0dXMSFwoPcmVxdWlyZXNfcmVib290GAIgASgIIhYKFFJvbGxiYWNrSW1hZ2VSZXF1ZXN0IlkKFVJvbGxiYWNrSW1hZ2VSZXNwb25zZRInCgZzdGF0dXMYASABKAsyFy5jcnlwdG9zLnYxLkltYWdlU3RhdHVzEhcKD3JlcXVpcmVzX3JlYm9vdBgCIAEoCCItChRBY3RpdmF0ZUltYWdlUmVxdWVzdBIVCg1jb25maXJtX2NhX2NuGAEgASgJIioKFUFjdGl2YXRlSW1hZ2VSZXNwb25zZRIRCglyZWJvb3RpbmcYASABKAgiOQoNUmVib290UmVxdWVzdBIVCg1jb25maXJtX2NhX2NuGAEgASgJEhEKCXBvd2VyX29mZhgCIAEoCCIjCg5SZWJvb3RSZXNwb25zZRIRCglyZWJvb3RpbmcYASABKAgiFwoVR2V0SW1hZ2VTdGF0dXNSZXF1ZXN0IkEKFkdldEltYWdlU3RhdHVzUmVzcG9uc2USJwoGc3RhdHVzGAEgASgLMhcuY3J5cHRvcy52MS5JbWFnZVN0YXR1cyKGAQoLSW1hZ2VTdGF0dXMSFgoOcnVubmluZ19zaGEyNTYYASABKAkSFQoNYWN0aXZlX3NoYTI1NhgCIAEoCRIXCg9wcmV2aW91c19zaGEyNTYYAyABKAkSFgoOcmVib290X3BlbmRpbmcYBCABKAgSFwoPcnVubmluZ192ZXJzaW9uGAUgASgJMsgbCgtOb2RlU2VydmljZRJOCgtBcHBseUNvbmZpZxIeLmNyeXB0b3MudjEuQXBwbHlDb25maWdSZXF1ZXN0Gh8uY3J5cHRvcy52MS5BcHBseUNvbmZpZ1Jlc3BvbnNlEkgKCUdldFN0YXR1cxIcLmNyeXB0b3MudjEuR2V0U3RhdHVzUmVxdWVzdBodLmNyeXB0b3MudjEuR2V0U3RhdHVzUmVzcG9uc2USXQoQTGlzdEluc3RhbGxEaXNrcxIjLmNyeXB0b3MudjEuTGlzdEluc3RhbGxEaXNrc1JlcXVlc3QaJC5jcnlwdG9zLnYxLkxpc3RJbnN0YWxsRGlza3NSZXNwb25zZRJOCgtHZXRJZGVudGl0eRIeLmNyeXB0b3MudjEuR2V0SWRlbnRpdHlSZXF1ZXN0Gh8uY3J5cHRvcy52MS5HZXRJZGVudGl0eVJlc3BvbnNlElYKDVN0YXJ0Q2VyZW1vbnkSIC5jcnlwdG9zLnYxLlN0YXJ0Q2VyZW1vbnlSZXF1ZXN0GiEuY3J5cHRvcy52MS5TdGFydENlcmVtb255UmVzcG9uc2UwARJCCgdTaWduQ1NSEhouY3J5cHRvcy52MS5TaWduQ1NSUmVxdWVzdBobLmNyeXB0b3MudjEuU2lnbkNTUlJlc3BvbnNlEmMKElNpZ25TdWJvcmRpbmF0ZUNTUhIlLmNyeXB0b3MudjEuU2lnblN1Ym9yZGluYXRlQ1NSUmVxdWVzdBomLmNyeXB0b3MudjEuU2lnblN1Ym9yZGluYXRlQ1NSUmVzcG9uc2USSAoJSXNzdWVMZWFmEhwuY3J5cHRvcy52MS5Jc3N1ZUxlYWZSZXF1ZXN0Gh0uY3J5cHRvcy52MS5Jc3N1ZUxlYWZSZXNwb25zZRJgChFHZXRTdWJvcmRpbmF0ZUNTUhIkLmNyeXB0b3MudjEuR2V0U3Vib3JkaW5hdGVDU1JSZXF1ZXN0GiUuY3J5cHRvcy52MS5HZXRTdWJvcmRpbmF0ZUNTUlJlc3BvbnNlEoEBChxTdWJtaXRTdWJvcmRpbmF0ZUNlcnRpZmljYXRlEi8uY3J5cHRvcy52MS5TdWJtaXRTdWJvcmRpbmF0ZUNlcnRpZmljYXRlUmVxdWVzdBowLmNyeXB0b3MudjEuU3VibWl0U3Vib3JkaW5hdGVDZXJ0aWZpY2F0ZVJlc3BvbnNlEmAKEVJldm9rZUNlcnRpZmljYXRlEiQuY3J5cHRvcy52MS5SZXZva2VDZXJ0aWZpY2F0ZVJlcXVlc3QaJS5jcnlwdG9zLnYxLlJldm9rZUNlcnRpZmljYXRlUmVzcG9uc2USSwoKTGlzdElzc3VlZBIdLmNyeXB0b3MudjEuTGlzdElzc3VlZFJlcXVlc3QaHi5jcnlwdG9zLnYxLkxpc3RJc3N1ZWRSZXNwb25zZRJaCg9MaXN0UmV2b2NhdGlvbnMSIi5jcnlwdG9zLnYxLkxpc3RSZXZvY2F0aW9uc1JlcXVlc3QaIy5jcnlwdG9zLnYxLkxpc3RSZXZvY2F0aW9uc1Jlc3BvbnNlEmkKFEdldElzc3VlZENlcnRpZmljYXRlEicuY3J5cHRvcy52MS5HZXRJc3N1ZWRDZXJ0aWZpY2F0ZVJlcXVlc3QaKC5jcnlwdG9zLnYxLkdldElzc3VlZENlcnRpZmljYXRlUmVzcG9uc2USTgoLRXhwb3J0Q0FLZXkSHi5jcnlwdG9zLnYxLkV4cG9ydENBS2V5UmVxdWVzdBofLmNyeXB0b3MudjEuRXhwb3J0Q0FLZXlSZXNwb25zZRJOCgtJbXBvcnRDQUtleRIeLmNyeXB0b3MudjEuSW1wb3J0Q0FLZXlSZXF1ZXN0Gh8uY3J5cHRvcy52MS5JbXBvcnRDQUtleVJlc3BvbnNlEl0KEEJlZ2luS2V5Um90YXRpb24SIy5jcnlwdG9zLnYxLkJlZ2luS2V5Um90YXRpb25SZXF1ZXN0GiQuY3J5cHRvcy52MS5CZWdpbktleVJvdGF0aW9uUmVzcG9uc2USZgoTQ29tcGxldGVLZXlSb3RhdGlvbhImLmNyeXB0b3MudjEuQ29tcGxldGVLZXlSb3RhdGlvblJlcXVlc3QaJy5jcnlwdG9zLnYxLkNvbXBsZXRlS2V5Um90YXRpb25SZXNwb25zZRJUCg1HZXRSZW5ld2FsQ1NSEiAuY3J5cHRvcy52MS5HZXRSZW5ld2FsQ1NSUmVxdWVzdBohLmNyeXB0b3MudjEuR2V0UmVuZXdhbENTUlJlc3BvbnNlEnUKGFN1Ym1pdFJlbmV3ZWRDZXJ0aWZpY2F0ZRIrLmNyeXB0b3MudjEuU3VibWl0UmVuZXdlZENlcnRpZmljYXRlUmVxdWVzdBosLmNyeXB0b3MudjEuU3VibWl0UmVuZXdlZENlcnRpZmljYXRlUmVzcG9uc2USPAoFUmVzZXQSGC5jcnlwdG9zLnYxLlJlc2V0UmVxdWVzdBoZLmNyeXB0b3MudjEuUmVzZXRSZXNwb25zZRJOCgtSZW1vdGVSZXNldBIeLmNyeXB0b3MudjEuUmVtb3RlUmVzZXRSZXF1ZXN0Gh8uY3J5cHRvcy52MS5SZW1vdGVSZXNldFJlc3BvbnNlEj8KBkF0dGVzdBIZLmNyeXB0b3MudjEuQXR0ZXN0UmVxdWVzdBoaLmNyeXB0b3MudjEuQXR0ZXN0UmVzcG9uc2USVAoNU2V0TWFuYWdlbWVudBIgLmNyeXB0b3MudjEuU2V0TWFuYWdlbWVudFJlcXVlc3QaIS5jcnlwdG9zLnYxLlNldE1hbmFnZW1lbnRSZXNwb25zZRJICglHZXRDb25maWcSHC5jcnlwdG9zLnYxLkdldENvbmZpZ1JlcXVlc3QaHS5jcnlwdG9zLnYxLkdldENvbmZpZ1Jlc3BvbnNlEk0KClN0YWdlSW1hZ2USHS5jcnlwdG9zLnYxLlN0YWdlSW1hZ2VSZXF1ZXN0Gh4uY3J5cHRvcy52MS5TdGFnZUltYWdlUmVzcG9uc2UoARJUCg1Sb2xsYmFja0ltYWdlEiAuY3J5cHRvcy52MS5Sb2xsYmFja0ltYWdlUmVxdWVzdBohLmNyeXB0b3MudjEuUm9sbGJhY2tJbWFnZVJlc3BvbnNlElQKDUFjdGl2YXRlSW1hZ2USIC5jcnlwdG9zLnYxLkFjdGl2YXRlSW1hZ2VSZXF1ZXN0GiEuY3J5cHRvcy52MS5BY3RpdmF0ZUltYWdlUmVzcG9uc2USVwoOR2V0SW1hZ2VTdGF0dXMSIS5jcnlwdG9zLnYxLkdldEltYWdlU3RhdHVzUmVxdWVzdBoiLmNyeXB0b3MudjEuR2V0SW1hZ2VTdGF0dXNSZXNwb25zZRI/CgZSZWJvb3QSGS5jcnlwdG9zLnYxLlJlYm9vdFJlcXVlc3QaGi5jcnlwdG9zLnYxLlJlYm9vdFJlc3BvbnNlEmAKEU1pbnRTY2VwQ2hhbGxlbmdlEiQuY3J5cHRvcy52MS5NaW50U2NlcENoYWxsZW5nZVJlcXVlc3QaJS5jcnlwdG9zLnYxLk1pbnRTY2VwQ2hhbGxlbmdlUmVzcG9uc2USYwoSTGlzdFNjZXBDaGFsbGVuZ2VzEiUuY3J5cHRvcy52MS5MaXN0U2NlcENoYWxsZW5nZXNSZXF1ZXN0GiYuY3J5cHRvcy52MS5MaXN0U2NlcENoYWxsZW5nZXNSZXNwb25zZRJmChNSZXZva2VTY2VwQ2hhbGxlbmdlEiYuY3J5cHRvcy52MS5SZXZva2VTY2VwQ2hhbGxlbmdlUmVxdWVzdBonLmNyeXB0b3MudjEuUmV2b2tlU2NlcENoYWxsZW5nZVJlc3BvbnNlEmYKE0xpc3RTY2VwRW5yb2xsbWVudHMSJi5jcnlwdG9zLnYxLkxpc3RTY2VwRW5yb2xsbWVudHNSZXF1ZXN0GicuY3J5cHRvcy52MS5MaXN0U2NlcEVucm9sbG1lbnRzUmVzcG9uc2USbAoVQXBwcm92ZVNjZXBFbnJvbGxtZW50EiguY3J5cHRvcy52MS5BcHByb3ZlU2NlcEVucm9sbG1lbnRSZXF1ZXN0GikuY3J5cHRvcy52MS5BcHByb3ZlU2NlcEVucm9sbG1lbnRSZXNwb25zZRJpChRSZWplY3RTY2VwRW5yb2xsbWVudBInLmNyeXB0b3MudjEuUmVqZWN0U2NlcEVucm9sbG1lbnRSZXF1ZXN0GiguY3J5cHRvcy52MS5SZWplY3RTY2VwRW5yb2xsbWVudFJlc3BvbnNlEmYKE0xpc3RUc2FDZXJ0aWZpY2F0ZXMSJi5jcnlwdG9zLnYxLkxpc3RUc2FDZXJ0aWZpY2F0ZXNSZXF1ZXN0GicuY3J5cHRvcy52MS5MaXN0VHNhQ2VydGlmaWNhdGVzUmVzcG9uc2USWgoPTGlzdEF1ZGl0RXZlbnRzEiIuY3J5cHRvcy52MS5MaXN0QXVkaXRFdmVudHNSZXF1ZXN0GiMuY3J5cHRvcy52MS5MaXN0QXVkaXRFdmVudHNSZXNwb25zZRJdChBWZXJpZnlBdWRpdENoYWluEiMuY3J5cHRvcy52MS5WZXJpZnlBdWRpdENoYWluUmVxdWVzdBokLmNyeXB0b3MudjEuVmVyaWZ5QXVkaXRDaGFpblJlc3BvbnNlQjRaMmdpdGh1Yi5jb20vQ3J5cHRPUy1QS0kvYXBpL2dvL2NyeXB0b3MvdjE7Y3J5cHRvc3YxYgZwcm90bzM", [file_cryptos_v1_audit, 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
@@ -1057,6 +1059,71 @@ export type ListRevocationsResponse = Message<"cryptos.v1.ListRevocationsRespons
export const ListRevocationsResponseSchema: GenMessage = /*@__PURE__*/
messageDesc(file_cryptos_v1_node, 47);
+/**
+ * GetIssuedCertificateRequest names the issued certificate to fetch.
+ *
+ * @generated from message cryptos.v1.GetIssuedCertificateRequest
+ */
+export type GetIssuedCertificateRequest = Message<"cryptos.v1.GetIssuedCertificateRequest"> & {
+ /**
+ * serial_hex is the hex-encoded serial of a certificate this node issued.
+ *
+ * @generated from field: string serial_hex = 1;
+ */
+ serialHex: string;
+};
+
+/**
+ * Describes the message cryptos.v1.GetIssuedCertificateRequest.
+ * Use `create(GetIssuedCertificateRequestSchema)` to create a new message.
+ */
+export const GetIssuedCertificateRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_v1_node, 48);
+
+/**
+ * GetIssuedCertificateResponse carries the certificate, its chain and its
+ * status.
+ *
+ * @generated from message cryptos.v1.GetIssuedCertificateResponse
+ */
+export type GetIssuedCertificateResponse = Message<"cryptos.v1.GetIssuedCertificateResponse"> & {
+ /**
+ * certificate_der is the DER encoding of the issued certificate.
+ *
+ * @generated from field: bytes certificate_der = 1;
+ */
+ certificateDer: Uint8Array;
+
+ /**
+ * chain_der is the DER-encoded chain, from the issuer up to the root.
+ *
+ * @generated from field: repeated bytes chain_der = 2;
+ */
+ chainDer: Uint8Array[];
+
+ /**
+ * status is "valid" | "revoked" | "expired".
+ *
+ * @generated from field: string status = 3;
+ */
+ status: string;
+
+ /**
+ * revoked_at is when the certificate was revoked (RFC3339); empty unless
+ * status is revoked.
+ *
+ * @generated from field: string revoked_at = 4;
+ */
+ revokedAt: string;
+};
+
+/**
+ * Describes the message cryptos.v1.GetIssuedCertificateResponse.
+ * Use `create(GetIssuedCertificateResponseSchema)` to create a new message.
+ */
+export const GetIssuedCertificateResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_v1_node, 49);
+
/**
* @generated from message cryptos.v1.ListInstallDisksRequest
*/
@@ -1068,7 +1135,7 @@ export type ListInstallDisksRequest = Message<"cryptos.v1.ListInstallDisksReques
* Use `create(ListInstallDisksRequestSchema)` to create a new message.
*/
export const ListInstallDisksRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_v1_node, 48);
+ messageDesc(file_cryptos_v1_node, 50);
/**
* @generated from message cryptos.v1.ListInstallDisksResponse
@@ -1085,7 +1152,7 @@ export type ListInstallDisksResponse = Message<"cryptos.v1.ListInstallDisksRespo
* Use `create(ListInstallDisksResponseSchema)` to create a new message.
*/
export const ListInstallDisksResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_v1_node, 49);
+ messageDesc(file_cryptos_v1_node, 51);
/**
* InstallDisk is one candidate whole-disk block device the node can install to.
@@ -1134,7 +1201,7 @@ export type InstallDisk = Message<"cryptos.v1.InstallDisk"> & {
* Use `create(InstallDiskSchema)` to create a new message.
*/
export const InstallDiskSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_v1_node, 50);
+ messageDesc(file_cryptos_v1_node, 52);
/**
* StageImageRequest is a header followed by chunks: exactly one begin message
@@ -1166,7 +1233,7 @@ export type StageImageRequest = Message<"cryptos.v1.StageImageRequest"> & {
* Use `create(StageImageRequestSchema)` to create a new message.
*/
export const StageImageRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_v1_node, 51);
+ messageDesc(file_cryptos_v1_node, 53);
/**
* StageImageBegin describes the transfer before any image bytes are sent.
@@ -1215,7 +1282,7 @@ export type StageImageBegin = Message<"cryptos.v1.StageImageBegin"> & {
* Use `create(StageImageBeginSchema)` to create a new message.
*/
export const StageImageBeginSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_v1_node, 52);
+ messageDesc(file_cryptos_v1_node, 54);
/**
* @generated from message cryptos.v1.StageImageResponse
@@ -1242,7 +1309,7 @@ export type StageImageResponse = Message<"cryptos.v1.StageImageResponse"> & {
* Use `create(StageImageResponseSchema)` to create a new message.
*/
export const StageImageResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_v1_node, 53);
+ messageDesc(file_cryptos_v1_node, 55);
/**
* @generated from message cryptos.v1.RollbackImageRequest
@@ -1255,7 +1322,7 @@ export type RollbackImageRequest = Message<"cryptos.v1.RollbackImageRequest"> &
* Use `create(RollbackImageRequestSchema)` to create a new message.
*/
export const RollbackImageRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_v1_node, 54);
+ messageDesc(file_cryptos_v1_node, 56);
/**
* @generated from message cryptos.v1.RollbackImageResponse
@@ -1277,7 +1344,7 @@ export type RollbackImageResponse = Message<"cryptos.v1.RollbackImageResponse">
* Use `create(RollbackImageResponseSchema)` to create a new message.
*/
export const RollbackImageResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_v1_node, 55);
+ messageDesc(file_cryptos_v1_node, 57);
/**
* @generated from message cryptos.v1.ActivateImageRequest
@@ -1298,7 +1365,7 @@ export type ActivateImageRequest = Message<"cryptos.v1.ActivateImageRequest"> &
* Use `create(ActivateImageRequestSchema)` to create a new message.
*/
export const ActivateImageRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_v1_node, 56);
+ messageDesc(file_cryptos_v1_node, 58);
/**
* @generated from message cryptos.v1.ActivateImageResponse
@@ -1318,7 +1385,7 @@ export type ActivateImageResponse = Message<"cryptos.v1.ActivateImageResponse">
* Use `create(ActivateImageResponseSchema)` to create a new message.
*/
export const ActivateImageResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_v1_node, 57);
+ messageDesc(file_cryptos_v1_node, 59);
/**
* RebootRequest confirms an operator-initiated reboot or power-off.
@@ -1349,7 +1416,7 @@ export type RebootRequest = Message<"cryptos.v1.RebootRequest"> & {
* Use `create(RebootRequestSchema)` to create a new message.
*/
export const RebootRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_v1_node, 58);
+ messageDesc(file_cryptos_v1_node, 60);
/**
* @generated from message cryptos.v1.RebootResponse
@@ -1369,7 +1436,7 @@ export type RebootResponse = Message<"cryptos.v1.RebootResponse"> & {
* Use `create(RebootResponseSchema)` to create a new message.
*/
export const RebootResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_v1_node, 59);
+ messageDesc(file_cryptos_v1_node, 61);
/**
* @generated from message cryptos.v1.GetImageStatusRequest
@@ -1382,7 +1449,7 @@ export type GetImageStatusRequest = Message<"cryptos.v1.GetImageStatusRequest">
* Use `create(GetImageStatusRequestSchema)` to create a new message.
*/
export const GetImageStatusRequestSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_v1_node, 60);
+ messageDesc(file_cryptos_v1_node, 62);
/**
* @generated from message cryptos.v1.GetImageStatusResponse
@@ -1399,7 +1466,7 @@ export type GetImageStatusResponse = Message<"cryptos.v1.GetImageStatusResponse"
* Use `create(GetImageStatusResponseSchema)` to create a new message.
*/
export const GetImageStatusResponseSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_v1_node, 61);
+ messageDesc(file_cryptos_v1_node, 63);
/**
* ImageStatus describes the images on the ESP and the one currently running.
@@ -1452,7 +1519,7 @@ export type ImageStatus = Message<"cryptos.v1.ImageStatus"> & {
* Use `create(ImageStatusSchema)` to create a new message.
*/
export const ImageStatusSchema: GenMessage = /*@__PURE__*/
- messageDesc(file_cryptos_v1_node, 62);
+ messageDesc(file_cryptos_v1_node, 64);
/**
* NodeService is the Phase 1 management surface for a single CryptOS node.
@@ -1619,6 +1686,19 @@ export const NodeService: GenService<{
input: typeof ListRevocationsRequestSchema;
output: typeof ListRevocationsResponseSchema;
},
+ /**
+ * GetIssuedCertificate returns a certificate this node issued, identified by
+ * its hex serial, with its chain and current status, so a client can fetch
+ * the certificate itself rather than only its inventory entry. NotFound if
+ * the serial is not in this node's issued set.
+ *
+ * @generated from rpc cryptos.v1.NodeService.GetIssuedCertificate
+ */
+ getIssuedCertificate: {
+ methodKind: "unary";
+ input: typeof GetIssuedCertificateRequestSchema;
+ output: typeof GetIssuedCertificateResponseSchema;
+ },
/**
* ExportCAKey returns an encrypted backup of this node's software CA key and
* certificate chain, sealed under the operator passphrase. The plaintext key
@@ -1938,6 +2018,29 @@ export const NodeService: GenService<{
input: typeof ListTsaCertificatesRequestSchema;
output: typeof ListTsaCertificatesResponseSchema;
},
+ /**
+ * ListAuditEvents returns audit log entries, oldest first, a page at a time,
+ * filtered by time range, event type and actor.
+ *
+ * @generated from rpc cryptos.v1.NodeService.ListAuditEvents
+ */
+ listAuditEvents: {
+ methodKind: "unary";
+ input: typeof ListAuditEventsRequestSchema;
+ output: typeof ListAuditEventsResponseSchema;
+ },
+ /**
+ * VerifyAuditChain verifies the signatures and hash chain over the whole
+ * stored audit log and reports whether it is intact and, if not, the first
+ * sequence number that failed.
+ *
+ * @generated from rpc cryptos.v1.NodeService.VerifyAuditChain
+ */
+ verifyAuditChain: {
+ methodKind: "unary";
+ input: typeof VerifyAuditChainRequestSchema;
+ output: typeof VerifyAuditChainResponseSchema;
+ },
}> = /*@__PURE__*/
serviceDesc(file_cryptos_v1_node, 0);
diff --git a/src/lib/crypto/key-backup.test.ts b/src/lib/crypto/key-backup.test.ts
new file mode 100644
index 0000000..5bdcf12
--- /dev/null
+++ b/src/lib/crypto/key-backup.test.ts
@@ -0,0 +1,203 @@
+/*
+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 "reflect-metadata";
+import { X509CertificateGenerator } from "@peculiar/x509";
+import { describe, expect, it } from "vitest";
+
+import {
+ BACKUP_PASSPHRASE_ALPHABET,
+ buildCredentialPkcs12,
+ generateBackupPassphrase,
+ keyBackupFilename,
+ keyBackupPem,
+ keyMatchesCertificate,
+ readCertificateDer,
+ readKeyBackup,
+} from "@/lib/crypto/key-backup";
+import { importEncryptedKey } from "@/lib/crypto/leaf-key";
+
+const EC = { name: "ECDSA", namedCurve: "P-384" } as const;
+
+const newKeys = () =>
+ crypto.subtle.generateKey(EC, true, ["sign", "verify"]) as Promise;
+
+const selfSigned = async (keys: CryptoKeyPair, cn = "alice@example.org") =>
+ X509CertificateGenerator.createSelfSigned({
+ keys,
+ name: `CN=${cn}`,
+ notAfter: new Date(Date.now() + 86_400_000),
+ notBefore: new Date(Date.now() - 60_000),
+ serialNumber: "01",
+ signingAlgorithm: { hash: "SHA-384", name: "ECDSA" },
+ });
+
+const pkcs8 = async (key: CryptoKey) => new Uint8Array(await crypto.subtle.exportKey("pkcs8", key));
+
+// A minimal DER walker for the PKCS#12 check below.
+const tlv = (b: Uint8Array, o: number) => {
+ let len = b[o + 1];
+ let start = o + 2;
+ if (len & 0x80) {
+ const n = len & 0x7f;
+ len = 0;
+ for (let k = 0; k < n; k++) len = len * 256 + b[start + k];
+ start += n;
+ }
+ return {
+ content: b.subarray(start, start + len),
+ next: start + len,
+ raw: b.subarray(o, start + len),
+ tag: b[o],
+ };
+};
+const kids = (c: Uint8Array) => {
+ const out: ReturnType[] = [];
+ for (let o = 0; o < c.length;) {
+ const t = tlv(c, o);
+ out.push(t);
+ o = t.next;
+ }
+ return out;
+};
+const inner = (t: ReturnType) => kids(t.content)[0];
+
+// shroudedKeyOf digs the shrouded-key bag's EncryptedPrivateKeyInfo out of a PFX.
+const shroudedKeyOf = (pfx: Uint8Array): Uint8Array => {
+ const [, authSafe] = kids(tlv(pfx, 0).content);
+ const authSafeOctets = inner(kids(authSafe.content)[1]);
+ const [keyContentInfo] = kids(tlv(authSafeOctets.content, 0).content);
+ const safeContents = inner(kids(keyContentInfo.content)[1]);
+ const [bag] = kids(tlv(safeContents.content, 0).content);
+ return inner(kids(bag.content)[1]).raw.slice();
+};
+
+describe("generateBackupPassphrase", () => {
+ it("draws 24 characters from a 64-symbol alphabet", () => {
+ expect(new Set(BACKUP_PASSPHRASE_ALPHABET).size).toBe(64);
+ const p = generateBackupPassphrase();
+ expect(p).toHaveLength(24);
+ for (const ch of p) expect(BACKUP_PASSPHRASE_ALPHABET).toContain(ch);
+ });
+
+ it("differs every time", () => {
+ expect(generateBackupPassphrase()).not.toBe(generateBackupPassphrase());
+ });
+});
+
+describe("keyBackupFilename", () => {
+ it("names the file after the level and the lower-cased email", () => {
+ expect(keyBackupFilename("admin", "Alice@Example.org")).toBe(
+ "fleetos-admin-alice@example.org.key.pem",
+ );
+ });
+});
+
+describe("key backup round trip", () => {
+ it("writes an encrypted PKCS#8 PEM that reads back to the same key", async () => {
+ const keys = await newKeys();
+ const pass = generateBackupPassphrase();
+ const pem = await keyBackupPem(keys.privateKey, pass);
+ expect(pem).toMatch(/^-----BEGIN ENCRYPTED PRIVATE KEY-----\n/);
+ expect(pem).not.toContain(pass);
+
+ const back = await readKeyBackup(new TextEncoder().encode(pem), pass);
+ expect(await pkcs8(back)).toEqual(await pkcs8(keys.privateKey));
+ });
+
+ it("also reads the backup as DER", async () => {
+ const keys = await newKeys();
+ const pass = generateBackupPassphrase();
+ const pem = await keyBackupPem(keys.privateKey, pass);
+ const b64 = pem.replaceAll(/-----[^-]+-----|\s/g, "");
+ const der = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
+ const back = await readKeyBackup(der, pass);
+ expect(await pkcs8(back)).toEqual(await pkcs8(keys.privateKey));
+ });
+
+ it("fails cleanly on a wrong passphrase", async () => {
+ const keys = await newKeys();
+ const pem = await keyBackupPem(keys.privateKey, generateBackupPassphrase());
+ await expect(
+ readKeyBackup(new TextEncoder().encode(pem), "not-the-right-passphrase-at-all"),
+ ).rejects.toThrow(/passphrase is wrong/i);
+ });
+
+ it("refuses a file that is not an encrypted key", async () => {
+ const pem = "-----BEGIN CERTIFICATE-----\nMAA=\n-----END CERTIFICATE-----\n";
+ await expect(readKeyBackup(new TextEncoder().encode(pem), "x".repeat(24))).rejects.toThrow(
+ /ENCRYPTED PRIVATE KEY/,
+ );
+ });
+});
+
+describe("readCertificateDer", () => {
+ it("reads PEM and DER to the same DER", async () => {
+ const cert = await selfSigned(await newKeys());
+ const der = new Uint8Array(cert.rawData);
+ expect(readCertificateDer(new TextEncoder().encode(cert.toString("pem")))).toEqual(der);
+ expect(readCertificateDer(der)).toEqual(der);
+ });
+
+ it("refuses something that is not a certificate", () => {
+ expect(() => readCertificateDer(new TextEncoder().encode("hello"))).toThrow(/certificate/i);
+ });
+});
+
+describe("keyMatchesCertificate", () => {
+ it("is true for the certificate's own key and false for another", async () => {
+ const keys = await newKeys();
+ const cert = await selfSigned(keys);
+ const der = new Uint8Array(cert.rawData);
+ expect(await keyMatchesCertificate(keys.privateKey, der)).toBe(true);
+ expect(await keyMatchesCertificate((await newKeys()).privateKey, der)).toBe(false);
+ });
+});
+
+describe("buildCredentialPkcs12", () => {
+ it("protects the PKCS#12 with the key backup's passphrase", async () => {
+ const keys = await newKeys();
+ const pass = generateBackupPassphrase();
+ const backup = new TextEncoder().encode(await keyBackupPem(keys.privateKey, pass));
+ const cert = await selfSigned(keys);
+
+ const pfx = await buildCredentialPkcs12({
+ backup,
+ certDer: new Uint8Array(cert.rawData),
+ chainDer: [],
+ friendlyName: "FleetOS admin (alice@example.org)",
+ passphrase: pass,
+ });
+
+ const key = await importEncryptedKey(shroudedKeyOf(pfx), pass);
+ expect(await pkcs8(key)).toEqual(await pkcs8(keys.privateKey));
+ });
+
+ it("refuses a certificate that does not match the backed-up key", async () => {
+ const pass = generateBackupPassphrase();
+ const backup = new TextEncoder().encode(await keyBackupPem((await newKeys()).privateKey, pass));
+ const other = await selfSigned(await newKeys());
+ await expect(
+ buildCredentialPkcs12({
+ backup,
+ certDer: new Uint8Array(other.rawData),
+ chainDer: [],
+ friendlyName: "x",
+ passphrase: pass,
+ }),
+ ).rejects.toThrow(/does not match/i);
+ });
+});
diff --git a/src/lib/crypto/key-backup.ts b/src/lib/crypto/key-backup.ts
new file mode 100644
index 0000000..3debbcd
--- /dev/null
+++ b/src/lib/crypto/key-backup.ts
@@ -0,0 +1,126 @@
+/*
+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 "reflect-metadata";
+import { X509Certificate } from "@peculiar/x509";
+
+import { exportEncryptedKey, importEncryptedKey, toPemEncryptedKey } from "@/lib/crypto/leaf-key";
+import { assemblePkcs12 } from "@/lib/crypto/pkcs12";
+
+// The operator's key backup: the browser-made private key as an encrypted
+// PKCS#8 file (PBES2, PBKDF2-HMAC-SHA256 and AES-256-CBC), saved to the
+// operator's disk before the CSR is shown. The external CA can take a day to
+// sign, and a reload or an expired session must not lose the key. The same
+// passphrase later protects the PKCS#12, so the operator keeps one secret. The
+// key never goes to the Fleet Manager in any form.
+
+// 64 symbols, so each random byte maps to one without modulo bias: 24 of them
+// carry 144 bits.
+export const BACKUP_PASSPHRASE_ALPHABET =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+const BACKUP_PASSPHRASE_LENGTH = 24;
+
+export const generateBackupPassphrase = (): string => {
+ let out = "";
+ for (const b of crypto.getRandomValues(new Uint8Array(BACKUP_PASSPHRASE_LENGTH))) {
+ out += BACKUP_PASSPHRASE_ALPHABET[b & 63];
+ }
+ return out;
+};
+
+export const keyBackupFilename = (level: string, email: string): string =>
+ `fleetos-${level}-${email.trim().toLowerCase()}.key.pem`;
+
+export const keyBackupPem = async (privateKey: CryptoKey, passphrase: string): Promise =>
+ toPemEncryptedKey(await exportEncryptedKey(privateKey, passphrase));
+
+const pemBody = (text: string, label: string): Uint8Array | undefined => {
+ const match = new RegExp(`-----BEGIN ${label}-----([^-]+)-----END ${label}-----`).exec(text);
+ if (!match) return undefined;
+ const b64 = match[1].replaceAll(/\s/g, "");
+ return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
+};
+
+const looksLikePem = (bytes: Uint8Array): boolean => bytes.length > 0 && bytes[0] !== 0x30;
+
+// readKeyBackup loads a key backup file (PEM or DER) with its passphrase.
+export const readKeyBackup = async (file: Uint8Array, passphrase: string): Promise => {
+ let der = file;
+ if (looksLikePem(file)) {
+ const body = pemBody(new TextDecoder().decode(file), "ENCRYPTED PRIVATE KEY");
+ if (!body) {
+ throw new Error(
+ "That file is not a key backup. Expected a PEM block beginning -----BEGIN ENCRYPTED PRIVATE KEY-----.",
+ );
+ }
+ der = body;
+ }
+ return importEncryptedKey(der, passphrase);
+};
+
+// readCertificateDer accepts a certificate as PEM or DER and returns its DER.
+export const readCertificateDer = (file: Uint8Array): Uint8Array => {
+ try {
+ const cert = looksLikePem(file)
+ ? new X509Certificate(new TextDecoder().decode(file).trim())
+ : new X509Certificate(new Uint8Array(file));
+ return new Uint8Array(cert.rawData);
+ } catch {
+ throw new Error(
+ "That is not a certificate. Expected PEM (-----BEGIN CERTIFICATE-----) or DER.",
+ );
+ }
+};
+
+const publicPoint = async (key: CryptoKey): Promise => {
+ const jwk = await crypto.subtle.exportKey("jwk", key);
+ return `${jwk.crv}:${jwk.x}:${jwk.y}`;
+};
+
+// keyMatchesCertificate checks that the certificate carries the public half of
+// the private key, so a PKCS#12 is never built from a mismatched pair.
+export const keyMatchesCertificate = async (
+ privateKey: CryptoKey,
+ certDer: Uint8Array,
+): Promise => {
+ try {
+ const cert = new X509Certificate(new Uint8Array(certDer));
+ const publicKey = await cert.publicKey.export();
+ return (await publicPoint(publicKey)) === (await publicPoint(privateKey));
+ } catch {
+ return false;
+ }
+};
+
+// buildCredentialPkcs12 opens the key backup with its passphrase and seals the
+// PKCS#12 with that same passphrase: the leaf, then the chain (the operator CA).
+export const buildCredentialPkcs12 = async (params: {
+ backup: Uint8Array;
+ certDer: Uint8Array;
+ chainDer: Uint8Array[];
+ friendlyName: string;
+ passphrase: string;
+}): Promise => {
+ const key = await readKeyBackup(params.backup, params.passphrase);
+ if (!(await keyMatchesCertificate(key, params.certDer))) {
+ throw new Error(
+ "The certificate does not match the key in this backup. Check that it was signed from this key's CSR.",
+ );
+ }
+ return assemblePkcs12(params.certDer, key, params.passphrase, params.chainDer, {
+ friendlyName: params.friendlyName,
+ });
+};
diff --git a/src/lib/crypto/pkcs12.test.ts b/src/lib/crypto/pkcs12.test.ts
index 0d501cd..4c34d7c 100644
--- a/src/lib/crypto/pkcs12.test.ts
+++ b/src/lib/crypto/pkcs12.test.ts
@@ -226,6 +226,35 @@ describe("assemblePkcs12", () => {
expect((imported.algorithm as EcKeyAlgorithm).namedCurve).toBe("P-384");
});
+ it("labels the key and the leaf with a friendly name when one is given", async () => {
+ const keys = await crypto.subtle.generateKey(ECDSA_P384, true, ["sign", "verify"]);
+ const { anchorDer, leafDer } = await makeAnchorAndLeaf(keys.publicKey);
+ const name = "FleetOS admin (admin@example.org)";
+
+ const pfx = await assemblePkcs12(leafDer, keys.privateKey, STRONG, [anchorDer], {
+ friendlyName: name,
+ });
+
+ // friendlyName (PKCS#9, 1.2.840.113549.1.9.20) as a BMPString, on the key
+ // bag and the leaf's cert bag only.
+ const oid = new Uint8Array([0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x14]);
+ const bmp = new Uint8Array(name.length * 2);
+ for (let i = 0; i < name.length; i += 1) bmp[i * 2 + 1] = name.charCodeAt(i);
+ const count = (needle: Uint8Array) => {
+ let n = 0;
+ for (let i = 0; i + needle.length <= pfx.length; i += 1) {
+ if (needle.every((b, k) => pfx[i + k] === b)) n += 1;
+ }
+ return n;
+ };
+ expect(count(oid)).toBe(2);
+ expect(count(bmp)).toBe(2);
+ expect(certBags(pfx)).toEqual([leafDer, anchorDer]);
+ expect(await unshroudKey(pfx, STRONG)).toEqual(
+ new Uint8Array(await crypto.subtle.exportKey("pkcs8", keys.privateKey)),
+ );
+ });
+
it("is deterministic in structure but not in ciphertext (random salt/iv)", async () => {
const { privateKey } = await generateLeafKeyAndCSR({ sans: [], subjectCn: "op" });
const a = await assemblePkcs12(dummyCertDer, privateKey, STRONG);
diff --git a/src/lib/crypto/pkcs12.ts b/src/lib/crypto/pkcs12.ts
index 58801bb..4566732 100644
--- a/src/lib/crypto/pkcs12.ts
+++ b/src/lib/crypto/pkcs12.ts
@@ -41,7 +41,7 @@ const bufferOf = (bytes: Uint8Array): ArrayBuffer => {
};
// This module assembles a passphrase-protected PKCS#12 (RFC 7292) in the
-// browser from a certificate the operator-CA node just signed and the private
+// browser from a certificate the external operator CA signed and the private
// key the browser minted for the operator credential. The key never leaves the
// browser in plaintext: it is PBES2-shrouded (the same PBKDF2-HMAC-SHA256 +
// AES-256-CBC envelope leaf-key already uses for encrypted PKCS#8) before it is
@@ -58,6 +58,7 @@ const OID_DATA = "1.2.840.113549.1.7.1";
const OID_PKCS12_PKCS8_SHROUDED_KEY_BAG = "1.2.840.113549.1.12.10.1.2";
const OID_PKCS12_CERT_BAG = "1.2.840.113549.1.12.10.1.3";
const OID_PKCS9_X509_CERTIFICATE = "1.2.840.113549.1.9.22.1";
+const OID_PKCS9_FRIENDLY_NAME = "1.2.840.113549.1.9.20";
const OID_SHA256 = "2.16.840.1.101.3.4.2.1";
const MAC_ITERATIONS = 210_000;
@@ -100,9 +101,22 @@ const exportShroudedPkcs8 = async (
};
// safeBag builds one SafeBag ::= SEQUENCE { bagId OID, bagValue [0] EXPLICIT
-// ANY }. bagAttributes are omitted (they are optional and unnecessary here).
-const safeBag = (bagId: string, bagValue: Uint8Array): Uint8Array =>
- derSequence(derOid(bagId), derContextExplicit(0, bagValue));
+// ANY, bagAttributes SET OF Attribute OPTIONAL }. The only attribute used is
+// the PKCS#9 friendlyName, which keystores show as the certificate's label.
+const safeBag = (bagId: string, bagValue: Uint8Array, friendlyName?: string): Uint8Array =>
+ friendlyName
+ ? derSequence(
+ derOid(bagId),
+ derContextExplicit(0, bagValue),
+ derElement(
+ 0x31,
+ derSequence(
+ derOid(OID_PKCS9_FRIENDLY_NAME),
+ derElement(0x31, derElement(0x1e, bmpString(friendlyName).slice(0, -2))),
+ ),
+ ),
+ )
+ : derSequence(derOid(bagId), derContextExplicit(0, bagValue));
// certBag wraps the DER certificate as a CertBag ::= SEQUENCE { certId OID,
// certValue [0] EXPLICIT OCTET STRING }.
@@ -265,6 +279,7 @@ export const assemblePkcs12 = async (
privateKey: CryptoKey,
passphrase: string,
chainDer: Uint8Array[] = [],
+ options: { friendlyName?: string } = {},
): Promise => {
if (passphrase.length < MIN_PASSPHRASE_LENGTH) {
throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LENGTH} characters.`);
@@ -272,12 +287,13 @@ export const assemblePkcs12 = async (
// Key SafeContents: one shrouded-key bag holding the encrypted PKCS#8.
const shroudedPkcs8 = await exportShroudedPkcs8(privateKey, passphrase);
- const keyBag = safeBag(OID_PKCS12_PKCS8_SHROUDED_KEY_BAG, shroudedPkcs8);
+ const keyBag = safeBag(OID_PKCS12_PKCS8_SHROUDED_KEY_BAG, shroudedPkcs8, options.friendlyName);
const keySafeContents = derSequence(keyBag);
// Cert SafeContents: the leaf's cert bag first, then one per chain cert.
const certSafeContents = derSequence(
- ...[certDer, ...chainDer].map((der) => safeBag(OID_PKCS12_CERT_BAG, certBag(der))),
+ safeBag(OID_PKCS12_CERT_BAG, certBag(certDer), options.friendlyName),
+ ...chainDer.map((der) => safeBag(OID_PKCS12_CERT_BAG, certBag(der))),
);
// AuthenticatedSafe ::= SEQUENCE OF ContentInfo. Both bag groups are carried
diff --git a/src/lib/download.ts b/src/lib/download.ts
new file mode 100644
index 0000000..209143d
--- /dev/null
+++ b/src/lib/download.ts
@@ -0,0 +1,41 @@
+/*
+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.
+*/
+
+// Browser downloads of bytes the page made itself (a CSR, a key backup, a
+// PKCS#12). The object URL is revoked straight away.
+export const downloadBytes = (filename: string, contents: Uint8Array, type: string): void => {
+ const copy = new Uint8Array(contents.length);
+ copy.set(contents);
+ const url = URL.createObjectURL(new Blob([copy], { type }));
+ const anchor = document.createElement("a");
+ anchor.download = filename;
+ anchor.href = url;
+ anchor.click();
+ URL.revokeObjectURL(url);
+};
+
+export const downloadText = (filename: string, text: string, type = "text/plain"): void =>
+ downloadBytes(filename, new TextEncoder().encode(text), type);
+
+// readFileBytes reads a chosen file. FileReader keeps it working where
+// Blob.arrayBuffer is missing.
+export const readFileBytes = (file: Blob): Promise =>
+ new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.addEventListener("load", () => resolve(new Uint8Array(reader.result as ArrayBuffer)));
+ reader.addEventListener("error", () => reject(new Error("The file could not be read.")));
+ reader.readAsArrayBuffer(file);
+ });
diff --git a/src/lib/fingerprint.ts b/src/lib/fingerprint.ts
new file mode 100644
index 0000000..cb900a3
--- /dev/null
+++ b/src/lib/fingerprint.ts
@@ -0,0 +1,20 @@
+/*
+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.
+*/
+
+// shortFingerprint shows the first 8 bytes of a lowercase hex SHA-256 in the
+// colon form operators compare against OpenSSL output.
+export const shortFingerprint = (sha256: string): string =>
+ sha256 === "" ? "-" : `${(sha256.slice(0, 16).match(/../g) ?? []).join(":").toUpperCase()}ā¦`;
diff --git a/src/lib/fleet/error-code.ts b/src/lib/fleet/error-code.ts
index eec364c..85f52a3 100644
--- a/src/lib/fleet/error-code.ts
+++ b/src/lib/fleet/error-code.ts
@@ -21,8 +21,24 @@ import { ConnectError } from "@connectrpc/connect";
// text. The full table is the manager's docs/error-codes.md.
const METADATA_KEY = "x-cryptos-error-code";
+// A 16xx code can carry a sub-reason under this key: the api's ErrorReason
+// value name without its ERROR_REASON_ prefix, for example "IS_NODE_CA".
+const REASON_METADATA_KEY = "x-cryptos-error-reason";
+
export const ErrorCode = {
OperatorCAUnconfigured: 1400,
+ BootstrapTokenInvalid: 1600,
+ BootstrapClosed: 1601,
+ BootstrapRateLimited: 1602,
+ BootstrapUnavailable: 1603,
+ BootstrapSessionInvalid: 1604,
+ OperatorCARejected: 1605,
+ CsrRejected: 1606,
+ OperatorCAManagedByConfig: 1607,
+ NoRevocationSource: 1608,
+ OperatorCAInUse: 1609,
+ CertRejected: 1610,
+ RequestInvalid: 1611,
} as const;
// errorCode returns the manager's numeric code carried by an error, or
@@ -35,3 +51,9 @@ export const errorCode = (error: unknown): number | undefined => {
const code = Number.parseInt(raw, 10);
return Number.isNaN(code) ? undefined : code;
};
+
+// errorReason returns the sub-reason carried with a 16xx code, or undefined.
+export const errorReason = (error: unknown): string | undefined => {
+ const raw = ConnectError.from(error).metadata.get(REASON_METADATA_KEY);
+ return raw === null || raw === "" ? undefined : raw;
+};
diff --git a/src/lib/fleet/error-copy.test.ts b/src/lib/fleet/error-copy.test.ts
new file mode 100644
index 0000000..cef202c
--- /dev/null
+++ b/src/lib/fleet/error-copy.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 { Code, ConnectError } from "@connectrpc/connect";
+import { describe, expect, it } from "vitest";
+
+import { ErrorCode, ErrorReason } from "@/gen/fleet/cryptos/fleet/v1/errors_pb";
+import { describeFleetError, fleetErrorMessage } from "@/lib/fleet/error-copy";
+
+const refusal = (code: number, reason?: string): ConnectError =>
+ new ConnectError("refused", Code.FailedPrecondition, {
+ "x-cryptos-error-code": String(code),
+ ...(reason ? { "x-cryptos-error-reason": reason } : {}),
+ });
+
+// The sub-reasons each code can carry, as the api's errors.proto lists them.
+const REASONS_BY_CODE: Record = {
+ 1600: [],
+ 1601: [],
+ 1602: [],
+ 1603: ["DATABASE_REQUIRED", "FIRST_RUN_DISABLED"],
+ 1604: [],
+ 1605: [
+ "NOT_A_CA",
+ "EXPIRING",
+ "KEY_TYPE",
+ "IS_NODE_CA",
+ "CRL_SIGN_MISSING",
+ "NO_CRL_NOT_ACKNOWLEDGED",
+ "CRL_UNREACHABLE",
+ "CRL_INVALID",
+ "CRL_ROLLBACK",
+ "OCSP_UNREACHABLE",
+ "OCSP_INVALID",
+ "NOT_CONFIRMED",
+ "ROTATION_IN_PROGRESS",
+ ],
+ 1606: ["SIZE", "SIGNATURE", "SUBJECT_MISMATCH", "KEY_TYPE"],
+ 1607: [],
+ 1608: ["STALE_CRL", "STALE_OCSP", "STALE_DENYLIST", "NO_CRL", "DATABASE_REQUIRED"],
+ 1609: [],
+ 1610: [
+ "NOT_CHAINED",
+ "NOT_ACTIVE_ANCHOR",
+ "WRONG_LEVEL",
+ "LEVEL_EXT_CRITICAL",
+ "EKU",
+ "KEY_USAGE",
+ "BASIC_CONSTRAINTS",
+ "SUBJECT_MISMATCH",
+ "KEY_TYPE",
+ "KEY_MISMATCH",
+ "EXPIRING",
+ "REVOKED",
+ "REVOKED_OCSP",
+ "OCSP_UNKNOWN",
+ "DUPLICATE",
+ ],
+ 1611: ["NOT_FOUND", "EXPIRED", "NOT_PENDING"],
+};
+
+const codes = Object.values(ErrorCode).filter((v): v is number => typeof v === "number" && v !== 0);
+
+describe("describeFleetError", () => {
+ it("covers every code in the 1600-1611 contract", () => {
+ expect([...codes].sort((a, b) => a - b)).toEqual(Object.keys(REASONS_BY_CODE).map(Number));
+ });
+
+ it.each(codes)("gives code %i its own title and detail", (code) => {
+ const copy = describeFleetError(refusal(code));
+ expect(copy.code).toBe(code);
+ expect(copy.title).not.toBe("");
+ expect(copy.detail).not.toBe("");
+ expect(copy.title).not.toBe("Request failed");
+ });
+
+ const pairs = Object.entries(REASONS_BY_CODE).flatMap(([code, reasons]) =>
+ reasons.map((reason) => [Number(code), reason] as const),
+ );
+
+ it.each(pairs)("gives %i/%s a detail of its own", (code, reason) => {
+ const generic = describeFleetError(refusal(code)).detail;
+ const copy = describeFleetError(refusal(code, reason));
+ expect(copy.reason).toBe(reason);
+ expect(copy.detail).not.toBe("");
+ expect(copy.detail).not.toBe(generic);
+ });
+
+ it("names every sub-reason in the contract under at least one code", () => {
+ const listed = new Set(Object.values(REASONS_BY_CODE).flat());
+ const names = Object.keys(ErrorReason).filter(
+ (k) => Number.isNaN(Number(k)) && k !== "UNSPECIFIED",
+ );
+ for (const name of names) expect(listed.has(name), name).toBe(true);
+ });
+
+ it("reads a shared sub-reason with its code", () => {
+ const ca = describeFleetError(refusal(1605, "KEY_TYPE")).detail;
+ const csr = describeFleetError(refusal(1606, "KEY_TYPE")).detail;
+ expect(ca).not.toBe(csr);
+ });
+
+ it("falls back to the code's detail for a sub-reason it does not know", () => {
+ const generic = describeFleetError(refusal(1610)).detail;
+ expect(describeFleetError(refusal(1610, "SOMETHING_NEW")).detail).toBe(generic);
+ });
+
+ it("keeps the error's own message when there is no 16xx code", () => {
+ const copy = describeFleetError(new Error("network down"));
+ expect(copy.code).toBeUndefined();
+ expect(copy.detail).toBe("network down");
+ });
+
+ it("builds one line with the title and the detail", () => {
+ const line = fleetErrorMessage(refusal(1610, "REVOKED_OCSP"));
+ expect(line).toMatch(/OCSP/);
+ expect(line).toContain(describeFleetError(refusal(1610)).title);
+ });
+});
diff --git a/src/lib/fleet/error-copy.ts b/src/lib/fleet/error-copy.ts
new file mode 100644
index 0000000..d7e1adf
--- /dev/null
+++ b/src/lib/fleet/error-copy.ts
@@ -0,0 +1,192 @@
+/*
+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 { errorCode, errorReason } from "@/lib/fleet/error-code";
+
+// What to tell the operator for each first-run, operator-CA and operator
+// credential refusal (codes 1600-1611). Branch on the code and sub-reason the
+// manager attaches, never on its message text. A sub-reason name can appear
+// under more than one code (KEY_TYPE, EXPIRING, SUBJECT_MISMATCH,
+// DATABASE_REQUIRED), so reason copy is keyed by "/".
+
+export interface FleetErrorCopy {
+ code?: number;
+ detail: string;
+ reason?: string;
+ title: string;
+}
+
+const CODE_COPY: Record = {
+ 1600: {
+ detail:
+ "The bootstrap token is wrong, expired or already used. Each token works once: take the newest one from the Fleet Manager's log and try again.",
+ title: "Bootstrap token not accepted",
+ },
+ 1601: {
+ detail:
+ "An admin certificate has already signed in, so first run is over. Sign in with your operator certificate instead.",
+ title: "First run is closed",
+ },
+ 1602: {
+ detail: "Too many failed attempts from this client. Wait a few minutes before trying again.",
+ title: "Too many attempts",
+ },
+ 1603: {
+ detail: "This Fleet Manager can't do that in its current configuration.",
+ title: "Not available here",
+ },
+ 1604: {
+ detail:
+ "The first-run session is unknown, expired or ended (a newer session replaces it). Start again with the newest bootstrap token from the log.",
+ title: "First-run session ended",
+ },
+ 1605: {
+ detail: "The operator CA certificate, or its CRL or OCSP settings, were refused.",
+ title: "Operator CA refused",
+ },
+ 1606: {
+ detail: "The certificate signing request was refused.",
+ title: "CSR refused",
+ },
+ 1607: {
+ detail:
+ "The operator CA comes from the Fleet Manager's config file (operatorCAPath), so it can't be changed here. Change the config instead.",
+ title: "Operator CA managed by config",
+ },
+ 1608: {
+ detail: "The Fleet Manager has no fresh revocation data for this certificate.",
+ title: "No fresh revocation data",
+ },
+ 1609: {
+ detail:
+ "Retiring this operator CA would leave no trusted active CA, and nobody could sign in. Register its replacement first.",
+ title: "Operator CA still in use",
+ },
+ 1610: {
+ detail: "The operator certificate was refused.",
+ title: "Certificate refused",
+ },
+ 1611: {
+ detail: "The credential request can't be used.",
+ title: "Credential request unusable",
+ },
+};
+
+const REASON_COPY: Record = {
+ "1603/DATABASE_REQUIRED":
+ "This needs Postgres (database_url), which this Fleet Manager runs without. First run and the Fleet Manager denylist keep their state there. Configure the operator CA with operatorCAPath, and revoke at the CA with a CRL.",
+ "1603/FIRST_RUN_DISABLED":
+ "First run is switched off (firstRun: disabled). Configure the operator CA with operatorCAPath.",
+
+ "1605/NOT_A_CA":
+ "The certificate isn't a CA: it needs basicConstraints CA:TRUE and key usage keyCertSign.",
+ "1605/EXPIRING":
+ "The CA certificate has less than 30 days left. Use a CA with a longer validity.",
+ "1605/KEY_TYPE": "The CA key must be ECDSA P-384 or P-256, or RSA of 3072 bits or more.",
+ "1605/IS_NODE_CA":
+ "That is a CryptOS node's CA. A CryptOS node can never be the operator CA; use an external CA such as an offline OpenSSL CA.",
+ "1605/CRL_SIGN_MISSING":
+ "A CRL source is set, but the CA's key usage lacks cRLSign, so its CRLs can't be verified. Reissue the CA with cRLSign, or choose no CRL.",
+ "1605/NO_CRL_NOT_ACKNOWLEDGED":
+ "Choosing no CRL needs the acknowledgement that revocations made at the CA won't be seen and MCP will be refused.",
+ "1605/CRL_UNREACHABLE":
+ "The Fleet Manager couldn't fetch the CRL from that URL. Check the URL and that the Fleet Manager can reach it.",
+ "1605/CRL_INVALID":
+ "The CRL failed verification against the CA: check that this CA signed it and that it has a nextUpdate.",
+ "1605/CRL_ROLLBACK":
+ "That CRL is older than the one the Fleet Manager holds. Publish or upload a newer CRL.",
+ "1605/OCSP_UNREACHABLE":
+ "The OCSP responder didn't answer the probe. Check the URL and that the responder is running.",
+ "1605/OCSP_INVALID":
+ "The OCSP responder answered, but its response failed validation (signer, EKU OCSPSigning, validity or format).",
+ "1605/NOT_CONFIRMED":
+ "The fingerprint wasn't confirmed, or doesn't match the CA. Compare it with the CA certificate and confirm again.",
+ "1605/ROTATION_IN_PROGRESS":
+ "A retiring operator CA already exists. Retire it before registering another CA.",
+
+ "1606/SIZE": "The CSR is larger than 4 KiB.",
+ "1606/SIGNATURE":
+ "The CSR's signature doesn't verify. It may be truncated, or not match its own key.",
+ "1606/SUBJECT_MISMATCH":
+ "The CSR's subject must be exactly one common name, the holder's email (CN=).",
+ "1606/KEY_TYPE": "The CSR key must be ECDSA P-384 or RSA of 3072 bits or more.",
+
+ "1608/STALE_CRL": "The operator CA's CRL is past its nextUpdate. Publish or upload a new CRL.",
+ "1608/STALE_OCSP":
+ "OCSP is configured for this certificate, but the responder gave no fresh answer and there is no fresh CRL to fall back on.",
+ "1608/STALE_DENYLIST":
+ "This Fleet Manager hasn't read its denylist for over 5 minutes (is Postgres reachable?).",
+ "1608/NO_CRL":
+ "MCP needs a CRL: this certificate's operator CA has no CRL source. Set one on the Operator CAs page.",
+ "1608/DATABASE_REQUIRED":
+ "The Fleet Manager denylist needs Postgres, which this Fleet Manager runs without. Revoke at the CA and publish a CRL instead.",
+
+ "1610/NOT_CHAINED":
+ "The certificate doesn't verify against the operator CA for client authentication (wrong CA, expired, or an unknown critical extension).",
+ "1610/NOT_ACTIVE_ANCHOR":
+ "The certificate chains to a retiring operator CA. New credentials must come from the active CA.",
+ "1610/WRONG_LEVEL":
+ "The level extension (1.3.6.1.4.1.59999.1.1) is missing or names the wrong level. Sign with the matching op_ section.",
+ "1610/LEVEL_EXT_CRITICAL":
+ "The level extension is marked critical. It must be non-critical, or browsers can't sign in with it.",
+ "1610/EKU": "The extended key usage must be exactly clientAuth.",
+ "1610/KEY_USAGE":
+ "The key usage must include digitalSignature, and must not include keyCertSign or cRLSign.",
+ "1610/BASIC_CONSTRAINTS": "basicConstraints must be present and CA:FALSE.",
+ "1610/SUBJECT_MISMATCH":
+ "The certificate's subject must be exactly one common name, the holder's email (CN=).",
+ "1610/KEY_TYPE": "The certificate key must be ECDSA P-384 or RSA of 3072 bits or more.",
+ "1610/KEY_MISMATCH":
+ "The certificate's public key differs from the request's. Upload the certificate signed from this request's CSR.",
+ "1610/EXPIRING": "The certificate has less than 1 day left.",
+ "1610/REVOKED": "The certificate is on the Fleet Manager denylist or in the CA's CRL.",
+ "1610/REVOKED_OCSP": "The operator CA's OCSP responder says this certificate is revoked.",
+ "1610/OCSP_UNKNOWN":
+ "The operator CA's OCSP responder doesn't know this certificate, which counts as revoked.",
+ "1610/DUPLICATE": "This certificate is already recorded.",
+
+ "1611/NOT_FOUND": "No credential request has that id.",
+ "1611/EXPIRED": "The credential request has expired (after 30 days). Make a new request.",
+ "1611/NOT_PENDING": "The credential request was already completed or cancelled.",
+};
+
+const plainMessage = (error: unknown, fallback: string): string =>
+ error instanceof Error && error.message !== "" ? error.message : fallback;
+
+// describeFleetError turns a refusal into a title and a detail. Anything
+// without a 16xx code keeps its own message.
+export const describeFleetError = (
+ error: unknown,
+ fallback = "The request failed.",
+): FleetErrorCopy => {
+ const code = errorCode(error);
+ const copy = code === undefined ? undefined : CODE_COPY[code];
+ if (code === undefined || copy === undefined) {
+ return { detail: plainMessage(error, fallback), title: "Request failed" };
+ }
+ const reason = errorReason(error);
+ const detail = (reason && REASON_COPY[`${code}/${reason}`]) || copy.detail;
+ return { code, detail, reason, title: copy.title };
+};
+
+// fleetErrorMessage is describeFleetError as one line, with the code to quote.
+export const fleetErrorMessage = (error: unknown, fallback?: string): string => {
+ const { code, detail, reason, title } = describeFleetError(error, fallback);
+ if (code === undefined) {
+ return detail;
+ }
+ return `${title}: ${detail} (error ${code}${reason ? ` ${reason}` : ""})`;
+};
diff --git a/src/lib/form.ts b/src/lib/form.ts
new file mode 100644
index 0000000..0536929
--- /dev/null
+++ b/src/lib/form.ts
@@ -0,0 +1,21 @@
+/*
+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.
+*/
+
+// Shared field styling and checks for the operator credential forms.
+export const fieldClass = "w-full rounded-md border bg-card px-3 py-2 font-mono text-sm";
+export const labelClass = "font-mono text-[11px] uppercase tracking-wider text-muted-foreground";
+
+export const looksLikeEmail = (v: string): boolean => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim());
diff --git a/src/lib/operators.test.ts b/src/lib/operators.test.ts
index 4da6cbe..44fb212 100644
--- a/src/lib/operators.test.ts
+++ b/src/lib/operators.test.ts
@@ -14,84 +14,259 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { afterEach, describe, expect, it, vi } from "vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as clientMod from "@/lib/fleet/client";
import * as modeMod from "@/lib/fleet/mode";
import {
- issueOperatorCredential,
+ __resetOperators,
+ cancelCredentialRequest,
+ createCredentialRequest,
+ credentialFileBase,
+ DENY_REASONS,
+ denyOperatorCredential,
+ levelExtfileSection,
+ listCredentialRequests,
listOperatorCredentials,
- revokeOperatorCredential,
+ recordOperatorCredential,
} from "@/lib/operators";
-afterEach(() => {
- vi.restoreAllMocks();
-});
+const live = (rpcs: Record) => {
+ vi.spyOn(modeMod, "fleetMode").mockReturnValue("live");
+ vi.spyOn(clientMod, "fleetClient").mockReturnValue(
+ rpcs as unknown as ReturnType,
+ );
+};
+
+beforeEach(() => __resetOperators());
+afterEach(() => vi.restoreAllMocks());
describe("operators (mock mode)", () => {
- it("lists a coherent set of mock credentials", async () => {
+ beforeEach(() => {
vi.spyOn(modeMod, "fleetMode").mockReturnValue("mock");
+ });
+
+ it("lists credentials with their kind, issuer and revocation sources", async () => {
const items = await listOperatorCredentials();
expect(items.length).toBeGreaterThan(0);
- expect(items[0]).toHaveProperty("commonName");
- expect(items[0]).toHaveProperty("level");
- expect(items[0]).toHaveProperty("serialHex");
+ const kinds = new Set(items.map((i) => i.kind));
+ expect(kinds.has("first_admin")).toBe(true);
+ expect(kinds.has("observed")).toBe(true);
+ expect(items.some((i) => i.denylisted)).toBe(true);
+ expect(items.every((i) => typeof i.crlRevoked === "boolean")).toBe(true);
});
- it("issues a mock credential returning cert bytes and a serial without a live call", async () => {
- vi.spyOn(modeMod, "fleetMode").mockReturnValue("mock");
- const result = await issueOperatorCredential(
- "op@acme.example",
- "operator",
- new Uint8Array([1]),
+ it("creates a pending request that returns the CSR, extfile section and command", async () => {
+ const created = await createCredentialRequest({
+ csrDer: new Uint8Array([0x30, 0x03, 0x02, 0x01, 0x01]),
+ email: "Bob@Example.org",
+ fullName: "Bob Example",
+ level: "operator",
+ });
+ expect(created.requestId).not.toBe("");
+ expect(created.csrPem).toMatch(/^-----BEGIN CERTIFICATE REQUEST-----/);
+ expect(created.extfileSection).toContain("DER:13:08:6F:70:65:72:61:74:6F:72");
+ expect(created.opensslCommand).toBe(
+ "openssl ca -config operator-ca.cnf -extensions op_operator -notext -in fleetos-operator-bob@example.org.csr -out fleetos-operator-bob@example.org.crt",
);
- expect(result.serialHex).toMatch(/^[0-9a-f:]+$/i);
- expect(result.certDer.length).toBeGreaterThan(0);
+
+ const pending = await listCredentialRequests();
+ expect(pending.map((r) => r.id)).toContain(created.requestId);
+ expect(pending.find((r) => r.id === created.requestId)?.email).toBe("bob@example.org");
});
- it("revokes a mock credential without a live call", async () => {
- vi.spyOn(modeMod, "fleetMode").mockReturnValue("mock");
- await expect(revokeOperatorCredential("AA:BB", 4)).resolves.toBeUndefined();
+ it("refuses an unknown level or an empty CSR before any call", async () => {
+ await expect(
+ createCredentialRequest({
+ csrDer: new Uint8Array([1]),
+ email: "a@example.org",
+ fullName: "A",
+ level: "root",
+ }),
+ ).rejects.toThrow(/level/i);
+ await expect(
+ createCredentialRequest({
+ csrDer: new Uint8Array(),
+ email: "a@example.org",
+ fullName: "A",
+ level: "viewer",
+ }),
+ ).rejects.toThrow(/csr/i);
});
- it("rejects an unknown level before any call", async () => {
- vi.spyOn(modeMod, "fleetMode").mockReturnValue("mock");
- await expect(issueOperatorCredential("op", "superuser", new Uint8Array([1]))).rejects.toThrow(
- /level/i,
+ it("cancels a pending request", async () => {
+ const [first] = await listCredentialRequests();
+ await cancelCredentialRequest(first.id);
+ expect((await listCredentialRequests()).map((r) => r.id)).not.toContain(first.id);
+ expect((await listCredentialRequests("")).find((r) => r.id === first.id)?.state).toBe(
+ "cancelled",
);
});
- it("rejects an empty CSR before any call", async () => {
- vi.spyOn(modeMod, "fleetMode").mockReturnValue("mock");
- await expect(issueOperatorCredential("op", "admin", new Uint8Array())).rejects.toThrow(/csr/i);
+ it("records a certificate against a request and completes it", async () => {
+ const [first] = await listCredentialRequests();
+ const { credential } = await recordOperatorCredential({
+ certDer: new Uint8Array([0x30, 0x00]),
+ requestId: first.id,
+ });
+ expect(credential.kind).toBe("requested");
+ expect(credential.email).toBe(first.email);
+ const done = (await listCredentialRequests("")).find((r) => r.id === first.id);
+ expect(done?.state).toBe("completed");
+ expect(done?.completedSerial).toBe(credential.serialHex);
+ });
+
+ it("denies a credential and warns to revoke at the CA as well", async () => {
+ const [target] = (await listOperatorCredentials()).filter((r) => !r.denylisted);
+ const result = await denyOperatorCredential({
+ note: "left the team",
+ reasonCode: 3,
+ serialHex: target.serialHex,
+ });
+ expect(result.warnings.join(" ")).toMatch(/revoke .* at your CA/i);
+ const after = (await listOperatorCredentials()).find((r) => r.serialHex === target.serialHex);
+ expect(after?.denylisted).toBe(true);
+ expect(after?.revoked).toBe(true);
+ });
+});
+
+describe("DENY_REASONS", () => {
+ it("offers RFC 5280 reason codes 0 to 10 except 7", () => {
+ expect(DENY_REASONS.map((r) => r.code).sort((a, b) => a - b)).toEqual([
+ 0, 1, 2, 3, 4, 5, 6, 8, 9, 10,
+ ]);
+ });
+});
+
+describe("credentialFileBase", () => {
+ it("replaces anything a shell could read as syntax, as the manager does", () => {
+ expect(credentialFileBase("admin", "a.b-c_d@example.org")).toBe(
+ "fleetos-admin-a.b-c_d@example.org",
+ );
+ expect(credentialFileBase("viewer", "x$(id)'y@example.org")).toBe(
+ "fleetos-viewer-x__id__y@example.org",
+ );
+ });
+});
+
+describe("levelExtfileSection", () => {
+ it("encodes each level as a PrintableString DER value", () => {
+ expect(levelExtfileSection("admin")).toContain(
+ "1.3.6.1.4.1.59999.1.1 = DER:13:05:61:64:6D:69:6E",
+ );
+ expect(levelExtfileSection("viewer")).toContain("DER:13:06:76:69:65:77:65:72");
});
});
describe("operators (live mode)", () => {
- it("routes issue through the FleetService and returns the signed cert", async () => {
- vi.spyOn(modeMod, "fleetMode").mockReturnValue("live");
- const issueOperatorCredentialRpc = vi
- .fn()
- .mockResolvedValue({ certDer: new Uint8Array([9, 9]), serialHex: "0A:0B" });
- vi.spyOn(clientMod, "fleetClient").mockReturnValue({
- issueOperatorCredential: issueOperatorCredentialRpc,
- } as unknown as ReturnType);
-
- const csr = new Uint8Array([1, 2, 3]);
- const result = await issueOperatorCredential("op@acme.example", "admin", csr);
- expect(issueOperatorCredentialRpc).toHaveBeenCalledWith({
- commonName: "op@acme.example",
- csrDer: csr,
+ it("maps every credential field from ListOperatorCredentials", async () => {
+ live({
+ listOperatorCredentials: vi.fn().mockResolvedValue({
+ items: [
+ {
+ commonName: "alice@example.org",
+ crlRevoked: true,
+ denylisted: false,
+ email: "alice@example.org",
+ firstSeenAt: "2026-09-01T00:00:00Z",
+ fullName: "Alice Example",
+ issuerSha256: "ab".repeat(32),
+ kind: "observed",
+ lastSeenAt: "2026-09-02T00:00:00Z",
+ level: "operator",
+ notAfter: "2027-09-01T00:00:00Z",
+ revoked: true,
+ serialHex: "0A:0B",
+ },
+ ],
+ }),
+ });
+ const [row] = await listOperatorCredentials();
+ expect(row).toEqual({
+ commonName: "alice@example.org",
+ crlRevoked: true,
+ denylisted: false,
+ email: "alice@example.org",
+ firstSeenAt: "2026-09-01T00:00:00Z",
+ fullName: "Alice Example",
+ issuerSha256: "ab".repeat(32),
+ kind: "observed",
+ lastSeenAt: "2026-09-02T00:00:00Z",
+ level: "operator",
+ notAfter: "2027-09-01T00:00:00Z",
+ revoked: true,
+ serialHex: "0A:0B",
+ });
+ });
+
+ it("sends only the level, email, name and CSR when creating a request", async () => {
+ const rpc = vi.fn().mockResolvedValue({
+ csrPem: "pem",
+ expiresAt: "2026-10-30T00:00:00Z",
+ extfileSection: "[ op_admin ]",
+ opensslCommand: "openssl ca",
+ requestId: "req-1",
+ });
+ live({ createOperatorCredentialRequest: rpc });
+ const csrDer = new Uint8Array([1, 2, 3]);
+ const created = await createCredentialRequest({
+ csrDer,
+ email: " Alice@Example.org ",
+ fullName: " Alice ",
level: "admin",
});
- expect(result.serialHex).toBe("0A:0B");
+ expect(rpc).toHaveBeenCalledWith({
+ csrDer,
+ email: "alice@example.org",
+ fullName: "Alice",
+ level: "admin",
+ });
+ expect(created.requestId).toBe("req-1");
+ });
+
+ it("lists pending requests by default", async () => {
+ const rpc = vi.fn().mockResolvedValue({ items: [] });
+ live({ listOperatorCredentialRequests: rpc });
+ await listCredentialRequests();
+ expect(rpc).toHaveBeenCalledWith({ state: "pending" });
+ });
+
+ it("records with the request id and returns the warnings", async () => {
+ const rpc = vi.fn().mockResolvedValue({
+ credential: { commonName: "a@example.org", serialHex: "01" },
+ warnings: ["validity over 400 days"],
+ });
+ live({ recordOperatorCredential: rpc });
+ const certDer = new Uint8Array([9]);
+ const result = await recordOperatorCredential({ certDer, requestId: "req-1" });
+ expect(rpc).toHaveBeenCalledWith({ certDer, fullName: "", requestId: "req-1" });
+ expect(result.warnings).toEqual(["validity over 400 days"]);
});
- it("surfaces a live revoke error to the caller (no silent fallback)", async () => {
- vi.spyOn(modeMod, "fleetMode").mockReturnValue("live");
- vi.spyOn(clientMod, "fleetClient").mockReturnValue({
- revokeOperatorCredential: vi.fn().mockRejectedValue(new Error("operator-CA unreachable")),
- } as unknown as ReturnType);
- await expect(revokeOperatorCredential("0A:0B", 1)).rejects.toThrow(/unreachable/);
+ it("denies with the reason, note and issuer and surfaces errors", async () => {
+ const rpc = vi.fn().mockResolvedValue({
+ issuerSha256: "cd".repeat(32),
+ revokedAt: "2026-09-30T00:00:00Z",
+ serialHex: "01",
+ warnings: ["also revoke at your CA"],
+ });
+ live({ revokeOperatorCredential: rpc });
+ await denyOperatorCredential({
+ issuerSha256: "cd".repeat(32),
+ note: "lost laptop",
+ reasonCode: 1,
+ serialHex: "01",
+ });
+ expect(rpc).toHaveBeenCalledWith({
+ issuerSha256: "cd".repeat(32),
+ note: "lost laptop",
+ reasonCode: 1,
+ serialHex: "01",
+ });
+
+ live({ revokeOperatorCredential: vi.fn().mockRejectedValue(new Error("denied")) });
+ await expect(
+ denyOperatorCredential({ note: "", reasonCode: 0, serialHex: "01" }),
+ ).rejects.toThrow(/denied/);
});
});
diff --git a/src/lib/operators.ts b/src/lib/operators.ts
index c1e0937..6740274 100644
--- a/src/lib/operators.ts
+++ b/src/lib/operators.ts
@@ -14,163 +14,454 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import "reflect-metadata";
+import { X509Certificate } from "@peculiar/x509";
+
+import { base64 } from "@/lib/crypto/leaf-key";
import { fleetClient } from "@/lib/fleet/client";
import { fleetMode } from "@/lib/fleet/mode";
-// Operator credential management (S9). The manager routes issuance and
-// revocation to the operator-CA node; the web only ever holds the returned
-// certificate and the serial. The operator's private key is minted in the
-// browser (leaf-key) and never leaves it in plaintext -- the PKCS#12 is
-// assembled and sealed client-side (pkcs12) before download.
+// Operator credentials. The operator CA is external and the Fleet Manager
+// never signs: an admin files a credential request from a CSR, the CA operator
+// signs it out of band, and the admin records the signed certificate. Revoking
+// puts the credential on the Fleet Manager's own denylist, which doesn't touch
+// the CA. A private key made in the browser never reaches this module.
export type OperatorLevel = "admin" | "operator" | "viewer";
-const LEVELS: OperatorLevel[] = ["viewer", "operator", "admin"];
+export const OPERATOR_LEVELS: OperatorLevel[] = ["viewer", "operator", "admin"];
+
+export const OPERATOR_LEVEL_OID = "1.3.6.1.4.1.59999.1.1";
-// OperatorCredentialRow is one issued credential as the list surface renders it.
export interface OperatorCredentialRow {
commonName: string;
+ crlRevoked: boolean;
+ denylisted: boolean;
+ email: string;
+ firstSeenAt: string;
+ fullName: string;
+ issuerSha256: string;
+ // "first_admin", "requested", "recorded", "observed" or "legacy_node".
+ kind: string;
+ lastSeenAt: string;
level: string;
notAfter: string;
revoked: boolean;
serialHex: string;
}
-// IssuedCredential is what issueOperatorCredential hands back: the signed
-// certificate (DER) and its hex serial. The browser assembles the PKCS#12 from
-// this cert plus the key it already holds.
-export interface IssuedCredential {
- certDer: Uint8Array;
+export interface CredentialRequestRow {
+ completedSerial: string;
+ createdAt: string;
+ createdByCn: string;
+ csrPem: string;
+ email: string;
+ expiresAt: string;
+ fullName: string;
+ id: string;
+ level: string;
+ state: string;
+}
+
+export interface CreatedCredentialRequest {
+ csrPem: string;
+ expiresAt: string;
+ extfileSection: string;
+ opensslCommand: string;
+ requestId: string;
+}
+
+export interface RecordedCredential {
+ credential: OperatorCredentialRow;
+ warnings: string[];
+}
+
+export interface DenyResult {
+ issuerSha256: string;
+ revokedAt: string;
serialHex: string;
+ warnings: string[];
}
-const isLevel = (v: string): v is OperatorLevel => (LEVELS as string[]).includes(v);
+// RFC 5280 CRLReason codes offered by the deny dialog. 7 is unassigned.
+export const DENY_REASONS: { code: number; label: string }[] = [
+ { code: 1, label: "keyCompromise" },
+ { code: 3, label: "affiliationChanged" },
+ { code: 4, label: "superseded" },
+ { code: 5, label: "cessationOfOperation" },
+ { code: 9, label: "privilegeWithdrawn" },
+ { code: 6, label: "certificateHold" },
+ { code: 2, label: "cACompromise" },
+ { code: 10, label: "aACompromise" },
+ { code: 8, label: "removeFromCRL" },
+ { code: 0, label: "unspecified" },
+];
+
+export const CA_REVOKE_REMINDER =
+ "The Fleet Manager denylist only stops this credential at this Fleet Manager. Also revoke the certificate at your CA and publish a new CRL.";
+
+export const isOperatorLevel = (v: string): v is OperatorLevel =>
+ (OPERATOR_LEVELS as string[]).includes(v);
+
+const hexPair = (b: number): string => b.toString(16).padStart(2, "0").toUpperCase();
+
+// levelExtfileSection is the OpenSSL extension section for a level, as the
+// manager returns it. Mock mode and the anonymous request page use it; live
+// screens show the manager's own copy.
+export const levelExtfileSection = (level: OperatorLevel): string => {
+ const der = [0x13, level.length, ...new TextEncoder().encode(level)].map(hexPair).join(":");
+ return [
+ `[ op_${level} ]`,
+ "basicConstraints = critical, CA:FALSE",
+ "keyUsage = critical, digitalSignature",
+ "extendedKeyUsage = clientAuth",
+ "subjectKeyIdentifier = hash",
+ "authorityKeyIdentifier = keyid",
+ `${OPERATOR_LEVEL_OID} = DER:${der}`,
+ ].join("\n");
+};
-// A small coherent mock set so the Operators page is exercisable offline. It is
-// self-consistent (one revoked, mixed levels) and mutated in place by the mock
-// issue/revoke paths so the flow feels live without a manager.
-let mockRows: OperatorCredentialRow[] = [
+// credentialFileBase is the file name stem the manager's signing command
+// uses: fleetos--, with anything a shell could read as syntax
+// replaced by "_". The CSR download uses it so the command works as given.
+export const credentialFileBase = (level: string, email: string): string =>
+ `fleetos-${level}-${email.replaceAll(/[^\w.@-]/g, "_")}`;
+
+export const levelOpensslCommand = (level: OperatorLevel, email: string): string => {
+ const base = credentialFileBase(level, email);
+ return `openssl ca -config operator-ca.cnf -extensions op_${level} -notext -in ${base}.csr -out ${base}.crt`;
+};
+
+export const csrToPem = (der: Uint8Array): string => {
+ const lines = base64(der).match(/.{1,64}/g) ?? [];
+ return `-----BEGIN CERTIFICATE REQUEST-----\n${lines.join("\n")}\n-----END CERTIFICATE REQUEST-----\n`;
+};
+
+const blankCredential: OperatorCredentialRow = {
+ commonName: "",
+ crlRevoked: false,
+ denylisted: false,
+ email: "",
+ firstSeenAt: "",
+ fullName: "",
+ issuerSha256: "",
+ kind: "",
+ lastSeenAt: "",
+ level: "",
+ notAfter: "",
+ revoked: false,
+ serialHex: "",
+};
+
+const toRow = (item: Partial): OperatorCredentialRow => ({
+ commonName: item.commonName ?? "",
+ crlRevoked: item.crlRevoked ?? false,
+ denylisted: item.denylisted ?? false,
+ email: item.email ?? "",
+ firstSeenAt: item.firstSeenAt ?? "",
+ fullName: item.fullName ?? "",
+ issuerSha256: item.issuerSha256 ?? "",
+ kind: item.kind ?? "",
+ lastSeenAt: item.lastSeenAt ?? "",
+ level: item.level ?? "",
+ notAfter: item.notAfter ?? "",
+ revoked: item.revoked ?? false,
+ serialHex: item.serialHex ?? "",
+});
+
+const toRequestRow = (item: Partial): CredentialRequestRow => ({
+ completedSerial: item.completedSerial ?? "",
+ createdAt: item.createdAt ?? "",
+ createdByCn: item.createdByCn ?? "",
+ csrPem: item.csrPem ?? "",
+ email: item.email ?? "",
+ expiresAt: item.expiresAt ?? "",
+ fullName: item.fullName ?? "",
+ id: item.id ?? "",
+ level: item.level ?? "",
+ state: item.state ?? "",
+});
+
+// The mock set is self-consistent (one of each kind, one denylisted, one
+// CRL-revoked, one pending request) and mutated in place so the flows feel
+// live without a manager.
+const MOCK_ISSUER = "9f".repeat(32);
+
+const seedCredentials = (): OperatorCredentialRow[] => [
{
+ ...blankCredential,
commonName: "operator@acme.example",
+ email: "operator@acme.example",
+ firstSeenAt: "2026-09-01T12:00:00Z",
+ fullName: "Ada Operator",
+ issuerSha256: MOCK_ISSUER,
+ kind: "first_admin",
+ lastSeenAt: "2026-09-30T09:00:00Z",
level: "admin",
- notAfter: "2027-01-01T00:00:00Z",
- revoked: false,
+ notAfter: "2027-09-01T00:00:00Z",
serialHex: "3A:7F:0C:91:D2:44:8B:1E",
},
{
+ ...blankCredential,
commonName: "auditor@acme.example",
+ email: "auditor@acme.example",
+ fullName: "Grace Auditor",
+ issuerSha256: MOCK_ISSUER,
+ kind: "requested",
level: "viewer",
- notAfter: "2026-11-15T00:00:00Z",
- revoked: false,
+ notAfter: "2027-03-15T00:00:00Z",
serialHex: "11:22:33:44:55:66",
},
{
+ ...blankCredential,
+ commonName: "ci-bot@acme.example",
+ email: "ci-bot@acme.example",
+ firstSeenAt: "2026-09-20T08:00:00Z",
+ issuerSha256: MOCK_ISSUER,
+ kind: "observed",
+ lastSeenAt: "2026-09-29T17:30:00Z",
+ level: "operator",
+ notAfter: "2027-01-10T00:00:00Z",
+ serialHex: "7C:01:AA:02",
+ },
+ {
+ ...blankCredential,
commonName: "former-op@acme.example",
+ denylisted: true,
+ email: "former-op@acme.example",
+ issuerSha256: MOCK_ISSUER,
+ kind: "recorded",
level: "operator",
- notAfter: "2026-09-01T00:00:00Z",
+ notAfter: "2027-02-01T00:00:00Z",
revoked: true,
serialHex: "DE:AD:BE:EF:00:01",
},
+ {
+ ...blankCredential,
+ commonName: "lost-laptop@acme.example",
+ crlRevoked: true,
+ email: "lost-laptop@acme.example",
+ issuerSha256: MOCK_ISSUER,
+ kind: "requested",
+ level: "operator",
+ notAfter: "2027-02-01T00:00:00Z",
+ revoked: true,
+ serialHex: "0B:AD:F0:0D",
+ },
+ {
+ ...blankCredential,
+ commonName: "old-node-issued@acme.example",
+ kind: "legacy_node",
+ level: "admin",
+ notAfter: "2026-12-01T00:00:00Z",
+ serialHex: "01:02:03",
+ },
+];
+
+const seedRequests = (): CredentialRequestRow[] => [
+ {
+ completedSerial: "",
+ createdAt: "2026-09-29T14:00:00Z",
+ createdByCn: "operator@acme.example",
+ csrPem: csrToPem(new Uint8Array([0x30, 0x03, 0x02, 0x01, 0x07])),
+ email: "new-hire@acme.example",
+ expiresAt: "2026-10-29T14:00:00Z",
+ fullName: "Linus New-Hire",
+ id: "3b0f6c1e-0d6a-4d8e-9c55-2f1f0a7e4b21",
+ level: "operator",
+ state: "pending",
+ },
];
-// listOperatorCredentials returns the issued credentials. `mock` returns the
-// in-memory set; live reads through ListOperatorCredentials (operator-readable).
+let mockCredentials = seedCredentials();
+let mockRequests = seedRequests();
+
+const randomHex = (bytes: number): string =>
+ Array.from(crypto.getRandomValues(new Uint8Array(bytes)), hexPair).join(":");
+
+const plusDays = (days: number): string =>
+ new Date(Date.now() + days * 86_400_000).toISOString().replace(/\.\d+Z$/, "Z");
+
+const mockCertCn = (certDer: Uint8Array): string => {
+ try {
+ return new X509Certificate(new Uint8Array(certDer)).subjectName.getField("CN")[0] ?? "";
+ } catch {
+ return "";
+ }
+};
+
+// listOperatorCredentials returns every credential the manager knows: recorded
+// ones and those only observed in use.
export const listOperatorCredentials = async (): Promise => {
if (fleetMode() === "mock") {
- return mockRows.map((r) => ({ ...r }));
+ return mockCredentials.map((r) => ({ ...r }));
}
const response = await fleetClient().listOperatorCredentials({});
- return response.items.map((item) => ({
- commonName: item.commonName,
- level: item.level,
- notAfter: item.notAfter,
- revoked: item.revoked,
- serialHex: item.serialHex,
- }));
+ return response.items.map((item) => toRow(item));
};
-// issueOperatorCredential validates the level and CSR, then routes the signing
-// to the operator-CA node through the manager. In `mock` mode it fabricates a
-// plausible cert/serial and appends a row so the page updates; in live mode a
-// manager/node error (missing operator- profile, revoked issuer, ...)
-// surfaces to the caller with no silent fallback. The CSR bytes and any key
-// material are never logged.
-export const issueOperatorCredential = async (
- commonName: string,
- level: string,
- csrDer: Uint8Array,
-): Promise => {
- if (!isLevel(level)) {
- throw new Error(`Unknown access level "${level}". Choose viewer, operator, or admin.`);
+// listCredentialRequests returns requests in a state ("pending" by default;
+// "" for all).
+export const listCredentialRequests = async (
+ state = "pending",
+): Promise => {
+ if (fleetMode() === "mock") {
+ return mockRequests.filter((r) => state === "" || r.state === state).map((r) => ({ ...r }));
+ }
+ const response = await fleetClient().listOperatorCredentialRequests({ state });
+ return response.items.map((item) => toRequestRow(item));
+};
+
+// createCredentialRequest files a pending request from a CSR. Only the CSR
+// crosses the wire; any key stays with whoever made it.
+export const createCredentialRequest = async (params: {
+ csrDer: Uint8Array;
+ email: string;
+ fullName: string;
+ level: string;
+}): Promise => {
+ const { csrDer, level } = params;
+ const email = params.email.trim().toLowerCase();
+ const fullName = params.fullName.trim();
+ if (!isOperatorLevel(level)) {
+ throw new Error(`Unknown access level "${level}". Choose viewer, operator or admin.`);
}
if (csrDer.length === 0) {
- throw new Error("A generated CSR is required to issue an operator credential.");
+ throw new Error("A CSR is required to request an operator credential.");
}
if (fleetMode() === "mock") {
- const serialHex = mockSerial();
- mockRows = [
- ...mockRows,
+ const id = crypto.randomUUID();
+ const csrPem = csrToPem(csrDer);
+ const expiresAt = plusDays(30);
+ mockRequests = [
{
- commonName,
+ completedSerial: "",
+ createdAt: plusDays(0),
+ createdByCn: "operator@acme.example",
+ csrPem,
+ email,
+ expiresAt,
+ fullName,
+ id,
level,
- notAfter: "2027-01-01T00:00:00Z",
- revoked: false,
- serialHex,
+ state: "pending",
},
+ ...mockRequests,
];
- // A short opaque byte string stands in for the signed certificate; the
- // mock flow never needs a parseable cert, only non-empty bytes to pack.
- return { certDer: new TextEncoder().encode(`mock-operator-cert-${serialHex}`), serialHex };
+ return {
+ csrPem,
+ expiresAt,
+ extfileSection: levelExtfileSection(level),
+ opensslCommand: levelOpensslCommand(level, email),
+ requestId: id,
+ };
}
- const response = await fleetClient().issueOperatorCredential({ commonName, csrDer, level });
- return { certDer: response.certDer, serialHex: response.serialHex };
+ const response = await fleetClient().createOperatorCredentialRequest({
+ csrDer,
+ email,
+ fullName,
+ level,
+ });
+ return {
+ csrPem: response.csrPem,
+ expiresAt: response.expiresAt,
+ extfileSection: response.extfileSection,
+ opensslCommand: response.opensslCommand,
+ requestId: response.requestId,
+ };
};
-// revokeOperatorCredential revokes a credential by serial with an RFC 5280
-// reason code. `mock` marks the row revoked; live routes to the operator-CA
-// node through the manager and surfaces errors to the caller.
-export const revokeOperatorCredential = async (
- serialHex: string,
- reasonCode: number,
-): Promise => {
+export const cancelCredentialRequest = async (requestId: string): Promise => {
if (fleetMode() === "mock") {
- mockRows = mockRows.map((r) => (r.serialHex === serialHex ? { ...r, revoked: true } : r));
+ mockRequests = mockRequests.map((r) =>
+ r.id === requestId ? { ...r, csrPem: "", state: "cancelled" } : r,
+ );
return;
}
- await fleetClient().revokeOperatorCredential({ reasonCode, serialHex });
+ await fleetClient().cancelOperatorCredentialRequest({ requestId });
};
-const mockSerial = (): string => {
- const bytes = crypto.getRandomValues(new Uint8Array(6));
- return Array.from(bytes)
- .map((b) => b.toString(16).padStart(2, "0").toUpperCase())
- .join(":");
+// recordOperatorCredential records a certificate the operator CA signed, with
+// the request it answers or, without one, as an out-of-band import.
+export const recordOperatorCredential = async (params: {
+ certDer: Uint8Array;
+ fullName?: string;
+ requestId?: string;
+}): Promise => {
+ const requestId = params.requestId ?? "";
+ const fullName = params.fullName?.trim() ?? "";
+
+ if (fleetMode() === "mock") {
+ const request = mockRequests.find((r) => r.id === requestId);
+ const serialHex = randomHex(8);
+ const cn = request?.email ?? mockCertCn(params.certDer);
+ const credential: OperatorCredentialRow = {
+ ...blankCredential,
+ commonName: cn || "recorded@acme.example",
+ email: cn || "recorded@acme.example",
+ fullName: request?.fullName ?? fullName,
+ issuerSha256: MOCK_ISSUER,
+ kind: request ? "requested" : "recorded",
+ level: request?.level ?? "operator",
+ notAfter: plusDays(365),
+ serialHex,
+ };
+ mockCredentials = [...mockCredentials, credential];
+ if (request) {
+ mockRequests = mockRequests.map((r) =>
+ r.id === requestId
+ ? { ...r, completedSerial: serialHex, csrPem: "", state: "completed" }
+ : r,
+ );
+ }
+ return { credential: { ...credential }, warnings: [] };
+ }
+
+ const response = await fleetClient().recordOperatorCredential({
+ certDer: params.certDer,
+ fullName,
+ requestId,
+ });
+ return { credential: toRow(response.credential ?? {}), warnings: [...response.warnings] };
+};
+
+// denyOperatorCredential puts a credential on the Fleet Manager denylist. It
+// never revokes at the CA; the result carries the reminder to do that too.
+export const denyOperatorCredential = async (params: {
+ issuerSha256?: string;
+ note: string;
+ reasonCode: number;
+ serialHex: string;
+}): Promise => {
+ const issuerSha256 = params.issuerSha256 ?? "";
+ if (fleetMode() === "mock") {
+ mockCredentials = mockCredentials.map((r) =>
+ r.serialHex === params.serialHex ? { ...r, denylisted: true, revoked: true } : r,
+ );
+ return {
+ issuerSha256: issuerSha256 || MOCK_ISSUER,
+ revokedAt: plusDays(0),
+ serialHex: params.serialHex,
+ warnings: [CA_REVOKE_REMINDER],
+ };
+ }
+ const response = await fleetClient().revokeOperatorCredential({
+ issuerSha256,
+ note: params.note,
+ reasonCode: params.reasonCode,
+ serialHex: params.serialHex,
+ });
+ return {
+ issuerSha256: response.issuerSha256,
+ revokedAt: response.revokedAt,
+ serialHex: response.serialHex,
+ warnings: [...response.warnings],
+ };
};
-// Test-only: restore the seeded mock set between tests.
+// Test-only: restore the seeded mock sets between tests.
export const __resetOperators = (): void => {
- mockRows = [
- {
- commonName: "operator@acme.example",
- level: "admin",
- notAfter: "2027-01-01T00:00:00Z",
- revoked: false,
- serialHex: "3A:7F:0C:91:D2:44:8B:1E",
- },
- {
- commonName: "auditor@acme.example",
- level: "viewer",
- notAfter: "2026-11-15T00:00:00Z",
- revoked: false,
- serialHex: "11:22:33:44:55:66",
- },
- {
- commonName: "former-op@acme.example",
- level: "operator",
- notAfter: "2026-09-01T00:00:00Z",
- revoked: true,
- serialHex: "DE:AD:BE:EF:00:01",
- },
- ];
+ mockCredentials = seedCredentials();
+ mockRequests = seedRequests();
};
diff --git a/src/lib/wire-fields.test.ts b/src/lib/wire-fields.test.ts
index ffe658b..ce39965 100644
--- a/src/lib/wire-fields.test.ts
+++ b/src/lib/wire-fields.test.ts
@@ -21,7 +21,10 @@ import {
ApprovalSchema,
AuditEventSchema as FleetAuditEventSchema,
McpKeySchema,
+ OperatorCredentialRequestSchema,
+ OperatorCredentialSchema,
} from "@/gen/fleet/cryptos/fleet/v1/fleet_pb";
+import { OperatorCASchema } from "@/gen/fleet/cryptos/fleet/v1/operator_ca_pb";
import { AuditEventSchema } from "@/gen/fleet/cryptos/v1/audit_pb";
import { MachineConfigSchema } from "@/gen/fleet/cryptos/v1/config_pb";
import { NodeStatusSchema } from "@/gen/fleet/cryptos/v1/status_pb";
@@ -142,4 +145,70 @@ describe("generated stubs keep every field the node sends", () => {
};
expect(roundTrip(NodeStatusSchema, wire)).toEqual(wire);
});
+
+ it("keeps every operator credential field", () => {
+ const wire = {
+ commonName: "alice@example.org",
+ crlRevoked: true,
+ denylisted: true,
+ email: "alice@example.org",
+ firstSeenAt: "2026-09-01T00:00:00Z",
+ fullName: "Alice Example",
+ issuerSha256: "ab".repeat(32),
+ kind: "observed",
+ lastSeenAt: "2026-09-02T00:00:00Z",
+ level: "operator",
+ notAfter: "2027-09-01T00:00:00Z",
+ revoked: true,
+ serialHex: "0A:0B",
+ };
+ expect(roundTrip(OperatorCredentialSchema, wire)).toEqual(wire);
+ });
+
+ it("keeps every credential request field", () => {
+ const wire = {
+ completedSerial: "0A",
+ createdAt: "2026-09-01T00:00:00Z",
+ createdByCn: "admin@example.org",
+ csrPem: "-----BEGIN CERTIFICATE REQUEST-----",
+ email: "bob@example.org",
+ expiresAt: "2026-10-01T00:00:00Z",
+ fullName: "Bob Example",
+ id: "req-1",
+ level: "viewer",
+ state: "completed",
+ };
+ expect(roundTrip(OperatorCredentialRequestSchema, wire)).toEqual(wire);
+ });
+
+ it("keeps every operator CA field", () => {
+ const wire = {
+ acknowledgements: ["OPERATOR_CA_ACKNOWLEDGEMENT_NO_CRL"],
+ crl: {
+ crlNumber: "4096",
+ fetchedAt: "2026-09-01T00:00:00Z",
+ lastError: "timeout",
+ nextUpdate: "2026-09-08T00:00:00Z",
+ revokedCount: "3",
+ stale: true,
+ thisUpdate: "2026-09-01T00:00:00Z",
+ },
+ crlLocation: "http://pki.example.org/operator.crl",
+ crlSource: "CRL_SOURCE_URL",
+ issuer: "CN=Example Operator CA",
+ managedByConfig: true,
+ notAfter: "2036-09-01T00:00:00Z",
+ ocspLastError: "unreachable",
+ ocspMode: "OCSP_MODE_URL",
+ ocspUrl: "http://ocsp.example.org/",
+ registeredAt: "2026-09-01T00:00:00Z",
+ retiredAt: "2026-09-10T00:00:00Z",
+ retiredReason: "retired",
+ sha256: "cd".repeat(32),
+ state: "OPERATOR_CA_STATE_RETIRED",
+ subject: "CN=Example Operator CA",
+ warnings: ["issued by a fleet node's CA"],
+ };
+ expect(roundTrip(OperatorCASchema, wire)).toEqual(wire);
+ });
});
diff --git a/src/pages/credential-request.test.tsx b/src/pages/credential-request.test.tsx
new file mode 100644
index 0000000..7891bab
--- /dev/null
+++ b/src/pages/credential-request.test.tsx
@@ -0,0 +1,119 @@
+/*
+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 "reflect-metadata";
+import { X509CertificateGenerator } from "@peculiar/x509";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import { generateBackupPassphrase, keyBackupPem } from "@/lib/crypto/key-backup";
+import { CredentialRequestPage } from "@/pages/credential-request";
+
+const { downloadBytes, downloadText, fleetClient } = vi.hoisted(() => ({
+ downloadBytes: vi.fn(),
+ downloadText: vi.fn(),
+ fleetClient: vi.fn(),
+}));
+vi.mock("@/lib/download", async (importOriginal) => ({
+ ...(await importOriginal()),
+ downloadBytes: (...a: unknown[]) => downloadBytes(...a),
+ downloadText: (...a: unknown[]) => downloadText(...a),
+}));
+vi.mock("@/lib/fleet/client", () => ({ fleetClient }));
+
+const fill = (label: RegExp, value: string) =>
+ fireEvent.change(screen.getByLabelText(label), { target: { value } });
+
+const network = {
+ beacon: vi.fn(),
+ fetch: vi.fn(),
+ xhr: vi.fn(),
+};
+
+beforeEach(() => {
+ vi.clearAllMocks();
+ vi.stubGlobal("fetch", network.fetch);
+ vi.spyOn(XMLHttpRequest.prototype, "open").mockImplementation(network.xhr);
+ Object.defineProperty(navigator, "sendBeacon", { configurable: true, value: network.beacon });
+});
+afterEach(() => {
+ vi.restoreAllMocks();
+ vi.unstubAllGlobals();
+});
+
+const expectNoNetwork = () => {
+ expect(network.fetch).not.toHaveBeenCalled();
+ expect(network.xhr).not.toHaveBeenCalled();
+ expect(network.beacon).not.toHaveBeenCalled();
+ expect(fleetClient).not.toHaveBeenCalled();
+};
+
+describe("CredentialRequestPage", () => {
+ it("makes the key, backup and CSR entirely in the browser, with no network calls", async () => {
+ render();
+ fill(/full name/i, "Dana Example");
+ fill(/^email/i, "Dana@Example.org");
+ fill(/access level/i, "operator");
+ fireEvent.click(screen.getByRole("button", { name: /make my key/i }));
+
+ await screen.findByLabelText(
+ /key backup passphrase/i,
+ { selector: "code" },
+ { timeout: 10_000 },
+ );
+ expect(screen.queryByLabelText("CSR", { selector: "pre" })).not.toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: /download key backup/i }));
+ fireEvent.click(screen.getByLabelText(/saved the passphrase/i));
+ fireEvent.click(screen.getByRole("button", { name: /continue/i }));
+
+ const csr = await screen.findByLabelText("CSR", { selector: "pre" });
+ expect(csr.textContent).toMatch(/BEGIN CERTIFICATE REQUEST/);
+ expect(downloadText.mock.calls[0][0]).toBe("fleetos-operator-dana@example.org.key.pem");
+ expectNoNetwork();
+ }, 30_000);
+
+ it("builds the PKCS#12 from the key backup and the signed certificate, still offline", async () => {
+ const keys = (await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-384" }, true, [
+ "sign",
+ "verify",
+ ])) as CryptoKeyPair;
+ const passphrase = generateBackupPassphrase();
+ const backup = await keyBackupPem(keys.privateKey, passphrase);
+ const cert = await X509CertificateGenerator.createSelfSigned({
+ keys,
+ name: "CN=dana@example.org",
+ notAfter: new Date(Date.now() + 86_400_000),
+ notBefore: new Date(Date.now() - 60_000),
+ serialNumber: "0b",
+ signingAlgorithm: { hash: "SHA-384", name: "ECDSA" },
+ });
+
+ render();
+ fireEvent.change(screen.getByLabelText(/^your key backup file/i), {
+ target: { files: [new File([backup], "k.key.pem")] },
+ });
+ fill(/^your signed certificate/i, cert.toString("pem"));
+ fill(/^key backup passphrase/i, passphrase);
+ await waitFor(() =>
+ expect(screen.getByRole("button", { name: /build my pkcs#12/i })).toBeEnabled(),
+ );
+ fireEvent.click(screen.getByRole("button", { name: /build my pkcs#12/i }));
+
+ await waitFor(() => expect(downloadBytes).toHaveBeenCalled(), { timeout: 15_000 });
+ expect(downloadBytes.mock.calls[0][0]).toBe("dana@example.org.p12");
+ expectNoNetwork();
+ }, 30_000);
+});
diff --git a/src/pages/credential-request.tsx b/src/pages/credential-request.tsx
new file mode 100644
index 0000000..c03f6ae
--- /dev/null
+++ b/src/pages/credential-request.tsx
@@ -0,0 +1,260 @@
+/*
+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 "reflect-metadata";
+import { X509Certificate } from "@peculiar/x509";
+import { useState } from "react";
+
+import { CopyBlock } from "@/components/copy-block";
+import { KeyBackupStep } from "@/components/key-backup-step";
+import { Wordmark } from "@/components/layout/wordmark";
+import { Button } from "@/components/ui/button";
+import { buildCredentialPkcs12, readCertificateDer } from "@/lib/crypto/key-backup";
+import { downloadBytes, readFileBytes } from "@/lib/download";
+import { fieldClass, labelClass, looksLikeEmail } from "@/lib/form";
+import {
+ credentialFileBase,
+ csrToPem,
+ OPERATOR_LEVEL_OID,
+ OPERATOR_LEVELS,
+ type OperatorLevel,
+} from "@/lib/operators";
+
+// The anonymous "Make a credential request" page. A future operator makes
+// their own key, encrypted key backup and CSR here, sends the CSR to a Fleet
+// Manager admin, and later builds their PKCS#12 from the backup and the
+// signed certificate. Everything happens in this browser: the page makes no
+// network calls at all, so it doesn't import the Fleet Manager client.
+
+const certIdentity = (certDer: Uint8Array): { email: string; level: string } => {
+ const cert = new X509Certificate(new Uint8Array(certDer));
+ const email = cert.subjectName.getField("CN")[0] ?? "operator";
+ const ext = cert.getExtension(OPERATOR_LEVEL_OID);
+ const value = ext ? new Uint8Array(ext.value) : new Uint8Array();
+ const level = value[0] === 0x13 ? new TextDecoder().decode(value.subarray(2)) : "operator";
+ return { email, level };
+};
+
+const MakeRequest = () => {
+ const [fullName, setFullName] = useState("");
+ const [email, setEmail] = useState("");
+ const [level, setLevel] = useState("operator");
+ const [making, setMaking] = useState(false);
+ const [csrPem, setCsrPem] = useState(null);
+
+ const cleanEmail = email.trim().toLowerCase();
+
+ if (csrPem) {
+ return (
+
+
+
+ Send this CSR, and nothing else, to a Fleet Manager admin with your name ({fullName}) and
+ the level you need ({level}). Keep the key backup and its passphrase to yourself. When the
+ signed certificate comes back, build your PKCS#12 below.
+
+ Make your own Fleet Manager operator key and certificate signing request in this browser.
+ Nothing on this page is sent anywhere: the key, its backup and the passphrase stay on your
+ machine, and only the CSR is for your admin.
+
+
+
+
1. Make your key and CSR
+
+
+
+
2. Build your PKCS#12
+
+ When your admin sends back the signed certificate, combine it with your key backup here.
+
- Listing, issuing and revoking operator credentials need an operator-CA node
- designated with operator_ca_node in the
- manager's configuration. Until one is set the manager also enforces no
- operator-certificate revocation, so a revoked credential keeps working until it
- expires.
-
- {/* "No operator credentials" read as "this fleet has no
- operators" while the reader was signed in as one (#85). The
- manager lists only what it issued itself, so a credential
- minted outside it is invisible here and cannot be
- enumerated. */}
-
-
- The manager has not issued any operator credentials.
-
-
- This lists credentials the manager issued. One minted outside it ā with OpenSSL
- against the operator CA, which is the documented path for a fleet with no spare
- node ā cannot be listed or revoked here, because the manager has no record of
- it.
-
- {signedInAs}
-
- Issuing and revoking from here need an operator-CA node designated with{" "}
- operator_ca_node. Without one the manager
- also enforces no operator-certificate revocation, so a revoked credential keeps
- working until it expires.
-