nixfleet_canonicalize/
lib.rs

1#![allow(clippy::doc_lazy_continuation)]
2//! JCS canonicalization (RFC 8785). LOADBEARING: every signer and verifier
3//! routes through here - do not reimplement, drift invalidates signatures
4//! fleet-wide.
5
6use anyhow::{Context, Result};
7use serde::Serialize;
8use sha2::Digest;
9
10/// JSON string -> JCS (RFC 8785) canonical form.
11pub fn canonicalize(input: &str) -> Result<String> {
12    let value: serde_json::Value =
13        serde_json::from_str(input).context("input is not valid JSON")?;
14    serde_jcs::to_string(&value).context("JCS canonicalization failed")
15}
16
17/// Hex-lowercase SHA-256 of `value`'s JCS-canonical bytes.
18pub fn sha256_jcs_hex<T: Serialize>(value: &T) -> Result<String> {
19    let canonical = serde_jcs::to_vec(value).context("JCS canonicalization failed")?;
20    let digest = sha2::Sha256::digest(&canonical);
21    Ok(hex::encode(digest))
22}
23
24#[cfg(test)]
25mod tests {
26    use super::*;
27
28    #[test]
29    fn sha256_jcs_hex_string_value_is_stable() {
30        let a = sha256_jcs_hex(&"hello").unwrap();
31        let b = sha256_jcs_hex(&"hello").unwrap();
32        assert_eq!(a, b);
33        assert_eq!(a.len(), 64);
34    }
35
36    #[test]
37    fn sha256_jcs_hex_struct_value_is_stable() {
38        #[derive(Serialize)]
39        struct S<'a> {
40            host: &'a str,
41            count: u32,
42        }
43        let a = sha256_jcs_hex(&S {
44            host: "host-02",
45            count: 7,
46        })
47        .unwrap();
48        let b = sha256_jcs_hex(&S {
49            host: "host-02",
50            count: 7,
51        })
52        .unwrap();
53        assert_eq!(a, b);
54    }
55
56    #[test]
57    fn sha256_jcs_hex_empty_string_is_distinct_from_other_input() {
58        let empty = sha256_jcs_hex(&"").unwrap();
59        let nonempty = sha256_jcs_hex(&"x").unwrap();
60        assert_ne!(empty, nonempty);
61        assert_eq!(empty.len(), 64);
62    }
63}