Phase 2 of #437. Close the loop between the Phase 1 owner key and the auth path that's already shipped.
What's already in jss
| Capability |
Where |
Phase 1 keypair on disk (/private/privkey.jsonld, CID v1.0 Multikey, 0o600 + WAC) |
src/keys/provision.js, wired in src/handlers/container.js + src/server.js |
LWS-CID authentication verifier (W3C LWS 1.0 / FPWD 2026-04-23): reads kid from a JWT header, fetches WebID profile, locates VM, verifies JWT against publicKeyJwk / publicKeyMultibase |
src/auth/lws-cid.js |
WebID profile generator with CID v1.0 verificationMethod / publicKeyMultibase / publicKeyJwk JSON-LD context |
src/webid/profile.js |
did:nostr: → WebID resolver (with redirect guards, cache) |
src/auth/did-nostr.js |
Local did:nostr resolution via JSS-published well-known DID-doc |
src/idp/well-known-did-nostr.js |
| Pubkey extractor from a profile's VM entries (handles both f-form Multikey and JWK with BIP-340 even-y check) |
src/auth/nostr-keys.js |
The gap
The seeded WebID profile (/profile/card.jsonld) has a verificationMethod slot but the Phase 1 public key never lands in it. So the entire authentication loop is broken at exactly one wire:
- An agent has the secret at
/private/privkey.jsonld.
- They sign a JWT (LWS-CID style:
kid = a fragment of their WebID profile pointing at a VM).
- The LWS-CID verifier fetches their WebID profile, looks for the VM by
kid, finds nothing, returns 401.
- The keypair we just minted can't actually authenticate the agent via the mechanism it was minted for.
Phase 2 connects these two ends.
Scope
When --provision-keys is on:
- After generating the keypair and writing
/private/privkey.jsonld, also inject a verificationMethod entry into the seeded WebID profile pointing at the public side of the same key.
- The VM should look something like:
{
"id": "<podUri>profile/card.jsonld#owner-key",
"type": "Multikey",
"controller": "<webId>",
"publicKeyMultibase": "fe70102…"
}
Same publicKeyMultibase value as in /private/privkey.jsonld. Optionally also a JWK form for tools that prefer it (the LWS-CID verifier already handles either).
- Flip the Phase 1 controller in
/private/privkey.jsonld from WebID to did:nostr:<npub> once we've confirmed jss's existing resolver round-trips. The Phase 1 design log explicitly anticipated this swap; Phase 2 is when it lands.
Acceptance
Tests
- Unit: profile generator, given a
publicKeyMultibase, emits the right VM shape; absent the option, the VM array stays empty.
- Round-trip: feed the generated VM through
extractNostrPubkeysFromProfile and assert the recovered xonly pubkey matches.
- Integration:
--provision-keys + a small client that signs an LWS-CID JWT with the on-disk secret + kid set to the VM's id → 200 on a WAC-protected GET. (Reuses the existing LWS-CID test harness if there is one; otherwise builds the JWT inline.)
- did:nostr round-trip:
resolveDidNostrLocally(pubkey) returns the pod owner's WebID after Phase 2 wiring.
Why this is small
No new crypto. No new auth path. No new resolver. No new file format. The verifier, the profile generator, the resolver, the extractor, and the keypair generator are all in tree already. Phase 2 is two wires plus a controller flip.
Out of scope
- Phase 3 work:
--key-passphrase / scrypt+aesgcm wrap, BIP-39 seed phrase derivation.
- Phase 4 work: hardware wallets, KMS, encrypted-at-rest.
- Anything that changes the LWS-CID verifier (no need — it already accepts the shape Phase 2 will produce).
Refs #437.
Phase 2 of #437. Close the loop between the Phase 1 owner key and the auth path that's already shipped.
What's already in jss
/private/privkey.jsonld, CID v1.0 Multikey, 0o600 + WAC)src/keys/provision.js, wired insrc/handlers/container.js+src/server.jskidfrom a JWT header, fetches WebID profile, locates VM, verifies JWT againstpublicKeyJwk/publicKeyMultibasesrc/auth/lws-cid.jsverificationMethod/publicKeyMultibase/publicKeyJwkJSON-LD contextsrc/webid/profile.jsdid:nostr:→ WebID resolver (with redirect guards, cache)src/auth/did-nostr.jsdid:nostrresolution via JSS-published well-known DID-docsrc/idp/well-known-did-nostr.jssrc/auth/nostr-keys.jsThe gap
The seeded WebID profile (
/profile/card.jsonld) has averificationMethodslot but the Phase 1 public key never lands in it. So the entire authentication loop is broken at exactly one wire:/private/privkey.jsonld.kid= a fragment of their WebID profile pointing at a VM).kid, finds nothing, returns 401.Phase 2 connects these two ends.
Scope
When
--provision-keysis on:/private/privkey.jsonld, also inject averificationMethodentry into the seeded WebID profile pointing at the public side of the same key.{ "id": "<podUri>profile/card.jsonld#owner-key", "type": "Multikey", "controller": "<webId>", "publicKeyMultibase": "fe70102…" }publicKeyMultibasevalue as in/private/privkey.jsonld. Optionally also a JWK form for tools that prefer it (the LWS-CID verifier already handles either)./private/privkey.jsonldfrom WebID todid:nostr:<npub>once we've confirmed jss's existing resolver round-trips. The Phase 1 design log explicitly anticipated this swap; Phase 2 is when it lands.Acceptance
--provision-keys, the seeded profile contains a VM withpublicKeyMultibasematching/private/privkey.jsonld.kidset to the VM'sid, presented to a WAC-protected resource, authenticates as the pod owner via the existing LWS-CID verifier — no changes to the verifier./private/privkey.jsonldisdid:nostr:<npub>, andresolveDidNostrToWebId(<pubkey>)resolves back to the pod owner's WebID locally (via the well-known DID-doc index).--provision-keysoff still produces a profile with no VM (today's default).extractNostrPubkeysFromProfileround-trips the new VM (same shape it already accepts for did:nostr verification).Tests
publicKeyMultibase, emits the right VM shape; absent the option, the VM array stays empty.extractNostrPubkeysFromProfileand assert the recovered xonly pubkey matches.--provision-keys+ a small client that signs an LWS-CID JWT with the on-disk secret +kidset to the VM'sid→ 200 on a WAC-protected GET. (Reuses the existing LWS-CID test harness if there is one; otherwise builds the JWT inline.)resolveDidNostrLocally(pubkey)returns the pod owner's WebID after Phase 2 wiring.Why this is small
No new crypto. No new auth path. No new resolver. No new file format. The verifier, the profile generator, the resolver, the extractor, and the keypair generator are all in tree already. Phase 2 is two wires plus a controller flip.
Out of scope
--key-passphrase/ scrypt+aesgcm wrap, BIP-39 seed phrase derivation.Refs #437.