Skip to main content

aitp_core/
jcs.rs

1//! RFC 8785 JSON Canonicalization Scheme (JCS).
2//!
3//! AITP signatures are computed over JCS-canonical JSON. JCS specifies
4//! deterministic serialization with no whitespace, lexicographically sorted
5//! object keys (UTF-16 code-unit ordering), ECMAScript-style number formatting,
6//! and well-defined Unicode handling.
7//!
8//! This module wraps the `serde_jcs` crate. We may fork or replace the backing
9//! crate if we discover correctness gaps; the public API here is the stable
10//! contract that protocol crates depend on.
11//!
12//! See [`docs/jcs.md`](../../../../docs/jcs.md) for
13//! the test vector strategy.
14
15use serde_json::Value;
16use sha2::{Digest, Sha256};
17
18/// Errors that can occur during canonicalization.
19#[derive(Debug, thiserror::Error)]
20#[non_exhaustive]
21pub enum JcsError {
22    /// JSON contained a non-finite number (NaN or Infinity); RFC 8785 forbids these.
23    #[error("number is not finite (NaN or Infinity is not permitted in JSON)")]
24    NonFiniteNumber,
25
26    /// Reserved. Duplicate object keys are *not* detected here: this
27    /// module canonicalizes an already-parsed [`serde_json::Value`], and
28    /// `serde_json` (built with `preserve_order`) collapses duplicate
29    /// keys last-wins at parse time, before canonicalization. Since both
30    /// signer and verifier canonicalize the same parsed value, there is
31    /// no signature split-brain (RFC 8785 operates on parsed JSON). This
32    /// variant is retained for API stability and is never constructed by
33    /// [`canonicalize`]; reject duplicate keys at a raw-bytes
34    /// deserialization step if a deployment's threat model requires it.
35    #[error("duplicate key '{0}' in JSON object")]
36    DuplicateKey(String),
37
38    /// Underlying serde error.
39    #[error("serialization failed: {0}")]
40    Serde(#[from] serde_json::Error),
41}
42
43/// Serialize a JSON value to RFC 8785 canonical JSON bytes.
44///
45/// The output is suitable as a signing input. Two implementations of JCS
46/// MUST produce byte-identical output for the same logical JSON document.
47pub fn canonicalize(value: &Value) -> Result<Vec<u8>, JcsError> {
48    serde_jcs::to_vec(value).map_err(JcsError::from)
49}
50
51/// Convenience: canonicalize any serde-Serializable value.
52pub fn canonicalize_serializable<T: serde::Serialize>(value: &T) -> Result<Vec<u8>, JcsError> {
53    serde_jcs::to_vec(value).map_err(JcsError::from)
54}
55
56/// Compute the SHA-256 of the canonical JSON.
57///
58/// This is the standard signing input for AITP signatures: every signed object
59/// is canonicalized then hashed, and the hash is signed with Ed25519.
60pub fn canonicalize_and_hash<T: serde::Serialize>(value: &T) -> Result<[u8; 32], JcsError> {
61    let bytes = canonicalize_serializable(value)?;
62    let digest = Sha256::digest(&bytes);
63    Ok(digest.into())
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use serde_json::json;
70
71    #[test]
72    fn canonicalize_empty_object() {
73        let v: Value = json!({});
74        let out = canonicalize(&v).unwrap();
75        assert_eq!(std::str::from_utf8(&out).unwrap(), "{}");
76    }
77
78    #[test]
79    fn canonicalize_sorts_keys() {
80        let v: Value = json!({"b": 1, "a": 2});
81        let out = canonicalize(&v).unwrap();
82        assert_eq!(std::str::from_utf8(&out).unwrap(), r#"{"a":2,"b":1}"#);
83    }
84
85    // Full test vector suite lives in tests/jcs_standard_vectors.rs and
86    // tests/aitp_signing_vectors.rs.
87}