Skip to main content
Version: 0.12

Debug the protocol without the agent

Goal

Exercise the CertOps agent protocol (register, heartbeat, claim, verify, result) against your control plane, or check a single signed job envelope, without deploying the Node.js agent. Two Node-free reference clients speak the same wire protocol against the same control-plane endpoints as the real agent: a Bash client (tokentimer-protocol.sh) and a PowerShell client (tokentimer-protocol.ps1).

Which tool do you want?

This page covers a debugging and verification tool, not a production agent: it does not claim, deploy, or renew certificates on its own. If you want an always-on agent that does that work for you, install the native agent instead. If you already run your own renewal scripts and just want to report their results to TokenTimer, see Connect an external executor.

Diagnostic identity only, never a real agent

register (and the register phase of --step all) always bootstraps a diagnostic agent identity, scoped to a single protocol_smoke verification job the control plane generates for it. A diagnostic identity can never claim, hold a lease on, or report a result for real certificate work, no matter what a job payload claims - that boundary is enforced server-side, not by the client. See Diagnostic registration below.

Getting the scripts

Both clients ship as source, not inside the agent release tarball: get them from the same tokentimer-core checkout or release you used to install the agent, at packages/agent/reference/.

CORE_VERSION="<version>" # e.g. 0.11.1 - match your deployment's supported agent version
curl -fsSL -O "https://raw.githubusercontent.com/tokentimerch/tokentimer-core/v${CORE_VERSION}/packages/agent/reference/tokentimer-protocol.sh"
curl -fsSL -O "https://raw.githubusercontent.com/tokentimerch/tokentimer-core/v${CORE_VERSION}/packages/agent/reference/tokentimer-protocol.ps1"
chmod +x tokentimer-protocol.sh

The Go verifier PowerShell needs (verifier/) isn't a single file, so grab the whole reference/ directory instead, either by browsing https://github.com/tokentimerch/tokentimer-core/tree/v<CORE_VERSION>/packages/agent/reference in your browser (swap in the same CORE_VERSION), or with a shallow, path-limited clone:

git clone --depth 1 --branch "v${CORE_VERSION}" --filter=blob:none --sparse \
https://github.com/tokentimerch/tokentimer-core.git tokentimer-core-reference
cd tokentimer-core-reference
git sparse-checkout set packages/agent/reference
cd packages/agent/reference

Why two clients

