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

Comments
No comments yet. Start the conversation.