Expand description
Admission control for JSON that is about to be signed or verified.
One document has one canonical form, and a signature means nothing unless
every party agrees which bytes those are. RFC 8785 says how to write them.
This crate decides which documents are allowed to have them, refuses the rest
on the raw bytes, and then hands the writing to
serde_json_canonicalizer.
§Why a separate layer
The two canonicalizers on crates.io are serde Serializers. That is the
right shape for their job and it fixes where they can stand: a value has
already been parsed before either of them is reached, and the faults that
matter to a verifier are the ones that vanish during that parse. A repeated
object member is gone – the map kept one of the two and nothing downstream
can tell it happened. There is no layer below them to put the check in, so the
check goes above them, on the bytes, which is this crate.
Measured against serde_json_canonicalizer 0.3.2 on 2026-09-19 (the run is
pinned in tests/incumbent_differential.rs, which fails if any of it stops
being true):
| input | the delegate | this crate |
|---|---|---|
{"a":1,"a":2} | {"a":2} via pipe, {"a":1} straight into its serializer | Error::DuplicateMember |
| 129 nested arrays | accepted by to_vec; 10,000 aborts the process | Error::TooDeep |
9007199254740993 | 9007199254740992, a different integer | Error::UnsafeInteger under admit_ijson; admit rewrites it the same way |
| 12 MB of array | accepted | Error::TooLarge |
The third row’s right-hand cell names admit_ijson and the qualification is
load-bearing: the RFC 8785 default admits that token and canonicalizes it to
9007199254740992, the same different integer the delegate produces, because
section 3.2.2.3 defers to ECMAScript and ECMAScript has one numeric type. The
The first row is the one to read twice. One document, one crate, two canonical
forms depending on which of its entry points you used, and both of them
signable.
What the delegate gets right, it gets right for us: it formats numbers through
ryu_js, which is the ECMAScript Number::toString variant RFC 8785 section
3.2.2.3 requires, and it reads the RFC’s own awkward reference number
333333333.33333329 correctly. This crate stands on that rather than carrying
a third copy of it.
§What it refuses
- A repeated object member (
Error::DuplicateMember). Collapsing a repeat to last-wins is a split view: two parties read the same bytes as two different documents and a signature over either reading verifies. Comparison is on the decoded name, so"a"and"a"are one member. - Nesting past a cap (
DEFAULT_MAX_DEPTH, 128 – the value RFC 8785’s in-toto profile made normative). Without a cap a crafted document overflows the stack, which no caller can catch and which happens before any signature is checked. - A string whose bytes are not Unicode scalar values
(
Error::StringNotScalar): an unpaired surrogate escape, a surrogate written directly in UTF-8, an overlong form, a raw control character. Checked before any decode, because a decoder that substitutes U+FFFD leaves"\ud800","\udc00"and a literal U+FFFD canonicalizing to identical bytes. - A number with no finite double (
Error::NonFiniteNumber), and under the opt-in RFC 7493 profile (admit_ijson) an integer at or above 2^53 (Error::UnsafeInteger) or a Unicode noncharacter. - An input past a size cap (
Error::TooLarge). A depth cap bounds the stack; a size cap bounds the heap.
§Examples
use jcs_admit::{admit, Error};
assert_eq!(admit(br#"{ "b": 1, "a": [1.0, 1e30] }"#)?, br#"{"a":[1,1e+30],"b":1}"#);
// The refusal the layer exists for, and it names the member.
assert!(matches!(
admit(br#"{"a":1,"a":2}"#),
Err(Error::DuplicateMember { .. })
));Structs§
- Options
- What to refuse beyond the RFC 8785 baseline.
Enums§
- Error
- Why a document was refused.
Constants§
- DEFAULT_
MAX_ BYTES - The default size cap on untrusted input, 20 MiB.
- DEFAULT_
MAX_ DEPTH - The default depth cap: at most 128 nested arrays and objects.
- MAX_
SUPPORTED_ DEPTH - The deepest nesting this crate honours, whatever a caller configures.
Functions§
- admit
- Admit one JSON document and return its RFC 8785 canonical form.
- admit_
ijson - Admit one JSON document under the RFC 7493 I-JSON profile.
- admit_
with - Admit one JSON document under explicit options.
- is_
canonical - Whether
inputis admissible AND already its own canonical form. - is_
canonical_ with - Whether
inputis admissible and already canonical underopts.