Proposal: This document explores a Nostr-native mechanism for discovering and verifying pubkey-owned services. It is deliberately narrow in scope, experimental in nature, and intended for use within the Nostr ecosystem only.
Summary
A Nostr-native mechanism for discovering and verifying pubkey-owned services inside the Nostr ecosystem, without relying on DNS or public certificate authorities.
It provides:
signed service discovery
endpoint identity binding
optional third-party attestation
short-lived, revocable trust
This is an application-layer trust system, not a browser HTTPS replacement.
Problem
Many Nostr applications need to connect to user-controlled services such as:
personal APIs
media servers
wallets
agents, bots, or bridges
Today this requires:
hard-coded URLs
blind trust in DNS and HTTPS
manual configuration
poor support for dynamic or non-DNS endpoints
Nostr already has strong cryptographic identity. What’s missing is a standard way to say:
“This pubkey owns that service, reachable here, using this key.”
Design Goals
Pubkey-first authority model
Works with dynamic IPs and non-DNS endpoints
Minimal protocol surface
Explicit trust decisions
No global roots or hidden authorities
Fully optional third-party attestation
Non-goals
Replacing DNS
Supporting browsers or legacy tooling
General internet certification
Core Objects
1. Service Record (published by service owner)
A parameterised replaceable event representing the current location and identity of a service.
Purpose Bind a pubkey to a reachable endpoint and its transport key.
Required tags
d – service identifier (eg api, media, wallet)
u – endpoint URI ( https://, wss://, tcp://, onion://)
k – endpoint public key fingerprint (eg SPKI hash)
exp – expiry timestamp
Rules
Latest valid, unexpired record wins
Multiple services per pubkey allowed via d
Short expiries recommended (7–30 days)
2. Certificate Attestation (optional, third-party)
A signed statement by a certifier pubkey asserting that a specific service record meets a defined standard.
Purpose
Provide additional assurance beyond self-assertion.
Required tags
subj – subject pubkey
srv – service id
e – service record event id
std – standard identifier (eg nostr-service-trust-v0.1)
lvl – trust level (self, verified, hardened)
nbf, exp – validity window
Client policy
Clients decide which certifier pubkeys they trust and what levels they require.
3. Revocation (certifier-published)
A signed event revoking a previously issued certificate.
Required tags
e – certificate event id
optional reason
Revocation overrides expiry.
Client Resolution Algorithm
-
Resolve current Service Record for (pubkey, service-id) from multiple relays
-
Verify signature and expiry
-
Fetch Certificate Attestations referencing that record
-
Filter by trusted certifier pubkeys and validity
-
Connect to endpoint
-
Verify endpoint key matches fingerprint k
-
Fail closed on mismatch unless user explicitly overrides
Trust Model
Primary authority: the pubkey
Certificates: optional and additive
Trust stores: explicit and visible to the user or app
No implicit global trust
This mirrors SSH known_hosts, not public-web HTTPS.
Threat Model
In scope (addressed)
Endpoint impersonation Prevented by binding endpoints to pubkeys and verifying key fingerprints.
Man-in-the-middle attacks Mitigated through key pinning and explicit trust decisions at the application layer.
Stale or hijacked endpoints Limited via short-lived service records, expiries, and revocation events.
CA misissuance or DNS compromise Avoided entirely by not relying on public CAs or DNS-based trust.
Relay inconsistency or partial censorship Mitigated by querying multiple relays and selecting the latest valid records.
Out of scope (not addressed)
Endpoint compromise after certification
Traffic analysis or metadata leakage
User key compromise
Global availability guarantees
Security Properties
Provides:
Endpoint authenticity
MITM resistance via key pinning
Controlled key rotation
Revocation with bounded blast radius
Does not claim:
Legal identity
Data privacy guarantees
Browser-level security
Intended Use Cases
Nostr apps connecting to user-run backends
Wallets and agents bound to pubkeys
Self-hosted or onion services
“Bring your own infrastructure” designs
Related Work and Novelty
This proposal builds on existing Nostr patterns but addresses a gap that is not currently standardised.
NIP-05 (DNS-based identifiers)
Focuses on identity discovery, not endpoint trust, key continuity, expiry, or revocation.
NIP-65 (Relay list metadata)
Demonstrates a discovery pattern but is scoped only to relays and lacks trust semantics.
NIP-89 (Application handlers)
Targets application capability discovery, not service ownership or secure connection establishment.
Attestation and credential patterns
Existing work is intentionally broad. This proposal defines a narrow, implementable trust layer focused on endpoint verification.
Novelty
The contribution is the composition of existing primitives into a focused mechanism that provides:
pubkey-authoritative service discovery
cryptographic binding between endpoint and transport key
explicit expiry and revocation
optional third-party attestations
an internal alternative to DNS and public CAs for Nostr apps
Status
Draft v0.1 Experimental. Intended for Nostr ecosystem tooling only.
Appendix A: Minimal Reference Implementation
A.1 Components
Service Publisher
Resolver / Verifier
Optional Certifier
A.2 Service Publisher (owner side)
service_id = "api" endpoint_uri = " https://example.net:8443" endpoint_key_fp = spki_hash(endpoint_tls_cert) expiry = now + 14 days
event = { kind: SERVICE_RECORD_KIND, pubkey: owner_pubkey, tags: [ ["d", service_id], ["u", endpoint_uri], ["k", endpoint_key_fp], ["exp", expiry] ] }
sign(event) publish_to_relays(event)
A.3 Resolver / Verifier (client side)
records = query_relays(pubkey, SERVICE_RECORD_KIND, service_id) record = select_latest_valid(records)
if record.expired: fail
certs = query_relays(CERT_ATTESTATION_KIND, record.id) valid = filter_trusted(certs)
if policy.requires_cert and valid.empty: fail
connect(record.endpoint)
if endpoint_key != record.k: fail
A.4 Certificate Issuer (optional)
cert = { kind: CERT_ATTESTATION_KIND, pubkey: certifier_pubkey, tags: [ ["subj", owner_pubkey], ["srv", service_id], ["e", service_record_id], ["std", "nostr-service-trust-v0.1"], ["lvl", "verified"], ["nbf", now], ["exp", now + 30 days] ] }
sign(cert) publish_to_relays(cert)
A.5 Notes
SQLite is sufficient for caching
No global registry required
Trust stores are app-defined
Fail-closed defaults recommended