{"record":{"id":"522bff9ceefb4e0d","repo":"stamparm/maltrail","slug":"hex-is-ascii","errorCode":null,"errorMessage":"hex is ascii","messagePattern":"hex is ascii","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"sensor/src/output.rs","lineNumber":90,"sourceCode":"                \"s3cr3t\",\n                b\"1767261603 \\\"2026-01-01 10:00:03.123456\\\" box 10.0.0.8 6666 5.5.5.5 80 TCP IP 5.5.5.5 \\\"malware (test)\\\" (static)\\n\",\n                \"157a86bdbdf4940dccfd73668f1e74e3\",\n            ),\n            (\"k\", b\"\", \"8bb990c40a7d61cb97597a942125025b\"),\n            (\n                \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\n                b\"short\",\n                \"c67f8c6d0fec3da7b0fd48be37f06a9d\",\n            ),\n            // a key longer than SHA-256's block is hashed first by HMAC; a payload that is not\n            // ASCII must be signed as the bytes it is, not as text\n            (\"čž secret\", \"unicode payload čž\\n\".as_bytes(), \"2b576daf9cbbebe124c8320c8a19fedb\"),\n        ];\n\n        for (secret, payload, expected_mac) in cases {\n            let out = mts_sign(secret, payload);\n            assert!(out.starts_with(b\"MTS1 \"), \"frame prefix missing\");\n            let mac = std::str::from_utf8(&out[5..37]).expect(\"hex is ascii\");\n            assert_eq!(mac, *expected_mac, \"MAC disagrees with core/log.py for secret {secret:?}\");\n            assert_eq!(&out[37..38], b\" \", \"one space between MAC and payload\");\n            assert_eq!(&out[38..], *payload, \"payload must be carried byte-for-byte\");\n        }\n    }\n}\n\npub struct OutputConfig {\n    pub sensor_name: String,\n    pub log_dir: PathBuf,\n    pub trails_file: PathBuf,\n    pub disable_local_log_storage: bool,\n    /// `LOCAL_LOG_FORMAT json`: write the event log as one JSON object per line.\n    pub local_log_json: bool,\n    pub console: bool,\n    pub log_server: Option<String>,\n    /// `LOG_SERVER_SECRET`: shared secret authenticating every LOG_SERVER datagram. `None` sends\n    /// them unsigned, which is what every deployment did before this existed.","sourceCodeStart":72,"sourceCodeEnd":108,"githubUrl":"https://github.com/stamparm/maltrail/blob/77cfb06d7606506d101bbcec0786c77166c4255e/sensor/src/output.rs#L72-L108","documentation":"Test assertion in macs_match_the_python_sender: mts_sign hex-encodes the 128-bit truncated MAC, so bytes 5..37 of the output frame must be valid ASCII hex; from_utf8(...).expect(\"hex is ascii\") fails if the encoding emitted non-ASCII bytes. It guards the wire format shared with the Python sender (core/log.py).","triggerScenarios":"A change to mts_sign's output framing (offset shift, binary tag instead of hex, different prefix length) makes out[5..37] non-UTF-8, or the slice indices no longer align with the 32-char hex MAC.","commonSituations":"Refactoring the MTS1 frame layout (prefix length, MAC length, separators) without updating the test's hardcoded offsets.","solutions":["Keep mts_sign emitting 'MTS1 ' + 32 hex chars + ' ' + payload; fix the encoder if the prefix/format changed","Update the test offsets if the frame format legitimately changed, and mirror the change in core/log.py","Verify with the golden test vectors (including the unicode-secret case) that MACs still match the Python implementation"],"exampleFix":"// before\nlet mac = std::str::from_utf8(&out[5..37]).expect(\"hex is ascii\");\n// after (offset updated to a new frame layout)\nlet mac = std::str::from_utf8(&out[PREFIX_LEN..PREFIX_LEN + 32]).expect(\"hex is ascii\");","handlingStrategy":"validation","validationCode":"let frame = mts_sign(secret, payload);\nassert!(frame.len() >= 38 && frame[5..37].iter().all(|b| b.is_ascii_hexdigit()), \"MAC field must be 32 ASCII hex chars\");","typeGuard":"fn is_hex_ascii(b: &[u8]) -> bool { b.iter().all(|c| c.is_ascii_hexdigit()) }","tryCatchPattern":"let mac = match std::str::from_utf8(&out[5..37]) {\n    Ok(s) if s.bytes().all(|b| b.is_ascii_hexdigit()) => s,\n    _ => { eprintln!(\"MTS1 frame MAC field malformed\"); return; }\n};","preventionTips":["Define the MTS1 frame layout as named constants and derive offsets from them","Change core/log.py and the Rust encoder together with shared test vectors","Add a wire-format unit test whenever the framing changes"],"tags":["rust","tests","encoding","hmac"],"backgroundTag":"unexpected-response-shape","analyzedSha":"77cfb06d7606506d101bbcec0786c77166c4255e","analyzedAt":"2026-09-13T03:50:16.010Z","contentChangedAt":"2026-09-13T03:50:16.010Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}