<!-- SPDX-License-Identifier: CC-BY-4.0 -- see /LICENSE-PROTOCOL. The Compliance Test Suite is MIT. -->

# Sigil Protocol — v1.0

**Status:** Released and **immutable**. This document is never edited.
Subsequent revisions live in `v1.1/`, `v2.0/`, etc. — never edit a released spec
in place. Corrections for v1.1 implementations live in
[v1.1 §11](../v1.1/SPEC.md); a correction requiring a canonical-form change gets
a new version number instead.

> **On the date, stated plainly because the record was ambiguous.** This line
> previously read "Draft (S11.T1). Once cut as v1.0, this document is
> immutable" — making the rule that governs the entire errata process
> conditional on an event no document ever recorded. The verifiable facts: the
> normative content has not changed since **2026-05-24**, when it was published
> alongside both reference verifiers and the Compliance Test Suite; and it has
> been treated as released since v1.1 was published as an amendment to it. The
> cut is therefore **recorded retroactively on 2026-07-27** against the
> 2026-05-24 content. No text was reconstructed and no normative section was
> touched to do it — the only edits this document has ever taken are its licence
> notice and this Status block, neither of which is normative.

**Citation:** _Sigil Protocol v1.0, §X.Y_ — every section in this document
is anchor-stable so external implementations can cite specific behaviour.

## 1. Conformance

The keywords **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY**
in this document are to be interpreted as described in RFC 2119.

A **conforming verifier** implements §3, §4, §5, §6, §7, and §9 exactly as
specified, and passes every fixture in the Compliance Test Suite (S11.T5)
without modification.

## 2. Terminology

- **Principal** — a human or organisation whose identity backs a signature.
- **Agent** — a delegated automation (script, AI, partner system) signing
  on a principal's behalf, see §7.
- **Envelope** — a structured request for one or more recipients to sign
  one or more fields on a specific document version.
- **Chain** — a hash-linked sequence of events; see §3.
- **Load chain** — the per-freight-load specialisation of the chain;
  see §6.
- **Capability token** — the JWS-compact authorisation a principal grants
  an agent; see §7.
- **Sealed PDF** — a PAdES-signed PDF carrying a Sigil seal; see §5.

## 3. Hash chain primitive

Every state-changing event in Sigil is appended to a hash-linked chain.
Each entry binds its content to its predecessor, making the chain
tamper-evident: any modification, insertion, or deletion breaks the
linkage detectably.

### 3.1 Entry shape

```
entry := {
  index:       u32        // dense, starts at 0
  timestamp:   ISO-8601   // RFC 3339, UTC; `Z` suffix
  type:        string     // event kind, e.g. ENVELOPE_SENT
  payloadHash: hex(64)    // sha256(canonical(payload))
  prevHash:    hex(64)    // hash of preceding entry, or genesis
  hash:        hex(64)    // computed; see §3.3
}
```

### 3.2 Genesis

The first entry in any chain MUST have:

- `index = 0`
- `prevHash = "0" repeated 64 times` (denoted `GENESIS_PREV_HASH`)

### 3.3 Entry hash computation

```
canonical := index || "\n" || timestamp || "\n" || type || "\n" || payloadHash || "\n" || prevHash
hash      := lower_hex(sha256(canonical_utf8_bytes))
```

The newline-delimited form is the canonical input. Implementations MUST
NOT add trailing newlines or whitespace.

### 3.4 Verification

Given a chain `E[0..n]`, a conforming verifier MUST:

1. Compute `expected_prev := GENESIS_PREV_HASH`.
2. For each `e` in `E`:
   1. Reject if `e.index != position_in_chain`.
   2. Reject if `e.prevHash != expected_prev`.
   3. Reject if `e.hash != hash_of(e)` per §3.3.
   4. Set `expected_prev := e.hash`.

A broken chain MUST report the first failing `index`.

### 3.5 Per-organisation chains

Every Sigil organisation has exactly one audit chain. A platform MAY
have multiple organisations; chains are independent.

## 4. Payload canonicalization

Where this spec refers to "the canonical form" of a payload, the
algorithm is:

1. Recursively normalise the value:
   - Objects: emit keys in lexicographic byte order.
   - Arrays: preserve order.
   - Strings: UTF-8 NFC.
   - Numbers: decimal, no leading zeros, no trailing `.0`, no exponent.
   - Booleans: `true` / `false`.
   - `null`.
2. Serialise as compact JSON (no whitespace).
3. Take SHA-256 of the UTF-8 bytes.

This is RFC 8785 (JCS) with two clarifications: NFC for strings, and
trailing-zero-free numbers. An implementation that uses pure JCS will
pass the CTS for all currently shipped event types — the clarifications
matter only at edge cases the v1.0 fixture set does not include.

