Skip to content

auth: LWS10-CID JWT verifier with ES256K (Schnorr-key dual-use, strict FPWD conformance) #397

@melvincarvalho

Description

@melvincarvalho

Phase 3a of #386 / concrete implementation of the #3 CID sub-bullet of #319.

Goal

A strict-conformant LWS 1.0 Authentication Suite: SSI via Controlled Identifiers (FPWD 2026-04-23) verifier on incoming HTTP requests, signed with ES256K (RFC8812 — ECDSA over secp256k1, registered JWS algorithm).

The trick that makes this Nostr-native without inventing extensions: same secp256k1 private key, two signature schemes. Existing Nostr identities sign Schnorr/BIP-340 for Nostr; the same key trivially signs ECDSA for ES256K JWTs. No new key material; no Schnorr-as-JWS gymnastics.

What changes

New module src/auth/lws-cid.js:

  1. Detect LWS-CID auth: `Authorization: Bearer ` where JWT header carries `alg: ES256K` (or any registered JWS alg) and `kid`.
  2. Per LWS10-CID §5: parse JWT, validate `sub === iss === client_id` (all the WebID URI), `exp` not past, `iat` recent, `aud` includes the pod origin.
  3. Dereference the WebID profile (the controlled identifier document, per CID 1.0). Locate `verificationMethod` whose `id` matches the JWT's `kid`.
  4. Decode `publicKeyJwk` (`kty: EC, crv: secp256k1, x, y`).
  5. Verify JWT signature per RFC7515 §5.2 using `@noble/secp256k1` (already in tree from NIP-98).
  6. Confirm the VM's `controller` matches the WebID (or matches the profile's declared `controller` for delegated cases).
  7. Return WebID as the authenticated identity, slot into existing auth-method preference order (Auth middleware: prefer OIDC/DPoP over NIP-98 when both are present on a request #306).

Acceptance

  • FPWD example JWT verifies as a fixture test (`test/lws-cid.test.js`)
  • A WebID profile with a `JsonWebKey` VM (P-256 or secp256k1) accepts a correctly-signed ES256K JWT and rejects a tampered one
  • Auth middleware preference order: OIDC > LWS-CID > NIP-98 (configurable, per Auth middleware: prefer OIDC/DPoP over NIP-98 when both are present on a request #306 spirit)
  • Wrong-`kid`, expired, missing-`aud` JWTs all rejected with appropriate WWW-Authenticate response
  • No regression in existing OIDC / NIP-98 paths

Pairs with doctor B.3

The client side — generating the matching profile shape and signing real JWTs — ships in JavaScriptSolidServer/doctor#3 (now linked at JavaScriptSolidServer/doctor#3). Plan is to build them together so each side validates against real output from the other before merging.

Out of scope

  • LWS10-did:key (separate, simpler — VM lookup is degenerate, the kid IS the key)
  • Schnorr/BIP-340 as a custom JWS alg (non-FPWD, deferred)
  • bip340-jcs-2025 Data Integrity (different envelope; for VC signing, not HTTP auth)
  • WebAuthn passkey VMs

Refs

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