# HumanProof protocol v1

**Normative integration contract.** A certificate attests to a signer's reported observations. It does not certify thought origin, that a human authored a document, trustworthy hardware, the truth of an event, or trusted time. The header observation class remains `localObservation`. The additive Creation Assurance v1 extension separately evaluates L1–L5 from the authenticated evidence; default recording supports at most L2, and the scoped native editor provider supports L3 under its additional contract. Managed enforcement and independent hardware attestation MUST NOT be inferred from observation mode, timing correlation, Keychain, Secure Enclave or TPM availability.

## Envelope and exact signed bytes

UTF-8 JSON envelope (JSON ordering itself is immaterial):

```json
{"schemaVersion":"humanproof/1","algorithm":"Ed25519","keyID":"<64 lowercase SHA256 hex of raw public key>","publicKey":"<base64 raw 32-byte Ed25519 public key>","payload":"<base64 exact UTF-8 payload JSON>","signature":"<base64 raw 64-byte Ed25519 signature>"}
```

Signature input is UTF-8 `HumanProof.Certificate.v1` followed by ONE NUL byte (`00`), followed by the decoded payload bytes. No additional length or canonicalization. Algorithms and schema versions are case sensitive. A public key supplied by the envelope is self asserted; callers must separately pin a previously trusted `keyID`.

Decoded payload:

```json
{"header":"<base64 exact header JSON>","events":[{"sequence":0,"body":"<base64 exact observation JSON>","previousHash":"<64 lowercase hex>","hash":"<64 lowercase hex>"}],"headHash":"<64 lowercase hex>","completion":{"endedAtMilliseconds":123,"status":"completed","fileBinding":{"sha256":"<64 lowercase hex>","byteCount":123,"fileName":"optional-display-name.txt"}}}
```

`fileBinding` is optional. `completion.status` is `completed` or `interrupted`. Empty events are valid; evidence explicitly reports an empty session. Envelope maximum 16 MiB. Maximum 100000 events, event body 16384 bytes, header 16384 bytes. Integer times/lengths/sequences must be nonnegative safe JSON integers (<= 9007199254740991). `monotonicNanoseconds` is a DECIMAL STRING because UInt64 cannot safely roundtrip through JavaScript numbers.

Decoded header JSON:

```json
{"protocolVersion":"humanproof/1","sessionID":"<UUID>","startedAtMilliseconds":123,"targetBundleID":"com.soulmen.ulysses3","mode":"observation","assurance":"localObservation","synthetic":false,"recorderVersion":"1.0.0"}
```

`mode` is `observation` or `sealedObservation`. Sealed observation requests additional best-effort telemetry and NEVER asserts prevention/enforcement.

Decoded observation JSON:

```json
{"kind":"input.correlated","timestampMilliseconds":123,"monotonicNanoseconds":"12345","metadata":{"source":"CGEvent"}}
```

No characters, key codes, window titles, clipboard data or document text belong in telemetry. Event kinds are extensible; unknown kinds MUST NOT imply stronger evidence. All fields are signed but remain claims of the local recorder. Non-monotonic local wall time produces an evidence warning rather than trusted-time assertions. Monotonic order must not decrease within a session.

The macOS app's recorder versions starting with 1.5.0 store monotonic values relative to that session's start, including pauses, rather than exporting raw boot uptime. These values are ordering and elapsed-time claims only; earlier compatible records may use a different local monotonic origin. Automatic app/document associations remain claims: a salted local document-reference identifier does not bind document bytes or establish input causality. An unavailable or ambiguous document reference remains application scope/unknown and must not inherit another document's identifier.

## Event hash chain

Let SHA256 produce 32 bytes. `UTF8(x)` denotes exact UTF-8 bytes; `NUL` is one zero byte. `U64BE(n)` is an eight-byte unsigned big-endian integer.