Both clients exist to give you a way to interact with the protocol that does not depend on a Node.js runtime, and to exercise two independent Ed25519 signature-verification implementations against the same signed job envelopes (OpenSSL for Bash, Go's standard library for PowerShell), so a bug specific to one implementation cannot silently pass both. That matters for self-hosted operators in particular: this is the tool to reach for when you need to confirm whether a delivery problem is your control plane, your network, or the agent, without spinning up a full agent install to find out.

ClientRequiresNever uses
tokentimer-protocol.shBash, curl, jq, OpenSSL 3, mkdir, rmNode.js, sed, awk, stat, mktemp, cat, tr, hostname
tokentimer-protocol.ps1PowerShell 5.1+ or pwsh, the bundled tokentimer-verify.exeNode.js, OpenSSL, any other Ed25519 tool

Each client is restricted to exactly the tools listed above, and that restriction is enforced by core's own CI, not just by review. The Bash client's mkdir/rm usage is scoped to one private, exclusively-created (mkdir -m 700) per-run directory it stages a verified payload in before handing it to OpenSSL, since openssl pkeyutl -verify -rawin refuses a pipe or process substitution; the directory is removed on every exit path, including signals, so another user on a shared host can never plant a file there ahead of time.

Build the PowerShell verifier once before first use

tokentimer-protocol.ps1 verifies signatures exclusively through a small bundled Go binary, tokentimer-verify.exe, never through OpenSSL: the two clients are required to exercise two independent Ed25519 implementations. If your copy doesn't already include a built verifier/dist/tokentimer-verify.exe, build it once (this needs Go and Node as build-time tools only; neither is a runtime dependency of the finished binary):

cd packages/agent/reference/verifier
node build.cjs

The build itself is unsigned. An unsigned build should never ship inside a production bundle; Authenticode signing of the release artifact is a separate, tracked step.

Credential handling

Both clients read a credential (or, for register's diagnostic-bootstrap request, a session cookie and CSRF token) exactly once, use it for the one request that needs it, and never put it somewhere another process on the same host could read it:

  • Never as a command-line argument or an Authorization: header passed via -H/argv - both leak through the process list (/proc/<pid>/cmdline on Linux, the command-line column in Windows process-listing tools).
  • The Bash client sends the bearer token, session cookie, and CSRF token to curl on standard input (curl --config -), never -H.
  • The PowerShell client sets it directly on the request object's header collection, never inside a command line.
  • Neither client ever writes a credential to a temporary file, and --json/-Json output is built from an explicit field allowlist that never includes one.

Point either client at a credential with --credential-file/-CredentialFile, or set the TOKENTIMER_AGENT_CREDENTIAL environment variable and omit the flag.

Diagnostic registration

register (and the register phase of --step all) always bootstraps a new diagnostic agent identity through the session-authenticated POST /api/v1/workspaces/:id/certops/agents/diagnostic-bootstrap route on your control plane, never the normal bearer-token agent-registration endpoint a real agent uses. The normal endpoint assigns agent_kind = 'normal' server-side, which would let either reference client claim and hold a lease on genuine certificate work in your deployment; the diagnostic-bootstrap route always assigns agent_kind = 'diagnostic' and hands back a protocol_smoke-only job instead, so there is no path from running this tool to touching a real certificate.

Because this route is an operator action rather than a machine-credential call, it requires the same session cookie and CSRF token your browser session already carries, not a bootstrap token. Obtain these out of band from your own session against your deployment (for example, by driving POST /auth/login and then GET /api/csrf-token with the same cookie jar) and pass them with --session-cookie-file/-SessionCookieFile and --csrf-token-file/-CsrfTokenFile (or the TOKENTIMER_SESSION_COOKIE/TOKENTIMER_CSRF_TOKEN environment variables). Neither client automates the login step itself. --workspace-id/-WorkspaceId is required for register/all, since the route is scoped to one workspace by path segment, and the route assigns the agentId itself: there is no client-side candidate-id generation for registration any more.

Common flags

BashPowerShellMeaning
--step STEP-Step STEPall, register, heartbeat, claim, verify, or result
--live-LiveContact the real control plane. Every step except verify requires it; verify refuses it, since verification is fully offline
--json-JsonEmit one machine-readable JSON object on stdout
--server-url URL-ServerUrl URLYour control-plane origin, e.g. https://tokentimer.example.com. https:// is required unless --allow-insecure-local-http/-AllowInsecureLocalHttp is also given for a localhost/127.0.0.1 target
--agent-id ID-AgentId IDAgent id. Required for heartbeat/claim/result. Ignored for register/all: the diagnostic-bootstrap route always assigns the agentId server-side (see Diagnostic registration above)
--agent-version VERSION-AgentVersion VERSIONReported agentVersion string
--workspace-id UUID-WorkspaceId UUIDWorkspace id. Required for register/all, since the diagnostic-bootstrap route is scoped to one workspace by path segment
--session-cookie-file PATH-SessionCookieFile PATHFile holding your session Cookie header value (register, all). See Diagnostic registration above
--csrf-token-file PATH-CsrfTokenFile PATHFile holding the X-CSRF-Token header value paired with the session cookie above (register, all)
--request-id ID-RequestId IDIdempotency key for the diagnostic-bootstrap request (register, all). A fresh one is generated when omitted
--credential-file PATH-CredentialFile PATHFile holding the ttagent_... credential (heartbeat/claim/result)
--envelope-file PATH-EnvelopeFile PATHv2 envelope JSON to verify (verify step); stdin is read when omitted
--claim-state-file PATH-ClaimStateFile PATHFile where claim records the job it just verified, and where a later, separate result invocation reads that state from. Required for result unless run via --step all/-Step all
--pubkey PATH-Pubkey PATHPEM SubjectPublicKeyInfo of the pinned Ed25519 signing key
--signing-key-id ID-SigningKeyId IDPinned signing key id. When given, the signed payload's signingKeyId must equal it
--allow-insecure-local-http-AllowInsecureLocalHttpPermit http:// for a localhost/127.0.0.1 target only
-h, --help(comment-header usage)Show usage

PowerShell-only flags (no Bash equivalent, since Bash never touches Authenticode or FIPS policy):

FlagMeaning
-VerifierPath PATHPath to tokentimer-verify.exe. Defaults to verifier/dist/tokentimer-verify.exe next to the script
-PinnedSignerSubject SUBJECT / -PinnedSignerThumbprint THUMBPRINTExpected Authenticode signer identity for the defense-in-depth self-check
-SkipSelfCheckSkip the Authenticode self-check. The self-check is defense in depth only, never the security boundary (see Known limitations), and a failed or skipped self-check does not by itself block a step

Steps

  • all: register, heartbeat, claim, verify, and report a result for one protocol_smoke job, in one run. Requires --live/-Live.
  • register: bootstrap a new diagnostic agent identity via the diagnostic-bootstrap route. Requires --live/-Live, --workspace-id/-WorkspaceId, a session cookie, and a CSRF token (see Diagnostic registration above).
  • heartbeat: send a heartbeat for an already-registered agent. Requires --live/-Live, --agent-id/-AgentId, and a credential. Both clients declare declaredCapabilities: ["signed-payload-b64-v1", "agent-id-binding-v1"] on every heartbeat: the first earns the v2 signed envelope this client verifies, and the second asserts that its identity gate fails closed on an absent agentId, not only a mismatched one.
  • claim: poll for jobs and verify any that come back. Requires --live/-Live, --agent-id/-AgentId, --pubkey/-Pubkey, and a credential. A diagnostic identity only ever receives protocol_smoke jobs, since it declares supportedActions: ["protocol_smoke"] on every claim request.
  • verify: verify one v2 envelope against a pinned public key. Runs fully offline; --live/-Live is neither required nor accepted.
  • result: report a result for a job already claimed and verified, either earlier in the same run (--step all) or by a prior, separate --step claim invocation. In the latter case --claim-state-file/-ClaimStateFile (the file claim wrote) is required, so a result call can only ever report a job this process actually verified. Requires --live/-Live, --agent-id/-AgentId, and a credential.

Verifying one signed envelope offline

Either client checks a single signed job envelope with no network access and no credential, using only a pinned public key, useful when you have a job payload from a log or a support case and want to confirm its signature yourself:

tokentimer-protocol.sh --step verify --pubkey pinned-key.pem --envelope-file job.json
tokentimer-protocol.ps1 -Step verify -Pubkey pinned-key.pem -EnvelopeFile job.json

Add --signing-key-id/-SigningKeyId to also pin the expected signing key id, and --json/-Json for a single machine-readable summary instead of a human-readable line on stderr.

Exit codes

CodeMeaning
0Success
1Signature verification failed (bad signature, wrong key, tampered payload, or an unrecognized envelope version)
2Usage error: bad flags, a missing required argument, or --live/-Live omitted or given for the wrong step
3Network or HTTP error talking to the control plane
4Local pre-gate failure: malformed base64/JSON, a field that fails structural validation, a workspace/agent identity mismatch, or any other rejection that must happen before a result can be reported
5A size-bounded field (the claim response body, the encoded or decoded payload) exceeded its declared limit

A signature-verdict failure (exit 1) or a pre-gate failure (exit 4) never produces a result report, a lease renewal, or any other post-verdict request: both clients stop at the failing step.

Known limitations

  • Bash's UTF-8 strictness for a verified payload is a leading byte-order-mark check, not a full byte-level UTF-8 validator, since jq's own UTF-8 handling is lossy and a general-purpose validator has the same NUL-byte constraints bash's bounded-read helper cannot represent. PowerShell's decoder is the full strict check (.NET's UTF8Encoding with throwOnInvalidBytes:$true), operating on the raw byte array before any shell-variable round trip.
  • The Authenticode self-check in tokentimer-protocol.ps1 is defense in depth only, run for operator visibility. It is not, and cannot be, the actual security boundary for a script interpreted by PowerShell; that boundary is whatever trusted launcher or application-control policy invokes the script in the first place. See Install an agent (Windows) - A note on trusting the PowerShell scripts for how this fits into the agent's own Windows trust model.
  • FIPS-only hosts must fail with an explicit unsupported-algorithm error rather than silently computing Ed25519 anyway; if you operate in a strict FIPS environment, treat that as a hard blocker on this tool, not a warning to work around.