Signer identity, and the evidence sidecars bound by hash - #73
Open
noah-pi wants to merge 4 commits into
Open
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
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.tsgainedbuildCsrandcsrToPemfor the request side.src/lib/siteCredential.ts), formatsourcekit-site/1at/.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 fromOrgCredentialfor 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.src/lib/identity.tsresolves the disclosure mode to a capture-time claim and ownsIdentityMode, 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.tsfollows 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_ANCHORSships 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 sharedSubScreenshell, with the detail behind each row rather than on the Settings page.The evidence sidecars, bound by hash
CaptureEvidencePathsnamed 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):readDirectoryAsyncpromises no order.The three states survive. A sink reporting
never-recordedhas 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.RawAudioCardrecomputes the file hash it already reads and states one of the three outcomes against the sealed value.Also
docs/IDENTITY.md, new: the three modes, what each rests on, and what none of it proves.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 --noEmitis clean.Ported to the closed tree, where
test:core,test:realandtest:verifyall pass. One gap worth naming: the closed tree's node harness importssrc/directly with no expo shim layer, so the digest suite runs only here.evidenceDigest.tsis 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