Skip to main content

Crate jcs_admit

Crate jcs_admit 

Source
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):

inputthe delegatethis crate
{"a":1,"a":2}{"a":2} via pipe, {"a":1} straight into its serializerError::DuplicateMember
129 nested arraysaccepted by to_vec; 10,000 aborts the processError::TooDeep
90071992547409939007199254740992, a different integerError::UnsafeInteger under admit_ijson; admit rewrites it the same way
12 MB of arrayacceptedError::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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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 input is admissible AND already its own canonical form.
is_canonical_with
Whether input is admissible and already canonical under opts.