```
anchor = SHA256(UTF8("HumanProof.Session.v1") || NUL || decodedHeaderBytes)
eventHash[i] = SHA256(UTF8("HumanProof.Event.v1") || NUL || previousHashBytes[32]
                      || U64BE(sequence) || U64BE(decodedBodyBytes.count) || decodedBodyBytes)
```

Sequence starts at 0 and is contiguous. The first previous hash is anchor; later previous hashes equal the prior calculated event hash. `headHash` is anchor for an empty session or the final event hash. JSON-internal whitespace is part of the hash/signature input. Verifiers MUST use the stored exact decoded bytes, not re-encode JSON.

Document binding is a streaming SHA256 and byte count of the explicitly selected file at finalization. It proves equality to those bytes only; no claim that this session caused those bytes. A verifier without the document reports `notChecked`, never document match. A mismatching supplied document fails verification even when certificate integrity is valid.

## Public Swift API

`SessionStore(rootURL: URL? = nil, identity: SigningIdentity? = nil) throws` defaults to `~/Library/Application Support/HumanProof`. It owns persistence and retrieves/creates one Ed25519 key in Keychain (service `de.humanproof.signing`, account `default-v1`). No other component should create a signing identity.

- `startSession(targetBundleID: String, mode: RecordingMode = .observation, synthetic: Bool = false, assuranceRequest: AssuranceRequest? = nil, minimumAssurance: AssuranceLevel? = nil) throws -> SessionRecorder`. The app explicitly supplies `.automatic`; omitted policy preserves the legacy programmatic contract.
- recorder: `header: SessionHeader`, `eventCount: Int`, `needsRollover: Bool`, `assurancePolicy: AssurancePolicy?`, `liveAssurance: AssuranceEvaluation?`; `append(kind: String, timestampMilliseconds: Int64, monotonicNanoseconds: UInt64, metadata: [String:String] = [:]) throws`; `finish(fileURL: URL? = nil) throws -> CertificateBundle`
- `listSessions() throws -> [SessionSummary]`, `loadCertificate(sessionID: String) throws -> CertificateBundle`, `loadTimeline(sessionID: String) throws -> [Observation]`, `recoverInterruptedSessions() throws -> [CertificateBundle]`, `deleteSession(sessionID: String) throws`
- `CertificateBundle.encoded(pretty: Bool = true) throws -> Data`
- `CertificateVerifier.verify(data: Data, fileURL: URL? = nil, trustedKeyIDs: Set<String> = []) -> VerificationReport`
- `SigningIdentity.ephemeral()` for explicitly synthetic/test use; `SigningIdentity.keychain()` for persistence.

`SessionSummary`: `id: String`, `targetBundleID: String`, `startedAtMilliseconds: Int64`, `status: SessionStatus`, `eventCount: Int`, `synthetic: Bool`. `SessionStatus`: `recording`, `completed`, `interrupted`.

`VerificationReport`: `integrityValid: Bool`, `signerTrusted: Bool`, `fileStatus: FileVerificationStatus` (`notBound`, `notChecked`, `matched`, `mismatched`, `unavailable`), `sessionID: String?`, `eventCount: Int`, `synthetic: Bool`, `errors: [String]`, `warnings: [String]`, `evidence: [String]`; `isValid: Bool` requires integrity and no verification errors; trust remains a separate dimension. UI must label integrity and trust independently.

## Creation Assurance v1

The normative [Assurance contract](./format/specifications/assurance/v1/CONTRACT.md), [machine-readable requirements](./format/specifications/assurance/v1/requirements.json), [claim schema](./format/schemas/assurance.schema.json) and [shared conformance cases](./format/fixtures/assurance/conformance.json) define one cumulative evaluator across Swift, Node and C#. `header.assurancePolicy` freezes the requested mode, numeric requested level (explicit `null` for Automatic), and institution minimum at session start. `payload.assurance` signs the complete evaluated result. Its numeric achieved level is explicit `null`/`unverified` when L1 is unmet; no L0 exists. A request never grants or caps the achieved level.

L1 requires document binding, chain and signature. L2 additionally requires a complete non-synthetic session, ordered temporal history, observed events/revisions, a final chain checkpoint, and no critical provenance gap. New participating native sessions start with `assurance_session_started` and finish with `assurance_session_ended` followed by `assurance_checkpoint`. The checkpoint commits its prior event count and exact chain head in `metadata.eventCount` and `metadata.headHash`. Recovery is interrupted, preserves the frozen policy and cannot issue L2. Word receipts receive Automatic evaluation without retrofitting native lifecycle events and currently issue at most L1. Existing records without either extension field remain legacy and receive no retroactive level.

L3 requires a supported verified physical input path with operating-system and document-mutation correlation and continuous classification. L4 additionally requires an enforced environment policy continuously upheld. L5 requires independent hardware attestation. L3 requirements are `unavailable` for ordinary observation/Word records. The additive [native editor input provider](./format/specifications/assurance/v1/NATIVE-INPUT-PROVENANCE.md), frozen by `header.inputProvenancePolicy`, can satisfy L3 within the trusted OS/recorder boundary using complete device/input/edit evidence, one-use tokens, opaque revision links and final file binding. It stores no intermediate text hashes or individual key timings. L4/L5 remain `unavailable` in production. Test-only normalized provider facts exercise L3–L5 without making them admissible certificate evidence. Record-supplied booleans, unknown events and a requested level cannot activate a provider.

Every known claim field must equal the verifier's independent evaluation after signature, header, entire chain and completion checks. A missing, malformed, unsupported or overstated claim produces `INVALID ASSURANCE CLAIM` and makes the report invalid. The requirements map contains exactly the defined vocabulary; unknown outer fields have no meaning. Policy and claim must both be present or both absent. Explicit `null` policy is invalid. `VerificationReport.assuranceEvaluation` is encoded as `assurance`; `assuranceClaimValid` is absent for legacy records and true/false for participating claims. Document-match and signer-trust results remain separate dimensions.

`AssuranceEvaluator.preflight(requested:minimumRequired:storageAvailable:signingAvailable:)` distinguishes capability (`maximumAvailableLevel`) from certification (`achievedLevel`, pending until signed and bound). `canStart` enforces available capability and prohibits manual selection below an institution minimum. `requiresFallback` identifies a manual target above current capability; the app requires explicit fallback while retaining that original request. `permitsStart` combines these checks. Recorder `liveAssurance` uses the same incremental rules, leaves final signature/binding pending, and reports irreversible potential-level downgrade transitions without rescanning the journal. Waiting, restored focus, later valid input or a successful checkpoint cannot erase earlier critical violations.

## Cumulative writing observations

Writing evidence is a separate dimension from `localObservation` assurance. It describes input signals and coverage, **not** the human or AI origin of a document. The certificate still contains no typed characters, clipboard text, raw key codes, or text-section provenance.

A recorder declaring `telemetry_started.metadata.inputEvidenceVersion = "1"` supports the current writing-evidence vocabulary. The supported method is `pasteDetection = "command_v_keycode_hint"`. Each `input_activity` interval repeats both fields. The method records a scoped Command-V key-code/modifier hint (including supported shift/option variants); it does not establish that the application accepted a paste or that any characters changed. Menu commands, drag-and-drop, imports, remapped shortcuts and pre-existing text are not comprehensively observed.

The verifier derives optional `VerificationReport.writingAssessment` only after validating the signature, header, entire event chain and completion. Failed cryptographic validation leaves it absent. The report's assessment is not itself a new signed claim; independent verifiers must rebuild it from authenticated events. Old report JSON without the property still decodes. Valid old certificates receive an assessment whose detection support is false, never a clean-history result.

Assessment fields:

