Skip to main content

celers_core/
task_signature.rs

1//! Task message signature verification (HMAC-SHA256).
2//!
3//! This module lets a producer cryptographically sign a task message and a
4//! consumer verify it, so that tampered or unsigned messages can be rejected
5//! before execution. The signature is an [HMAC] over a *canonical*
6//! serialization of the task's identifying fields — its id, name, positional
7//! arguments and keyword arguments — so two semantically identical messages
8//! always produce the same MAC regardless of incidental map ordering.
9//!
10//! # Why a native HMAC implementation?
11//!
12//! `celers-core` is intentionally dependency-light and additive: it does not
13//! pull in a crypto stack. Rather than add `hmac`/`sha2` here, this module
14//! ships a small, self-contained, constant-foldable implementation of
15//! **SHA-256** and **HMAC-SHA256** in pure Rust (verified against the FIPS
16//! 180-4 and RFC 4231 test vectors in the unit tests). The public surface is
17//! intentionally generic so it can be swapped for a hardware/`RustCrypto`
18//! backend later without breaking callers.
19//!
20//! [HMAC]: https://datatracker.ietf.org/doc/html/rfc2104
21//!
22//! # Example
23//!
24//! ```rust
25//! use celers_core::task_signature::{TaskSigner, SignedFields};
26//! use celers_core::sanitize::TaskValue;
27//! use uuid::Uuid;
28//!
29//! let signer = TaskSigner::new(b"super-secret-shared-key");
30//!
31//! let fields = SignedFields::new(Uuid::nil(), "tasks.add")
32//!     .with_args(vec![TaskValue::from(2), TaskValue::from(3)])
33//!     .with_kwarg("note", TaskValue::from("hello"));
34//!
35//! let sig = signer.sign(&fields);
36//!
37//! // The same signer verifies an untouched message.
38//! assert!(signer.verify(&fields, &sig).is_ok());
39//!
40//! // Any tampering (here, a changed argument) is rejected.
41//! let tampered = SignedFields::new(Uuid::nil(), "tasks.add")
42//!     .with_args(vec![TaskValue::from(2), TaskValue::from(4)]);
43//! assert!(signer.verify(&tampered, &sig).is_err());
44//! ```
45
46use crate::sanitize::TaskValue;
47use serde::{Deserialize, Serialize};
48use std::fmt;
49use uuid::Uuid;
50
51// ===========================================================================
52// SHA-256 (FIPS 180-4)
53// ===========================================================================
54
55/// SHA-256 block size in bytes.
56const SHA256_BLOCK_BYTES: usize = 64;
57
58/// SHA-256 digest size in bytes.
59pub const SHA256_DIGEST_BYTES: usize = 32;
60
61/// SHA-256 round constants (first 32 bits of the fractional parts of the cube
62/// roots of the first 64 primes).
63const SHA256_K: [u32; 64] = [
64    0x428a_2f98,
65    0x7137_4491,
66    0xb5c0_fbcf,
67    0xe9b5_dba5,
68    0x3956_c25b,
69    0x59f1_11f1,
70    0x923f_82a4,
71    0xab1c_5ed5,
72    0xd807_aa98,
73    0x1283_5b01,
74    0x2431_85be,
75    0x550c_7dc3,
76    0x72be_5d74,
77    0x80de_b1fe,
78    0x9bdc_06a7,
79    0xc19b_f174,
80    0xe49b_69c1,
81    0xefbe_4786,
82    0x0fc1_9dc6,
83    0x240c_a1cc,
84    0x2de9_2c6f,
85    0x4a74_84aa,
86    0x5cb0_a9dc,
87    0x76f9_88da,
88    0x983e_5152,
89    0xa831_c66d,
90    0xb003_27c8,
91    0xbf59_7fc7,
92    0xc6e0_0bf3,
93    0xd5a7_9147,
94    0x06ca_6351,
95    0x1429_2967,
96    0x27b7_0a85,
97    0x2e1b_2138,
98    0x4d2c_6dfc,
99    0x5338_0d13,
100    0x650a_7354,
101    0x766a_0abb,
102    0x81c2_c92e,
103    0x9272_2c85,
104    0xa2bf_e8a1,
105    0xa81a_664b,
106    0xc24b_8b70,
107    0xc76c_51a3,
108    0xd192_e819,
109    0xd699_0624,
110    0xf40e_3585,
111    0x106a_a070,
112    0x19a4_c116,
113    0x1e37_6c08,
114    0x2748_774c,
115    0x34b0_bcb5,
116    0x391c_0cb3,
117    0x4ed8_aa4a,
118    0x5b9c_ca4f,
119    0x682e_6ff3,
120    0x748f_82ee,
121    0x78a5_636f,
122    0x84c8_7814,
123    0x8cc7_0208,
124    0x90be_fffa,
125    0xa450_6ceb,
126    0xbef9_a3f7,
127    0xc671_78f2,
128];
129
130/// SHA-256 initial hash values (first 32 bits of the fractional parts of the
131/// square roots of the first 8 primes).
132const SHA256_H0: [u32; 8] = [
133    0x6a09_e667,
134    0xbb67_ae85,
135    0x3c6e_f372,
136    0xa54f_f53a,
137    0x510e_527f,
138    0x9b05_688c,
139    0x1f83_d9ab,
140    0x5be0_cd19,
141];
142
143/// Streaming SHA-256 hasher (pure Rust, no external dependencies).
144///
145/// Used internally to build [`HmacSha256`]. It is exposed publicly because a
146/// canonical message hash is occasionally useful on its own (for example, to
147/// key a deduplication cache), and exposing it is purely additive.
148#[derive(Clone)]
149pub struct Sha256 {
150    state: [u32; 8],
151    buffer: [u8; SHA256_BLOCK_BYTES],
152    buffer_len: usize,
153    total_len: u64,
154}
155
156impl Default for Sha256 {
157    #[inline]
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163impl fmt::Debug for Sha256 {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        // Never leak partial state contents.
166        f.debug_struct("Sha256")
167            .field("total_len", &self.total_len)
168            .finish_non_exhaustive()
169    }
170}
171
172impl Sha256 {
173    /// Create a new, empty SHA-256 hasher.
174    #[inline]
175    #[must_use]
176    pub const fn new() -> Self {
177        Self {
178            state: SHA256_H0,
179            buffer: [0u8; SHA256_BLOCK_BYTES],
180            buffer_len: 0,
181            total_len: 0,
182        }
183    }
184
185    /// Feed more data into the hasher.
186    pub fn update(&mut self, mut data: &[u8]) {
187        self.total_len = self.total_len.wrapping_add(data.len() as u64);
188
189        // Fill an existing partial buffer first.
190        if self.buffer_len > 0 {
191            let need = SHA256_BLOCK_BYTES - self.buffer_len;
192            let take = need.min(data.len());
193            self.buffer[self.buffer_len..self.buffer_len + take].copy_from_slice(&data[..take]);
194            self.buffer_len += take;
195            data = &data[take..];
196
197            if self.buffer_len == SHA256_BLOCK_BYTES {
198                let block = self.buffer;
199                Self::compress(&mut self.state, &block);
200                self.buffer_len = 0;
201            }
202        }
203
204        // Process full blocks directly from the input.
205        while data.len() >= SHA256_BLOCK_BYTES {
206            let mut block = [0u8; SHA256_BLOCK_BYTES];
207            block.copy_from_slice(&data[..SHA256_BLOCK_BYTES]);
208            Self::compress(&mut self.state, &block);
209            data = &data[SHA256_BLOCK_BYTES..];
210        }
211
212        // Stash the remainder.
213        if !data.is_empty() {
214            self.buffer[..data.len()].copy_from_slice(data);
215            self.buffer_len = data.len();
216        }
217    }
218
219    /// Consume the hasher and return the 32-byte digest.
220    #[must_use]
221    pub fn finalize(mut self) -> [u8; SHA256_DIGEST_BYTES] {
222        let bit_len = self.total_len.wrapping_mul(8);
223
224        // Append the 0x80 padding byte.
225        self.update_byte(0x80);
226
227        // Pad with zeros until the buffer is 56 bytes mod 64 (room for the
228        // 8-byte length).
229        while self.buffer_len != 56 {
230            self.update_byte(0x00);
231        }
232
233        // Append the message length as a 64-bit big-endian integer.
234        let len_bytes = bit_len.to_be_bytes();
235        for b in len_bytes {
236            self.update_byte(b);
237        }
238
239        debug_assert_eq!(self.buffer_len, 0, "buffer must be flushed after padding");
240
241        let mut out = [0u8; SHA256_DIGEST_BYTES];
242        for (chunk, word) in out.chunks_exact_mut(4).zip(self.state.iter()) {
243            chunk.copy_from_slice(&word.to_be_bytes());
244        }
245        out
246    }
247
248    /// One-shot convenience: hash `data` and return its digest.
249    #[must_use]
250    pub fn digest(data: &[u8]) -> [u8; SHA256_DIGEST_BYTES] {
251        let mut h = Self::new();
252        h.update(data);
253        h.finalize()
254    }
255
256    /// Internal helper used only during finalization, where we know `total_len`
257    /// has already been accounted for and must not be double-counted.
258    #[inline]
259    fn update_byte(&mut self, byte: u8) {
260        self.buffer[self.buffer_len] = byte;
261        self.buffer_len += 1;
262        if self.buffer_len == SHA256_BLOCK_BYTES {
263            let block = self.buffer;
264            Self::compress(&mut self.state, &block);
265            self.buffer_len = 0;
266        }
267    }
268
269    /// The SHA-256 compression function operating on a single 64-byte block.
270    #[allow(clippy::many_single_char_names)]
271    fn compress(state: &mut [u32; 8], block: &[u8; SHA256_BLOCK_BYTES]) {
272        let mut w = [0u32; 64];
273        for (i, word) in w.iter_mut().enumerate().take(16) {
274            let j = i * 4;
275            *word = u32::from_be_bytes([block[j], block[j + 1], block[j + 2], block[j + 3]]);
276        }
277        for i in 16..64 {
278            let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
279            let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
280            w[i] = w[i - 16]
281                .wrapping_add(s0)
282                .wrapping_add(w[i - 7])
283                .wrapping_add(s1);
284        }
285
286        let mut a = state[0];
287        let mut b = state[1];
288        let mut c = state[2];
289        let mut d = state[3];
290        let mut e = state[4];
291        let mut f = state[5];
292        let mut g = state[6];
293        let mut h = state[7];
294
295        for i in 0..64 {
296            let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
297            let ch = (e & f) ^ ((!e) & g);
298            let temp1 = h
299                .wrapping_add(s1)
300                .wrapping_add(ch)
301                .wrapping_add(SHA256_K[i])
302                .wrapping_add(w[i]);
303            let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
304            let maj = (a & b) ^ (a & c) ^ (b & c);
305            let temp2 = s0.wrapping_add(maj);
306
307            h = g;
308            g = f;
309            f = e;
310            e = d.wrapping_add(temp1);
311            d = c;
312            c = b;
313            b = a;
314            a = temp1.wrapping_add(temp2);
315        }
316
317        state[0] = state[0].wrapping_add(a);
318        state[1] = state[1].wrapping_add(b);
319        state[2] = state[2].wrapping_add(c);
320        state[3] = state[3].wrapping_add(d);
321        state[4] = state[4].wrapping_add(e);
322        state[5] = state[5].wrapping_add(f);
323        state[6] = state[6].wrapping_add(g);
324        state[7] = state[7].wrapping_add(h);
325    }
326}
327
328// ===========================================================================
329// HMAC-SHA256 (RFC 2104 / RFC 4231)
330// ===========================================================================
331
332/// Inner/outer pad constants for HMAC.
333const HMAC_IPAD: u8 = 0x36;
334const HMAC_OPAD: u8 = 0x5c;
335
336/// HMAC-SHA256 keyed-hash message authentication code (pure Rust).
337///
338/// Construct with a secret key, feed message bytes via [`HmacSha256::update`],
339/// then call [`HmacSha256::finalize`] for the 32-byte tag.
340#[derive(Clone)]
341pub struct HmacSha256 {
342    inner: Sha256,
343    outer_key: [u8; SHA256_BLOCK_BYTES],
344}
345
346impl fmt::Debug for HmacSha256 {
347    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348        // Never leak the derived key material.
349        f.debug_struct("HmacSha256").finish_non_exhaustive()
350    }
351}
352
353impl HmacSha256 {
354    /// Create a new HMAC-SHA256 instance keyed with `key`.
355    ///
356    /// Keys longer than the block size (64 bytes) are first hashed, exactly as
357    /// specified by RFC 2104; shorter keys are zero-padded.
358    #[must_use]
359    pub fn new(key: &[u8]) -> Self {
360        let mut block_key = [0u8; SHA256_BLOCK_BYTES];
361        if key.len() > SHA256_BLOCK_BYTES {
362            let digest = Sha256::digest(key);
363            block_key[..SHA256_DIGEST_BYTES].copy_from_slice(&digest);
364        } else {
365            block_key[..key.len()].copy_from_slice(key);
366        }
367
368        let mut inner_key = [0u8; SHA256_BLOCK_BYTES];
369        let mut outer_key = [0u8; SHA256_BLOCK_BYTES];
370        for i in 0..SHA256_BLOCK_BYTES {
371            inner_key[i] = block_key[i] ^ HMAC_IPAD;
372            outer_key[i] = block_key[i] ^ HMAC_OPAD;
373        }
374
375        let mut inner = Sha256::new();
376        inner.update(&inner_key);
377
378        // Best-effort scrub of the intermediate key buffers.
379        block_key.iter_mut().for_each(|b| *b = 0);
380        inner_key.iter_mut().for_each(|b| *b = 0);
381
382        Self { inner, outer_key }
383    }
384
385    /// Feed message bytes into the MAC.
386    #[inline]
387    pub fn update(&mut self, data: &[u8]) {
388        self.inner.update(data);
389    }
390
391    /// Finish and return the 32-byte authentication tag.
392    #[must_use]
393    pub fn finalize(self) -> [u8; SHA256_DIGEST_BYTES] {
394        let inner_digest = self.inner.finalize();
395        let mut outer = Sha256::new();
396        outer.update(&self.outer_key);
397        outer.update(&inner_digest);
398        outer.finalize()
399    }
400
401    /// One-shot convenience: compute HMAC-SHA256 over `data` with `key`.
402    #[must_use]
403    pub fn mac(key: &[u8], data: &[u8]) -> [u8; SHA256_DIGEST_BYTES] {
404        let mut h = Self::new(key);
405        h.update(data);
406        h.finalize()
407    }
408}
409
410/// Compare two byte slices in constant time with respect to their contents.
411///
412/// Returns `true` only when the slices are equal. The running time depends on
413/// the (public) length of the slices but not on *where* they first differ, so
414/// it does not leak how many leading bytes of a forged tag were correct.
415#[must_use]
416pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
417    if a.len() != b.len() {
418        return false;
419    }
420    let mut diff: u8 = 0;
421    for (x, y) in a.iter().zip(b.iter()) {
422        diff |= x ^ y;
423    }
424    diff == 0
425}
426
427// ===========================================================================
428// Hex helpers (lower-case, no external dependency)
429// ===========================================================================
430
431/// Encode bytes as a lower-case hexadecimal string.
432#[must_use]
433pub fn to_hex(bytes: &[u8]) -> String {
434    const LUT: &[u8; 16] = b"0123456789abcdef";
435    let mut out = String::with_capacity(bytes.len() * 2);
436    for &b in bytes {
437        out.push(LUT[(b >> 4) as usize] as char);
438        out.push(LUT[(b & 0x0f) as usize] as char);
439    }
440    out
441}
442
443/// Decode a hexadecimal string (any case) into bytes.
444///
445/// # Errors
446///
447/// Returns [`SignatureError::MalformedSignature`] if the input has an odd
448/// length or contains a non-hex character.
449pub fn from_hex(s: &str) -> Result<Vec<u8>, SignatureError> {
450    let bytes = s.as_bytes();
451    if !bytes.len().is_multiple_of(2) {
452        return Err(SignatureError::MalformedSignature(
453            "hex string has odd length".to_string(),
454        ));
455    }
456    let mut out = Vec::with_capacity(bytes.len() / 2);
457    let mut i = 0;
458    while i < bytes.len() {
459        let hi = hex_val(bytes[i])?;
460        let lo = hex_val(bytes[i + 1])?;
461        out.push((hi << 4) | lo);
462        i += 2;
463    }
464    Ok(out)
465}
466
467#[inline]
468fn hex_val(c: u8) -> Result<u8, SignatureError> {
469    match c {
470        b'0'..=b'9' => Ok(c - b'0'),
471        b'a'..=b'f' => Ok(c - b'a' + 10),
472        b'A'..=b'F' => Ok(c - b'A' + 10),
473        _ => Err(SignatureError::MalformedSignature(format!(
474            "invalid hex character: {:?}",
475            c as char
476        ))),
477    }
478}
479
480// ===========================================================================
481// Signature errors
482// ===========================================================================
483
484/// Errors produced while signing or verifying a task message.
485#[derive(Debug, Clone, PartialEq, Eq)]
486pub enum SignatureError {
487    /// The provided signature did not match the recomputed MAC. The message
488    /// was tampered with, signed with a different key, or corrupted.
489    Mismatch,
490
491    /// The message carried no signature but verification was requested.
492    MissingSignature,
493
494    /// The signature bytes/string were not well-formed (e.g. invalid hex or a
495    /// wrong-length tag).
496    MalformedSignature(String),
497
498    /// The signature used an algorithm this verifier does not understand.
499    UnsupportedAlgorithm(String),
500}
501
502impl fmt::Display for SignatureError {
503    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504        match self {
505            SignatureError::Mismatch => {
506                write!(f, "task signature verification failed: MAC mismatch")
507            }
508            SignatureError::MissingSignature => {
509                write!(f, "task message is unsigned but a signature is required")
510            }
511            SignatureError::MalformedSignature(why) => {
512                write!(f, "malformed task signature: {why}")
513            }
514            SignatureError::UnsupportedAlgorithm(alg) => {
515                write!(f, "unsupported task signature algorithm: {alg}")
516            }
517        }
518    }
519}
520
521impl std::error::Error for SignatureError {}
522
523impl From<SignatureError> for crate::CelersError {
524    fn from(err: SignatureError) -> Self {
525        crate::CelersError::Other(format!("signature error: {err}"))
526    }
527}
528
529// ===========================================================================
530// Signed fields & canonical serialization
531// ===========================================================================
532
533/// The identifying fields of a task that participate in the signature.
534///
535/// Deliberately a *projection* of a full task message: only the fields that
536/// determine task identity and behaviour are signed (id, name, args, kwargs),
537/// so signatures are stable across transport metadata that may legitimately
538/// change in flight (timestamps, retry counters, routing hints, …).
539#[derive(Debug, Clone, PartialEq, Default)]
540pub struct SignedFields {
541    /// Unique task identifier.
542    pub id: Uuid,
543    /// Task name / type.
544    pub name: String,
545    /// Positional arguments.
546    pub args: Vec<TaskValue>,
547    /// Keyword arguments. Order is irrelevant — canonicalization sorts keys.
548    pub kwargs: Vec<(String, TaskValue)>,
549}
550
551impl SignedFields {
552    /// Create a new set of signed fields from a task id and name.
553    #[must_use]
554    pub fn new(id: Uuid, name: impl Into<String>) -> Self {
555        Self {
556            id,
557            name: name.into(),
558            args: Vec::new(),
559            kwargs: Vec::new(),
560        }
561    }
562
563    /// Set the positional arguments.
564    #[must_use]
565    pub fn with_args(mut self, args: Vec<TaskValue>) -> Self {
566        self.args = args;
567        self
568    }
569
570    /// Append a single positional argument.
571    #[must_use]
572    pub fn with_arg(mut self, value: TaskValue) -> Self {
573        self.args.push(value);
574        self
575    }
576
577    /// Set the keyword arguments.
578    #[must_use]
579    pub fn with_kwargs(mut self, kwargs: Vec<(String, TaskValue)>) -> Self {
580        self.kwargs = kwargs;
581        self
582    }
583
584    /// Append a single keyword argument.
585    #[must_use]
586    pub fn with_kwarg(mut self, key: impl Into<String>, value: TaskValue) -> Self {
587        self.kwargs.push((key.into(), value));
588        self
589    }
590
591    /// Produce the canonical byte serialization that is fed to the MAC.
592    ///
593    /// The encoding is length-prefixed and field-tagged so that no two
594    /// distinct field layouts can collide (i.e. it is *unambiguous*):
595    /// concatenating differently-split components can never yield the same
596    /// byte stream. Keyword arguments are sorted by key so map ordering does
597    /// not affect the result.
598    #[must_use]
599    pub fn canonical_bytes(&self) -> Vec<u8> {
600        let mut out = Vec::new();
601
602        // Domain separation tag + version, so signatures from this scheme can
603        // never be confused with raw HMACs of arbitrary data.
604        out.extend_from_slice(b"celers.task.sig.v1");
605
606        // id (always 16 bytes, but length-prefix anyway for uniformity).
607        write_field(&mut out, b'I', self.id.as_bytes());
608
609        // name.
610        write_field(&mut out, b'N', self.name.as_bytes());
611
612        // args: count, then each value's canonical bytes.
613        write_len(&mut out, b'A', self.args.len() as u64);
614        for arg in &self.args {
615            let encoded = canonical_value_bytes(arg);
616            write_field(&mut out, b'a', &encoded);
617        }
618
619        // kwargs: sorted by key, count, then key/value pairs.
620        let mut sorted: Vec<&(String, TaskValue)> = self.kwargs.iter().collect();
621        sorted.sort_by(|x, y| x.0.cmp(&y.0));
622        write_len(&mut out, b'K', sorted.len() as u64);
623        for (key, value) in sorted {
624            write_field(&mut out, b'k', key.as_bytes());
625            let encoded = canonical_value_bytes(value);
626            write_field(&mut out, b'v', &encoded);
627        }
628
629        out
630    }
631}
632
633/// Write a tagged, length-prefixed field: `tag | len(8, be) | bytes`.
634fn write_field(out: &mut Vec<u8>, tag: u8, bytes: &[u8]) {
635    out.push(tag);
636    out.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
637    out.extend_from_slice(bytes);
638}
639
640/// Write a tagged count: `tag | value(8, be)`.
641fn write_len(out: &mut Vec<u8>, tag: u8, value: u64) {
642    out.push(tag);
643    out.extend_from_slice(&value.to_be_bytes());
644}
645
646/// Canonical, collision-free byte encoding of a single [`TaskValue`].
647///
648/// Each variant is tagged so that, e.g., the integer `1` and the string
649/// `"1"` and the boolean `true` never share an encoding. Containers recurse
650/// with the same length-prefixed framing used at the top level, and object
651/// keys are sorted.
652fn canonical_value_bytes(value: &TaskValue) -> Vec<u8> {
653    let mut out = Vec::new();
654    match value {
655        TaskValue::Null => out.push(b'0'),
656        TaskValue::Bool(b) => {
657            out.push(b'b');
658            out.push(u8::from(*b));
659        }
660        TaskValue::Int(i) => {
661            out.push(b'i');
662            out.extend_from_slice(&i.to_be_bytes());
663        }
664        TaskValue::UInt(u) => {
665            out.push(b'u');
666            out.extend_from_slice(&u.to_be_bytes());
667        }
668        TaskValue::Float(f) => {
669            out.push(b'f');
670            // Normalize NaN so distinct NaN bit-patterns canonicalize equally,
671            // and normalize the sign of zero so +0.0 / -0.0 agree.
672            let bits = if f.is_nan() {
673                f64::NAN.to_bits()
674            } else if *f == 0.0 {
675                0.0_f64.to_bits()
676            } else {
677                f.to_bits()
678            };
679            out.extend_from_slice(&bits.to_be_bytes());
680        }
681        TaskValue::String(s) => {
682            out.push(b's');
683            out.extend_from_slice(&(s.len() as u64).to_be_bytes());
684            out.extend_from_slice(s.as_bytes());
685        }
686        TaskValue::Bytes(bytes) => {
687            out.push(b'B');
688            out.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
689            out.extend_from_slice(bytes);
690        }
691        TaskValue::Array(items) => {
692            out.push(b'L');
693            out.extend_from_slice(&(items.len() as u64).to_be_bytes());
694            for item in items {
695                let encoded = canonical_value_bytes(item);
696                out.extend_from_slice(&(encoded.len() as u64).to_be_bytes());
697                out.extend_from_slice(&encoded);
698            }
699        }
700        TaskValue::Object(entries) => {
701            out.push(b'O');
702            let mut sorted: Vec<&(String, TaskValue)> = entries.iter().collect();
703            sorted.sort_by(|x, y| x.0.cmp(&y.0));
704            out.extend_from_slice(&(sorted.len() as u64).to_be_bytes());
705            for (k, v) in sorted {
706                out.extend_from_slice(&(k.len() as u64).to_be_bytes());
707                out.extend_from_slice(k.as_bytes());
708                let encoded = canonical_value_bytes(v);
709                out.extend_from_slice(&(encoded.len() as u64).to_be_bytes());
710                out.extend_from_slice(&encoded);
711            }
712        }
713    }
714    out
715}
716
717// ===========================================================================
718// Signature value
719// ===========================================================================
720
721/// Algorithm identifier carried alongside a signature for forward
722/// compatibility.
723#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
724pub enum SignatureAlgorithm {
725    /// HMAC with SHA-256.
726    #[serde(rename = "HMAC-SHA256")]
727    HmacSha256,
728}
729
730impl SignatureAlgorithm {
731    /// The wire/string name of the algorithm.
732    #[must_use]
733    pub const fn as_str(self) -> &'static str {
734        match self {
735            SignatureAlgorithm::HmacSha256 => "HMAC-SHA256",
736        }
737    }
738}
739
740impl fmt::Display for SignatureAlgorithm {
741    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
742        f.write_str(self.as_str())
743    }
744}
745
746/// A computed task signature: the algorithm plus the hex-encoded MAC tag.
747///
748/// This is a serializable value that can be attached to a task message (for
749/// example, in a header or sidecar field) and round-tripped through any of the
750/// crate's serializers.
751#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
752pub struct TaskSignature {
753    /// MAC algorithm used to produce `tag`.
754    pub algorithm: SignatureAlgorithm,
755    /// Lower-case hex encoding of the MAC tag.
756    pub tag: String,
757}
758
759impl TaskSignature {
760    /// Construct an HMAC-SHA256 signature from raw tag bytes.
761    #[must_use]
762    pub fn hmac_sha256(tag: &[u8]) -> Self {
763        Self {
764            algorithm: SignatureAlgorithm::HmacSha256,
765            tag: to_hex(tag),
766        }
767    }
768
769    /// Decode the hex tag back into raw bytes.
770    ///
771    /// # Errors
772    ///
773    /// Returns [`SignatureError::MalformedSignature`] if the hex is invalid.
774    pub fn tag_bytes(&self) -> Result<Vec<u8>, SignatureError> {
775        from_hex(&self.tag)
776    }
777}
778
779impl fmt::Display for TaskSignature {
780    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
781        write!(f, "{}:{}", self.algorithm.as_str(), self.tag)
782    }
783}
784
785// ===========================================================================
786// Signer
787// ===========================================================================
788
789/// Signs and verifies task messages with a shared secret using HMAC-SHA256.
790///
791/// A `TaskSigner` holds the secret key. Producers call [`TaskSigner::sign`] to
792/// obtain a [`TaskSignature`]; consumers call [`TaskSigner::verify`] (or
793/// [`TaskSigner::verify_optional`]) to authenticate a received message.
794#[derive(Clone)]
795pub struct TaskSigner {
796    key: Vec<u8>,
797}
798
799impl fmt::Debug for TaskSigner {
800    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
801        // Never print the key.
802        f.debug_struct("TaskSigner")
803            .field("key_len", &self.key.len())
804            .finish()
805    }
806}
807
808impl TaskSigner {
809    /// Create a signer from a shared secret key.
810    ///
811    /// Any key length is accepted (HMAC handles short and long keys), but for
812    /// HMAC-SHA256 a key of at least 32 bytes of entropy is recommended.
813    #[must_use]
814    pub fn new(key: impl AsRef<[u8]>) -> Self {
815        Self {
816            key: key.as_ref().to_vec(),
817        }
818    }
819
820    /// Compute the raw MAC tag bytes over the canonical serialization.
821    #[must_use]
822    pub fn mac(&self, fields: &SignedFields) -> [u8; SHA256_DIGEST_BYTES] {
823        HmacSha256::mac(&self.key, &fields.canonical_bytes())
824    }
825
826    /// Sign a set of task fields, returning a serializable [`TaskSignature`].
827    #[must_use]
828    pub fn sign(&self, fields: &SignedFields) -> TaskSignature {
829        TaskSignature::hmac_sha256(&self.mac(fields))
830    }
831
832    /// Verify a task message against its signature.
833    ///
834    /// The comparison is constant-time. Any tampering with the signed fields,
835    /// a wrong key, or a corrupted/forged tag yields an error.
836    ///
837    /// # Errors
838    ///
839    /// * [`SignatureError::UnsupportedAlgorithm`] if the signature names an
840    ///   algorithm other than HMAC-SHA256.
841    /// * [`SignatureError::MalformedSignature`] if the tag is not valid hex or
842    ///   has the wrong length.
843    /// * [`SignatureError::Mismatch`] if the recomputed MAC does not match.
844    pub fn verify(
845        &self,
846        fields: &SignedFields,
847        signature: &TaskSignature,
848    ) -> Result<(), SignatureError> {
849        if signature.algorithm != SignatureAlgorithm::HmacSha256 {
850            return Err(SignatureError::UnsupportedAlgorithm(
851                signature.algorithm.as_str().to_string(),
852            ));
853        }
854
855        let provided = signature.tag_bytes()?;
856        if provided.len() != SHA256_DIGEST_BYTES {
857            return Err(SignatureError::MalformedSignature(format!(
858                "expected {SHA256_DIGEST_BYTES}-byte tag, got {} bytes",
859                provided.len()
860            )));
861        }
862
863        let expected = self.mac(fields);
864        if constant_time_eq(&expected, &provided) {
865            Ok(())
866        } else {
867            Err(SignatureError::Mismatch)
868        }
869    }
870
871    /// Verify a message whose signature may be absent.
872    ///
873    /// When `signature` is `None` this returns
874    /// [`SignatureError::MissingSignature`], which lets a caller distinguish an
875    /// *unsigned* message from a *badly-signed* one and reject both.
876    ///
877    /// # Errors
878    ///
879    /// Returns [`SignatureError::MissingSignature`] when no signature is
880    /// supplied, or any error from [`TaskSigner::verify`] otherwise.
881    pub fn verify_optional(
882        &self,
883        fields: &SignedFields,
884        signature: Option<&TaskSignature>,
885    ) -> Result<(), SignatureError> {
886        match signature {
887            Some(sig) => self.verify(fields, sig),
888            None => Err(SignatureError::MissingSignature),
889        }
890    }
891
892    /// Convenience predicate: `true` iff the message verifies.
893    #[must_use]
894    pub fn is_valid(&self, fields: &SignedFields, signature: &TaskSignature) -> bool {
895        self.verify(fields, signature).is_ok()
896    }
897}
898
899#[cfg(test)]
900mod tests {
901    use super::*;
902
903    // --- SHA-256 known-answer tests (FIPS 180-4 examples) ---
904
905    #[test]
906    fn sha256_empty() {
907        let d = Sha256::digest(b"");
908        assert_eq!(
909            to_hex(&d),
910            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
911        );
912    }
913
914    #[test]
915    fn sha256_abc() {
916        let d = Sha256::digest(b"abc");
917        assert_eq!(
918            to_hex(&d),
919            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
920        );
921    }
922
923    #[test]
924    fn sha256_two_block() {
925        let d = Sha256::digest(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq");
926        assert_eq!(
927            to_hex(&d),
928            "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"
929        );
930    }
931
932    #[test]
933    fn sha256_million_a() {
934        // FIPS 180-4: one million 'a' characters.
935        let mut h = Sha256::new();
936        let chunk = vec![b'a'; 1000];
937        for _ in 0..1000 {
938            h.update(&chunk);
939        }
940        let d = h.finalize();
941        assert_eq!(
942            to_hex(&d),
943            "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"
944        );
945    }
946
947    #[test]
948    fn sha256_streaming_matches_oneshot() {
949        let data = b"the quick brown fox jumps over the lazy dog";
950        let oneshot = Sha256::digest(data);
951        let mut h = Sha256::new();
952        for byte in data {
953            h.update(std::slice::from_ref(byte));
954        }
955        assert_eq!(oneshot, h.finalize());
956    }
957
958    // --- HMAC-SHA256 known-answer tests (RFC 4231) ---
959
960    #[test]
961    fn hmac_rfc4231_case1() {
962        let key = [0x0b_u8; 20];
963        let data = b"Hi There";
964        let mac = HmacSha256::mac(&key, data);
965        assert_eq!(
966            to_hex(&mac),
967            "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
968        );
969    }
970
971    #[test]
972    fn hmac_rfc4231_case2() {
973        let key = b"Jefe";
974        let data = b"what do ya want for nothing?";
975        let mac = HmacSha256::mac(key, data);
976        assert_eq!(
977            to_hex(&mac),
978            "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
979        );
980    }
981
982    #[test]
983    fn hmac_rfc4231_case3() {
984        let key = [0xaa_u8; 20];
985        let data = [0xdd_u8; 50];
986        let mac = HmacSha256::mac(&key, &data);
987        assert_eq!(
988            to_hex(&mac),
989            "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe"
990        );
991    }
992
993    #[test]
994    fn hmac_rfc4231_case6_long_key() {
995        // Key longer than the block size must be hashed first.
996        let key = [0xaa_u8; 131];
997        let data = b"Test Using Larger Than Block-Size Key - Hash Key First";
998        let mac = HmacSha256::mac(&key, data);
999        assert_eq!(
1000            to_hex(&mac),
1001            "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"
1002        );
1003    }
1004
1005    // --- hex helpers ---
1006
1007    #[test]
1008    fn hex_roundtrip() {
1009        let bytes = [0x00, 0x0f, 0xa5, 0xff, 0x10];
1010        let s = to_hex(&bytes);
1011        assert_eq!(s, "000fa5ff10");
1012        assert_eq!(from_hex(&s).unwrap(), bytes);
1013        // Mixed case decoding.
1014        assert_eq!(
1015            from_hex("00Fa5Fff10").unwrap(),
1016            [0x00, 0xfa, 0x5f, 0xff, 0x10]
1017        );
1018    }
1019
1020    #[test]
1021    fn hex_rejects_bad_input() {
1022        assert!(from_hex("abc").is_err()); // odd length
1023        assert!(from_hex("zz").is_err()); // non-hex
1024    }
1025
1026    #[test]
1027    fn constant_time_eq_works() {
1028        assert!(constant_time_eq(b"abcd", b"abcd"));
1029        assert!(!constant_time_eq(b"abcd", b"abce"));
1030        assert!(!constant_time_eq(b"abc", b"abcd"));
1031    }
1032
1033    // --- sign / verify round-trip ---
1034
1035    fn sample_fields() -> SignedFields {
1036        SignedFields::new(Uuid::from_u128(0x1234_5678_9abc_def0), "tasks.add")
1037            .with_args(vec![TaskValue::from(2_i64), TaskValue::from(3_i64)])
1038            .with_kwarg("note", TaskValue::from("hello"))
1039            .with_kwarg("flag", TaskValue::from(true))
1040    }
1041
1042    #[test]
1043    fn sign_verify_roundtrip() {
1044        let signer = TaskSigner::new(b"shared-secret-key-shared-secret!");
1045        let fields = sample_fields();
1046        let sig = signer.sign(&fields);
1047        assert_eq!(sig.algorithm, SignatureAlgorithm::HmacSha256);
1048        assert!(signer.verify(&fields, &sig).is_ok());
1049        assert!(signer.is_valid(&fields, &sig));
1050    }
1051
1052    #[test]
1053    fn kwarg_order_does_not_matter() {
1054        let signer = TaskSigner::new(b"key");
1055        let a = SignedFields::new(Uuid::nil(), "t")
1056            .with_kwarg("a", TaskValue::from(1_i64))
1057            .with_kwarg("b", TaskValue::from(2_i64));
1058        let b = SignedFields::new(Uuid::nil(), "t")
1059            .with_kwarg("b", TaskValue::from(2_i64))
1060            .with_kwarg("a", TaskValue::from(1_i64));
1061        assert_eq!(signer.mac(&a), signer.mac(&b));
1062    }
1063
1064    #[test]
1065    fn tamper_args_detected() {
1066        let signer = TaskSigner::new(b"key");
1067        let fields = sample_fields();
1068        let sig = signer.sign(&fields);
1069
1070        let tampered = SignedFields::new(fields.id, &fields.name)
1071            .with_args(vec![TaskValue::from(2_i64), TaskValue::from(4_i64)])
1072            .with_kwarg("note", TaskValue::from("hello"))
1073            .with_kwarg("flag", TaskValue::from(true));
1074        assert_eq!(
1075            signer.verify(&tampered, &sig),
1076            Err(SignatureError::Mismatch)
1077        );
1078    }
1079
1080    #[test]
1081    fn tamper_name_detected() {
1082        let signer = TaskSigner::new(b"key");
1083        let fields = sample_fields();
1084        let sig = signer.sign(&fields);
1085        let mut tampered = fields.clone();
1086        tampered.name = "tasks.subtract".to_string();
1087        assert!(signer.verify(&tampered, &sig).is_err());
1088    }
1089
1090    #[test]
1091    fn tamper_id_detected() {
1092        let signer = TaskSigner::new(b"key");
1093        let fields = sample_fields();
1094        let sig = signer.sign(&fields);
1095        let mut tampered = fields.clone();
1096        tampered.id = Uuid::from_u128(0xdead_beef);
1097        assert!(signer.verify(&tampered, &sig).is_err());
1098    }
1099
1100    #[test]
1101    fn tamper_kwarg_value_detected() {
1102        let signer = TaskSigner::new(b"key");
1103        let fields = sample_fields();
1104        let sig = signer.sign(&fields);
1105        let tampered = SignedFields::new(fields.id, &fields.name)
1106            .with_args(fields.args.clone())
1107            .with_kwarg("note", TaskValue::from("HELLO"))
1108            .with_kwarg("flag", TaskValue::from(true));
1109        assert!(signer.verify(&tampered, &sig).is_err());
1110    }
1111
1112    #[test]
1113    fn wrong_key_detected() {
1114        let signer = TaskSigner::new(b"key-one");
1115        let other = TaskSigner::new(b"key-two");
1116        let fields = sample_fields();
1117        let sig = signer.sign(&fields);
1118        assert_eq!(other.verify(&fields, &sig), Err(SignatureError::Mismatch));
1119    }
1120
1121    #[test]
1122    fn unsigned_rejected() {
1123        let signer = TaskSigner::new(b"key");
1124        let fields = sample_fields();
1125        assert_eq!(
1126            signer.verify_optional(&fields, None),
1127            Err(SignatureError::MissingSignature)
1128        );
1129        // And a signed message still passes through verify_optional.
1130        let sig = signer.sign(&fields);
1131        assert!(signer.verify_optional(&fields, Some(&sig)).is_ok());
1132    }
1133
1134    #[test]
1135    fn malformed_signature_rejected() {
1136        let signer = TaskSigner::new(b"key");
1137        let fields = sample_fields();
1138
1139        let bad_hex = TaskSignature {
1140            algorithm: SignatureAlgorithm::HmacSha256,
1141            tag: "nothex!!".to_string(),
1142        };
1143        assert!(matches!(
1144            signer.verify(&fields, &bad_hex),
1145            Err(SignatureError::MalformedSignature(_))
1146        ));
1147
1148        let wrong_len = TaskSignature {
1149            algorithm: SignatureAlgorithm::HmacSha256,
1150            tag: "aabb".to_string(),
1151        };
1152        assert!(matches!(
1153            signer.verify(&fields, &wrong_len),
1154            Err(SignatureError::MalformedSignature(_))
1155        ));
1156    }
1157
1158    #[test]
1159    fn distinct_types_do_not_collide() {
1160        // Integer 1, string "1", and bool true must canonicalize differently.
1161        let signer = TaskSigner::new(b"key");
1162        let as_int = SignedFields::new(Uuid::nil(), "t").with_arg(TaskValue::from(1_i64));
1163        let as_str = SignedFields::new(Uuid::nil(), "t").with_arg(TaskValue::from("1"));
1164        let as_bool = SignedFields::new(Uuid::nil(), "t").with_arg(TaskValue::from(true));
1165        let m_int = signer.mac(&as_int);
1166        let m_str = signer.mac(&as_str);
1167        let m_bool = signer.mac(&as_bool);
1168        assert_ne!(m_int, m_str);
1169        assert_ne!(m_int, m_bool);
1170        assert_ne!(m_str, m_bool);
1171    }
1172
1173    #[test]
1174    fn arg_boundary_not_ambiguous() {
1175        // ["a", "b"] must not collide with ["ab"] thanks to length prefixing.
1176        let signer = TaskSigner::new(b"key");
1177        let split = SignedFields::new(Uuid::nil(), "t")
1178            .with_args(vec![TaskValue::from("a"), TaskValue::from("b")]);
1179        let joined = SignedFields::new(Uuid::nil(), "t").with_arg(TaskValue::from("ab"));
1180        assert_ne!(signer.mac(&split), signer.mac(&joined));
1181    }
1182
1183    #[test]
1184    fn signature_serde_roundtrip() {
1185        let signer = TaskSigner::new(b"key");
1186        let fields = sample_fields();
1187        let sig = signer.sign(&fields);
1188        let json = serde_json::to_string(&sig).expect("serialize");
1189        let restored: TaskSignature = serde_json::from_str(&json).expect("deserialize");
1190        assert_eq!(sig, restored);
1191        assert!(signer.verify(&fields, &restored).is_ok());
1192    }
1193
1194    #[test]
1195    fn signature_display_and_debug_do_not_leak_key() {
1196        let signer = TaskSigner::new(b"top-secret-key-material-here!!!!");
1197        let dbg = format!("{signer:?}");
1198        assert!(!dbg.contains("top-secret"));
1199        assert!(dbg.contains("key_len"));
1200    }
1201}