Skip to content

fix(cli): hash the approval token the relay stored, not the spelling - #6878

Open
Chessing234 wants to merge 1 commit into
block:mainfrom
Chessing234:cli-approval-token-canonical
Open

fix(cli): hash the approval token the relay stored, not the spelling#6878
Chessing234 wants to merge 1 commit into
block:mainfrom
Chessing234:cli-approval-token-canonical

Conversation

@Chessing234

Copy link
Copy Markdown
Contributor

Found by reading cmd_approve_step against the relay's lookup; no issue filed.

The approval token is hashed as typed

validate_uuid(approval_token)?;
// The relay expects d-tag = hex(SHA256(token)), not the raw token UUID.
let token_hash = hex::encode(Sha256::digest(approval_token.as_bytes()));

The comment is right about the contract and the code hashes the wrong bytes. The token the relay stored came from generate_approval_token (crates/buzz-workflow/src/executor.rs:779):

fn generate_approval_token(_run_id: Uuid, _step_id: &str) -> String {
    Uuid::new_v4().to_string()
}

— canonical lowercase hyphenated — and handle_workflow_approval looks it up by the digest of that string (command_executor.rs:1036-1044, get_approval_by_stored_hash).

validate_uuid only checks that the argument parses, and Uuid::parse_str accepts uppercase, the unhyphenated 32-character form, braces, and a urn:uuid: prefix. Three of those four spellings hash to a digest the relay has never stored, so the lookup misses and the ingest is refused as:

invalid: approval not found

which is the same message a genuinely expired or already-answered approval produces. The operator holds a valid, pending token and is told it doesn't exist — so they go looking at the workflow run, not at their paste. Uppercase is not a contrived input here: it's what you get from a token copied out of a system that upper-cases identifiers, or typed from a screenshot.

The fix parses first and hashes the canonical form, in a small helper so the relay's half of the contract has somewhere to be pinned by a test.

Four workflow queries have the same gap

workflows list, workflows get, workflows runs and workflows update call validate_uuid and then put the raw argument into #h/#d. Every h/d tag in the tree is canonical and a NIP-01 generic tag filter compares byte for byte, so a non-canonical spelling matches nothing:

$ buzz workflows get --workflow 550E8400-E29B-41D4-A716-446655440000
null

workflows update is the worst of the four — its revision lookup misses, so it exits with workflow <id> not found for a workflow that exists. It already had let wf_uuid = parse_uuid(workflow_id)? on the line above and was still filtering on the string.

Tests

Three in approval_token_tests: every spelling validate_uuid accepts hashes to the same digest as the canonical one; the digest is pinned as hex(SHA256(canonical bytes)) so a change to either side of the contract is a red test rather than another "approval not found"; and a non-UUID token is a usage error.

Related

Same root cause as #6877 (channels/messages), and as #5966 / #5970 / #5984 for the hex-id half. Independent diffs — no shared symbols, no overlapping files. repos and projects still have the query half of this; separate PR.

Testing

  • cargo test -p buzz-cli --lib — 366 passed, 0 failed
  • cargo clippy -p buzz-cli --all-targets — clean
  • cargo fmt --all -- --check — clean

Not exercised against a live relay: the "approval not found" claim is read from get_approval_by_stored_hash and the IngestError::Rejected arm above it, not observed.

workflows approve computes hex(SHA256(approval_token.as_bytes())) over the
argument exactly as typed. The token the relay stored came from
generate_approval_token, i.e. Uuid::new_v4().to_string() — canonical
lowercase hyphenated — and the relay looks the approval up by the digest of
that string.

validate_uuid passes four spellings of the same UUID, and three of them
hash to a digest the relay has never seen. get_approval_by_stored_hash then
finds nothing and the ingest is refused as "approval not found", which
points whoever ran it at an expired or already-answered request rather than
at their own paste.

Parse first and hash the canonical form, in one helper so the relay's half
of the contract has somewhere to be pinned.

The same canonicalization is applied to the four workflow queries, which
filter #h/#d on the raw argument and so return an empty list — or, in
workflows update, "workflow not found" — for a workflow that exists.
workflows update already had the parsed value on the line above.

Signed-off-by: Taksh <takshkothari09@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant