Search by

steddle / seal

mischasigtermans

The fingerprint and canonical JSON that Steddle's products store proof with: SHA-256 in one spelling, and RFC 8785 over I-JSON.

Package info

github.com/steddle/seal

pkg:composer/steddle/seal

Statistics

Installs: 43

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.1 2026-11-14 20:29 UTC

This package is auto-updated.

Last update: 2026-11-15 01:47:22 UTC


README

Steddle Seal: proof a file existed, by a time anyone can check

Steddle Seal

Packagist Tests License

The fingerprint and canonical JSON that Steddle's products store proof with.

Three parts. A fingerprint is a SHA-256 value written one way, so two systems that store the same bytes agree on its text. Canonical JSON turns data into one exact byte sequence (RFC 8785 over I-JSON), so anyone who holds the data gets the same fingerprint. A statement is what a seal stamps: a JSON object that names its form and points at files by their fingerprints. SPEC.md says how a seal is made and checked. Plain PHP: no framework, nothing registered, no requirement beyond ext-mbstring.

Installation

composer require steddle/seal

Requires

  • PHP 8.3 or later, with ext-mbstring

Quick start

use Steddle\Seal\Canonical;
use Steddle\Seal\Fingerprint;
use Steddle\Seal\Json;

$record = Json::decode('{ "b": [1.0, 1e0], "a": null }');

Canonical::json($record);                           // {"a":null,"b":[1,1]}
(string) Fingerprint::of(Canonical::json($record)); // sha256:7773f186...c753bb05

The same record, written with other spacing, member order or number forms, gives the same canonical bytes and so the same fingerprint.

Fingerprint

  • Fingerprint::of($bytes) hashes bytes.
  • Fingerprint::read($text) takes 64 hex characters in either case, bare or after sha256:.
  • A fingerprint prints sha256: and 64 lowercase hex characters. Two are equal when their 32 bytes are.
  • A short form, white space, a trailing line break or another prefix is refused, never trimmed.

Canonical JSON

  • Json::decode($text) reads I-JSON and refuses a member name twice, NaN or an infinity, half a surrogate pair, and any text that isn't JSON.
  • Canonical::json($value) writes members sorted by UTF-16 code units and numbers as ECMAScript prints them.
  • Every refusal is a Refused exception. Its why is a fixed code (duplicate_name, not_finite, lone_surrogate, not_json, wrong_length, not_hex, unknown_prefix), so callers can branch on it. Where one member is at fault, pointer gives its RFC 6901 JSON Pointer, such as /records/3/id.

Statement

  • Statement::of($value) and Statement::read($json) take what a seal stamps: one JSON object that names its form and points at files and data with references. They refuse a statement that breaks the rules in SPEC.md, with the rule as why, and Statement::problems($value) lists every rule it breaks.
  • bytes() is its RFC 8785 form, fingerprint() the fingerprint of those bytes, and references() every reference by its JSON Pointer.
  • Reference::to($fingerprint, $over) points at a file (bytes), at data in its canonical form (rfc8785) or at another form by name.

Vectors

vectors/ is the contract: fingerprint.json, canonical.json and statement.json state every input and its expected output or refusal, RFC 8785's number table among them. Another implementation in any language is equal to this one when it gives the same answer on every case.

composer install
vendor/bin/pest

Changelog

See CHANGELOG.md.

Credits

License

The code is MIT. See LICENSE. The test vectors in vectors/ are CC BY 4.0, as their notice says. The mark in art/ is Steddle's and covered by neither (notice).