use std::collections::BTreeSet;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use serde_json::{Map, Value};
use uuid::Uuid;
use crate::Form;
use crate::critical::check_critical;
use crate::form::form_of;
use crate::gzip::inflate;
use crate::identity::{parse_uuid_text, unsigned_identity};
use crate::json::parse_object;
use crate::read::{MAX_SIGNED_LAYERS, Rejection};
pub(crate) fn signed_identity(
object: &Map<String, Value>,
budget: &mut u64,
supported: &BTreeSet<String>,
) -> Result<Uuid, Rejection> {
let mut header_uuids = Vec::new();
let mut inner = open_layer(object, budget, &mut header_uuids)?;
let mut layers = 1;
let uuid = loop {
match form_of(&inner) {
Ok(Form::Signed) if layers == MAX_SIGNED_LAYERS => return Err(Rejection::TooDeep),
Ok(Form::Signed) => {
inner = open_layer(&inner, budget, &mut header_uuids)?;
layers += 1;
}
Ok(Form::Unsigned) => {
let uuid = unsigned_identity(&inner)?;
check_critical(&inner, supported)?;
break uuid;
}
_ => return Err(Rejection::BadPayload),
}
};
for header_uuid in &header_uuids {
match header_uuid {
Value::String(text) if parse_uuid_text(text) == Ok(uuid) => {}
_ => return Err(Rejection::UuidMismatch),
}
}
Ok(uuid)
}
fn open_layer(
object: &Map<String, Value>,
budget: &mut u64,
header_uuids: &mut Vec<Value>,
) -> Result<Map<String, Value>, Rejection> {
header_uuids.extend(header_uuids_of(object)?.into_iter().cloned());
let Some(Value::String(payload)) = object.get("payload") else {
return Err(Rejection::BadPayload);
};
let bytes = URL_SAFE_NO_PAD
.decode(payload)
.map_err(|_| Rejection::BadPayload)?;
let (inner, gzip) = inflate(&bytes, None, *budget)?;
if gzip {
*budget -= inner.len() as u64;
}
parse_object(&inner).map_err(|r| match r {
Rejection::NotJson => Rejection::BadPayload,
other => other,
})
}
fn header_uuids_of(object: &Map<String, Value>) -> Result<Vec<&Value>, Rejection> {
let Some(Value::Array(signatures)) = object.get("signatures") else {
return Err(Rejection::BadPayload);
};
let mut uuids = Vec::new();
for signature in signatures {
let Value::Object(signature) = signature else {
return Err(Rejection::BadPayload);
};
match signature.get("header") {
None => {}
Some(Value::Object(header)) => uuids.extend(header.get("uuid")),
Some(_) => return Err(Rejection::BadPayload),
}
}
Ok(uuids)
}
#[cfg(test)]
mod tests {
use super::*;
use base64::Engine;
use base64::engine::general_purpose::{STANDARD, URL_SAFE, URL_SAFE_NO_PAD};
use flate2::Compression;
use flate2::write::GzEncoder;
use serde_json::json;
use std::io::Write;
const ID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
const OTHER: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc47";
const LIMIT: u64 = 1 << 20;
fn unsigned() -> Value {
json!({"uuid": ID, "created_at": "2026-10-23T00:00:00Z", "parties": []})
}
fn jws(payload: String, header: Value) -> Value {
json!({"payload": payload, "signatures": [{"header": header, "protected": "", "signature": ""}]})
}
fn b64(bytes: &[u8]) -> String {
URL_SAFE_NO_PAD.encode(bytes)
}
fn gz(bytes: &[u8]) -> Vec<u8> {
let mut e = GzEncoder::new(Vec::new(), Compression::default());
e.write_all(bytes).expect("compress");
e.finish().expect("finish")
}
fn sign(inner: &Value, header: Value) -> Value {
jws(b64(inner.to_string().as_bytes()), header)
}
fn run(outer: &Value, limit: u64, supported: &[&str]) -> Result<Uuid, Rejection> {
let Value::Object(map) = outer else {
panic!("object")
};
let supported = supported.iter().map(|s| s.to_string()).collect();
let mut budget = limit;
signed_identity(map, &mut budget, &supported)
}
fn id() -> Uuid {
ID.parse().expect("test uuid")
}
#[test]
fn reads_the_uuid_of_the_signed_vcon() {
let cases = [
(
"header uuid matches",
sign(&unsigned(), json!({"alg": "RS256", "uuid": ID})),
),
(
"header uuid in upper case",
sign(&unsigned(), json!({"uuid": ID.to_uppercase()})),
),
("no header uuid", sign(&unsigned(), json!({"alg": "RS256"}))),
(
"no header at all",
json!({"payload": b64(unsigned().to_string().as_bytes()), "signatures": [{}]}),
),
(
"gzipped payload",
jws(
b64(&gz(unsigned().to_string().as_bytes())),
json!({"uuid": ID}),
),
),
(
"two layers, re-signed",
sign(&sign(&unsigned(), json!({"uuid": ID})), json!({"uuid": ID})),
),
(
"three layers, middle one gzipped",
sign(
&jws(
b64(&gz(sign(&unsigned(), json!({})).to_string().as_bytes())),
json!({"uuid": ID}),
),
json!({}),
),
),
];
for (what, outer) in cases {
assert_eq!(run(&outer, LIMIT, &[]), Ok(id()), "{what}");
}
}
#[test]
fn refuses_a_header_uuid_that_differs() {
let cases = [
("outer header", sign(&unsigned(), json!({"uuid": OTHER}))),
("not a uuid", sign(&unsigned(), json!({"uuid": "nope"}))),
("not a string", sign(&unsigned(), json!({"uuid": 5}))),
(
"second signature",
json!({"payload": b64(unsigned().to_string().as_bytes()),
"signatures": [{"header": {"uuid": ID}}, {"header": {"uuid": OTHER}}]}),
),
(
"inner layer",
sign(
&sign(&unsigned(), json!({"uuid": OTHER})),
json!({"uuid": ID}),
),
),
(
"outer layer",
sign(
&sign(&unsigned(), json!({"uuid": ID})),
json!({"uuid": OTHER}),
),
),
];
for (what, outer) in cases {
assert_eq!(
run(&outer, LIMIT, &[]),
Err(Rejection::UuidMismatch),
"{what}"
);
}
}
#[test]
fn refuses_a_payload_that_is_not_a_vcon() {
let text = unsigned().to_string();
let cases = [
(
"payload not a string",
json!({"payload": 1, "signatures": []}),
),
(
"padding",
jws(URL_SAFE.encode(text.as_bytes() as &[u8]) + "==", json!({})),
),
(
"standard alphabet",
jws(STANDARD.encode([0xfb_u8, 0xff, 0xbf]), json!({})),
),
(
"whitespace",
jws(format!(" {}", b64(text.as_bytes())), json!({})),
),
("not JSON", jws(b64(b"hello"), json!({}))),
("JSON array", jws(b64(b"[]"), json!({}))),
("no form inside", jws(b64(b"{}"), json!({}))),
(
"encrypted inside",
jws(
b64(json!({"ciphertext": "x", "recipients": []})
.to_string()
.as_bytes()),
json!({}),
),
),
(
"signatures not an array",
json!({"payload": b64(text.as_bytes()), "signatures": {}}),
),
(
"a signature not an object",
json!({"payload": b64(text.as_bytes()), "signatures": [1]}),
),
(
"a header not an object",
json!({"payload": b64(text.as_bytes()), "signatures": [{"header": []}]}),
),
];
for (what, outer) in cases {
assert_eq!(
run(&outer, LIMIT, &[]),
Err(Rejection::BadPayload),
"{what}"
);
}
}
#[test]
fn checks_the_inner_vcon_like_an_unsigned_one() {
let no_uuid = json!({"created_at": "x", "parties": []});
assert_eq!(
run(&sign(&no_uuid, json!({})), LIMIT, &[]),
Err(Rejection::MissingUuid),
"a signed vCon's payload needs a uuid as an unsigned vCon does"
);
let critical = json!({"uuid": ID, "created_at": "x", "critical": ["x"]});
assert_eq!(
run(&sign(&critical, json!({})), LIMIT, &[]),
Err(Rejection::UnsupportedCritical(vec!["x".to_owned()])),
"the payload's critical list is checked against what the caller supports"
);
assert_eq!(
run(&sign(&critical, json!({})), LIMIT, &["x"]),
Ok(id()),
"the same payload passes once the caller supports x"
);
let dup = jws(b64(br#"{"uuid": "a", "uuid": "b"}"#), json!({}));
assert_eq!(
run(&dup, LIMIT, &[]),
Err(Rejection::DuplicateKey),
"the payload is parsed with the same duplicate-name check"
);
}
#[test]
fn all_layers_share_one_inflation_budget() {
let padded = json!({"uuid": ID, "created_at": "x", "pad": " ".repeat(3000)});
let inner = jws(b64(&gz(padded.to_string().as_bytes())), json!({}));
let outer = jws(b64(&gz(inner.to_string().as_bytes())), json!({}));
let inner_size = padded.to_string().len() as u64;
let outer_size = inner.to_string().len() as u64;
assert_eq!(
run(&outer, inner_size + outer_size, &[]),
Ok(id()),
"a budget of both layers' inflated sizes is enough"
);
assert_eq!(
run(&outer, inner_size + outer_size - 1, &[]),
Err(Rejection::TooLarge),
"one byte less is not: the two layers draw on one budget"
);
assert!(
inner_size.max(outer_size) < inner_size + outer_size - 1,
"each layer alone fits the smaller budget, so only a shared budget explains the refusal"
);
}
}