Repository navigation
Conversation
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Summary
Adds ts keys list and ts keys verify, shared service-URL validation, local-fixture tests, and key-rotation guidance. No concrete correctness defects found across the nine changed files; the documented feature-spec requirement remains unmet.
Cross-cutting / body-level findings
-
🔧 Missing required feature spec — The new CLI commands at
crates/trusted-server-cli/src/commands/keys/mod.rs:19–25meetCONTRIBUTING.md's definition of a feature. It requires a linked design spec with at least two recorded review rounds. The PR instead cites issue #1431 as its spec; neither that issue nor the changed files supplies the required design/review log.Proposed resolution: add and link a reviewed spec covering:
# Signing-Key Inspection CLI ## Command and output contracts ## Adapter impact ## Configuration, consent, and privacy ## Failure handling ## Compatibility and rollout ## Testing ## Review log
Apply manually — this requires a new document, actual review rounds, and a PR-description update, not a contiguous suggestion.
CI Status
- integration tests: PASS
- browser integration tests: PASS
- trace runtime and browser acceptance: PASS
- integration tests (Fastly EC lifecycle): PASS
- cargo test (axum native): PASS
- CodeQL: PASS
- Analyze (actions): PASS
- CI Node pin guard: PASS
- cargo test: PASS (required)
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- Analyze (rust): PASS
- CLAUDE.md symlink guard: PASS
- cargo fmt: PASS (required)
- worker-deadlines (spin): PASS
- cargo test (cross-adapter parity): PASS
- ts dev lint domains (changed lines): PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- format-docs: PASS (required)
- HTML EOF runtime (Fastly): PASS
- vitest: PASS
- Analyze (javascript-typescript): PASS
- docs-proposal scripts: PASS
- worker-deadlines (cloudflare): PASS
- format-typescript: PASS (required)
- prepare integration artifacts: PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- Analyze (python): PASS
- Analyze (javascript-typescript): PASS
Local git diff --check: PASS. Reviewer worktree clean at 8df9a40010c4279b6e067306994029dfe7db5847. Builds and tests were not rerun locally because remote CI passed; no suggestions required scratch verification.
aram356
left a comment
There was a problem hiding this comment.
Summary
Adds ts keys list and ts keys verify on a shared url_guard::service_endpoint. Three issues block merge:
- Signatures that start with
-are rejected by clap. This is about one in 64 signatures, since they are base64url. - The 404 hint misdirects, while the status a broken signing store actually returns (500) gets no hint.
- Three behaviours the code promises have no test.
Both commands were exercised against the real Axum adapter seeded with Ed25519 keys. Every suggestion below passes cargo fmt, cargo clippy-cli and the full trusted-server-cli suite, both alone and applied together.
6 of the 8 inline comments below carry a one-click GitHub
suggestion— use Commit suggestion (or Add suggestion to batch for several at once) to apply them as commits on the PR branch. The remaining comments describe the fix in prose because the change touches multiple files or lines outside the diff and can't be auto-applied.
Blocking
🔧 wrench
--signatureand--payloadreject values that start with-— see inline atcrates/trusted-server-cli/src/commands/keys/mod.rs:55- The 404 hint points the wrong way; a 500 gets no hint — see inline at
crates/trusted-server-cli/src/commands/keys/list.rs:66 - Three behaviours the code promises have no test — see inline at
crates/trusted-server-cli/tests/keys_cli.rs:181
Non-blocking
🤔 thinking / ⛏ nitpick
- 🤔 The guide doesn't say what
ts keys verifyproves — see inline atdocs/guide/key-rotation.md:328 - 🤔 "internal verification error" reads like a bad signature — see inline at
crates/trusted-server-cli/src/commands/keys/verify.rs:42 - 🤔 Service-controlled text reaches the terminal unescaped — see inline at
crates/trusted-server-cli/src/commands/keys/list.rs:42 - ⛏ The HTTPS error claims to protect credentials
ts keysnever sends — see inline atcrates/trusted-server-cli/src/url_guard.rs:56 - ⛏ The "byte for byte" test trims the newline it checks — see inline at
crates/trusted-server-cli/tests/keys_cli.rs:84
Cross-cutting / body-level findings
-
♻️ Converge with #1500 before the second of the two PRs merges.
- Command and dispatch: both PRs add
commands/keys/mod.rs, with incompatible shapes. Here it isKeysCommand { List, Verify }withrun(command, out) -> CliResult<RunOutcome>; in #1500 it isKeysCommand { Rotate, Deactivate }withrun(command, confirm, out) -> CliResult<()>. - HTTP plumbing exists twice:
sendhere andadmin_client::post_admin_jsonthere each build a runtime and a client withREQUEST_TIMEOUTandPolicy::none(), then read the body. - URL checks exist twice:
url_guard::service_endpointand #1500'sadmin_endpointare the same check, except for the dead userinfo test this PR correctly drops. - Proposed end state: whichever PR lands second should leave one
KeysCommand(list, verify, rotate, deactivate) and onerunthat returnsRunOutcomeand takes the confirmer. It should also leave one transport with optional Basic auth, used byts cache purgetoo. TheKeyshelp line ("Inspect a deployed service's request-signing keys") should cover rotation as well.
- Command and dispatch: both PRs add
-
🏕 Other guides still teach curl for these two routes:
docs/guide/error-reference.md:475(curl … | jq '.jwks.keys[].kid')docs/guide/request-signing.md:118-180and:336docs/guide/api-reference.md:685and:742- In this guide: the "Testing" list (
key-rotation.md:620-622) and the "Signature Verification Fails After Rotation" and "Key Not in JWKS" troubleshooting entries (:649-672)
Each one can switch to
ts keys listorts keys verifywith a one-line edit. -
📝 Release notes and the CLI index don't mention
ts keys.CHANGELOG.md### Addedhas no entry for the two commands. The hand-maintained command index indocs/guide/cli.mdhas nots keys,ts keys listorts keys verifyrows; it already lacksts cacheandts origintoo. -
📝 On the open questions:
--payload-file/stdin:/verify-signaturecaps the whole JSON body at 4096 bytes, and a 5000-byte--payloadgot413 Payload Too Largefrom the Axum adapter. The payload is also a JSON string verified as its UTF-8 bytes. A file input therefore can't carry large or binary payloads. It would still help with quoting and with$(cat file)dropping trailing newlines, so it fits a follow-up rather than this PR.--jsonforverify: printing the service's answer verbatim, with the same exit codes, would mirrorlist --json. Also a follow-up.
-
📝 The earlier spec finding still applies:
CONTRIBUTING.md("What counts as a feature") lists a new CLI command as a feature that needs a linked, agent-reviewed spec.
CI Status
- integration tests: PASS
- browser integration tests: PASS
- trace runtime and browser acceptance: PASS
- integration tests (Fastly EC lifecycle): PASS
- cargo test (axum native): PASS
- CodeQL: PASS
- Analyze (actions): PASS
- CI Node pin guard: PASS
- cargo test: PASS (required)
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- Analyze (rust): PASS
- CLAUDE.md symlink guard: PASS
- cargo fmt: PASS (required)
- worker-deadlines (spin): PASS
- cargo test (cross-adapter parity): PASS
- ts dev lint domains (changed lines): PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- format-docs: PASS (required)
- HTML EOF runtime (Fastly): PASS
- vitest: PASS
- Analyze (javascript-typescript): PASS
- docs-proposal scripts: PASS
- worker-deadlines (cloudflare): PASS
- format-typescript: PASS (required)
- prepare integration artifacts: PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- Analyze (python): PASS
- Analyze (javascript-typescript): PASS
| #[arg(long)] | ||
| pub payload: String, | ||
|
|
||
| /// Base64-encoded signature to check. | ||
| #[arg(long)] |
There was a problem hiding this comment.
🔧 wrench — --signature and --payload reject values that start with -
Trusted Server signs with unpadded base64url (crates/trusted-server-core/src/request_signing/signing.rs:167), and - is in that alphabet, so about one signature in 64 starts with it. clap then reads the value as a flag. Against the Axum adapter seeded with a real key:
$ ts keys verify --service http://127.0.0.1:18980 --kid ts-2024-02-15 \
--payload example-payload --signature -LRTQNDx-hNzROGcDj8yJhE-CU3nejfN4NSaacyH0vp8JKNxY5USfb72JchC4jUuLOv7BEjDoXq-Whovxhm6Cg
error: unexpected argument '-L' found
That exits 2, so a rotation script that passes a captured signature fails at random. The only workaround is --signature=-…, which neither --help nor the guide mentions. With the change below, signatures starting with - verify against the same service.
| #[arg(long)] | |
| pub payload: String, | |
| /// Base64-encoded signature to check. | |
| #[arg(long)] | |
| #[arg(long, allow_hyphen_values = true)] | |
| pub payload: String, | |
| /// Base64-encoded signature to check. | |
| #[arg(long, allow_hyphen_values = true)] |
A regression test for the run.rs tests module (apply by hand, since it's outside this diff). Without the fix it fails at the parse step:
#[test]
fn keys_verify_accepts_values_that_start_with_a_hyphen() {
// Trusted Server signs with unpadded base64url, whose alphabet includes `-`, so
// about one signature in 64 starts with one.
let args = parse(&[
"ts",
"keys",
"verify",
"--service",
"https://edge.example.com",
"--kid",
"ts-2024-01-15",
"--payload",
"-example-payload",
"--signature",
"-LRTQNDx",
]);
let Command::Keys(crate::commands::keys::KeysCommand::Verify(verify)) = args.command else {
panic!("expected keys verify command");
};
assert_eq!(
verify.payload, "-example-payload",
"should keep the payload"
);
assert_eq!(verify.signature, "-LRTQNDx", "should keep the signature");
}| let hint = if status.as_u16() == 404 { | ||
| " — request signing may be disabled on this service" | ||
| } else { | ||
| "" | ||
| }; |
There was a problem hiding this comment.
🔧 wrench — The 404 hint points the wrong way, and the real "keys unreadable" status gets no hint
Every adapter registers GET /.well-known/trusted-server.json unconditionally (the route table in docs/guide/api-reference.md; crates/trusted-server-adapter-axum/tests/routes.rs says "5xx from missing signing keys is acceptable; 404 is not"). handle_trusted_server_discovery maps an unreadable jwks_store to TrustedServerError::Configuration, which answers 500. Against the real Axum adapter, and a plain host that answers 404:
$ ts keys list --service http://127.0.0.1:18990 # Axum, no signing stores
[ts] fetching the discovery document failed: 500 Internal Server Error
An internal error occurred
$ ts keys list --service http://127.0.0.1:18991 # not a Trusted Server
[ts] fetching the discovery document failed: 404 Not Found — request signing may be disabled on this service
An operator who mistyped --service is sent off to debug request signing, and one whose keys are actually unreadable gets no hint at all.
| let hint = if status.as_u16() == 404 { | |
| " — request signing may be disabled on this service" | |
| } else { | |
| "" | |
| }; | |
| // Every adapter registers the discovery route, so a 404 means `--service` names | |
| // something other than a Trusted Server, while unreadable signing stores answer 500. | |
| let hint = match status.as_u16() { | |
| 404 => " — check that --service names a Trusted Server deployment", | |
| 500 => " — the service could not read its signing keys; check its jwks_store", | |
| _ => "", | |
| }; |
To pin both hints, replace list_reports_a_failed_fetch_as_an_error in tests/keys_cli.rs (apply by hand; it fails without the change above):
#[test]
fn list_reports_a_failed_fetch_as_an_error() {
for (status, hint) in [(404, "names a Trusted Server"), (500, "jwks_store")] {
let server = FixtureServer::start(move |_request| {
FixtureResponse::html("error").with_status(status)
});
let mut out = Vec::new();
let error = run(KeysCommand::List(list_args(&server, false)), &mut out)
.expect_err("should fail on an error status");
assert!(
error.contains(&status.to_string()),
"should name the status: {error}"
);
assert!(
error.contains(hint),
"should point at the likely cause: {error}"
);
assert!(out.is_empty(), "should print no keys");
}
}| "should explain the transport requirement: {error}" | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
🔧 wrench — Three behaviours the code promises have no test
Each of these mutants passes every trusted-server-cli test at this head:
- M1: remove
.redirect(reqwest::redirect::Policy::none())fromsend(keys/mod.rs:97), the guarantee its doc comment spells out. - M2:
let mut output = if args.json { body } else { render_keys(&body)? };, which drops the "Validated even for --json" check (list.rs:73-75). - M3: drop
if !status.is_success()inrun_verify(verify.rs:65), so a413reports asinvalid verification responseinstead of naming the status.
tests/cache_purge.rs already has the redirect counterpart for purge. These three tests pass at this head and fail under M1, M2 and M3 respectively:
| } | |
| } | |
| #[test] | |
| fn list_does_not_follow_a_redirect() { | |
| let server = FixtureServer::start(|request| { | |
| if request.path == "/.well-known/trusted-server.json" { | |
| FixtureResponse::html("") | |
| .with_status(302) | |
| .with_header("location", "/moved.json") | |
| } else { | |
| json_response(discovery_body()) | |
| } | |
| }); | |
| let mut out = Vec::new(); | |
| let error = run(KeysCommand::List(list_args(&server, false)), &mut out) | |
| .expect_err("should refuse a redirected discovery document"); | |
| assert!(error.contains("302"), "should name the redirect: {error}"); | |
| assert!(out.is_empty(), "should print no keys"); | |
| assert_eq!(server.request_count(), 1, "should not follow the redirect"); | |
| } | |
| #[test] | |
| fn list_json_rejects_a_page_that_is_not_a_discovery_document() { | |
| let server = FixtureServer::start(|_request| FixtureResponse::html("<html>login</html>")); | |
| let mut out = Vec::new(); | |
| run(KeysCommand::List(list_args(&server, true)), &mut out) | |
| .expect_err("should validate the document even with --json"); | |
| assert!(out.is_empty(), "should not print the page as keys"); | |
| } | |
| #[test] | |
| fn verify_reports_a_rejected_request_as_an_error() { | |
| let server = FixtureServer::start(|_request| { | |
| FixtureResponse::html("payload too large").with_status(413) | |
| }); | |
| let mut out = Vec::new(); | |
| let error = | |
| run(KeysCommand::Verify(verify_args(&server)), &mut out).expect_err("should fail on a 413"); | |
| assert!(error.contains("413"), "should name the status: {error}"); | |
| assert!(out.is_empty(), "should print no verification result"); | |
| } |
| Pass `--json` to print the discovery document exactly as the service returned it. To | ||
| check a signature end to end against the new key, use `ts keys verify`, which exits | ||
| non-zero when the signature does not verify: | ||
|
|
||
| ```bash | ||
| ts keys verify --service https://edge.example.com \ | ||
| --kid ts-2024-02-15 --payload '<payload>' --signature '<signature>' | ||
| ``` | ||
|
|
||
| Both commands require HTTPS, with plain HTTP allowed only for loopback development | ||
| services. |
There was a problem hiding this comment.
🤔 thinking — The guide doesn't say what ts keys verify proves
/verify-signature looks up the JWK stored under the kid (signing.rs:198-203) and never checks active-kids. Deactivating without --delete leaves that JWK in the store (rotation.rs:247-264). Against the Axum adapter with active-kids = ts-2024-02-15 and a JWK still stored for ts-2024-01-15:
$ ts keys list --service http://127.0.0.1:18980
ts-2024-02-15 OKP Ed25519
$ ts keys verify --service http://127.0.0.1:18980 --kid ts-2024-01-15 --payload example-payload --signature 8MhiV1yY…
verified: signature is valid for kid ts-2024-01-15
A passing verify therefore doesn't mean partners, who fetch the published JWKS, can verify. The PR description's "against a published kid" overstates it. The guide also says only "exits non-zero", while a script can tell a signature that didn't verify (1) from a request that failed (2).
| Pass `--json` to print the discovery document exactly as the service returned it. To | |
| check a signature end to end against the new key, use `ts keys verify`, which exits | |
| non-zero when the signature does not verify: | |
| ```bash | |
| ts keys verify --service https://edge.example.com \ | |
| --kid ts-2024-02-15 --payload '<payload>' --signature '<signature>' | |
| ``` | |
| Both commands require HTTPS, with plain HTTP allowed only for loopback development | |
| services. | |
| Pass `--json` to print the discovery document exactly as the service returned it. To | |
| check a signature end to end against the new key, use `ts keys verify`: | |
| ```bash | |
| ts keys verify --service https://edge.example.com \ | |
| --kid ts-2024-02-15 --payload '<payload>' --signature '<signature>' | |
| ``` | |
| It exits `0` when the service reports that the signature verified, `1` when the service | |
| reports that it did not (an unknown kid reports this way too), and `2` when the check | |
| could not be made, for example because the service was unreachable or answered with an | |
| error status. The service checks the key stored under the kid whether or not that kid is | |
| still active, so a deactivated key that was kept in storage still verifies. Use | |
| `ts keys list` to see which keys partners can fetch. | |
| Both commands require HTTPS, with plain HTTP allowed only for loopback development | |
| services. |
| let detail = response | ||
| .error | ||
| .map(|error| format!(" ({error})")) | ||
| .unwrap_or_default(); |
There was a problem hiding this comment.
🤔 thinking — "internal verification error" reads like a bad signature
The service folds an unknown kid, a malformed signature, and an unreadable signing store into one sanitized error value (endpoints.rs:143-151, documented under POST /verify-signature in api-reference.md). Against the Axum adapter, a kid that doesn't exist prints:
not verified: Verification error for kid ts-2099-01-01 (internal verification error)
That gives the operator nothing to act on. With the change below the same call prints the following, and still exits 1:
not verified: Verification error for kid ts-2099-01-01 (the service could not check it: the kid may not exist, the signature may not be 64 bytes of base64, or the service could not read its keys)
| let detail = response | |
| .error | |
| .map(|error| format!(" ({error})")) | |
| .unwrap_or_default(); | |
| // The service folds an unknown kid, a malformed signature, and an unreadable signing | |
| // store into this one sanitized value, so name the possibilities instead of echoing it. | |
| let detail = match response.error.as_deref() { | |
| Some("internal verification error") => " (the service could not check it: the kid \ | |
| may not exist, the signature may not be 64 bytes of base64, or the service could \ | |
| not read its keys)" | |
| .to_owned(), | |
| Some(error) => format!(" ({error})"), | |
| None => String::new(), | |
| }; |
| writeln!( | ||
| rendered, | ||
| "{}\t{}\t{}", | ||
| key.kid.as_deref().unwrap_or("<no kid>"), |
There was a problem hiding this comment.
🤔 thinking — Service-controlled text reaches the terminal unescaped
ts keys list needs no credentials, so pointing it at someone else's service is a natural use, and kid, kty and crv come straight from that service's document. Against the Axum adapter serving a JWK whose kid holds ESC[2K ESC[1G, a tab, and a fake column (shown with cat -v):
ts-2024-02-15 OKP Ed25519
^[[2K^[[1Gts-2024-09-09 forged OKP Ed25519
On a terminal the escape sequence erases the line, and the tab adds a column for anything that splits the output on tabs. The CLI already escapes page-controlled text with escape_terminal_text (ad_templates/output.rs:17-41, used in commands/audit/page.rs:66-69). Apply by hand, since the import, the call sites and the test are separate ranges:
diff --git a/crates/trusted-server-cli/src/commands/keys/list.rs b/crates/trusted-server-cli/src/commands/keys/list.rs
--- a/crates/trusted-server-cli/src/commands/keys/list.rs
+++ b/crates/trusted-server-cli/src/commands/keys/list.rs
@@ -4,6 +4,7 @@ use core::fmt::Write as _;
use serde::Deserialize;
+use crate::ad_templates::output::escape_terminal_text;
use crate::commands::keys::{KeysRequest, ListArgs, send};
use crate::error::{CliResult, cli_error};
@@ -39,9 +40,9 @@ fn render_keys(body: &str) -> CliResult<String> {
writeln!(
rendered,
"{}\t{}\t{}",
- key.kid.as_deref().unwrap_or("<no kid>"),
- key.kty.as_deref().unwrap_or("-"),
- key.crv.as_deref().unwrap_or("-"),
+ escape_terminal_text(key.kid.as_deref().unwrap_or("<no kid>")),
+ escape_terminal_text(key.kty.as_deref().unwrap_or("-")),
+ escape_terminal_text(key.crv.as_deref().unwrap_or("-")),
)
.expect("should write to a String");
}
@@ -116,6 +117,24 @@ mod tests {
);
}
+ #[test]
+ fn service_controlled_fields_cannot_forge_lines_or_columns() {
+ let body = json!({
+ "version": "1.0",
+ "jwks": { "keys": [
+ { "kty": "OKP", "crv": "Ed25519", "kid": "\u{1b}[2Kts-2024-09-09\tforged" },
+ ]}
+ })
+ .to_string();
+
+ let rendered = render_keys(&body).expect("should render a valid document");
+
+ assert_eq!(
+ rendered, "\\u{001B}[2Kts-2024-09-09\\u{0009}forged\tOKP\tEd25519\n",
+ "should escape control characters, tabs included, in service-controlled fields"
+ );
+ }
+
#[test]
fn a_body_that_is_not_a_discovery_document_is_an_error() {
assert!(render_keys("<html></html>").is_err(), "should reject HTML");interpret in verify.rs prints the service's kid, message and error the same way, so it should get the same wrapping.
| pub(crate) fn service_endpoint(service: &str, path: &str) -> CliResult<String> { | ||
| let mut url = | ||
| reqwest::Url::parse(service).map_err(|_| "--service must be an absolute HTTPS URL")?; | ||
| require_credential_safe_transport(&url, "--service")?; |
There was a problem hiding this comment.
⛏ nitpick — The HTTPS error claims to protect credentials that ts keys never sends
$ ts keys list --service http://edge.example.com
[ts] --service requires HTTPS to protect the credentials sent with the request; HTTP is allowed only for loopback development services
keys/mod.rs:3-4 says neither subcommand sends credentials. HTTPS still matters here, since it keeps a forged JWKS or a forged "verified": true from being injected in transit, so only the reason is wrong. The wording lives in the shared require_credential_safe_transport (url_guard.rs:29-34, outside this diff), so apply by hand:
diff --git a/crates/trusted-server-cli/src/url_guard.rs b/crates/trusted-server-cli/src/url_guard.rs
--- a/crates/trusted-server-cli/src/url_guard.rs
+++ b/crates/trusted-server-cli/src/url_guard.rs
@@ -1,4 +1,5 @@
-//! Transport checks shared by the commands that send an operator's credentials to a URL.
+//! Transport checks shared by the commands that send an operator's credentials to a URL
+//! or report what a service answered.
//!
//! One implementation rather than one per command: a second copy is a second place for the
//! loopback exemption to drift, and every command that gets this wrong sends a session
@@ -28,8 +29,8 @@ fn is_loopback(url: &reqwest::Url) -> bool {
pub(crate) fn require_credential_safe_transport(url: &reqwest::Url, flag: &str) -> CliResult<()> {
if url.scheme() != "https" && !(url.scheme() == "http" && is_loopback(url)) {
return cli_error(format!(
- "{flag} requires HTTPS to protect the credentials sent with the request; HTTP is \
- allowed only for loopback development services"
+ "{flag} requires HTTPS so the request and the service's answer cannot be read or \
+ altered in transit; HTTP is allowed only for loopback development services"
));
}
if !url.username().is_empty() || url.password().is_some() {The new wording stays accurate for ts origin probe-shareability --url and ts cache purge, and the existing tests only match HTTPS.
| assert_eq!( | ||
| rendered.trim_end_matches('\n'), | ||
| body, | ||
| "should print the service's document byte for byte" | ||
| ); |
There was a problem hiding this comment.
⛏ nitpick — This "byte for byte" test trims the newline it says it checks
With trim_end_matches('\n'), the test still passes if the newline push in run_list (list.rs:76-78) is deleted. Asserting the exact output pins it:
| assert_eq!( | |
| rendered.trim_end_matches('\n'), | |
| body, | |
| "should print the service's document byte for byte" | |
| ); | |
| assert_eq!( | |
| rendered, | |
| format!("{body}\n"), | |
| "should print the service's document byte for byte, plus a final newline" | |
| ); |
Summary
ts keys list [--json]so operators can confirm which signing keys a service publishes in/.well-known/trusted-server.jsonwithout the admin password or hand-written curl.ts keys verifyto check a signature end to end against a published kid; exits non-zero when it does not verify, so it works in rotation scripts.url_guard::service_endpointfromts cache purgeso every--serviceflag shares the same base-URL validation (HTTPS-only for non-loopback, no credentials/path/query/fragment).Changes
crates/trusted-server-cli/src/url_guard.rsservice_endpointhelpercrates/trusted-server-cli/src/commands/cache/purge.rsservice_endpointinstead of local validationcrates/trusted-server-cli/src/commands/keys/mod.rskeyscommand group, discovery fetch helperscrates/trusted-server-cli/src/commands/keys/list.rsts keys list [--json]crates/trusted-server-cli/src/commands/keys/verify.rsts keys verifycrates/trusted-server-cli/src/commands/mod.rs,src/run.rskeyscrates/trusted-server-cli/tests/keys_cli.rsdocs/guide/key-rotation.mdts keys list/verifyfor confirming publicationCloses
Closes #1431
Overlap with #1430
This overlaps with the sibling PR for #1430 (
ts keys rotate/deactivate, branchadd-ts-keys-commands). Both addcrates/trusted-server-cli/src/commands/keys/mod.rs, editrun.rs, and extract shared URL validation, so whichever merges second needs a rebase. Once #1430 lands, the key-rotation guide's curl examples for rotate/deactivate should switch tots keys.Open questions
verifyaccept--payload-file/ stdin for large or binary payloads?verifysupport--jsonoutput?Spec
Spec: Scoped by issue #1431 (small CLI addition).
Test plan
cargo test-fastly && cargo test-fastly-reuse && cargo test-axum && cargo test-cloudflare && cargo test-spincargo fmt --all -- --checkcd crates/trusted-server-js/lib && npx vitest run(1838 passed / 1 skipped, Node 24.20.0)cd crates/trusted-server-js/lib && npm run formatcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute servecargo test -p trusted-server-cli --target <host>),template_build_digest, parityChecklist
unwrap()in production code — useexpect("should ...")logmacros (notprintln!)