# Echo Claim Proofs: ECP-0.1

Status: experimental, independently implementable reference profile. ECP is evidence;
it creates **no execution authority** and changes no HAP-1, AO-1, presence or payment bytes.
The first release supports local file-digest signing and verification only.

## Meaning and exclusions

A valid signature authenticates a key's statement about a SHA-256 file digest and byte
length. It does not establish authorship, copyright, truth, ownership, legal identity,
human review, biological presence, authorization, execution or settlement.
The expected key must be obtained through a separate trusted channel. A key embedded
in a proof is not, by itself, a trusted identity. The same file can have many claims.

`signedAtMs` is an integrity-protected **signer assertion**, not trusted time. No clock
freshness, key rotation, key revocation, timestamp, consent, use limit, delegation or
agent attribution is evaluated by ECP-0.1. Unknown fields do not enable these features.
An old signature can verify after a key is compromised; current trust is a separate policy.

## Closed envelope

Exact top-level members: `schema, kind, signer, payload, signedAtMs, nonce, signature`.

| Member | Required value |
| --- | --- |
| `schema` | `"ECP-0.1"` |
| `kind` | `"file-digest"` |
| `signer` | Exactly `{algorithm, publicKey, keyId}` |
| `signer.algorithm` | Exactly `"Ed25519"` or `"ES256"`; no fallback/negotiation |
| `signer.publicKey` | Lowercase hex: raw 32-byte Ed25519 or uncompressed 65-byte P-256 point starting `04` |
| `signer.keyId` | 64 lowercase hex characters; computed below |
| `payload` | Exactly `{sha256, bytes}` |
| `payload.sha256` | SHA-256 of the exact file bytes, lowercase hex |
| `payload.bytes` | Integer from 0 through 67,108,864 (64 MiB) |
| `signedAtMs` | Integer Unix milliseconds from 0 through 8,640,000,000,000,000 |
| `nonce` | 16 cryptographically random bytes, lowercase hex; mandatory |
| `signature` | 64-byte signature, lowercase hex |

No file name, path, MIME type, file content, public URL or private key belongs in the
envelope. File bytes are hashed without text decoding, newline conversion or Unicode
normalization. An empty file is distinct from an absent file argument.

## Encoding

The wire is UTF-8 canonical JSON with an optional **single terminal LF** and a maximum
of 4096 bytes including that LF. All supported strings and field names are ASCII.
Objects have their keys sorted by ascending ASCII value, use JSON string escaping,
and have no insignificant whitespace. All numbers use unsigned decimal integer
notation without leading zeros. Negative zero, fractions, exponents, non-finite values,
duplicate keys, escaped alternate spellings, BOMs, CRLF, arrays, null, booleans and
unknown members are rejected. `signature` is included in the canonical wire.

This is a restricted JCS-compatible schema, not a general-purpose JSON canonicalizer.
Validation first checks input bounds and canonical encoding, then the profile and
closed shape. Canonicalization is bounded to depth 5, 10 members per object, ASCII
strings up to 256 characters and safe unsigned integers (at most 2^53-1). Property
names match `[A-Za-z][A-Za-z0-9]*`. Programmatic serializers reject accessors and
non-plain objects without invoking accessors. New schemas require a new profile;
unsupported profiles must never be accepted as a weaker known profile.

## Cryptography and identifiers

`C(x)` denotes UTF-8 canonical JSON of x, without a terminal LF. `H(x)` denotes
lowercase hex SHA-256 of bytes x. Domain strings are case-sensitive ASCII, and `\n`
is one byte `0a`.

```
keyId = H("ECP-0.1/key\n" || algorithm || "\n" || publicKeyHex)
message = "ECP-0.1/claim\n" || C(envelope without signature)
claimId = H("ECP-0.1/proof\n" || C(complete signed envelope))
```

The signature authenticates every field of the body. Replacing a field creates a new
claim that requires a new signature. A timestamp, if added in a later detached format,
must bind the immutable `claimId`; it cannot modify this envelope. No timestamp field
or bare block height is trusted here. Claim IDs include signatures, so independently
created signatures can produce different claim IDs for the same file and body.

* **Ed25519:** pure Ed25519 over `message` (no additional prehash). The public key and
  signature R encoding must be canonical, non-identity points in the prime-order
  subgroup; S must be canonical, less than the subgroup order. Reject small-order,
  mixed-order and noncanonical points. The reference uses strict verification,
  not ZIP-215 acceptance. This intentionally narrows the accepted Ed25519 set.
