Skip to content

Profile-to-key linking aligned with LWS 1.0 (FPWDs 2026-04-23) via W3C Controlled Identifiers v1.0 #386

@melvincarvalho

Description

@melvincarvalho

Tracker for converging the existing profile/key-linking issues (#42, #6, #4, #39, #86, #319) onto the W3C Linked Web Storage 1.0 Authentication Suite FPWDs published 2026-04-23 — and on the substrate they delegate to, W3C Controlled Identifiers v1.0.

This issue does not propose a new design from scratch. It captures the design that fell out of reading the LWS FPWDs against JSS's current state, so future PRs can reference one place.


Key insight

LWS auth FPWDs delegate identity-document semantics to W3C Controlled Identifiers 1.0. A WebID becomes LWS-conformant for self-signed auth (LWS10-CID, LWS10-did:key) by being a valid CID document — i.e. by carrying verificationMethod, authentication, assertionMethod, controller, publicKeyJwk/publicKeyMultibase triples. No separate DID document needed for the WebID-controlled case.

This converges several historically separate JSS workstreams onto one mechanism.


Current JSS state

src/webid/profile.js already declares cid: and lws: prefixes (prep work landed) but writes zero verification material. The path to conformance is additive — no breaking changes to existing profiles.


Proposed profile shape

A WebID with three keys: a Nostr secp256k1 (NIP-98), a did:key Ed25519 (LWS10), and a passkey/WebAuthn credential.

{
  "@context": [
    "https://www.w3.org/ns/cid/v1",
    {
      "foaf": "http://xmlns.com/foaf/0.1/",
      "solid": "http://www.w3.org/ns/solid/terms#",
      "schema": "http://schema.org/",
      "pim": "http://www.w3.org/ns/pim/space#",
      "ldp": "http://www.w3.org/ns/ldp#",
      "owl": "http://www.w3.org/2002/07/owl#",
      "lws": "https://www.w3.org/ns/lws#",
      "nostr": "https://w3id.org/nostr/vocab#",
      "alsoKnownAs": { "@id": "https://www.w3.org/ns/activitystreams#alsoKnownAs", "@type": "@id" },
      "sameAs":      { "@id": "owl:sameAs", "@type": "@id" }
    }
  ],
  "id": "https://alice.solid.social/profile/card.jsonld#me",
  "type": ["foaf:Person"],
  "controller": "https://alice.solid.social/profile/card.jsonld#me",

  "verificationMethod": [
    { "id": "...#nostr-key-1",  "type": "Multikey",   "controller": "...#me", "publicKeyMultibase": "fe70102…" },
    { "id": "...#did-key-1",    "type": "Multikey",   "controller": "...#me", "publicKeyMultibase": "z6MkpT…"  },
    { "id": "...#passkey-1",    "type": "JsonWebKey", "controller": "...#me", "publicKeyJwk": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" } }
  ],
  "authentication":   ["...#nostr-key-1", "...#did-key-1", "...#passkey-1"],
  "assertionMethod":  ["...#did-key-1"],

  "alsoKnownAs": ["did:nostr:124c0…", "did:key:z6Mk…"],
  "sameAs":      "did:nostr:124c0…"
}

Notes:

  • https://www.w3.org/ns/cid/v1 is the first @context entry. CID-aware verifiers process this directly; existing JSS predicates (oidcIssuer, pim:storage, ldp:inbox, service, …) live in the inline prefix object so JSON-LD output is byte-stable for everything else.
  • Nostr secp256k1 uses did:nostr's multikey recipe (parity byte + multicodec 0xe7,0x01 + base16-lower) so a NIP-98 verifier can decode straight to the 32-byte x-only key without dereferencing a DID document.
  • alsoKnownAs is the forward link to DID URIs (matches did:nostr's conventional binding); sameAs keeps the existing src/auth/did-nostr.js resolver working unchanged.

Verification flow (key K → WebID W)

  1. Identify subject from credential: sub (LWS-CID JWT), pubkey hex (NIP-98), DID URI (did:key), credential ID (WebAuthn)
  2. Fetch the WebID profile (which is the CID document)
  3. Find matching verificationMethod — by kid for JWT, by raw key match for NIP-98
  4. Confirm it appears in authentication (or assertionMethod for credential issuance)
  5. Verify the cryptographic proof using that key
  6. Confirm the verification method's controller is W (or W is its own controller)

This is profile-only for LWS-CID and WebAuthn; profile-or-DID-doc for did:nostr (existing two-way owl:sameAsalsoKnownAs from #42); DID-doc-only for did:key (the DID is the public key).


Migration path

Smallest delta — ~80–120 LOC:

Change Where
Switch @context to array form so cid/v1 is the imported context src/webid/profile.js:generateProfileJsonLd
Accept keys[] parameter; emit verificationMethod + authentication (+ optional assertionMethod) when supplied same
Pass keys through from signup (nostr pubkey, did:key, passkey credential) src/handlers/container.js:createPodStructure, src/idp/interactions.js registration paths
jss migrate add-keys admin command to upgrade existing pods (idempotent merge — only adds triples) new bin/jss.js subcommand

Back-compat: profiles created without keys are byte-identical to today. Existing JSS resolvers (did-nostr, webid-tls) continue to work; the new design generalises the WebID-TLS pattern (cert:keyverificationMethod).


Open spec choices (FPWDs don't bind us; we get to decide)

# Question Suggested default
1 Profile vs DID-doc primary WebID is canonical for LWS-CID; DID docs supplementary via alsoKnownAs
2 kid fragment shape #<purpose>-<n>, never reuse after revocation
3 Key rotation Append-only verificationMethod; revoked keys removed from authentication but stay with expirationDate
4 NIP-98 Schnorr inside CID's Multikey Use did:nostr's encoding verbatim; no NostrPublicKey extension
5 Passkey/WebAuthn vocabulary JsonWebKey (P-256 EC); credentialId stays in private account record, not profile
6 alsoKnownAs provenance Bidirectional check for did:nostr (existing); trust WebID-side for did:key (no DID doc)
7 controller self-reference vs delegation Self-control only for v1; delegation/recovery later
8 Recovery keys lws:recoveryMethod extension until standardised
9 assertionMethod membership Only did:key for now (Schnorr/passkey are auth-only in practice)
10 Profile MIME content negotiation Existing application/ld+json at profile/card.jsonld already conforms

Phasing

This tracker captures design. Concrete shippable phases:

Phase 1 — Profile emits keys (PR-shaped):

  • src/webid/profile.js accepts keys[]; emits CID triples
  • @context array form
  • New pods at signup get the keys they registered with
  • No verifier-side changes yet — purely additive
  • Tests for the JSON-LD structure

Phase 2 — Migration command:

  • jss migrate add-keys to upgrade existing pods
  • Idempotent; safe to re-run

Phase 3 — LWS10-CID verifier:

Phase 4 — Cross-protocol unification:


Refs

Converges these issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestnostrNostr relay, did:nostr auth, NIP-related

    Type

    No type
    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