| Field | Meaning |
| --- | --- |
| `trackingSupported` | At least one start declares evidence version `1`. |
| `trackingComplete` | Support exists and every observed start/input segment explicitly declares version `1` and the supported method. Any unsupported earlier segment permanently makes this false. This field is vocabulary coverage, **not** proof of continuous sensor delivery. |
| `pasteShortcutCount` | Sum of `input_activity.metadata.pasteShortcutCount`; hints, not confirmed insertions or inserted character counts. |
| `keyboardEventCount` | Sum of `quartzKeyDown`; keyboard events, including shortcuts, not text characters. |
| `timingMatchCount` | Sum of `timingCorrelated`; heuristic temporal matches, not human-authorship evidence. |
| `unknownSourceIndicatorCount` | Sum of `privateSourceStateIndicators` and `quartzWithoutTimingMatch`; these indicators may overlap and do not prove automation. |
| `gapCount` | Number of coverage indicators: `observation_gap` except `state=end`, `clock_discontinuity`, `session.recovered`, and `capture_scope_changed` with `targetFocusedAndEligible=false`. Not a duration or count of unique interruptions. |
| `malformedEvidence` | At least one relevant counter is malformed, exceeds the safe-integer bound, overflows cumulatively, or an interval claims more paste shortcuts than Quartz key-down events. Integrity can still be valid; semantic counts cannot support a positive provenance claim. |
| `textOrigin` | Always `unknown` in this implementation, even with no paste hints, perfect timing correlation and a matching final file. |

All counters use canonical nonnegative decimal strings in event metadata (no signs, spaces, decimal/exponent forms or leading zeroes except `0`), bounded by `9007199254740991`. Omitted sparse counters mean zero. Malformed individual values contribute zero and set `malformedEvidence`; previously observed evidence is retained. Aggregate overflow saturates at the same bound and sets the flag. A paste count exceeding its interval's key-down count is retained but marked malformed. Relevant hints in unsupported segments remain visible; unsupported status never erases them.

Assessment always covers the **entire session chain**. There is no rolling window, cooldown, confidence recovery, percentage of human text, or elapsed-time rule. Waiting thirty seconds, waiting an hour, typing further, pausing, resuming, or restarting telemetry cannot erase earlier paste hints or coverage gaps. A new session does not establish where its initial text came from. Unknown event kinds do not improve the assessment. Live UI uses `WritingAssessmentAccumulator.observe(_:)` for an incremental equivalent; exported verification uses `WritingAssessment.assess(observations:)` over the authenticated history.

## Recovery and portability

Each local session has exact header bytes and an append-only newline-delimited event journal. Every acknowledged append is synchronized to disk. Final certificates are written atomically. Advisory per-session locks prevent recovery or deletion of an active recorder. Recovery validates the entire journal and can discard only a final incomplete line, with explicit recovery evidence; a malformed complete line fails closed. Recovered sessions are finalized as interrupted, never completed. A session already carrying a final certificate is not recovered again. Exported JSON contains everything needed for offline integrity verification, but no private key and no externally trusted identity/time.

## Parser and resource constraints

Reject duplicate JSON object keys at every level, including escaped spellings and Unicode canonically equivalent (NFC-equivalent) keys. Reject nesting deeper than 32, negative/fractional/exponent number encodings, unsafe integers, invalid UTF-8, malformed Unicode escapes and trailing non-whitespace data. Base64 must have the canonical RFC 4648 standard alphabet/padding (decode then re-encode comparison). Decimal monotonic strings have no sign or leading zero except the string `0`. Additional unknown object fields are ignored and convey no assurance. Creation Assurance v1 is an additive signed extension; an extension-aware verifier MUST validate its policy, claim and evidence together. Older verifiers may check envelope integrity but cannot assert that an assurance claim is valid. Changes to the base cryptographic contract require a protocol version change.

Each JSON parse is additionally limited to **1,000,000 values** in total and **100,000 entries per container**. The root counts as one value; every array element and object value counts, including nested containers, while object keys do not count as additional values. Container entries are array elements or object key/value pairs. Swift and Node enforce these allocation budgets before adding an over-budget entry. Limits apply separately to the envelope and each decoded payload/header/event parse; the existing byte limits also remain in force. Duplicate-key diagnostics do not echo attacker-supplied keys.

