cellos-core 0.8.0-pre

CellOS domain types and ports — typed authority, formation DAG, CloudEvent envelopes, RBAC primitives. No I/O.
Documentation
//! Deterministic canonical-JSON encoding for cryptographic signing payloads
//! (ADR-0028).
//!
//! ## Why this exists
//!
//! Signing and verifying must agree on **exactly** the bytes that are hashed.
//! The historical [`crate::trust_keys::canonical_event_signing_payload`]
//! serialized a [`CloudEventV1`] with `serde_json::to_vec`, which emits keys in
//! **struct field-declaration order**. That made the signed byte stream a
//! hostage to the Rust source: re-ordering a `struct` field, or adding a new
//! `#[serde(flatten)]`-ed optional attribute, could silently shift the signed
//! bytes for events that do not even populate the new field, breaking offline
//! verification of receipts emitted by an older build.
//!
//! This module replaces that with a **sorted-keys (JCS-style, RFC 8785-aligned)**
//! canonicalizer: every JSON object has its members emitted in lexicographic
//! order of their keys (by Unicode scalar / UTF-8 byte order, which coincide for
//! the ASCII attribute names cellos uses), recursively, with no insignificant
//! whitespace. The result is a function of the *logical* JSON value only — not
//! of declaration order — so:
//!
//!   * two independent serializers of the same logical object produce
//!     byte-identical output, and
//!   * adding an optional field that a given object **omits** cannot change that
//!     object's canonical bytes.
//!
//! ## Versioning
//!
//! The encoding is versioned via [`CANONICAL_PAYLOAD_VERSION`]. The current
//! version is `1` = "sorted-keys JCS-style". The version is **not** mixed into
//! the byte stream (so it does not perturb existing signatures whose verifier
//! uses the same encoder); it exists so a future, wire-incompatible
//! canonicalization (e.g. a different number normalization for a new signed
//! artifact) can be selected explicitly per ADR-0028 §Deferred without an
//! ambiguous silent break.
//!
//! ## Back-compat note (ADR-0028)
//!
//! cellos signs and verifies with the *same* encoder at runtime, so swapping
//! `serde_json::to_vec` for this sorted-keys encoder is a transparent
//! round-trip change for live signing: every existing
//! `sign → verify` test stays green because both halves move together. No
//! pinned known-answer byte vector existed in the suite to update. The one
//! behavioural guarantee this *adds* is the omitted-optional-field stability
//! exercised by the doc-test below.
//!
//! ## Scope
//!
//! This is a JSON canonicalizer for the value space cellos actually signs
//! (CloudEvents and purpose-built signing CloudEvents: objects, strings, bools,
//! null, arrays, and integers/finite numbers that originate from `serde_json`).
//! It does **not** attempt full RFC 8785 ECMAScript number serialization for
//! arbitrary floats; cellos signing payloads do not carry non-integer numbers,
//! and `serde_json`'s own number formatting is deterministic for the integers
//! that do appear. A non-finite float is rejected rather than silently encoded.

use serde::Serialize;
use serde_json::Value;

use crate::error::CellosError;

/// Canonical signing-payload encoding version (ADR-0028).
///
/// `1` = sorted-keys, JCS-style (RFC 8785-aligned), no insignificant
/// whitespace, recursive object-key ordering. See the module docs.
pub const CANONICAL_PAYLOAD_VERSION: u32 = 1;

