# legible-core

The **reference** implementation of legible's canonical record engine: the function that gives every Appoint/Trace/Nod its `id`.

```text
id = lowercase_hex( SHA-256( JCS( canonical_content ) ) )
```

Only `canonical_content` is hashed; `id`/`type`/`security`/`published` stay outside (`nrc` is inside the core). "Correct" is defined outside the code: by the canonical specification, exercised in part by the **conformance vectors** (`../../conformance/vectors.json`). The test fails if the engine does not reproduce every `id` and every JCS string byte-for-byte.

## API

- `jcs(&Value) -> String` — JCS / RFC 8785 canonicalization.
- `parse_draft_document(&str) -> Result<Value, String>` — draft-only reading,
  returning metadata and recovery notices. Never used to parse signed records.
- `record_id(&Value) -> String` — the `id` from `canonical_content`.
- `verify_id(&str, &Value) -> bool` — the Verifier's **id** path (pure, offline).
- `ed25519_pubkey_from_did_key(&str) -> Option<[u8;32]>` — extracts the Ed25519 key embedded in a `did:key` (the inaugural baseline embeds the verifying key in the DID itself).
- `did_key_from_openssh_pubkey(&str) -> Result<String>` — derives a `did:key` from an OpenSSH Ed25519 public key (the reverse; lets the CLI produce a node's DID with no external tooling).
- `verify_signature(author_did, id, sshsig_pem) -> bool` — the Verifier's **signature** path: validates the Ed25519/SSH signature (sshsig) over the `id` string, namespace `legible`, against the author's `did:key`. Pure and offline.
- `validate_canonical_content(typ, &Value) -> Vec<Issue>` / `validate_record(yaml) -> Result<Vec<Issue>>` — the write-path structural guards (spec §3.6 + the §3 shapes): required fields, enums, weight ∈ [0, 1], trace count, evidence, the per-form Nod blocks, `nrc` on an Appoint. Pure.
- `Graph::load_dir(&Path) -> Result<Graph>` — the **Indexer**: rebuilds the Appoint/Trace/Nod graph in RAM from the loose files in `.legible/`, verifying each `id`. `Graph::referential_issues()` runs the checks that need the graph: `target`/`source`/`nods`/`about-nod` resolve, assertion cycles are reported as warnings rather than invalidity. There is no registration-time ordering rule; lifecycle constraints and query-specific acyclic projections must be checked separately. Queries: `ancestors`, `nods_on`, `responses_on`.
- `timestamp_proofs(yaml)` / `rfc3161_request(id)` / `rfc3161_response_status(der)` — **anteriority, the ceiling**: list `security.timestamp-proofs` (evidence that the core existed by a given time) and build/read RFC 3161 TSA requests. These Rust functions do not cryptographically verify `.tsr` tokens. The experimental Python/OpenSSL adapter described under Scope provides a separate verification path. Readers choose the witnesses they trust and may require several independent ceilings; multiple timestamps are not a condition of record validity.

> **Signature scheme:** sshsig over the `id` string (lowercase hex, UTF-8, no trailing newline), namespace `legible` — signable with `ssh-keygen -Y sign -n legible`, the same key/tool as Git. The namespace is checked: a signature from another namespace (e.g. `git`) does **not** validate a legible record. Timestamps (RFC 3161 etc.) are a separate path.

## Run

```bash
cargo test
```

Expected: the `reproduces_every_conformance_vector` test green — the engine reproduces every vector's `jcs` string and `id`, byte for byte.

The vectors are **regenerated by this engine and never edited by hand** (`cargo run --example regen_vectors -- ../../conformance/vectors.json`), which is what keeps them from drifting — but a single implementation agreeing with itself is not cross-implementation evidence. Two independent checks exist today:

- **Canonicalization** — `legible-py/tests/test_vectors_independently.py` is a JCS written from §3 and `CANONICALIZATION.md`, importing nothing from this engine. Its Python structural canonicalizer uses an independent ECMAScript numeric oracle: Node.js (a subprocess) or JavaScriptCore on macOS. One of these runtimes is required for this test. It reproduces every vector's canonical string and id, and runs in CI. It exists because the engine validating vectors the engine generated proves nothing about the engine — and it pins three rules this engine had wrong: UTF-16 key order, ES6 exponential form below 1e-6, and `-0.0` collapsing to `0`.
- **The identity profile** — `did_document.rs` and `legible-py/legible/did_document.py` are independent and tested against the same `did-vectors.json` and `did-vectors-recovery.json`.

A full second engine — write path, verifier, indexer — is still the most useful contribution this repository can receive (`../../CONTRIBUTING.md`).

## CLI (`legible`)

The write path, with signing always in the author's hands:

```bash
legible id          --in content.json                        # just compute the id
legible build       --type appoint --in content.json --repo <dir>   # validate, write UNSIGNED + instructions
                    [--evidence <file>] (repeatable)                 # SHA-256 the file into `evidence` (content never enters)
                    # `created` absent? injected as now (UTC) — an explicit value is never overwritten
legible seal        --record <file> --sig <file.sig>         # insert the signature and verify
legible verify      --record <file>                          # id + signature
legible validate    --record <file>                          # write-path rules (spec §3.6)
legible check       --repo <dir>                             # load the graph and validate referentially
legible timestamp   --record <file> [--tsa <url>]            # RFC 3161 ceiling: fetch <id>.tsr AND wire the
                                                              #   ref into security.timestamp-proofs (outside
                                                              #   the hash — id unchanged, nothing re-signed)
legible anteriority --record <file>                          # ceilings (RFC 3161) listed
legible did         --pubkey <file.pub>                      # derive the did:key from an Ed25519 public key
legible redact      --target <id> --author <did> --repo <dir>  # prepare UNSIGNED redaction notice; review/sign/seal separately
```

Optional author-contact profiles have a separate signing namespace,
`legible-contact`. `contact-id --in content.json` computes the ID to sign;
`contact-seal --in content.json --sig id.sig --repo <dir>` verifies and installs
it. `contact-verify --in profile.json` checks an exchanged file, and
`contact-import --in profile.json --repo <dir>` installs a newer verified
revision. These commands never sign with the author's key or share the file.
The stable destination is `.legible/contacts/contact-<sha256-of-DID>.json`;
see [the optional profile](../../profiles/author-contact.md). Keep this store
ignored by Git unless sharing a particular version is deliberately selected.

`content.json` is just the `canonical_content` (the meaningful fields). `build` **validates** (spec §3.6) and only then computes the `id`, writes `<repo>/.legible/<type>s/<id>.<ext>` unsigned, and prints the exact `ssh-keygen -Y sign` command. Some nonconformant inputs are currently accepted; passing the implemented guards is not full schema conformance. **Private key material does not pass through Rust build/seal; the supplied signature does** — the author signs the `id` with their own key and `seal` only inserts the result.

## Tolerant draft reading (implementation behavior)

`legible parse-draft --in draft.md` (or `--in -` for stdin) reads Genealogy
0.1 Markdown frontmatter. It accepts UTF-8 BOM and CRLF. If strict YAML parsing
fails, it may read an unquoted, single-line `title`, `formulation`, `statement`
or `consequence` containing `: ` as text, reporting the field and line in
`recoveries`. JSON-quoted values are recommended when preparing files.

This bounded recovery is a reading aid, not a second format or certification
of the original YAML. It never rewrites source bytes or supplies missing authors,
versions, dates, references or amounts. Ambiguous quotes, comments, structures,
duplicate keys, anchors, aliases and tags are not repaired. Full draft-field
checks remain in the Python importer; a successful parse establishes neither
authorship nor truth. Signed JSON/YAML uses the unchanged strict parser.

The Python library retains the original byte digest and stores the versioned
reader diagnostics separately in transport `reading` metadata. A receiver
reproduces that interpretation; it must not silently treat normalized text as
the original contribution. No network, signing or publication occurs.

## Scope

The Rust component supplies canonicalization, content addressing, baseline
signature verification, unsigned preparation/sealing and implemented structural
checks. Its in-memory graph projection authenticates retained records before
selected referential and lifecycle checks; assertion cycles produce warnings,
not record invalidity. Tests cover identified cases, not complete conformance
certification. Authenticating withdrawal after its target disappears still needs
persistent authority evidence.

The Rust timestamp helper obtains/lists RFC 3161 artifacts; listing is not token
verification. The separate experimental Python `legible.temporal` adapter invokes
OpenSSL to check proof binding and the supplied certificate chain under caller
trust policy. It checks neither online revocation nor whether the record signature
itself existed at the token time. No peer-wide erasure, required discovery service,
or claim of real third-party interoperability follows from these local components.

> Status: `0.1.0`, pre-publication. License: code under Apache-2.0 (see `../../LICENSE.md`).
