# Optional detached presence report attestations, version 1

`presence-attestation.py` adds local evidence attribution to the existing
`HAP-1/WA-compatibility-report/1` report. The runner remains unchanged and has no
signing dependency or signing flag. Unsigned local reports remain supported.
Signing is a separate, explicit operation that writes a new sidecar; it never
rewrites the report, metadata, corpus or runner. This is report attestation,
distinct from WebAuthn authenticator attestation (`none` in WA-1).

**A valid signature proves only that the holder of the independently pinned
signer key attested to these exact evidence bytes and declarations.** Associating
that key with a person or organization requires separate authentication. It does
not prove that the declared implementation ran, that an executable matches its
source revision, trustworthy execution, implementation independence, independent
operation, security review, correctness, physical presence, or authority to
perform an action. A dishonest signer can attest to fabricated evidence. Public
corpus answers can be memorized. The [review guide](PRESENCE-REVIEW.md) continues
to apply without relaxing any of its evidence requirements.

## Wire contract

The sidecar is an exact two-field JSON object: `payload` and `signature`.
`signature` is the 64-byte Ed25519 signature as 128 lowercase hex characters.
No embedded public key, certificate, key URL, key discovery, algorithm negotiation
or trust-on-first-use mechanism is accepted. The key ID is a comparison value;
the verifier never uses it to fetch a key.

The signed payload has exactly these fields, all strings:

| Field | Meaning |
| --- | --- |
| `schema` | Exactly `HAP-1/WA-report-attestation/1`; versions do not fall back |
| `algorithm` | Exactly `Ed25519` |
| `profile` | Exactly `HAP-1/WA-1` |
| `reportSchema` | Exactly `HAP-1/WA-compatibility-report/1` |
| `scope` | Exactly `published-corpus-only` |
| `reportSha256` | SHA-256 of the **original report file bytes**, including any final LF |
| `corpusSha256` | Report corpus digest, matching the retained pin and bundled fixed corpus |
| `runnerSha256` | Report runner source digest, matching the independently retained pin |
| `implementationSource` | Declared source repository, matching the report and retained pin |
| `implementationRevision` | Declared lowercase 40- or 64-hex revision, matching the report and pin |
| `implementationMetadataSha256` | SHA-256 of the complete canonical `implementation` object, with no LF |
| `signerKeyId` | `ed25519-sha256:` followed by SHA-256 of the raw 32-byte pinned Ed25519 public key |

Digests are 64 lowercase hex characters. Metadata has the unchanged
[runner constraints](PRESENCE-ADAPTER.md), including source, revision, name,
runtime and dependencies. The report digest binds **all** report fields,
including outcomes, summary, declarations and negative claim flags. The explicit
metadata digest also covers name, runtime and dependency substitutions.

Canonical JSON means ASCII encoding, keys sorted lexicographically, no spaces,
and JSON strings escaped as Python `json.dumps(..., sort_keys=True,
separators=(',', ':'), ensure_ascii=True, allow_nan=False)`. All field names and
allowed string values in this contract are ASCII. The signature preimage is:

```text
ASCII("HAP-1/WA-report-attestation/1") || 0x00 || canonical(payload)
```

The prefix includes exactly one NUL byte. The payload has no final LF. Sign these
bytes directly with Ed25519; do not prehash or sign a hex digest string. The
schema, algorithm, profile, report version and signer identity are themselves
signed, preventing reuse across report types, profiles or protocol versions.
Changes to the signed contract require an explicit new version.

Report, sidecar and pin inputs must each be canonical JSON, optionally followed
by **one** LF. Duplicate members at any depth, unknown/missing fields, BOMs,
trailing JSON, invalid encodings and malformed signatures refuse. Report summary
counts must be integers, not booleans or floats. Its 111 case names and operations
must match the pinned corpus in order, with no missing, duplicate, added or
skipped cases. Counts, stopped-run behavior and complete/match flags must agree.
All existing negative claim flags must be literally false. A correctly signed
but malformed, cross-profile or inconsistent report still refuses.

The report is parsed for validation but **never normalized before hashing**.
Removing a final LF produces different report bytes and requires a different
attestation, even though the JSON has the same meaning. Reusing an attestation
over a different report fails. Rechecking the same historical bytes is allowed:
this contract has no timestamp, trusted clock, expiry, challenge, freshness or
one-use promise. It cannot prevent replay of the same historical evidence as
"new." There is no online revocation check; a local trust policy must withdraw
compromised keys and separately establish dates or freshness when required.

## Caller-owned trust pin

Both commands require a separate canonical pin with exactly these fields:

```json
{"corpusSha256":"28e100171668c4f8ebfdd1600690ce779f168a272af717a347f7babe2801f4b2","implementationRevision":"1111111111111111111111111111111111111111","implementationSource":"https://example.test/team/verifier","profile":"HAP-1/WA-1","reportSchema":"HAP-1/WA-compatibility-report/1","runnerSha256":"2fb8cdaa3d6af65faf3b7225a7a137c3d09eede256cf2a45e6dce5a7c6f3ccd6","schema":"HAP-1/WA-report-attestation-pin/1","signerPublicKey":"REPLACE_WITH_AUTHENTICATED_64_LOWERCASE_HEX_PUBLIC_KEY"}
```