/// Serialize `value` to deterministic canonical-JSON bytes (sorted object keys,
/// no insignificant whitespace).
///
/// This is the single canonical encoder every cellos signer/verifier routes
/// through (ADR-0028). The output is a pure function of the logical JSON value:
/// independent serializers of the same value agree byte-for-byte, and omitting
/// an optional field never changes the bytes of objects that omit it.
///
/// # Errors
///
/// Returns [`CellosError::InvalidSpec`] if `value` contains a non-finite
/// floating-point number (which has no canonical JSON form).
///
/// # Examples
///
/// A fixed logical object always produces the same fixed canonical bytes, and
/// adding an optional field that the object omits does not change them:
///
/// ```
/// use cellos_core::canonical::canonical_json_bytes;
/// use serde_json::json;
///
/// // Keys deliberately supplied out of lexicographic order.
/// let obj = json!({ "b": 2, "a": 1, "nested": { "y": true, "x": "v" } });
/// let bytes = canonical_json_bytes(&obj).expect("canonical");
/// assert_eq!(
///     bytes,
///     br#"{"a":1,"b":2,"nested":{"x":"v","y":true}}"#.to_vec(),
/// );
///
/// // The SAME logical object expressed with a different insertion order is
/// // byte-identical — canonicalization depends only on the value.
/// let reordered = json!({ "nested": { "x": "v", "y": true }, "a": 1, "b": 2 });
/// assert_eq!(canonical_json_bytes(&reordered).unwrap(), bytes);
///
/// // An optional field that is omitted (absent key) never appears, so an
/// // object that omits it is byte-identical to one that never had it.
/// let without = json!({ "a": 1, "b": 2, "nested": { "x": "v", "y": true } });
/// assert_eq!(canonical_json_bytes(&without).unwrap(), bytes);
/// ```
pub fn canonical_json_bytes(value: &Value) -> Result<Vec<u8>, CellosError> {
    let mut out = Vec::new();
    write_canonical(value, &mut out)?;
    Ok(out)
}

/// Serialize any [`Serialize`] type to deterministic canonical-JSON bytes.
///
/// Convenience wrapper that first lowers `value` into a [`serde_json::Value`]
/// (so `#[serde(flatten)]`, `skip_serializing_if`, and `rename_all` are all
/// honored exactly as the type declares) and then canonicalizes. This is the
/// entry point signers use for typed payloads such as `CloudEventV1`.
///
/// # Errors
///
/// Returns [`CellosError::InvalidSpec`] if `value` fails to serialize or
/// produces a non-finite number.
pub fn canonical_payload<T: Serialize>(value: &T) -> Result<Vec<u8>, CellosError> {
    let json = serde_json::to_value(value)
        .map_err(|e| CellosError::InvalidSpec(format!("canonical_payload: serialize: {e}")))?;
    canonical_json_bytes(&json)
}

/// Recursively write the canonical form of `value` into `out`.
fn write_canonical(value: &Value, out: &mut Vec<u8>) -> Result<(), CellosError> {
    match value {
        Value::Null => out.extend_from_slice(b"null"),
        Value::Bool(b) => out.extend_from_slice(if *b { b"true" } else { b"false" }),
        Value::Number(n) => {
            // A non-integer `serde_json::Number` carries an `f64`; reject any
            // non-finite value so it can never be silently signed. (serde_json
            // already represents NaN/Inf as `Null`, but guard explicitly.)
            if let Some(f) = n.as_f64() {
                if n.as_i64().is_none() && n.as_u64().is_none() && !f.is_finite() {
                    return Err(CellosError::InvalidSpec(
                        "canonical json: non-finite number has no canonical form".into(),
                    ));
                }
            }
            // Integer and finite-number formatting is deterministic in
            // serde_json; reuse it rather than re-implementing radix output.
            out.extend_from_slice(n.to_string().as_bytes());
        }
        Value::String(s) => write_json_string(s, out),
        Value::Array(items) => {
            out.push(b'[');
            for (i, item) in items.iter().enumerate() {
                if i > 0 {
                    out.push(b',');
                }
                write_canonical(item, out)?;
            }
            out.push(b']');
        }
        Value::Object(map) => {
            // Collect and sort keys lexicographically by their Unicode scalar
            // sequence. For the ASCII attribute names cellos uses this is the
            // same as UTF-8 byte order; `str`'s `Ord` is codepoint order which
            // is the RFC 8785 requirement.
            let mut keys: Vec<&String> = map.keys().collect();
            keys.sort_unstable();
            out.push(b'{');
            for (i, key) in keys.iter().enumerate() {
                if i > 0 {
                    out.push(b',');
                }
                write_json_string(key, out);
                out.push(b':');
                // `keys` came from `map`, so the lookup cannot miss.
                write_canonical(&map[key.as_str()], out)?;
            }
            out.push(b'}');
        }
    }
    Ok(())
}

