Skip to content

Signer identity, and the evidence sidecars bound by hash - #73

Open
noah-pi wants to merge 4 commits into
mainfrom
claude/source-kit-open-source-launch-w4030i
Open

Signer identity, and the evidence sidecars bound by hash#73
noah-pi wants to merge 4 commits into
mainfrom
claude/source-kit-open-source-launch-w4030i

Conversation

@noah-pi

@noah-pi noah-pi commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Two changes that both concern what a record commits to: who signed it, and which bytes it reaches.

Signer identity

A self-signed device key proves consistency and nothing about who you are. Three credentials can supply the other half, and none of them moves the private key out of the Secure Enclave.

  • Personal certificate (src/lib/personalCert.ts). The device builds a PKCS#10 request and signs it with the Enclave key; an authority checks the person and issues against that key. The certificate belongs to the person rather than to this app, so any tool that writes CAWG identity assertions can use it. src/lib/cert.ts gained buildCsr and csrToPem for the request side.
  • Website credential (src/lib/siteCredential.ts), format sourcekit-site/1 at /.well-known/sourcekit-site.json. TLS vouches for the domain, and nothing in the file is signed, so it carries less than a certificate does. It is a separate type from OrgCredential for exactly that reason: the compiler will not let a caller mistake the weaker claim for the stronger one. Every member entry must hash its own published key.
  • Organization credential, unchanged, now sitting alongside the other two.

src/lib/identity.ts resolves the disclosure mode to a capture-time claim and owns IdentityMode, which moved here from the store — what a mode means belongs with the modes, and the store only remembers which one is on. src/lib/deviceKey.ts follows the mode when it assembles the certificate chain, defaulting to anonymous so an unset preference fails safe.

Recognition follows the CAWG interim trust model in force until 31 March 2027 (src/lib/identityTrustList.ts). A certificate carrying the document-signing purpose is recognized outright, as the specification requires. One carrying email protection is recognized only when its chain reaches an anchor list the device holds. PINNED_IDENTITY_ANCHORS ships empty and is fetched at runtime, so until a list is fetched every issuer reads as self-asserted — the honest report of what this device can check, not a verdict on the certificate.

No name a user typed reaches a capture. The free-fill byline is gone, so a name in a record is always something a verifier can go and check.

Three routes under app/identity/ (personal, website, organization) behind a shared SubScreen shell, with the detail behind each row rather than on the Settings page.

The evidence sidecars, bound by hash

CaptureEvidencePaths named the raw LPCM master, the sensor log and the ring buffer by path. A path says where a file was and nothing about what it held, so a swapped master or a rewritten sensor log left every signature valid. The evidence a desk reaches for first was the evidence nothing bound.

Each sink now carries a digest, taken before the record is signed (src/lib/evidenceDigest.ts):

  • file sink — SHA-256 over the bytes.
  • directory sink — SHA-256 over the listing: each frame's own digest beside its name, sorted by name, hashed together. The sort is what makes it reproducible; readDirectoryAsync promises no order.

The three states survive. A sink reporting never-recorded has no digest by construction, a sink whose file cannot be read yields null, and a reader that finds no digest says the sidecar is uncommitted rather than assuming a match. The fields are optional, so records sealed before this change are read as carrying no digests rather than as mismatches.

RawAudioCard recomputes the file hash it already reads and states one of the three outcomes against the sealed value.

Also

  • Deletes a doc comment in the store describing a weather opt-in setting that does not exist. The archive lookup is gated by the tap on "Check the archive" and by nothing else.
  • docs/IDENTITY.md, new: the three modes, what each rests on, and what none of it proves.
  • README and site/index.html: the credentials section covered only the organization route and claimed it was the only thing that can attach a name to a key. It now covers all three. The raw audio paragraph promised the master's hash was signed into the record in a clause that ran on from the resampling sentence; that is now true of all three sidecars, and the sentence says which and how.

Testing

33/33 lab suites pass. Two are new:

  • tests/test-signer-identity.mts — 25 checks with OpenSSL as the gold standard: it verifies the CSR self-signature, issues a certificate against it, and walks that certificate through the install path including the refusals.
  • tests/test-evidence-digest.mts — 16 checks: the file digest against node crypto, the directory digest for order independence, content sensitivity and rename sensitivity, and every absence path.