This is a template, not a usable trust record. Replace the example source,
revision and key from independently authenticated information. The published
runner digest above identifies the unchanged PR #62 `presence-conformance.py`.
Authenticate your release and compare the actual runner and corpus file digests
before retaining them. Bind the implementation source/revision to the source you
reviewed and reproduced. Do not populate the pin from an untrusted report or
sidecar, and do not accept a key supplied beside an untrusted attestation without
authenticating it separately. File separation alone supplies no trust.

A new runner, corpus, profile, key or implementation revision is an explicit
review/pin update, not an automatically accepted replacement. This verifier
supports the fixed WA-1 corpus shipped here. It does not execute, download or
inspect an implementation, rerun the runner, assess dependencies, or establish
which executable generated the report. `implementationMetadataVerified` remains
false after successful verification. No Money Musk account, central signer,
registry, endpoint or approval is required.

## Local signing and verification

Use Python 3.10+ on POSIX with `requirements-verify.txt` installed in your chosen
environment. Linux is exercised in CI. Verification uses no private key and
performs no network requests. Provision dependencies before disconnecting.

Use a dedicated Ed25519 report-signing key, separate from authority, payment,
delegate, executor and authenticator keys. The signing CLI accepts an unencrypted
PKCS#8 PEM file owned by the current user with no group/other permissions. Store
it in a protected directory and keep it out of the adapter's environment and
mounts. Run untrusted adapters in isolation without signing credentials. Only
move their public report to the signing environment after reviewing it.

For a new local test signer, the following creates a fresh key directory and
files exclusively. It prints no private material. Authenticate the public key
with intended verifiers through a separate channel before using it in a pin:

```sh
python3 - <<'PY'
import os
from pathlib import Path
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
os.umask(0o077)
directory = Path('report-signer')
directory.mkdir(mode=0o700)
key = Ed25519PrivateKey.generate()
with (directory / 'private.pem').open('xb') as f:
    f.write(key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()))
with (directory / 'public.hex').open('x') as f:
    f.write(key.public_key().public_bytes_raw().hex() + '\n')
PY

python3 hap1/presence-attestation.py sign \
  --report local-report.json --pin retained-attester-pin.json \
  --private-key report-signer/private.pem --out local-report.attestation.json

python3 hap1/presence-attestation.py verify \
  --report local-report.json --pin retained-attester-pin.json \
  --attestation local-report.attestation.json
```

Neither command launches an adapter. Signing checks the complete report and pin
and requires the private key to match the pin. Files are bounded regular files:
report 131,072 bytes; pin, sidecar and PEM key 16,384 bytes each; corpus 1 MiB.
Final-component symlinks, special files, oversized/empty inputs and changes
detected during a read refuse. Parent directories must remain under your control.
Platforms without `O_NOFOLLOW` and `O_NONBLOCK` refuse. Output is created
exclusively with mode 0600; existing files and symlinks are never overwritten.
A killed/interrupted write may leave a partial sidecar; it will not verify.

Successful signing exits **0** with `attestationCreated:true`. Successful
verification exits **0** with `attestationVerified:true`, a signer fingerprint,
the exact report digest, and the report's `complete`/`corpusMatch` values.
**Exit 0 from verification does not mean the corpus passed.** Correctly formed
mismatch and incomplete reports can be signed and verified. Claim flags
`executionAuthorized`, `implementationMetadataVerified`, `independenceAssessed`,
`securityAuditPerformed`, `statefulAuthorityTested` and
`physicalAuthenticatorTested` always remain false.

Every refusal exits **1** with `attestationVerified:false` and fixed
`INVALID_REPORT_ATTESTATION`, without paths, key material, metadata, input
excerpts or exceptions. Missing/duplicate/unknown flags refuse. Explicit
verification with no sidecar fails; it never silently downgrades to unsigned
success. Users who want unsigned reports continue using the original runner.

## Portable hostile fixtures

`presence-attestation-vectors.json` contains golden report and preimage bytes,
pins, sidecars, and 84 acceptance/refusal cases. Raw report/sidecar bytes use
standard base64 solely as a fixture transport; do not normalize them. Cases
reference the `reportsBase64`, `attestationsBase64`, and `pins` maps. The seed and
all keys are **public synthetic test keys**; never trust them for real evidence
or authority. These are project-authored tests, not independent review.

```sh
python3 scripts/hap1-presence-attestation-vectors.py --check
python3 scripts/hap1-presence-attestation-regressions.py -v
```

The regression suite checks hostile cases through both the verifier and CLI,
cross-checks the golden Ed25519 signature with Node crypto, runs the real
unchanged runner against the golden report bytes, and checks local file handling,
private key permissions, missing-attestation refusal and report preservation.
The existing adapter and presence conformance suites continue to run separately.

References: [Ed25519 API](https://cryptography.io/en/46.0.0/hazmat/primitives/asymmetric/ed25519/),
[adapter/report contract](PRESENCE-ADAPTER.md), [independent review](PRESENCE-REVIEW.md).
