{"record":{"id":"281a4adac0be728f","repo":"zeroclaw-labs/zeroclaw","slug":"hmac-accepts-any-key-length","errorCode":null,"errorMessage":"HMAC accepts any key length","messagePattern":"HMAC accepts any key length","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"info","filePath":"crates/zeroclaw-runtime/src/agent/tool_receipts.rs","lineNumber":83,"sourceCode":"    }\n\n    /// Verify a receipt against the expected tool execution parameters.\n    /// Parses the timestamp from the receipt string, recomputes the HMAC,\n    /// and compares. Returns `false` for malformed, tampered, or fabricated receipts.\n    pub fn verify(\n        &self,\n        receipt: &str,\n        tool_name: &str,\n        args: &serde_json::Value,\n        result: &str,\n    ) -> bool {\n        let Some((timestamp, provided_hash)) = parse_receipt(receipt) else {\n            return false;\n        };\n        let Ok(provided_bytes) = URL_SAFE_NO_PAD.decode(provided_hash) else {\n            return false;\n        };\n        let mut mac = HmacSha256::new_from_slice(&self.key).expect(\"HMAC accepts any key length\");\n        mac.update(tool_name.as_bytes());\n        mac.update(b\"|\");\n        mac.update(args.to_string().as_bytes());\n        mac.update(b\"|\");\n        mac.update(result.as_bytes());\n        mac.update(b\"|\");\n        mac.update(timestamp.to_string().as_bytes());\n        mac.verify_slice(&provided_bytes).is_ok()\n    }\n\n    fn compute_hmac(\n        &self,\n        tool_name: &str,\n        args: &serde_json::Value,\n        result: &str,\n        timestamp: u64,\n    ) -> Vec<u8> {\n        let mut mac = HmacSha256::new_from_slice(&self.key).expect(\"HMAC accepts any key length\");","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/agent/tool_receipts.rs#L65-L101","documentation":"During receipt verification, HmacSha256::new_from_slice() returns a Result that is unwrapped with expect(\"HMAC accepts any key length\"). HMAC-SHA256 legally accepts keys of any length, and the generator's key is always a random 32-byte value, so this expect encodes a static invariant and is unreachable in practice.","triggerScenarios":"No runtime input reaches this panic: the key comes from ReceiptKeyGenerator, which always produces a 32-byte key (or a test key via with_key). It could only fire if the code were refactored to feed new_from_slice a value that violates HMAC's key contract, which cannot happen by length alone.","commonSituations":"None for users; maintainers see it in stack traces only if a future refactor changes the key source. Its presence in traces usually just marks the verification path (verify -> jws_verify and the call_tool_* recovery flows), not a real fault.","solutions":["Treat a hit here as a code-regression signal: inspect ReceiptKeyGenerator for a changed key length or an altered construction path.","Upgrade to the latest zeroclaw version in case a refactor already addressed it.","If forking, keep the key at 32 bytes or switch to new_from_slice error propagation."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// If you construct generators from custom key material, reject empty keys up front:\nfn make_generator(key: Vec<u8>) -> anyhow::Result<ReceiptKeyGenerator> {\n    anyhow::ensure!(!key.is_empty(), \"receipt key must be non-empty\");\n    Ok(ReceiptKeyGenerator::with_key(key))\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Do not modify ReceiptKeyGenerator's 32-byte key contract without updating the HMAC invariants.","Rely on verify()'s bool return for all untrusted input; it never panics on malformed receipts.","Run the call_tool_* receipt tests after touching tool_receipts.rs."],"tags":["rust","crypto","hmac","invariant","tool-receipts"],"backgroundTag":"hmac-key-length-invalid","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}