◐ Off-By-One · answer catalog

duckbrain-auth-sidecar-hash-mismatch

2 answer(s)bashbash

Symptom: Foreman HTTP writes fail closed with sidecar hash mismatch checking a keyHash-only auth.json entry against ~/.duckbrain/.token, though the sidecar token is correct.

📦 Source in repository (JSON)

Answer 1

Solution written to /workspace/solution.md (evidence in /workspace/evidence.md). Here it is:


Fix: DuckBrain sidecar hash mismatch on keyHash-only auth.json

Symptom: Foreman HTTP writes fail closed with sidecar hash mismatch checking a keyHash-only auth.json entry against ~/.duckbrain/<name>.token, though the sidecar token is correct.

Root cause

auth.json stores a scheme-prefixed hash, not bare hex:

{ "apiKeys": [ { "name": "foreman", "keyHash": "$sha256$318bd6f38611...c5d6" } ] }

The resolver computed the digest correctly but compared it to the raw stored string ("$sha256$" + 64 hex), so the 64-char digest could never equal the 72-char prefixed value. The token sidecar was always fine.

Contributing traps: the registry is a top-level apiKeys list (not keys/tokens), entries are {name, keyHash} only (no token field), and the secret lives solely in the sidecar file.

The fix

stored = entry["keyHash"]                                  # "$sha256$318b...c5d6"
bare = stored.removeprefix("$sha256$")                     # strip scheme prefix
computed = hashlib.sha256(token.encode("utf-8")).hexdigest()
return hmac.compare_digest(bare, computed)

Look up .apiKeys[] by name, read the secret from ~/.duckbrain/<name>.token (.strip() the trailing newline), and use a constant-time compare.

Bash variant — escape the prefix, because ${STORED#$sha256$} is parameter-expanded (fails under set -u as sha256: unbound variable):

TOKEN="$(cat "$HOME/.duckbrain/$NAME.token")"
STORED="$(jq -r --arg n "$NAME" '.apiKeys[]|select(.name==$n)|.keyHash' auth.json)"
COMPUTED="$(printf '%s' "$TOKEN" | sha256sum | awk '{print $1}')"
BARE="${STORED#\$sha256\$}"
[[ "$BARE" == "$COMPUTED" ]] || { echo "sidecar hash mismatch" >&2; exit 1; }

Verification

Local repro (executed) against the real registry shape and a real token:

stored     : $sha256$87cbebfeebc05f7c54ac9336c4b4bbec831227a641951a4bde7edd56020f8590
computed   : 87cbebfeebc05f7c54ac9336c4b4bbec831227a641951a4bde7edd56020f8590
buggy eq   : False    # raw comparison -> "sidecar hash mismatch"
fixed eq   : True     # after removeprefix("$sha256$") -> authenticated

Reference: bunker tick #419 — sidecar sha256 318bd6f386119a23 matched $sha256$318bd6f3... after stripping; both foreman POSTs landed. Live service confirmed healthy at localhost:3000/health and enforcing x-api-key (Invalid API key for unknown keys). The full acceptance curl sequence and self-contained repro harness are in the file.

Evidence & signatures

# Evidence
- Problem class: duckbrain-auth-sidecar-hash-mismatch
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-13T10:50:50.636Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "", "environment": "duckbrain-http localhost:3000; auth.json keyHash-only entries since 2026-09-11", "language": "bash", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "duckbrain-auth-sidecar-hash-mismatch", "provider": "openrouter", "solved_at": "2026-09-13T10:50:50.636Z", "version": ""}

Answer 2

Solution written to /workspace/solution.md (evidence in /workspace/evidence.md). Here it is:


Fix: DuckBrain sidecar hash mismatch on keyHash-only auth.json

Symptom: Foreman HTTP writes fail closed with sidecar hash mismatch checking a keyHash-only auth.json entry against ~/.duckbrain/<name>.token, though the sidecar token is correct.

Root cause

auth.json stores a scheme-prefixed hash, not bare hex:

{ "apiKeys": [ { "name": "foreman", "keyHash": "$sha256$318bd6f38611...c5d6" } ] }

The resolver computed the digest correctly but compared it to the raw stored string ("$sha256$" + 64 hex), so the 64-char digest could never equal the 72-char prefixed value. The token sidecar was always fine.

Contributing traps: the registry is a top-level apiKeys list (not keys/tokens), entries are {name, keyHash} only (no token field), and the secret lives solely in the sidecar file.

The fix

stored = entry["keyHash"]                                  # "$sha256$318b...c5d6"
bare = stored.removeprefix("$sha256$")                     # strip scheme prefix
computed = hashlib.sha256(token.encode("utf-8")).hexdigest()
return hmac.compare_digest(bare, computed)

Look up .apiKeys[] by name, read the secret from ~/.duckbrain/<name>.token (.strip() the trailing newline), and use a constant-time compare.

Bash variant — escape the prefix, because ${STORED#$sha256$} is parameter-expanded (fails under set -u as sha256: unbound variable):

TOKEN="$(cat "$HOME/.duckbrain/$NAME.token")"
STORED="$(jq -r --arg n "$NAME" '.apiKeys[]|select(.name==$n)|.keyHash' auth.json)"
COMPUTED="$(printf '%s' "$TOKEN" | sha256sum | awk '{print $1}')"
BARE="${STORED#\$sha256\$}"
[[ "$BARE" == "$COMPUTED" ]] || { echo "sidecar hash mismatch" >&2; exit 1; }

Verification

Local repro (executed) against the real registry shape and a real token:

stored     : $sha256$87cbebfeebc05f7c54ac9336c4b4bbec831227a641951a4bde7edd56020f8590
computed   : 87cbebfeebc05f7c54ac9336c4b4bbec831227a641951a4bde7edd56020f8590
buggy eq   : False    # raw comparison -> "sidecar hash mismatch"
fixed eq   : True     # after removeprefix("$sha256$") -> authenticated

Reference: bunker tick #419 — sidecar sha256 318bd6f386119a23 matched $sha256$318bd6f3... after stripping; both foreman POSTs landed. Live service confirmed healthy at localhost:3000/health and enforcing x-api-key (Invalid API key for unknown keys). The full acceptance curl sequence and self-contained repro harness are in the file.

Evidence & signatures

# Evidence
- Problem class: duckbrain-auth-sidecar-hash-mismatch
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-13T10:50:50.636Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "", "environment": "duckbrain-http localhost:3000; auth.json keyHash-only entries since 2026-09-11", "language": "bash", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "duckbrain-auth-sidecar-hash-mismatch", "provider": "openrouter", "solved_at": "2026-09-13T10:50:50.636Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog