jcs

package
v0.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package jcs canonicalizes JSON per RFC 8785 with the one deviation the Truestamp wire format requires: an integer literal is emitted exactly as it was parsed, never round-tripped through an IEEE-754 double.

RFC 8785 §3.2.2.3 defines every JSON number by parsing it into a double and re-serializing it per ECMA-262, which silently rounds 2^53 + 1 down to 2^53. The Truestamp producer emits integers at arbitrary precision instead, so reproducing a claims_hash or entropy_hash means matching the producer rather than the strict reading of the RFC. Appendix C.2a of the whitepaper pins both halves as normative vectors: 2^53 and 2^53 + 1 canonicalize to distinct strings with distinct 0x11 digests.

The cost of that choice is that such a bundle is not portably verifiable by a strict RFC 8785 implementation, which Appendix E.4 requires a verifier to report rather than hide. Canonicalize therefore hands back the offending literals alongside the canonical bytes so callers can surface them.

Everything else — UTF-16 code-unit key ordering, string escaping, ES6 float formatting, negative-zero normalization — is delegated unchanged to github.com/gowebpki/jcs, which this package wraps rather than replaces. For any valid input free of oversized integers the output is byte-identical to calling that library directly, which is what makes adopting this package incapable of moving an existing digest.

The one thing the wrapper adds on top of the library is a strict RFC 8259 gate, because the library is not a validating parser and silently rewrites several malformed documents into different valid ones. See rejectNonRFC8259.

Index

Constants

View Source
const MaxExactInteger int64 = 1 << 53

MaxExactInteger is 2^53. Every integer at or below it in absolute value is exactly representable as an IEEE-754 double; 2^53 + 1 is the first that is not. The comparison against it is strictly greater-than, so 2^53 itself is in range — matching Appendix C.2a, which labels it "exactly representable", and the reference verifier's @max_exact_integer.

View Source
const MaxSafeInteger int64 = (1 << 53) - 1

MaxSafeInteger is 2^53 - 1, the PRODUCER bound: RFC 8785 Appendix B's SHOULD ("values to be interpreted as true integers SHOULD be in the range -9007199254740991 to 9007199254740991") and JavaScript's Number.MAX_SAFE_INTEGER. UnsafeIntegers rejects strictly greater in magnitude, so 2^53 - 1 is accepted and 2^53 is not.

It is deliberately ONE LESS than MaxExactInteger, the verifier bound directly above. Do not unify them, and do not "fix" the off-by-one:

  • The producer is strict. Emitting 2^53 is legal by the letter of the round-trip argument but violates Appendix B's SHOULD, and the Truestamp server enforces exactly this bound (Truestamp.SafeIntegers in truestamp-v2), so a value this package accepts and the server rejects would be a 422 the user cannot act on.
  • The verifier is lenient. 2^53 itself round-trips through a double exactly, so warning on it would raise a false alarm about a bundle every conforming implementation can in fact check.

Be strict in what you emit, lenient in what you accept. The one-value gap is the whole point; TestThresholdsDifferByOne fails loudly if it closes.

Variables

This section is empty.

Functions

func Canonicalize

func Canonicalize(data []byte) (canonical []byte, oversized []string, err error)

Canonicalize returns the RFC 8785 canonical form of data.

data MUST be a single well-formed RFC 8259 document; anything else is an error rather than a best-effort canonicalization, because the underlying library rewrites several malformed documents into different valid ones and the caller cannot tell a digest of the input from a digest of the rewrite. See rejectNonRFC8259.

oversized names every integer literal in data whose absolute value exceeds MaxExactInteger, one entry per occurrence, numerically ascending so oversized[0] is the smallest offender. It is nil whenever data stays inside the safe range, and in that case the canonical bytes are byte-identical to github.com/gowebpki/jcs.Transform.

func OversizedIntegers

func OversizedIntegers(data []byte) []string

OversizedIntegers reports the integer literals in data that fall outside the exactly representable double range, for callers that want the Appendix E.4 portability signal without paying for canonicalization. It has no error channel, so a document the scanner cannot read reports nothing; that is not a silent accept the way it would be in Canonicalize, because a caller reaching this entry point still has to canonicalize the same bytes to get a digest, and Canonicalize rejects them.

func Transform

func Transform(data []byte) ([]byte, error)

Transform is Canonicalize with the portability report discarded, for the call sites that do not surface it.

func UnsafeIntegerMessage

func UnsafeIntegerMessage(path, literal string) string

UnsafeIntegerMessage renders the submitter-facing explanation of one offending value.

The wording is a deliberate byte-for-byte mirror of Truestamp.SafeIntegers.message/2 in truestamp-v2, which the server surfaces as an Ash InvalidAttribute on the claims field. A user who hits the local guard and a user who hits the server guard must read the same sentence, or the two halves of one rule look like two unrelated rules.

Types

type UnsafeInteger

type UnsafeInteger struct {
	// Path is the dotted key path to the value, rooted at the label the
	// caller passed to [UnsafeIntegers]: object keys joined with ".", array
	// indices as "[i]" — e.g. "claims.metadata.rows[0].id". The syntax
	// matches Truestamp.SafeIntegers on the server so the local and remote
	// rejections name the same location for the same value.
	Path string

	// Literal is the integer exactly as the user wrote it, never
	// round-tripped through a float. Rendering it anywhere — an error
	// message, a JSON field — MUST use this string rather than a numeric
	// type, or the report reproduces the very rounding it is warning about.
	Literal string
}

UnsafeInteger names one integer in a decoded document that a conforming producer must not emit, together with the dotted key path that locates it.

func UnsafeIntegers

func UnsafeIntegers(root string, v any) []UnsafeInteger

UnsafeIntegers reports every integer in v whose magnitude exceeds MaxSafeInteger, one entry per occurrence, so a user fixes them all in one pass instead of one 422 at a time. It returns nil when v is portable.

root labels the top of each returned path; the Truestamp producer passes "claims" to match the server's field name.

v is a document decoded with encoding/json.Decoder.UseNumber — that is load-bearing, not incidental. Without it every number arrives as a float64 with the offending literal already destroyed, and this walk would inspect the rounded value and report nothing. json.Number is therefore the only decoded form that can be judged; Go's own integer types are accepted too (nothing in the producer path builds claims that way today, but they marshal to exact integer literals, so leaving a hole there would be a latent regression). Floats in any form are deliberately never reported: the producer rule is about integer literals, exactly as the verifier guard is, and a large-magnitude value SPELLED as a float ("1e21", "1.5") is not an integer literal. That classification is delegated to the same base-10 big.Int parse [scanIntegers] uses, so the two cannot drift apart.

Ordering is deterministic — Go map iteration is randomized, so object keys are walked in sorted order, arrays in index order. Two runs over the same document produce the same slice in the same order, which is what lets a caller print the list and a test assert on it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL