How Podkey creates, presents, and authenticates a did:nostr identity in the browser
Draft — version 1did: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.
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.
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.
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.
Podkey injects a NIP-07
window.nostr object into every page. The implemented surface:
| Method | Behaviour |
|---|---|
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.
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)>
nonce tag supplements NIP-98's 1-second created_at resolution so
verifiers can reject replays exactly; verifiers should enforce
single use per nonce within their freshness window.payload tag binds the request body: verifiers must
reject a body-bearing request whose hash does not match.u and method tags
against the actual request, verify the Schnorr signature, and then treat
did:nostr:<pubkey> as the authenticated identity.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.
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.
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.
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.
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.
did:nostr is a stable global identifier by design; every origin the user consents to
learns the same pubkey and can correlate on it. This inherits the method specification's
correlation caveats.did:nostr per the method
specification (verifier side).