/// Write a JSON string literal (with surrounding quotes and minimal escaping)
/// matching `serde_json`'s compact escaping so canonical output is
/// byte-identical to a compact `serde_json` serialization of the same string.
fn write_json_string(s: &str, out: &mut Vec<u8>) {
    out.push(b'"');
    for c in s.chars() {
        match c {
            '"' => out.extend_from_slice(b"\\\""),
            '\\' => out.extend_from_slice(b"\\\\"),
            '\n' => out.extend_from_slice(b"\\n"),
            '\r' => out.extend_from_slice(b"\\r"),
            '\t' => out.extend_from_slice(b"\\t"),
            '\u{08}' => out.extend_from_slice(b"\\b"),
            '\u{0c}' => out.extend_from_slice(b"\\f"),
            c if (c as u32) < 0x20 => {
                // Other control characters: \u00XX lower-hex, matching serde_json.
                let code = c as u32;
                out.extend_from_slice(format!("\\u{code:04x}").as_bytes());
            }
            c => {
                let mut buf = [0u8; 4];
                out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
            }
        }
    }
    out.push(b'"');
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn sorts_object_keys_recursively() {
        let v = json!({ "c": 3, "a": 1, "b": { "z": 26, "a": 1 } });
        let bytes = canonical_json_bytes(&v).unwrap();
        assert_eq!(bytes, br#"{"a":1,"b":{"a":1,"z":26},"c":3}"#.to_vec());
    }

    #[test]
    fn independent_serializers_agree() {
        // Two logically-equal objects built with different insertion order.
        let a = json!({ "x": [1, 2, 3], "y": "v", "z": null });
        let b = json!({ "z": null, "y": "v", "x": [1, 2, 3] });
        assert_eq!(
            canonical_json_bytes(&a).unwrap(),
            canonical_json_bytes(&b).unwrap()
        );
    }

    #[test]
    fn omitted_optional_field_does_not_change_bytes() {
        // An object that omits an optional key is byte-identical to the same
        // object that never carried it. (A `None` serde field serializes to an
        // absent key under `skip_serializing_if`, i.e. it simply is not in the
        // map — this models that.)
        let with_omitted = json!({ "a": 1, "b": 2 });
        let baseline = json!({ "b": 2, "a": 1 });
        assert_eq!(
            canonical_json_bytes(&with_omitted).unwrap(),
            canonical_json_bytes(&baseline).unwrap()
        );

        // Whereas POPULATING the optional field DOES change the bytes (sanity:
        // we are not dropping data).
        let with_present = json!({ "a": 1, "b": 2, "c": 3 });
        assert_ne!(
            canonical_json_bytes(&with_present).unwrap(),
            canonical_json_bytes(&baseline).unwrap()
        );
    }

    #[test]
    fn array_order_is_preserved() {
        let a = json!([3, 1, 2]);
        assert_eq!(canonical_json_bytes(&a).unwrap(), b"[3,1,2]".to_vec());
    }

    #[test]
    fn string_escaping_matches_serde_json_compact() {
        let v = json!({ "k": "a\"b\\c\n\t\u{01}d" });
        // serde_json compact serialization is the byte reference for a
        // single-key object (one key => key order is irrelevant).
        let reference = serde_json::to_vec(&v).unwrap();
        assert_eq!(canonical_json_bytes(&v).unwrap(), reference);
    }

    #[test]
    fn unicode_passes_through_unescaped() {
        let v = json!({ "k": "héllo-世界" });
        let reference = serde_json::to_vec(&v).unwrap();
        assert_eq!(canonical_json_bytes(&v).unwrap(), reference);
    }

    #[test]
    fn canonical_payload_lowers_typed_value() {
        #[derive(serde::Serialize)]
        struct T {
            b: u8,
            a: u8,
        }
        let bytes = canonical_payload(&T { b: 2, a: 1 }).unwrap();
        assert_eq!(bytes, br#"{"a":1,"b":2}"#.to_vec());
    }

    #[test]
    fn version_is_one() {
        assert_eq!(CANONICAL_PAYLOAD_VERSION, 1);
    }
}