## 5. Sealed PDF envelope

A **sealed PDF** is the canonical artifact a counterparty receives. It
combines a flattened source PDF with one PAdES-B-LTA conformant signature.

### 5.1 Signing posture

- **Algorithm:** RSASSA-PKCS1-v1_5 with SHA-256 (PKCS #1 v1.5), as PAdES
  requires.
- **Certificate:** X.509 v3, organizationName = `Sigil Document Signing`
  in dev, the deployer's commercial CA-issued cert in production.
- **Timestamp token:** RFC 3161, attached as an Unsigned Attribute
  (`id-aa-timeStampToken`). Implementations SHOULD include a TSA token
  but MUST tolerate its absence (timestamps may fail transiently).

### 5.2 Verification

A verifier MUST:

1. Read the `/ByteRange` of the signature dictionary.
2. Reconstruct the signed bytes — everything except the placeholder
   signature value — and compute SHA-256.
3. Validate the CMS SignedData per RFC 5652 §5.1, with PAdES profile
   constraints per ETSI EN 319 142-1.
4. Confirm the signing cert's subject CN matches a configured trust
   policy (the verifier's responsibility — Sigil ships a permissive
   policy for the reference verifiers).
5. If a TSA token is present, validate it per RFC 3161 §2.4.

## 6. Load chain

A freight load has its own per-load chain that binds GPS coordinates
into the canonical payload form. The base primitive is §3; the
extensions are:

### 6.1 Per-entry GPS binding

`payload` for a load chain entry MAY include:

```
geo := {
  latitude:   number    // WGS84 degrees, six decimal digits max
  longitude:  number    // WGS84 degrees, six decimal digits max
  accuracyMeters: number
  capturedFrom:   "DEVICE" | "IP_GEOIP" | "NONE"
}
```

`geo` is hashed as part of `payloadHash` (§3.1). Therefore moving a
location after the fact breaks the chain at the affected entry — the
load chain is GPS-evidence-bearing, not GPS-advisory.

### 6.2 Cross-reference to org chain

Every load chain mutation MUST emit a corresponding `LOAD_EVENT_SEALED`
entry on the load owner's organisation chain (§3.5) carrying:

```
payload := { loadId, kind, chainIndex, hash }
```

The two chains cross-reference: a load chain proves "this event happened
to this load"; the org chain proves "this org's history includes this
load event".

## 7. Capability tokens

See [`docs/protocol/CAPABILITY_TOKENS.md`](../CAPABILITY_TOKENS.md) for the
full token format, scope grammar, and issuance/verification protocol.
That document is normative for v1.0 and is reproduced here by reference.

The verifier integration points:

- **§7.1** A signed envelope whose `EnvelopeRecipient.agentTokenId` is set
  MUST be presented in any audit surface as "principal _via agent_ label",
  per the cert convention.
- **§7.2** A conforming verifier MAY consult a Sigil-published JWKS to
  verify principal Ed25519 keys offline. The JWKS URL is implementation-
  defined for v1.0; v1.1 will standardise it.

## 8. Verification report shape

A conforming verifier produces a report with at least the following fields:

```
report := {
  status:           "VALID" | "TAMPERED" | "INVALID" | "UNKNOWN"
  signatureValid:   boolean
  signerCommonName: string | null
  detail:           string
  recognized:       boolean    // matched a known seal
  document: {
    subject:      string | null
    finalizedAt:  ISO-8601 | null
    sha256:       hex(64)
    recipients:   [ { name, email, role, status, completedAt } ]
  } | null
  timestamp: {
    authority:    string
    at:           ISO-8601
  } | null
}
```

`status = TAMPERED` MUST be returned when §5.2 step 2 produces a hash
that does not match the signed bytes.

`status = UNKNOWN` MUST be returned when the document is not a Sigil
seal at all (no PAdES signature dictionary, or no matching public seal
record).

## 9. Protocol versioning

- Released versions: `v1.0`, `v1.1`, `v2.0`, etc.
- A verifier MUST advertise the maximum protocol version it implements.
- A producer MUST stamp the producing version into any output where it
  matters (currently: the JWS `typ` for capability tokens is fixed at
  `sigil-cap+jwt` for v1.x).

## Appendix A: reference implementations

- TypeScript (S11.T2): `protocol/reference-verifier-typescript/`
- Python (S11.T3): `protocol/reference-verifier-python/`

Both pass the Compliance Test Suite (S11.T5) and serve as the
ground-truth interpretation of any spec ambiguity. Where this document
disagrees with both reference implementations, the reference
implementations are correct and a spec patch follows in the next
minor version.