npx tsc --noEmit is clean.

Ported to the closed tree, where test:core, test:real and test:verify all pass. One gap worth naming: the closed tree's node harness imports src/ directly with no expo shim layer, so the digest suite runs only here. evidenceDigest.ts is byte-identical in both trees, so it is the same code under test, but the closed tree has no suite of its own covering it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P3RisvkEsx2cgseTRMmUdB


Generated by Claude Code

claude added 4 commits August 28, 2026 15:02
Signer Information becomes three peer credentials, and the typed byline is
gone. A name a user simply typed is not something a verifier can check, so
there is nowhere left to type one.

Website (new, sourcekit-site/1): publish one file at
/.well-known/sourcekit-site.json listing the phones allowed to sign as you.
No certificate authority, no account, and no DNS record — the file rests on
the certificate already on the website. It is a separate type from
OrgCredential rather than a flag on it, so the weaker claim cannot be
mistaken for the stronger one at any call site.

Verified Identity (new): the app builds a PKCS#10 certification request and
signs it with the Enclave key, which is how the request proves it controls
the key it names. Trust is evaluated against anchor lists the device holds
and reported as trusted or self-asserted, never assumed. The pinned list
ships empty and is filled from the published list at runtime; no fingerprint
is written by hand.

Organization Credential moves to its own screen with the key-sharing half
it always needed.

The x5chain now follows the identity mode. Anonymous signs with the bare
device certificate: shipping the org chain named an organization the capture
had promised to leave out.

Settings keeps three rows and one line. Everything a row used to explain
lives behind the tap.

Tests: the certification request is verified by OpenSSL, issued against by a
test CA, and the issued certificate is walked back through the install path,
including the key-mismatch and missing-purpose refusals. The site document
round-trips and every way it can disagree with itself is rejected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P3RisvkEsx2cgseTRMmUdB
The record named the raw LPCM master, the sensor log and the ring buffer
by path. A path says where a file was and nothing about what it held, so a
swapped master or a rewritten sensor log left every signature valid. The
evidence a desk reaches for first was the evidence nothing bound.

Each sink now carries a digest taken before the record is signed:

  file sink       SHA-256 over the bytes.
  directory sink  SHA-256 over the listing — each frame's own digest beside
                  its name, sorted by name, hashed together. The sort is
                  what makes it reproducible; readDirectoryAsync promises
                  no order.

The three states survive. A sink that reports 'never-recorded' has no
digest by construction, a sink whose file cannot be read yields null, and
a reader that finds no digest says the sidecar is uncommitted rather than
assuming a match. Records sealed before this change carry none, so the
fields are optional and their absence is reported as absence.

The raw audio card recomputes the file hash it already reads and states
one of the three outcomes against the sealed value.

Also removes a doc comment in the store describing a weather opt-in
setting that does not exist: the archive lookup is gated by the tap on
"Check the archive" and by nothing else.

Lab: 32/32 suites, including a new evidence-digest suite that checks the
file digest against node crypto, the directory digest for order
independence and content sensitivity, and every absence path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P3RisvkEsx2cgseTRMmUdB
Anonymous, personal and organization, what credential each one rests on,
and who vouches for it: a certificate authority, a domain over TLS, or an
organization's own CA. States the recognition rule under the CAWG interim
trust model, and why an unrecognized issuer reads as self-asserted rather
than as a failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P3RisvkEsx2cgseTRMmUdB
The credentials section named only the organization route and claimed it
was the only thing that can attach a name to a key. A personal certificate
and a website credential both do, on different evidence: an authority that
checked a person, and a domain that published a key over TLS. Each is now
described with what it rests on and what it does not carry.

The raw audio paragraph promised the master's hash was signed into the
record, in a clause that ran on from the resampling sentence. That is now
true of all three sidecars, and the sentence says which and how, including
what a reader does when it finds no hash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P3RisvkEsx2cgseTRMmUdB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants