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}