Skip to content

feat: --provision-keys Phase 2 — wire the owner key into the WebID profile, close the LWS-CID loop (#437) #443

Description

@melvincarvalho

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:

  1. 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.
  2. 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).
  3. 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

  • On --provision-keys, the seeded profile contains a VM with publicKeyMultibase matching /private/privkey.jsonld.
  • A JWT signed with the secret, kid set to the VM's id, presented to a WAC-protected resource, authenticates as the pod owner via the existing LWS-CID verifier — no changes to the verifier.
  • The Phase 1 controller in /private/privkey.jsonld is did:nostr:<npub>, and resolveDidNostrToWebId(<pubkey>) resolves back to the pod owner's WebID locally (via the well-known DID-doc index).
  • No regression: --provision-keys off still produces a profile with no VM (today's default).
  • extractNostrPubkeysFromProfile round-trips the new VM (same shape it already accepts for did:nostr verification).

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions