Thread

Nostr-Native Service Discovery and Trust (Draft v0.1)

Exploring a small, Nostr-native way for apps to discover and verify pubkey-owned services. Instead of relying on DNS and public CAs, this approach uses signed service records, key pinning, and optional third-party attestations to let Nostr apps securely connect to user-run backends. It is intentionally narrow, experimental, and aimed at builders rather than end users.

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

  1. Resolve current Service Record for (pubkey, service-id) from multiple relays

  2. Verify signature and expiry

  3. Fetch Certificate Attestations referencing that record

  4. Filter by trusted certifier pubkeys and validity

  5. Connect to endpoint

  6. Verify endpoint key matches fingerprint k

  7. 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

Replies (0)

No replies yet. Be the first to leave a comment!