use serde_json::{Map, Number, Value};
use sha2::{Digest, Sha256};
use std::fmt;
pub const MAX_SAFE_INTEGER: i64 = (1i64 << 53) - 1;
pub const CANONICALIZATION_JCS: &str = "jcs";
pub fn is_local_only_field(key: &str) -> bool {
key == "signature" || key == "key_id"
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CanonicalError {
Float(String),
UnsafeInt(String),
UnsupportedType,
NotFormat4,
CanonicalizationIdMissing,
CanonicalizationIdNotString,
UnsupportedCanonicalizationId(String),
}
impl fmt::Display for CanonicalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CanonicalError::Float(path) => write!(
f,
"float at {path}: §5.1 forbids floating-point values in digest-bearing fields"
),
CanonicalError::UnsafeInt(path) => write!(
f,
"unsafe integer at {path}: outside ±{MAX_SAFE_INTEGER} (§5.1)"
),
CanonicalError::UnsupportedType => write!(f, "unsupported type in JCS serialization"),
CanonicalError::NotFormat4 => write!(f, "format_version must be \"4\""),
CanonicalError::CanonicalizationIdMissing => {
write!(f, "canonicalization_id is required")
}
CanonicalError::CanonicalizationIdNotString => {
write!(f, "canonicalization_id must be a string")
}
CanonicalError::UnsupportedCanonicalizationId(id) => {
write!(f, "unsupported canonicalization_id {id:?}")
}
}
}
}
pub fn is_float(n: &Number) -> bool {
let s = n.to_string();
s.contains(['.', 'e', 'E'])
}
pub fn is_unsafe_int(n: &Number) -> bool {
let s = n.to_string();
let digits = s.strip_prefix('-').unwrap_or(&s);
match digits.len().cmp(&16) {
std::cmp::Ordering::Greater => true,
std::cmp::Ordering::Less => false,
std::cmp::Ordering::Equal => digits > "9007199254740991",
}
}
fn compare_utf16(a: &str, b: &str) -> std::cmp::Ordering {
let au: Vec<u16> = a.encode_utf16().collect();
let bu: Vec<u16> = b.encode_utf16().collect();
au.cmp(&bu)
}
fn sorted_keys(m: &Map<String, Value>) -> Vec<&String> {
let mut keys: Vec<&String> = m.keys().collect();
keys.sort_by(|a, b| compare_utf16(a, b));
keys
}
fn jcs_string(s: &str, out: &mut String) {
out.push('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\u{8}' => out.push_str("\\b"),
'\t' => out.push_str("\\t"),
'\n' => out.push_str("\\n"),
'\u{c}' => out.push_str("\\f"),
'\r' => out.push_str("\\r"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
}
fn jcs_value(v: &Value, out: &mut String) -> Result<(), CanonicalError> {
match v {
Value::Null => {
out.push_str("null");
Ok(())
}
Value::Bool(b) => {
out.push_str(if *b { "true" } else { "false" });
Ok(())
}
Value::String(s) => {
jcs_string(s, out);
Ok(())
}
Value::Number(n) => {
if is_float(n) {
return Err(CanonicalError::Float(String::new()));
}
if is_unsafe_int(n) {
return Err(CanonicalError::UnsafeInt(String::new()));
}
let text = n.to_string();
out.push_str(if text == "-0" { "0" } else { &text });
Ok(())
}
Value::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
jcs_value(item, out)?;
}
out.push(']');
Ok(())
}
Value::Object(map) => {
out.push('{');
for (i, key) in sorted_keys(map).into_iter().enumerate() {
if i > 0 {
out.push(',');
}
jcs_string(key, out);
out.push(':');
jcs_value(&map[key], out)?;
}
out.push('}');
Ok(())
}
}
}
pub fn jcs(v: &Value) -> Result<Vec<u8>, CanonicalError> {
let mut out = String::new();
jcs_value(v, &mut out)?;
Ok(out.into_bytes())
}
pub fn json_digest(v: &Value) -> Result<String, CanonicalError> {
let bytes = jcs(v)?;
let hash = Sha256::digest(&bytes);
Ok(hex::encode(hash))
}
pub fn normalize(v: &Value) -> Value {
match v {
Value::Object(map) => {
let mut out = Map::with_capacity(map.len());
for (key, value) in map {
let normalized = normalize(value);
let empty = match &normalized {
Value::Null => true,
Value::Array(items) => items.is_empty(),
Value::Object(members) => members.is_empty(),
_ => false,
};
if !empty {
out.insert(key.clone(), normalized);
}
}
Value::Object(out)
}
Value::Array(items) => Value::Array(items.iter().map(normalize).collect()),
other => other.clone(),
}
}
pub fn compute_capsule_id(capsule: &Map<String, Value>) -> Result<String, CanonicalError> {
match capsule.get("format_version") {
Some(Value::String(v)) if v == "4" => {}
_ => return Err(CanonicalError::NotFormat4),
}
let algorithm = match capsule.get("canonicalization_id") {
None => return Err(CanonicalError::CanonicalizationIdMissing),
Some(Value::String(s)) => s,
Some(_) => return Err(CanonicalError::CanonicalizationIdNotString),
};
if algorithm != CANONICALIZATION_JCS {
return Err(CanonicalError::UnsupportedCanonicalizationId(
algorithm.clone(),
));
}
let mut canonical = Map::with_capacity(capsule.len());
for (k, v) in capsule {
if k == "capsule_id" || is_local_only_field(k) {
continue;
}
canonical.insert(k.clone(), v.clone());
}
json_digest(&Value::Object(canonical))
}
pub fn float_paths(v: &Value, path: &str) -> Vec<String> {
walk_paths(v, path, is_float, usize::MAX, usize::MAX)
}
pub fn unsafe_int_paths(v: &Value, path: &str) -> Vec<String> {
walk_paths(v, path, is_unsafe_only, usize::MAX, usize::MAX)
}
pub(crate) fn float_paths_limited(v: &Value, limit: usize, max_chars: usize) -> Vec<String> {
walk_paths(v, "", is_float, limit, max_chars)
}
pub(crate) fn unsafe_int_paths_limited(v: &Value, limit: usize, max_chars: usize) -> Vec<String> {
walk_paths(v, "", is_unsafe_only, limit, max_chars)
}
fn is_unsafe_only(n: &Number) -> bool {
!is_float(n) && is_unsafe_int(n)
}
enum Step<'a> {
Root,
Key(&'a str),
Index(usize),
}
struct Frame<'a> {
step: Step<'a>,
children: Children<'a>,
}
enum Children<'a> {
Object(std::vec::IntoIter<(&'a String, &'a Value)>),
Array(std::iter::Enumerate<std::slice::Iter<'a, Value>>),
}
fn walk_paths(
root: &Value,
root_path: &str,
matches: impl Fn(&Number) -> bool,
limit: usize,
max_chars: usize,
) -> Vec<String> {
let mut out = Vec::new();
let mut stack: Vec<Frame<'_>> = Vec::new();
if limit == 0 {
return out;
}
if let Some(children) = children_of(root) {
stack.push(Frame {
step: Step::Root,
children,
});
} else if matches!(root, Value::Number(n) if matches(n)) {
out.push(join(root_path, &stack, None, max_chars));
return out;
}
while let Some(frame) = stack.last_mut() {
let (child, step) = match &mut frame.children {
Children::Object(members) => match members.next() {
Some((key, child)) => (child, Step::Key(key.as_str())),
None => {
stack.pop();
continue;
}
},
Children::Array(items) => match items.next() {
Some((i, child)) => (child, Step::Index(i)),
None => {
stack.pop();
continue;
}
},
};
if let Some(children) = children_of(child) {
stack.push(Frame { step, children });
} else if matches!(child, Value::Number(n) if matches(n)) {
out.push(join(root_path, &stack, Some(&step), max_chars));
if out.len() >= limit {
break;
}
}
}
out
}
fn children_of(v: &Value) -> Option<Children<'_>> {
match v {
Value::Object(map) => {
let mut members: Vec<(&String, &Value)> = map.iter().collect();
members.sort_by(|a, b| a.0.cmp(b.0));
Some(Children::Object(members.into_iter()))
}
Value::Array(items) => Some(Children::Array(items.iter().enumerate())),
_ => None,
}
}
fn join(root_path: &str, stack: &[Frame<'_>], last: Option<&Step<'_>>, max_chars: usize) -> String {
let mut path = Clipped::new(max_chars);
path.push(root_path);
let steps = stack.iter().map(|f| &f.step).chain(last);
for step in steps {
if path.full {
break;
}
match step {
Step::Root => {}
Step::Key(key) => {
if !path.text.is_empty() {
path.push(".");
}
path.push(key);
}
Step::Index(i) => path.push(&format!("[{i}]")),
}
}
if path.text.is_empty() && !path.full {
return "<root>".to_string();
}
if path.full {
path.text.push_str("...");
}
path.text
}
struct Clipped {
text: String,
room: usize,
full: bool,
}
impl Clipped {
fn new(room: usize) -> Self {
Clipped {
text: String::new(),
room,
full: false,
}
}
fn push(&mut self, s: &str) {
if self.full {
return;
}
match s.char_indices().nth(self.room) {
Some((cut, _)) => {
self.text.push_str(&s[..cut]);
self.room = 0;
self.full = true;
}
None => {
self.room -= s.chars().count();
self.text.push_str(s);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn normalize_removes_absent_members_bottom_up() {
let raw = json!({
"a": null, "b": [], "c": {}, "d": {"e": null, "f": {"g": []}},
"h": [null, {}, []], "i": 0, "j": "", "k": false,
});
assert_eq!(
normalize(&raw),
json!({"h": [null, {}, []], "i": 0, "j": "", "k": false})
);
assert_eq!(normalize(&json!(null)), json!(null));
}
#[test]
fn jcs_key_order_uses_utf16_code_units_not_scalar_values() {
let v = json!({ "\u{ffff}": 1, "\u{10000}": 2 });
let out = String::from_utf8(jcs(&v).expect("no floats/unsafe ints")).expect("utf8");
let astral_pos = out.find("\u{10000}").expect("astral key present");
let bmp_pos = out.find("\u{ffff}").expect("bmp key present");
assert!(
astral_pos < bmp_pos,
"expected {out:?} to order the astral key first"
);
}
#[test]
fn negative_zero_serializes_as_zero() {
let v: Value = serde_json::from_str(r#"{"a":-0}"#).expect("json");
assert_eq!(jcs(&v).expect("jcs"), br#"{"a":0}"#);
}
#[test]
fn number_paths_walk_keys_in_byte_order() {
let v: Value =
serde_json::from_str(r#"{"b":[1.5,{"\uffff":1,"\ud800\udc00":2.5}],"a":1e2}"#)
.expect("json");
assert_eq!(float_paths(&v, ""), ["a", "b[0]", "b[1].\u{10000}"]);
assert_eq!(float_paths(&Value::from(1.5), ""), ["<root>"]);
let big: Value =
serde_json::from_str("[9007199254740992,-9007199254740991]").expect("json");
assert_eq!(unsafe_int_paths(&big, ""), ["[0]"]);
}
#[test]
fn limited_paths_stop_at_the_limit_and_clip_each_path() {
let v: Value = serde_json::from_str(r#"{"b":[1.5,2.5,3.5],"a":{"xyz":0.5}}"#).unwrap();
assert_eq!(
float_paths(&v, "pre"),
["pre.a.xyz", "pre.b[0]", "pre.b[1]", "pre.b[2]"]
);
assert_eq!(float_paths_limited(&v, 2, usize::MAX), ["a.xyz", "b[0]"]);
assert_eq!(
float_paths_limited(&v, 9, 3),
["a.x...", "b[0...", "b[1...", "b[2..."]
);
assert_eq!(float_paths_limited(&v, 0, 9), Vec::<String>::new());
assert_eq!(float_paths_limited(&Value::from(1.5), 9, 2), ["<root>"]);
assert_eq!(float_paths_limited(&v, 1, 0), ["..."]);
let wide: Value = serde_json::from_str("{\"é\u{10000}x\": 1.5}").unwrap();
assert_eq!(float_paths_limited(&wide, 1, 2), ["é\u{10000}..."]);
assert_eq!(float_paths_limited(&wide, 1, 3), ["é\u{10000}x"]);
let big: Value =
serde_json::from_str(r#"[9007199254740992, 1.5, 9007199254740993]"#).unwrap();
assert_eq!(unsafe_int_paths_limited(&big, 1, 9), ["[0]"]);
}
#[test]
fn is_unsafe_int_boundary() {
assert!(!is_unsafe_int(&Number::from(9007199254740991i64)));
assert!(is_unsafe_int(&Number::from(9007199254740992i64)));
assert!(!is_unsafe_int(&Number::from(-9007199254740991i64)));
}
}