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).
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.
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.
| Client | Requires | Never uses |
|---|---|---|
tokentimer-protocol.sh | Bash, curl, jq, OpenSSL 3, mkdir, rm | Node.js, sed, awk, stat, mktemp, cat, tr, hostname |
tokentimer-protocol.ps1 | PowerShell 5.1+ or pwsh, the bundled tokentimer-verify.exe | Node.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.
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>/cmdlineon 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/-Jsonoutput 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
| Bash | PowerShell | Meaning |
|---|---|---|
--step STEP | -Step STEP | all, register, heartbeat, claim, verify, or result |
--live | -Live | Contact the real control plane. Every step except verify requires it; verify refuses it, since verification is fully offline |
--json | -Json | Emit one machine-readable JSON object on stdout |
--server-url URL | -ServerUrl URL | Your 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 ID | Agent 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 VERSION | Reported agentVersion string |
--workspace-id UUID | -WorkspaceId UUID | Workspace id. Required for register/all, since the diagnostic-bootstrap route is scoped to one workspace by path segment |
--session-cookie-file PATH | -SessionCookieFile PATH | File holding your session Cookie header value (register, all). See Diagnostic registration above |
--csrf-token-file PATH | -CsrfTokenFile PATH | File holding the X-CSRF-Token header value paired with the session cookie above (register, all) |
--request-id ID | -RequestId ID | Idempotency key for the diagnostic-bootstrap request (register, all). A fresh one is generated when omitted |
--credential-file PATH | -CredentialFile PATH | File holding the ttagent_... credential (heartbeat/claim/result) |
--envelope-file PATH | -EnvelopeFile PATH | v2 envelope JSON to verify (verify step); stdin is read when omitted |
--claim-state-file PATH | -ClaimStateFile PATH | File 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 PATH | PEM SubjectPublicKeyInfo of the pinned Ed25519 signing key |
--signing-key-id ID | -SigningKeyId ID | Pinned signing key id. When given, the signed payload's signingKeyId must equal it |
--allow-insecure-local-http | -AllowInsecureLocalHttp | Permit 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):
| Flag | Meaning |
|---|---|
-VerifierPath PATH | Path to tokentimer-verify.exe. Defaults to verifier/dist/tokentimer-verify.exe next to the script |
-PinnedSignerSubject SUBJECT / -PinnedSignerThumbprint THUMBPRINT | Expected Authenticode signer identity for the defense-in-depth self-check |
-SkipSelfCheck | Skip 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 oneprotocol_smokejob, 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 declaredeclaredCapabilities: ["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 absentagentId, 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 receivesprotocol_smokejobs, since it declaressupportedActions: ["protocol_smoke"]on every claim request.verify: verify one v2 envelope against a pinned public key. Runs fully offline;--live/-Liveis 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 claiminvocation. In the latter case--claim-state-file/-ClaimStateFile(the fileclaimwrote) is required, so aresultcall 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
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Signature verification failed (bad signature, wrong key, tampered payload, or an unrecognized envelope version) |
| 2 | Usage error: bad flags, a missing required argument, or --live/-Live omitted or given for the wrong step |
| 3 | Network or HTTP error talking to the control plane |
| 4 | Local 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 |
| 5 | A 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
UTF8EncodingwiththrowOnInvalidBytes:$true), operating on the raw byte array before any shell-variable round trip. - The Authenticode self-check in
tokentimer-protocol.ps1is 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.
Related
- Install an agent (Linux) - the full production agent this tool is a lighter-weight alternative to.
- Install an agent (Windows) - the same ACL and trust model these clients build on, applied to the agent's Windows Service installer.
- Enable CertOps - control-plane setup these clients talk to.
- Reconciling interrupted jobs - what to do with a job the protocol layer flags as needing attention.