Field limits are **UTF-8 byte lengths**: `targetBundleID` 1–512; `recorderVersion` 1–128; `kind` 1–128; at most 32 metadata entries; metadata keys 1–128 and values 0–2048; `fileName` 0–4096. Schema `maxLength` is an auxiliary character bound; the implementation additionally enforces bytes. Empty strings are allowed only where explicitly stated. JSON Schemas describe the decoded structures; schema validation alone cannot check signatures, exact-byte hashes, base64 decoded sizes, safe parser behavior or monotonic ordering.

The local recorder reserves recovery space and rejects a new normal append before its journal exceeds 11 MiB or 99999 events for legacy sessions. Assurance-participating sessions reserve three protocol slots and accept at most 99997 events before finalization, including their start marker; the reserved slots accommodate end/checkpoint markers and recovery. It retains space for explicit recovery evidence and a final certificate under the 16 MiB envelope limit. A failed append is not acknowledged; the application can finalize the previously accepted events. A full session must be finished and a new session started. These limits are availability bounds, not assurance upgrades.

`SessionRecorder.needsRollover` becomes true at 98976 events or a 10.5 MiB payload budget, leaving 1024 protocol event slots or 512 KiB before the corresponding hard bound. Legacy sessions count journal bytes. Participating sessions additionally count a conservative 128 KiB allowance for fixed header/claim/completion fields and an incremental upper bound for each newly added violation, including JSON escaping. Both the early signal and hard append guard use this combined budget, so repeated signed violation details cannot crowd out finalization or recovery. Existing size limits are never raised. It is an advisory signal to flush sensors and finalize the current segment; it does not relax append/recovery limits, automatically bind a document, or extend the old signed record. Each following segment has its own session ID and chain, even when the app links its reported document scope.

`SessionRecorder.interrupt() throws -> CertificateBundle` closes an intentionally interrupted recording without document binding. `SessionSummary` additionally includes `mode: RecordingMode` and `endedAtMilliseconds: Int64?`. `SessionStore.importCertificate(data:)` verifies before import and refuses duplicate IDs. Keychain creation is lazy at first signing operation, so inspecting/importing certificates does not create a private key. SessionStore and SessionRecorder are synchronous and require one serialized caller; per-session advisory locks protect across processes.

`VerificationReport` JSON includes computed `isValid`. This requires cryptographic integrity and no assurance-claim or requested document-check errors, **not** signer trust. Consumers requiring signer authentication must also require `signerTrusted`. The CLI enforces an explicitly supplied `--trusted-key` pin with exit 2 on mismatch; it exits 1 for integrity/document/operation errors. Verification without a pin may exit 0 while explicitly reporting self-asserted signer identity.

A newly created recovery observation uses the prior event's monotonic value plus one, saturating at `UInt64.max` (one for an empty journal), and records `monotonicBasis=reconstructed-order-only`. This reconstructs ordering; it does not measure elapsed time across a crash or reboot. Previously durable recovery markers keep their original value and are reused on retry. Tail removal and the marker that describes discarded bytes are published together in one atomic journal replacement. Its new wall time is explicitly local. Recovery uses the currently available local signing key; the unfinished journal was not externally witnessed or cryptographically authenticated before final signing. Imported entries missing their original signed certificate fail closed rather than being re-signed as local recovery.

See `./format/schemas/certificate.schema.json` and `./format/schemas/payload.schema.json`. `./format/fixtures/synthetic-certificate.json` was produced by the Swift implementation, binds `synthetic-document.txt`, and is consumed by the independent Node verifier tests. Synthetic fixtures never constitute physical-device capture evidence.

Erase-all also removes regular library-owned `NativeDocuments/<sessionID>.txt` files under pinned-directory safeguards. Unknown names, links and unsafe entries are retained and reported. This does not change individual session deletion or external export semantics.


## Website distribution status
The website supplies synthetic examples and the portable verifier for evaluation. This is not a native installer release or an institutional security approval. The scoped native input provider remains experimental in the public website offering. L4 and L5 are unavailable.
