stamparm/maltrail · error
hex is ascii
Error message
hex is ascii
What it means
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).
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
Example fix
// before
let mac = std::str::from_utf8(&out[5..37]).expect("hex is ascii");
// after (offset updated to a new frame layout)
let mac = std::str::from_utf8(&out[PREFIX_LEN..PREFIX_LEN + 32]).expect("hex is ascii"); Defensive patterns
Strategy: validation
Validate before calling
let frame = mts_sign(secret, payload); assert!(frame.len() >= 38 && frame[5..37].iter().all(|b| b.is_ascii_hexdigit()), "MAC field must be 32 ASCII hex chars");
Type guard
fn is_hex_ascii(b: &[u8]) -> bool { b.iter().all(|c| c.is_ascii_hexdigit()) } Try / catch
let mac = match std::str::from_utf8(&out[5..37]) {
Ok(s) if s.bytes().all(|b| b.is_ascii_hexdigit()) => s,
_ => { eprintln!("MTS1 frame MAC field malformed"); return; }
}; Prevention
- 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
When it happens
Trigger: 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.
Common situations: Refactoring the MTS1 frame layout (prefix length, MAC length, separators) without updating the test's hardcoded offsets.
Related errors
AI-assisted analysis of stamparm/maltrail@77cfb06d76 (2026-09-13).
Data as JSON: /api/errors/522bff9ceefb4e0d.
Report an issue: GitHub.
Appendix: source
Thrown at sensor/src/output.rs:90
"s3cr3t",
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",
"157a86bdbdf4940dccfd73668f1e74e3",
),
("k", b"", "8bb990c40a7d61cb97597a942125025b"),
(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
b"short",
"c67f8c6d0fec3da7b0fd48be37f06a9d",
),
// a key longer than SHA-256's block is hashed first by HMAC; a payload that is not
// ASCII must be signed as the bytes it is, not as text
("čž secret", "unicode payload čž\n".as_bytes(), "2b576daf9cbbebe124c8320c8a19fedb"),
];
for (secret, payload, expected_mac) in cases {
let out = mts_sign(secret, payload);
assert!(out.starts_with(b"MTS1 "), "frame prefix missing");
let mac = std::str::from_utf8(&out[5..37]).expect("hex is ascii");
assert_eq!(mac, *expected_mac, "MAC disagrees with core/log.py for secret {secret:?}");
assert_eq!(&out[37..38], b" ", "one space between MAC and payload");
assert_eq!(&out[38..], *payload, "payload must be carried byte-for-byte");
}
}
}
pub struct OutputConfig {
pub sensor_name: String,
pub log_dir: PathBuf,
pub trails_file: PathBuf,
pub disable_local_log_storage: bool,
/// `LOCAL_LOG_FORMAT json`: write the event log as one JSON object per line.
pub local_log_json: bool,
pub console: bool,
pub log_server: Option<String>,
/// `LOG_SERVER_SECRET`: shared secret authenticating every LOG_SERVER datagram. `None` sends
/// them unsigned, which is what every deployment did before this existed.View on GitHub (pinned to 77cfb06d76)