Podkey

Podkey did:nostr Identity Specification

How Podkey creates, presents, and authenticates a did:nostr identity in the browser

Draft — version 1

This document is a work in progress and may be updated, replaced, or obsoleted at any time. It profiles the Nostr DID Method Specification for a browser-held identity, following that document's structure and conventions. The key words must, must not, should, and may are to be interpreted as in RFC 2119. Key custody mechanics (encrypted vaults, passkey unlock and derivation) are out of scope here and are specified in the companion Podkey Passkey Identity Specification.

1. Introduction

did:nostr makes a Nostr public key a W3C decentralized identifier: no registry, no blockchain, no identity provider — the key is the identity. What the method specification leaves open is where that key lives and how it is exercised safely from a web page.

Podkey answers that as a Manifest V3 browser extension: it holds exactly one secp256k1 keypair per profile, never releases the private key to any page, and exposes the identity through two narrow, user-consented surfaces — the NIP-07 capability object for Nostr applications, and a NIP-98 HTTP authentication profile for servers such as Solid pods. This document specifies both surfaces and the identity lifecycle around them, so that servers and applications can interoperate with a Podkey-held did:nostr identity without depending on extension internals.

2. Core concepts

2.1 Identifier

The identity is a BIP-340 x-only secp256k1 public key, encoded as 64 lowercase hex characters, used directly in the DID scheme of the method specification:

const pubkey = await window.nostr.getPublicKey()
const did = `did:nostr:${pubkey}`
// did:nostr:3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d

Implementations must use the raw hex form in DIDs and protocol messages. The bech32 forms (npub… for display, nsec… for private-key interchange per NIP-19) are presentation formats only.

2.2 One identity, one key

Podkey manages a single active identity. Everything the extension emits — a signed event, an authentication header, an encrypted payload — is attributable to that one did:nostr. There is no key rotation within an identity: rotating means creating a new identity, as in the method specification's update semantics.

2.3 Custody boundary

The private key exists in plaintext only inside extension memory for the duration of a browser session. At rest it is encrypted on-device; unlocking uses a passphrase or a passkey (see the companion specification). Web pages interact only with the surfaces below; a page never receives, and cannot request, key material. Every response that crosses the page boundary is a public key, a signed event, an encrypted/decrypted payload, or an authentication header.

3. Presentation: the NIP-07 capability surface

Podkey injects a NIP-07 window.nostr object into every page. The implemented surface:

MethodBehaviour
getPublicKey()Returns the 64-hex public key after per-origin consent; consent establishes revocable origin trust.
signEvent(event)Signs any event kind (BIP-340 Schnorr). Trusted origins sign without a prompt; untrusted origins always prompt.
nip44.encrypt(pubkey, plaintext) / nip44.decrypt(pubkey, ciphertext)NIP-44 v2 payloads, gated by the same origin-trust model.

getRelays() is intentionally absent: Podkey holds no relay list, and a missing method is the honest signal to capability-probing clients. Applications must feature-detect rather than assume the full NIP-07 surface.

4. Authentication: the NIP-98 HTTP profile

Podkey authenticates HTTP requests with NIP-98: a signed kind-27235 event carried in the Authorization header. Podkey emits tokens with this exact shape:

{
  "kind": 27235,
  "created_at": <unix seconds>,
  "tags": [
    ["u", "<exact request URL>"],
    ["method", "<HTTP method>"],
    ["nonce", "<16 random bytes, hex>"],
    ["payload", "<sha256 of the request body, hex>"]   // present iff the request has a body
  ],
  "content": "",
  "pubkey": "<64-hex>",
  "id": "…", "sig": "…"
}

Authorization: Nostr <base64(JSON event)>

This is how a Podkey identity authenticates to Solid pods with no OAuth redirect and no identity-provider account: the pod verifies the token and resolves the DID per the method specification. Automatic (promptless) NIP-98 signing is opt-in and restricted to exactly-matching trusted hosts; by default every authentication is user-approved.

5. Operations

5.1 Create

Generate a fresh secp256k1 keypair inside the extension, or import an existing key (64-hex or nsec, normalised at the import boundary). The DID is did:nostr:<pubkey> from the moment the key exists — no registration step, per the method specification.

5.2 Read (resolve)

Podkey does not resolve DIDs; it is the subject, not a resolver. Consumers resolve a Podkey-presented DID exactly as the method specification describes — minimal resolution derives a valid DID document from the public key alone, offline, which is sufficient to verify anything Podkey signs.

5.3 Update / Deactivate

Not applicable at the extension layer. Podkey publishes no events on its own; profile data (kind 0), relay lists, and social graph are the domain of Nostr applications the user signs into via NIP-07. Abandoning an identity is done by forgetting the key (after backup) and creating a new one.

5.4 Backup and restore

The identity survives as an exported private key (nsec or hex). Restoring it into any NIP-07 signer — Podkey or another — restores the same did:nostr. This portability is a property of the key itself and is the canonical way an identity moves between devices and implementations.

6. Security considerations

7. Privacy considerations

8. Implementations

9. Resources