* **ES256:** ECDSA P-256 with SHA-256 over `message`. Signature encoding is fixed-width
  32-byte big-endian r followed by 32-byte big-endian s (not ASN.1 DER). Require
  `1 <= r < n` and `1 <= s <= floor(n/2)`, where
  `n = ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551`.
  The signer normalizes s to `min(s, n-s)`; the verifier rejects high-s signatures.
  The public point must lie on P-256 and use the uncompressed canonical encoding.

ECP `keyId` is a full, algorithm-bound fingerprint. Short Echo display IDs and the
display resolver's shortened fingerprint must not be used as verification pins.
The browser reuses an existing local spine key. It never generates a replacement
identity or exports a private key through this workflow. Signing self-verifies the
result to catch a stored public/private key mismatch before returning a proof.

## Verification report and order

Reports are observations, not signed attestations. Every report includes `code`,
`signature`, `payload`, `signer`, `timestamp:"not_provided"`, and `authority:"none"`.

1. Parse bounded canonical input. Failure: `INPUT_INVALID`. An otherwise canonical
   unknown/missing schema or unsupported signing algorithm: `PROFILE_UNSUPPORTED`.
2. Validate optional expected key pin (64 lowercase hex), then supplied payload size.
   An invalid pin gives `INPUT_INVALID`; an invalid/oversized byte buffer gives
   `PAYLOAD_TOO_LARGE`. Ordinary CLI file-opening errors give `INPUT_INVALID`.
3. Recompute `keyId`. Mismatch: `KEY_MISMATCH`.
4. Verify the signature and key/point rules. Failure: `SIGNATURE_INVALID`.
5. Only after a valid signature, return `claimId`, `signerId` and `signedAtMs`.
6. Compare an optional file's exact bytes/length and optional independently obtained
   expected key. Report both dimensions. A file mismatch takes reason-code precedence
   (`PAYLOAD_MISMATCH`), then a key mismatch (`SIGNER_MISMATCH`), otherwise `CLAIM_VALID`.

`signature`: `unchecked | valid | invalid`.
`payload`: `unchecked | not_supplied | matches | mismatch`.
`signer`: `unchecked | not_pinned | matches_expected | mismatch`.
`CLAIM_VALID` means the supported claim checks passed **for the supplied inputs**.
Without a file it verifies only the signed digest statement. Without a pin the
signer's identity is not established. A matching pin still does not prove a human
reviewed the file. No report can authorize an action or activate an existing grant.

## Local storage, privacy and operations

File bytes are held in tab memory only. Exported/saved proofs contain public keys,
digests and self-reported times; these can link activity or identify guessed files.
This is not encrypted or zero-knowledge evidence. There is no server upload endpoint,
remote proof resolver, network request or telemetry payload in the ECP engine.
The containing website can still load its ordinary assets and page analytics.

Local history is a separate, unencrypted IndexedDB database `echo:claim-proofs:1`.
Saving is explicit. Atomic transactions deduplicate by full claim ID and cap history
at 100 proofs across competing connections. Reads reverify every record; corrupted
or unavailable storage is reported as unavailable, not as zero activity. Clearing tab
inputs does not delete saved history. Removing one local copy is not revocation.
Browser storage is not a tamper-proof ledger, backup, or global counter.

Once loaded, the browser's signing and checking functions need no network. Reloading
the website offline is not guaranteed. The CLI is offline after its dependencies are
installed. It reads only explicitly selected regular local files with bounded reads;
it never follows a URL contained in evidence. Installation can require internet.

## Compatibility and future work

No Authority signed fields or gate order change in this release. Future mandatory
claim requirements need an explicitly selected, signed Authority extension that binds
claim IDs, signer expectations and the exact action payload. Old verifiers must refuse
that profile. Global max-use limits, revocation freshness and delegated agent signing
are deliberately outside ECP-0.1; a standalone offline proof cannot enforce them.

The reference browser implementation and independent Python implementation are
maintained by this project. Agreement on the published vectors is interoperability
evidence, not an independent security audit or proof of deployment-wide enforcement.

References: [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785),
[RFC 8032](https://www.rfc-editor.org/rfc/rfc8032),
[OpenTimestamps](https://opentimestamps.org/), and the repository's `hap1/SPEC.md`.
