# LENTRA passport format · v0.1 (draft)

A passport is a signed record of a machine event: one JSON object, six claim groups, one Ed25519 signature, and a place in a hash chain. This document is the normative specification. It is deliberately small: a competent engineer should be able to write an independent verifier from this page alone. The reference verifier lives at `GET https://lentra.io/api/v0/verifier.mjs` and the extended design rationale at `https://lentra.io/wiki/`.

Canonical source of this document: `GET https://lentra.io/api/v0/spec`.

## 1. Envelope

A passport is a JSON object with exactly these top-level members:

| Member | Type | Meaning |
| --- | --- | --- |
| `passport_version` | string | Format version. This document defines `"0.1"`. Verifiers MUST reject versions they do not implement. |
| `id` | string | Globally unique, lexically sortable identifier, prefix `lp_`. |
| `issuer` | object | `{ "name": string, "key_id": string }`. `key_id` names the signing key in the issuer's registry. |
| `claims` | object | Exactly six groups: `source`, `observation`, `method`, `context`, `quality`, `rights`. |
| `chain` | object | `{ "seq": integer, "prev": string }`. See section 4. |
| `signature` | object | `{ "alg": "ed25519", "key_id": string, "value": base64 }`. Detached signature over the canonical body. |

Future versions may add a `countersignatures` array (qualified timestamps, RFC 3161). Verifiers of v0.1 MUST reject records whose `passport_version` they do not know, and MUST tolerate unknown fields inside `claims` (section 6).

## 2. Canonical form

Signatures are computed over bytes, so two parties must derive byte-identical input from the same record. The canonical form has four rules:

1. **Strip.** Remove the `signature` member. Everything else is the body.
2. **Sort.** Serialise objects with keys in lexicographic (code unit) order, at every nesting level.
3. **No insignificant whitespace.** No spaces, no newlines outside string values.
4. **Never re-encode numbers.** Profiles use integer units (bytes, milliseconds, sequence numbers). An implementation MUST NOT reformat a number it received; the received spelling is authoritative. Duplicate keys are malformed and MUST be rejected.

Strings are serialised as UTF-8 with standard JSON escaping and no Unicode normalisation. The reference implementation is the `canon()` function in the reference verifier; it is nine lines.

## 3. Signature

`signature.value` is the base64 Ed25519 signature over the UTF-8 bytes of the canonical body. Verification:

1. Resolve `signature.key_id` in the issuer's published registry (section 5).
2. Recompute the canonical body and verify Ed25519 over those exact bytes.
3. Check `chain.prev` against the record it claims to follow, when the predecessor is available.
4. Apply your own policy. A passport states what was observed; it never states conclusions, and sufficiency is the relying party's judgement.

## 4. Chain

Records issued by one node form a series. `chain.seq` increments by one per record. `chain.prev` is `"sha256:" + hex(sha256(canonical(previous record)))` where the digest is computed over the **full previous record including its signature**, in canonical form (rules 2 to 4 applied; nothing stripped). The first record of a series uses `"sha256:genesis"`.

Consequence: editing any historical record changes its digest and detaches every later record. The chain proves order and integrity of what exists; it does not prove completeness or wall-clock time. Those bounds come from sequence accounting, distribution of records to other parties, and timestamp countersignatures.

## 5. Issuer keys

Issuers publish a registry at a stable URL (for records issued by this site: `https://lentra.io/keys/issuers.json`):

```json
{ "keys": [ { "key_id": "...", "alg": "ed25519", "public_key_b64": "<raw 32 bytes, base64>", "status": "active | retired | revoked | demo" } ] }
```

Retired keys are never removed: records signed under them must verify forever. `status` is data for the relying party's judgement, not part of signature verification.

## 6. Profiles

The envelope is direction-agnostic. A profile fixes the vocabulary inside the six groups for one capture direction. The six group questions never change: who, when, how, against what, with what result, with what legal consequence.

- **ingress** (drafted, in production on this site): a machine visit to a property's content. `source` holds the claimed agent and network evidence (`claimed_agent`, `peer_ip`, `peer_asn`, `reverse_dns`, `forward_confirmed`); `context` pins the terms in force (`requested_url`, `robots_txt_sha256`, `robots_state`, `license_signals`); `quality` holds status, digests and anomalies; `method.capture` names the mechanism honestly (`passive_mirror`, `access_log_tail`).
- **observation** (drafted): an outbound fetch by the operator's own machine.
- **session** (drafted): a model interaction at an API boundary, prompt chain and parameters included.

Verifiers MUST verify signatures over whatever claim fields are present and MUST NOT fail on unknown claim fields. Interpretation of fields is the relying party's layer. Field meanings, once published, are never redefined; changes arrive as new fields.

## 7. Verification without the issuer

Every step above runs locally and offline given the record and a copy of the registry. No step contacts LENTRA, and none contacts the issuer. This property is normative: any extension that would require a live call to verify a record is not a conforming extension.

## 8. Status

v0.1 is a draft in active use by the capture node recording machine traffic to lentra.io (`GET /api/v0/records/recent`, chain head at `GET /api/v0/records/head`). Planned for v0.2: the `countersignatures` member for qualified timestamps. The change discipline is documented in the wiki's versioning chapter: strict on the envelope, tolerant on claims, and no record ever orphaned.
