UQRP Protocol Specification

Draft

Universal Query Response Protocol for DNS-Based Data Storage

Live DNS Query
Try:

Overview

ResolveDB transforms DNS infrastructure into a data distribution platform. Data is encoded in DNS queries and responses, leveraging global DNS caching for sub-millisecond access.

Explicit Parameters Design

ResolveDB uses explicit parameters for all context-dependent queries. The server never infers client identity, location, or preferences from the source IP address. This design provides predictability, privacy, cache efficiency, and auditability.

Core Principles

PrincipleImplementation
Deterministic responsesIdentical queries MUST return identical responses regardless of source
Explicit parameters onlyLocation, IP, and context MUST be provided as query parameters
No source IP inferenceServer MUST NOT use the querier's IP for any business logic
Proxy-transparentQueries through VPNs, DoH, or proxies work identically to direct queries

Benefits

BenefitDescription
PredictabilitySame query = same result. Debug from anywhere, test from CI, results never surprise you.
Cache efficiencyNo ECS scope fragmentation. One cached response serves all users worldwide.
PrivacyServer never learns client's real IP or location. You control what data is shared.
AuditabilityInspect any query string to see exactly what data the server receives.
CompatibilityWorks through DoH/DoT resolvers, VPNs, corporate proxies, Tor—all correctly.

Why Traditional GeoDNS Breaks

Traditional DNS-based services infer client location from source IP, causing:

  1. Unpredictable results - Same query returns different data from different networks
  2. Proxy/VPN breakage - Queries return data for the proxy's location, not yours
  3. Cache fragmentation - ECS-scoped responses create thousands of cache entries per /24 subnet
  4. Privacy leakage - Server logs reveal your approximate location
  5. Testing difficulty - Can't reproduce production behavior in CI/staging

Explicit Parameter Pattern

# CORRECT: Client explicitly provides context
get.ip-8-8-8-8.geoip.v1.resolvedb.net           # GeoIP for specific IP
get.city-newyork.weather.v1.resolvedb.net       # Weather for named location
get.lat-40d7128.lon--74d0060.weather.v1.resolvedb.net  # Weather for coordinates
get.region-eu.config.myapp.v1.resolvedb.net     # EU-specific configuration

# WRONG: Implicit context (rejected or undefined behavior)
get.weather.v1.resolvedb.net                    # ← What location? Rejected!
geoip.self.v1.resolvedb.net                     # ← Rejected with FormErr

Privacy Best Practices

For applications handling sensitive data, ResolveDB supports multiple layers of protection:

LayerFeatureDescription
TransportDoH/DoTQuery authoritative servers via DNS-over-HTTPS to encrypt queries in transit
AuthenticationJWT tokensUse auth-<token> prefix for authenticated queries
Payload encryptionAES-256-GCMClient-side encrypt data before storing; server never sees plaintext
Token privacyHash referencesUse h-<hash> prefix to avoid exposing tokens in DNS logs
Namespace isolationPrivate namespacesUse organization namespaces (myorg.resolvedb.net) for access control

Client-side encryption example:

// Encrypt before storing - server never sees plaintext
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data);

// Store encrypted blob
await resolvedb.put('secrets.api-key.myapp.v1', base64(iv + encrypted));

// Retrieve and decrypt client-side
const blob = await resolvedb.get('secrets.api-key.myapp.v1');
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: blob.iv }, key, blob.ciphertext);
Client                    DNS Resolver              ResolveDB Authoritative
   |                           |                              |
   |-- get.city-nyc.weather.v1.resolvedb.net ---------------->|
   |                           |                              |
   |<-- TXT "v=rdb1;s=ok;d={"temp":72}" ---------------------|
   |                           |                              |
   |-- (cached globally) ----->|                              |

Write Operations

Important: Write operations (put, delete) are handled via the HTTP API at api.resolvedb.io, not via DNS queries. DNS is a read-optimized protocol; writes flow through the API.

Why API for Writes?

  • DNS queries are limited to 253 characters (FQDN limit)
  • DNS lacks reliable delivery guarantees for mutations
  • Authentication is simpler over HTTPS
  • Write confirmation requires bidirectional communication

API Examples

# Create/update a resource
curl -X PUT https://api.resolvedb.io/v1/namespaces/myapp/resources/config \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"theme": "dark", "locale": "en-US"}'

# Delete a resource
curl -X DELETE https://api.resolvedb.io/v1/namespaces/myapp/resources/config \
  -H "Authorization: Bearer <token>"

# List resources
curl https://api.resolvedb.io/v1/namespaces/myapp/resources \
  -H "Authorization: Bearer <token>"

After a write, the data is immediately available via DNS queries:

dig TXT get.config.myapp.v1.resolvedb.net +short
# "v=rdb1;s=ok;t=data;f=json;d={\"theme\":\"dark\",\"locale\":\"en-US\"}"

Namespace Architecture

Hostname Scoping Protocol (HSP)

resolvedb.<tld>                          # Root domain (.com/.net/.org/.io)
├── public.resolvedb.<tld>               # Public data services
├── user.resolvedb.<tld>                 # User-owned namespaces
├── system.resolvedb.<tld>               # System operations
└── registry.resolvedb.<tld>             # Service registry

Public Namespace (public.resolvedb.<tld>)

Globally accessible data through standardized interfaces. Supports pluggable providers including MCPs.

<query>.<parameters>.<service>.<version>.public.resolvedb.<tld>

Examples:
current.celsius.newyork.weather.v1.public.resolvedb.net
price.aapl.nasdaq.stock.v1.public.resolvedb.net
latest.headlines.tech.news.v1.public.resolvedb.net

Service Registration:

_service.weather.v1.public.resolvedb.net    TXT  "v=rdb1;type=mcp;endpoint=weather-mcp://api;schema=v1.2.0"
_caps.weather.v1.public.resolvedb.net       TXT  "methods=current,forecast;formats=json,xml;auth=none"
_schema.weather.v1.public.resolvedb.net     URI  "https://schemas.resolvedb.net/weather/v1/openapi.json"

User Namespace (user.resolvedb.<tld>)

Isolated data storage for individuals and organizations.

<data>.<collection>.<user-id>.user.resolvedb.<tld>
<data>.<app>.<org-identifier>.user.resolvedb.<tld>

Examples:
profile.settings.alice.user.resolvedb.net
config.myapp.prod.acme-corp.user.resolvedb.net

Dual Namespace Addressing

Organizations receive both a human-readable vanity name and a stable hash ID:

TypeFormatExample
Vanity name<org-name> (1-32 chars)acme-corp
Hash ID16 character hex (64 bits minimum)a7f3b2c4e8d9f012

Both resolve to identical data:

config.myapp.acme-corp.user.resolvedb.net      # vanity (default)
config.myapp.a7f3b2c4e8d9f012.user.resolvedb.net  # hash (privacy mode)

Use cases:

  • Vanity name: Default for readability, documentation, sharing
  • Hash ID: Privacy-sensitive contexts where org name shouldn't leak in DNS traffic; stable identifier that survives org renames

Behavior:

  • Vanity names are aliases pointing to the underlying hash ID
  • Hash IDs are immutable and assigned at namespace creation
  • Org renames update the vanity alias without changing the hash ID
  • API responses include both identifiers (see Claiming a Namespace)

Hash ID Security Requirements:

RequirementValueRationale
Minimum length16 hex chars (64 bits)Birthday bound collision resistance
DerivationDeterministicSHA256(namespace_name || creation_timestamp || server_salt)[0:16]
Collision checkREQUIREDServer MUST verify uniqueness before assignment

Security Rationale:

6-8 hex characters (24-32 bits) provides collision at only ~4K-16K namespaces (birthday bound). With 64 bits, collision requires ~2^32 namespaces, providing adequate safety margin.

Namespace Claim:

_claim.alice.user.resolvedb.net        TXT  "v=rdb1;pubkey=<ed25519-base64>;sig=<signature-base64>;exp=1735689600"
_claim.acme-corp.user.resolvedb.net    TXT  "v=rdb1;id=a7f3b2c4e8d9f012;org=acme-corp;admin=admin@acme.com;verified=true"

Claim Signature Verification

Signature Scope:

The signature in claim records MUST cover a canonicalized representation:

signed_data = SHA256(
    namespace_name_lowercase ||  # "alice" or "acme-corp"
    ":" ||
    pubkey_bytes ||              # 32-byte Ed25519 public key
    ":" ||
    exp_timestamp                # Big-endian 64-bit Unix timestamp
)

signature = Ed25519.sign(owner_private_key, signed_data)

Verification Process:

def verify_claim(claim_record):
    # 1. Parse claim fields
    namespace = claim_record.name.split('.')[1]  # Extract from _claim.<namespace>...
    pubkey = base64_decode(claim_record['pubkey'])
    sig = base64_decode(claim_record['sig'])
    exp = int(claim_record['exp'])

    # 2. Check expiration
    if exp < current_unix_timestamp():
        return ClaimResult.EXPIRED

    # 3. Reconstruct signed data
    signed_data = sha256(
        namespace.lower().encode() + b':' +
        pubkey + b':' +
        exp.to_bytes(8, 'big')
    )

    # 4. Verify signature
    if not ed25519_verify(pubkey, signed_data, sig):
        return ClaimResult.INVALID_SIGNATURE

    return ClaimResult.VALID

Key Rotation:

To rotate keys, the owner signs with the OLD key authorizing the NEW key:

_claim.<namespace>.user.resolvedb.net TXT "v=rdb1;pubkey=<new-key>;prev_pubkey=<old-key>;sig=<signed-by-old>;exp=..."

The signature for rotation covers: new_pubkey || ":" || prev_pubkey || ":" || exp

Revocation:

Compromised keys are revoked by publishing a revocation record signed by any valid key in the chain:

_revoke.<key-fingerprint>.<namespace>.user.resolvedb.net TXT "v=rdb1;reason=compromised;revoked_at=1704067200;sig=<signature>"

System Namespace (system.resolvedb.<tld>)

Reserved for health, metrics, and status.

_health.system.resolvedb.net TXT "v=rdb1;status=healthy;tld=net;region=global"

Namespace Registration

Naming Rules

RuleConstraint
Length1-32 characters
Charactersa-z, 0-9, - (hyphen)
Start/EndMust start and end with alphanumeric
CaseCase-insensitive (stored lowercase)

Reserved Namespaces

The following namespaces cannot be claimed by users:

CategoryNamespacesPurpose
Systempublic, system, registry, admin, rootCore platform operations
Infrastructureapi, www, cdn, dnsInfrastructure confusion prevention
Nameserversns*, ns01, ns02, ns03 (pattern)Nameserver confusion
Emailmail, email, smtp, imap, mxEmail infrastructure
Protocolhttp, https, ftp, ssh, sftpProtocol confusion
Brandresolvedb, rdbBrand protection
DNS convention_* (underscore prefix)RFC compliance
Short namesAll 2-character namesISO country code conflicts

Pattern Matching:

Namespaces matching these patterns are also reserved:

  • ns[0-9]* - Any nameserver-like pattern
  • v[0-9]+ - Version-like patterns (conflicts with protocol version)
  • test*, dev*, staging* - Reserved for platform testing

Validation Implementation:

fn is_reserved_namespace(name: &str) -> bool {
    let lower = name.to_lowercase();

    // Exact matches
    const RESERVED: &[&str] = &[
        "public", "system", "registry", "admin", "root",
        "api", "www", "cdn", "dns", "mail", "email", "smtp", "imap", "mx",
        "http", "https", "ftp", "ssh", "sftp", "resolvedb", "rdb"
    ];
    if RESERVED.contains(&lower.as_str()) { return true; }

    // Pattern matches
    if lower.starts_with('_') { return true; }       // DNS convention
    if lower.len() <= 2 { return true; }             // Too short
    if lower.starts_with("ns") && lower[2..].chars().all(|c| c.is_ascii_digit()) { return true; }
    if lower.starts_with('v') && lower[1..].chars().all(|c| c.is_ascii_digit()) { return true; }

    false
}

Claiming a Namespace

Namespaces are claimed via the API:

curl -X POST https://api.resolvedb.io/v1/namespaces \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"name": "myapp", "public_key": "<ed25519-pubkey>"}'

Response includes both identifiers:

{
  "name": "myapp",
  "id": "a7f3b2c4e8d9f012",
  "owner": "user-12345",
  "created": 1704067200
}

The claim record is then queryable via either identifier:

_claim.myapp.user.resolvedb.net   TXT  "v=rdb1;id=a7f3b2c4e8d9f012;pubkey=<ed25519>;owner=<user-id>;created=1704067200"
_claim.a7f3b2c4e8d9f012.user.resolvedb.net  TXT  "v=rdb1;id=a7f3b2c4e8d9f012;name=myapp;pubkey=<ed25519>;owner=<user-id>;created=1704067200"

Renaming a namespace:

curl -X PATCH https://api.resolvedb.io/v1/namespaces/myapp \
  -H "Authorization: Bearer <token>" \
  -d '{"name": "myapp-v2"}'

The hash ID (a7f3b2c4e8d9f012) remains stable; existing queries using the hash continue to work. Queries using the old vanity name return notfound after rename.

Access Control Model

PermissionDescription
readQuery resources in namespace
writeCreate/update resources
deleteRemove resources
listEnumerate resources
admin:grantGrant permissions to others
admin:revokeRevoke permissions
admin:transferTransfer namespace ownership

Cross-Namespace Access:

  • public.* namespaces: readable by all, writable by registered providers
  • user.* namespaces: private by default, owner controls access
  • Access grants are stored in _acl.<namespace>.user.resolvedb.<tld>

Query Format

Structure

<operation>.<params>.<resource>.<namespace>.<version>.resolvedb.<tld>
ComponentRequiredDescription
operationYesAction to perform
paramsNoEncoded parameters
resourceYesData resource name
namespaceYesScope (public, user, system)
versionYesProtocol version (v1)
resolvedbYesProtocol marker
tldYes.com, .net, .org, .io

Formal Grammar (ABNF)

; Query structure
query         = operation "." [params "."] resource "." namespace "." version ".resolvedb." tld
operation     = "get" / "put" / "delete" / "list" / "search" / "info" / "health" / "geoip" / "watch"
params        = encoded-param *("." encoded-param)

; Parameter encodings (all use hyphen separators, NOT colons)
encoded-param = plain-param / b64-param / b32-param / hex-param / auth-param
              / chunk-param / hash-param / geo-param / cursor-param
              / ts-param / nonce-param / limit-param / offset-param

; Plain parameters: alphanumeric, cannot start/end with hyphen (RFC 1035)
plain-param   = ALPHANUM *61(ALPHA / DIGIT / "-") [ALPHANUM]
ALPHANUM      = ALPHA / DIGIT

; Encoding prefixes
b64-param     = "b64-" 1*base64url
b32-param     = "b32-" 1*base32                    ; Case-insensitive encoding
hex-param     = "hex-" 1*HEXDIG
auth-param    = "auth-" (jwt-token / hash-ref)     ; Full JWT or hash reference
hash-ref      = "h-" 32*64HEXDIG                   ; HMAC-SHA256 reference (128+ bits)
chunk-param   = "chunk-" index "-" total "-" hash
hash-param    = "h-" 32*64HEXDIG                   ; Content hash (128+ bits minimum)

; GeoIP: lat/lon with 'd' as decimal separator (colons invalid in DNS)
geo-param     = "geo-" coordinate "-" coordinate
coordinate    = ["-"] 1*3DIGIT ["d" 1*6DIGIT]      ; e.g., 40d7128 for 40.7128

; Pagination
cursor-param  = "cursor-" (1*43base64url / hash-ref)  ; Max 63 chars per label
limit-param   = "limit-" 1*4DIGIT                  ; 1-1000
offset-param  = "offset-" 1*10DIGIT

; Replay protection (required for auth queries)
ts-param      = "ts-" 10DIGIT                      ; Unix timestamp
nonce-param   = "nonce-" 8*16ALPHANUM              ; Random, unique per request

; Structural elements
resource      = label
namespace     = label *("." label)
version       = "v" 1*DIGIT
tld           = "com" / "net" / "org" / "io"

; Labels per RFC 1035: alphanumeric start/end, max 63 chars
label         = ALPHANUM *61(ALPHA / DIGIT / "-") [ALPHANUM]

; Character classes
base64url     = ALPHA / DIGIT / "-" / "_"          ; URL-safe, no padding
base32        = %x41-5A / "2" / "3" / "4" / "5" / "6" / "7"  ; A-Z, 2-7
jwt-token     = 3*(base64url ".") base64url        ; header.payload.signature format
index         = 1*DIGIT
total         = 1*DIGIT
hash          = 16*64HEXDIG                        ; Minimum 64 bits for collision resistance

Grammar Notes:

  • All prefixes use hyphens (-), never colons (:) - colons are invalid in DNS labels per RFC 1035
  • Labels MUST start and end with alphanumeric characters (RFC 1035 Section 2.3.1)
  • JWT tokens in auth- SHOULD use hash references (auth-h-<hash>) for tokens exceeding 63 chars
  • Hash references (h-) require minimum 32 hex chars (128 bits) for brute-force resistance

Operations

OperationDescriptionAuth RequiredTransport
getRetrieve dataNo (public) / Yes (user)DNS
putStore dataYesHTTP API only
deleteRemove dataYesHTTP API only
listList resourcesDependsDNS
searchSearch resourcesDependsDNS
watchSubscribe to changesYesDNS (returns WebSocket URL)
infoResource metadataNoDNS
healthSystem healthNoDNS
geoipClient IP geolocationNoDNS

Parameter Encoding

Parameters requiring special characters are encoded using DNS-safe prefixes. All prefixes use hyphens (-) as separators since colons are not valid in DNS labels per RFC 1035.

PrefixEncodingUse Case
(none)Plain alphanumericSimple keys
b64-Base64 URL-safeJSON, binary, complex params
b32-Base32Case-insensitive
hex-HexadecimalBinary hashes
auth-JWT tokenAuthentication
chunk-Chunk referenceLarge data
h-Hash referenceContent-addressed
bdt-Blind Device TokenIoT device identity
ctp-Cohort TokenUser targeting
sig-Namespace SignatureMulti-tenant auth

Security Token Prefixes

For use cases requiring enhanced security guarantees beyond basic authentication, UQRP defines three specialized token prefixes. These patterns provide cryptographic guarantees that prevent enumeration, privacy leakage, and cross-tenant access.

Blind Device Token (bdt-)

Provides device identity without exposing device IDs in queries. Used for IoT and industrial configurations.

Token Derivation:

device_secret = HKDF-SHA256(
    ikm  = factory_master_secret,
    salt = device_id,
    info = "resolvedb-bdt-v1"
)
blind_token = hex(SHA256(device_secret || factory_id || epoch_week)[0:16])

Query Format:

get.bdt-<32-hex-chars>.config.<factory-namespace>.v1.resolvedb.<tld>

Example:

get.bdt-a7f3b2c4e8d9f012a7f3b2c4e8d9f012.config.acme-factory.v1.resolvedb.net

Validation:

  1. Server maintains index of blind_token → device_id mappings
  2. Accepts tokens for current week AND previous week (seamless rotation)
  3. Returns E018 (bdtinvalid) for unknown tokens

Response Encryption: Responses MAY be encrypted with the device's derived secret:

v=rdb1;s=ok;t=data;e=aes;f=json;ttl=300;d=<AES-256-GCM(device_secret, config)>

Security Properties:

PropertyGuarantee
Device enumeration resistance2^128 token space
Identity privacyDevice ID never in query
RotationAutomatic weekly (epoch_week)
Factory isolationToken bound to factory_id

Cohort Token Pattern (ctp-)

Enables server-side user targeting without exposing user identity or targeting rules in queries.

Token Structure:

cohort_token = base64url(AES-256-GCM(
    key   = app_secret,
    nonce = random(12),
    data  = CBOR({
        "u": SHA256(user_id)[0:8],      // 8-byte user hash
        "s": segment_bitmap,             // 4-byte bitmap (32 targeting bits)
        "t": floor(unix_time / 300)      // 5-minute bucket
    })
))

Segment Bitmap (32 bits):

Bit 0:  is_premium         Bit 16: experiment_a
Bit 1:  is_beta_user       Bit 17: experiment_b
Bit 2:  is_internal        Bit 18: experiment_c
Bit 3:  (reserved)         Bit 19: experiment_d
Bit 4:  platform_ios       Bit 20-23: (reserved)
Bit 5:  platform_android   Bit 24: locale_en
Bit 6:  platform_web       Bit 25: locale_es
Bit 7:  platform_desktop   Bit 26: locale_fr
Bit 8:  region_na          Bit 27: locale_de
Bit 9:  region_eu          Bit 28: locale_ja
Bit 10: region_apac        Bit 29: locale_zh
Bit 11: region_latam       Bit 30: locale_pt
Bit 12-15: tier (0-15)     Bit 31: custom_flag

Query Format:

get.ctp-<base64url-token>.<resource>.<namespace>.v1.resolvedb.<tld>

Example:

get.ctp-dGVzdHRva2VuMTIzNDU2Nzg5MGFiY2RlZg.dark-mode.flags.myapp.v1.resolvedb.net

Validation:

  1. Server decrypts token with app's registered secret
  2. Validates timestamp (reject if >5 minutes old)
  3. Evaluates targeting rules against segment bitmap
  4. Returns evaluated flag values, NOT targeting rules

Security Properties:

PropertyGuarantee
User identity privacyOnly 8-byte hash in encrypted token
Targeting rule privacyRules evaluated server-side
Cache efficiencySame cohort (bitmap) = same cache entry
Replay window5-minute token expiry

Error Codes:

CodeStatusDescription
E019secviolCTP token decryption failed
E020secviolCTP token expired (>5 min)

Namespace-Bound Signature (sig-)

Cryptographically binds queries to a specific tenant namespace, preventing cross-tenant access even with stolen tokens.

Signature Derivation:

timestamp = unix_epoch_seconds()
material = UTF8(operation + "." + resource + "." + namespace + ".v1|" + timestamp + "|" + tenant_id)
signature = hex(HMAC-SHA256(tenant_query_key, material)[0:8])

Query Format:

get.sig-<16-hex-chars>-t-<unix-timestamp>.<resource>.<namespace>.v1.resolvedb.<tld>

Example:

get.sig-a3f2e8c1d4b5a678-t-1704067200.config.acme-corp.v1.resolvedb.net

Validation:

  1. Extract namespace from query FQDN
  2. Look up tenant's tenant_query_key by namespace
  3. Recompute expected signature using extracted timestamp
  4. Constant-time compare signatures
  5. Verify timestamp within 5-minute window
  6. Return E018 (siginvalid) for any failure

Combined with JWT (Defense in Depth): For maximum security, combine signature validation with JWT:

get.sig-<sig>-t-<ts>.auth-h-<jwt-hash>.<resource>.<namespace>.v1.resolvedb.net

Server verifies:

  1. JWT claims contain matching tenant field
  2. Query namespace matches JWT tenant
  3. Signature is valid for query namespace

Security Properties:

PropertyGuarantee
Cross-tenant preventionSignature cryptographically bound to namespace
Token theft resistanceAttacker needs query_key, not just JWT
Replay window5-minute timestamp validation
Bug immunityWorks even if authorization code has bugs

Error Codes:

CodeStatusDescription
E018secviolSignature validation failed
E021secviolTimestamp outside valid window
E022secviolNamespace mismatch (JWT vs query)

Security Token Summary

PrefixUse CaseKey DerivationExpiryError Codes
bdt-IoT device identityHKDF from factory secretWeekly rotationE018
ctp-User targetingAES-256-GCM with app secret5 minutesE019, E020
sig-Multi-tenant authHMAC-SHA256 with tenant key5 minutesE018, E021, E022

RFC Conformance

All security token prefixes conform to:

  • RFC 1035: Labels ≤63 chars, FQDN ≤253 chars, valid chars [a-z0-9-]
  • RFC 4648: Base64url encoding for CTP tokens
  • RFC 5869: HKDF key derivation for BDT
  • RFC 5116: AEAD (AES-256-GCM) for CTP encryption
  • RFC 2104: HMAC for NBA signatures

Query Examples

# Simple (no params)
get.weather.public.v1.resolvedb.net

# With location param (plain alphanumeric)
get.newyork.weather.public.v1.resolvedb.net

# Base64 encoded params (JSON with special chars)
# {"lat":40.7128} -> eyJsYXQiOjQwLjcxMjh9
get.b64-eyJsYXQiOjQwLjcxMjh9.location.public.v1.resolvedb.net

# Authenticated (JWT token)
get.auth-eyJhbGciOiJFZDI1NTE5In0.profile.acme.v1.resolvedb.net

# Chunked data (index-total-hash format)
get.chunk-0-10-abc123.largedata.enterprise.v1.resolvedb.net

# Hash-addressed (for long params)
get.h-a1b2c3d4e5f6g7h8.cache.public.v1.resolvedb.net

# Conditional
get.if-modified-since-1704067200.data.user.v1.resolvedb.net

# GeoIP lookup (explicit IP required)
get.ip-8-8-8-8.geoip.v1.resolvedb.net

Response Format

TXT Record Response (v1)

v=rdb1;s=<status>;t=<type>;e=<encoding>;f=<format>;c=<chunks>;h=<hash>;ttl=<seconds>;sig=<signature>;seq=<sequence>;ts=<timestamp>;err=<error-code>;retry=<seconds>;d=<data>
FieldDescriptionValues
vProtocol versionrdb1
sStatus codeSee status codes
tResponse typedata, url, multi, stream, encrypted
eEncodingplain, b64, b32, hex, compressed, encrypted
fFormatjson, xml, protobuf, msgpack, text, binary
cChunk infocurrent/total (e.g., 1/3)
hSHA-256 hashFirst 16+ chars
ttlCache delegation durationSeconds (see TTL Cache Delegation)
sigEd25519 signatureBase64 encoded
seqSequence numberFor ordering multi-part
tsTimestampUnix epoch
errError codeMachine-readable error (e.g., E001)
retryRetry afterSeconds until retry is appropriate
dData payloadEncoded data

Status Codes

CodeHTTP EquivDescription
ok200Success
partial206Partial content (chunked response)
redirect301See URL in data
notfound404Resource not found
auth401Authentication required
forbidden403Access denied
ratelimit429Too many requests
invalid400Malformed query
toolarge413Response exceeds limits
secviol400Security violation (signature invalid, replay detected)
error500Server error
unavail503Service unavailable

Error Codes

Machine-readable error codes for programmatic handling:

CodeStatusDescriptionRetryableRecovery Strategy
E001invalidMalformed query syntaxNoFix query format
E002invalidUnknown operationNoUse valid operation
E003invalidInvalid encoding prefixNoUse b64-, hex-, etc.
E004notfoundResource does not existNoCheck resource path
E005notfoundNamespace does not existNoRegister namespace first
E006authMissing authenticationNoInclude auth-<token>
E007authToken expiredNoRefresh token
E008authToken invalidNoCheck token format/signature
E009forbiddenInsufficient permissionsNoRequest access grant
E010ratelimitRate limit exceededYesWait for retry seconds
E011toolargePayload exceeds 64KBNoUse chunking protocol
E012errorInternal server errorYesRetry with backoff
E013unavailService temporarily unavailableYesRetry with backoff

Error Response Example:

v=rdb1;s=ratelimit;err=E010;retry=60;d=Rate limit exceeded

Error Privacy Mode

For privacy-sensitive namespaces, error responses may leak information about resource existence:

ErrorInformation Leaked
E004 (Resource not found)Namespace exists, specific resource doesn't
E005 (Namespace not found)Namespace doesn't exist
E009 (Forbidden)Resource exists, user lacks permission

Privacy Mode Behavior:

When privacy mode is enabled for a namespace, servers SHOULD return unified error responses:

# Standard mode (informative):
v=rdb1;s=notfound;err=E004;d=Resource not found
v=rdb1;s=notfound;err=E005;d=Namespace not found
v=rdb1;s=forbidden;err=E009;d=Access denied

# Privacy mode (uniform):
v=rdb1;s=notfound;err=E004;d=Not found or access denied

Configuration:

Privacy mode is set per-namespace via claim record:

_claim.<namespace>.user.resolvedb.net TXT "v=rdb1;...;privacy=high"
Privacy LevelBehavior
normalDistinct errors (E004, E005, E009)
highUnified error (always E004 for not-found/forbidden)

Trade-offs:

  • Normal: Better debugging, faster issue resolution
  • High: Prevents enumeration, hides internal structure

Response Examples

# Success with JSON
v=rdb1;s=ok;t=data;e=plain;f=json;h=a1b2c3d4e5f6g7h8;ttl=300;d={"temp":72,"unit":"F"}

# URL redirect (large data)
v=rdb1;s=redirect;t=url;ttl=3600;d=https://cdn.resolvedb.cloud/blob/abc123

# Multi-record (chunked)
v=rdb1;s=ok;t=multi;c=1/3;seq=1;h=abc123;d=<chunk1>
v=rdb1;s=ok;t=multi;c=2/3;seq=2;h=abc123;d=<chunk2>
v=rdb1;s=ok;t=multi;c=3/3;seq=3;h=abc123;d=<chunk3>

# Streaming endpoint (see WebSocket Session Security below)
v=rdb1;s=ok;t=stream;d=wss://stream.resolvedb.net/session/<session-token>

# Rate limited with retry hint
v=rdb1;s=ratelimit;err=E010;retry=60;ttl=1;d=Rate limit exceeded. Retry after 60s

# Encrypted
v=rdb1;s=ok;t=encrypted;e=aes256gcm;k=<ephemeral-pubkey>;d=<ciphertext>

# GeoIP response
v=rdb1;s=ok;t=data;f=json;ttl=300;d={"country":"US","region":"NY","city":"New York","lat":40.7128,"lon":-74.006}

GeoIP Operation

The geoip operation returns geographic location data for a specified IP address. Following the Privacy by Design principle, the IP address MUST be provided as an explicit parameter.

Query Format

get.ip-<encoded-ip>.geoip.v1.resolvedb.net

IP Encoding:

  • IPv4: Replace dots with hyphens (e.g., 8.8.8.8ip-8-8-8-8)
  • IPv6: Replace colons with hyphens, :: becomes -- (e.g., 2001:4860:4860::8888ip-2001-4860-4860--8888)

Response

{
  "ip": "8.8.8.8",
  "country": "US",
  "country_name": "United States",
  "region": "CA",
  "region_name": "California",
  "city": "Mountain View",
  "postal": "94035",
  "lat": 37.386,
  "lon": -122.084,
  "timezone": "America/Los_Angeles",
  "asn": 15169,
  "org": "Google LLC"
}

Examples

# Lookup a specific IPv4 address
dig TXT get.ip-8-8-8-8.geoip.v1.resolvedb.net +short
# "v=rdb1;s=ok;t=data;f=json;ttl=300;d={\"ip\":\"8.8.8.8\",\"country\":\"US\",...}"

# Lookup a specific IPv6 address
dig TXT get.ip-2001-4860-4860--8888.geoip.v1.resolvedb.net +short

# Lookup Cloudflare DNS
dig TXT get.ip-1-1-1-1.geoip.v1.resolvedb.net +short

Privacy Note

The server does NOT use the querier's source IP for GeoIP lookups. The client must explicitly provide the IP address they want to look up. This ensures:

  • Consistent results regardless of where the query originates
  • Correct behavior through DoH/DoT resolvers, VPNs, and proxies
  • No privacy leakage of the client's actual IP
  • Cacheable responses (same query = same result)

WebSocket Session Security (CRITICAL)

The watch operation returns a WebSocket URL for real-time updates. Session security is critical.

Session Token Format

Session tokens MUST be cryptographically secure and short-lived:

session_token = Base64URL(HMAC-SHA256(server_secret,
    tenant_id || resource_path || created_timestamp || client_ip_hash
))[0:32]  # 256-bit truncated to 32 chars
ComponentPurpose
tenant_idBinds session to authenticated user
resource_pathBinds to specific watched resource
created_timestampEnables expiration check
client_ip_hashOptional IP binding for added security

Connection Security

Handshake Requirements:

StepRequirement
1Client connects with Origin header matching allowed origins
2Server validates session token (MUST be < 5 minutes old)
3Server validates client IP matches token creation IP (optional)
4Server sends initial resource state
5Bidirectional communication established

Rate Limiting:

  • Maximum 10 WebSocket connections per tenant per minute
  • Maximum 100 concurrent connections per tenant

Session Timeouts:

TimeoutDurationAction
Idle30 minutesDisconnect with close code 1000
Maximum24 hoursForce reconnection with new token
Token validity5 minutesReject if token older

Reconnection Protocol

On disconnect, clients MUST:

  1. Obtain new session token via fresh watch DNS query
  2. Connect with new token (old tokens are single-use)
  3. Server sends full state, not just delta

Token Single-Use Enforcement:

Session tokens are consumed on first use. Reusing a token returns:

WebSocket close code: 4401
Reason: "Session token already used"

URL Security Concerns

Session tokens in WebSocket URLs are visible in:

  • Server access logs
  • Browser history
  • Referrer headers (if page navigates)

Mitigations:

  • Short token validity (5 minutes)
  • Single-use tokens
  • Consider passing token via WebSocket subprotocol header: Sec-WebSocket-Protocol: resolvedb-v1, token-<session_token>

Error Codes

Close CodeMeaning
4400Invalid session token
4401Session token already used
4403Access denied to resource
4429Rate limit exceeded

Pagination

For list and search operations that return multiple results, pagination is supported via cursor-based navigation.

Query Parameters

ParameterFormatDescription
limit-Nlimit-50Maximum results per page (1-1000, default 100)
offset-Noffset-200Skip N results (for simple pagination)
cursor-TOKENcursor-abc123Opaque cursor for next page

Response Fields

{
  "items": [...],
  "cursor": "eyJsYXN0X2lkIjoiMTIzIn0",
  "hasMore": true,
  "total": 523
}
FieldDescription
itemsArray of results for current page
cursorOpaque token for next page (Base64-encoded, URL-safe, HMAC-signed)
hasMoreBoolean indicating more results exist
totalTotal count (approximate for large sets, omit for privacy-sensitive namespaces)

Cursor Integrity (CRITICAL)

Cursors MUST be cryptographically signed to prevent manipulation attacks.

Cursor Format:

cursor = Base64URL(cursor_data) + "." + Base64URL(signature)

cursor_data = JSON({
    "last_id": "<last_item_id>",
    "tenant": "<tenant_id>",
    "query_hash": "<sha256_of_original_query_params>",
    "created": <unix_timestamp>
})

signature = HMAC-SHA256(server_secret, cursor_data)[0:16]  # 128-bit truncated

Validation Requirements:

Servers MUST:

  1. Verify HMAC signature before using cursor
  2. Reject cursors older than 1 hour (prevents stale enumeration)
  3. Verify tenant matches authenticated user (if applicable)
  4. Verify query_hash matches current query parameters (prevents cross-query cursor reuse)

Attack Prevention:

AttackMitigation
Cursor tamperingHMAC signature verification
Cross-user cursor theftTenant binding in cursor data
Cross-query cursor reuseQuery hash binding
Stale cursor enumeration1-hour expiration

Error Response:

Invalid cursors return:

v=rdb1;s=invalid;err=E017;d=Invalid or expired cursor
CodeStatusDescription
E017invalidCursor validation failed

Privacy Considerations

For privacy-sensitive namespaces:

  • total field SHOULD be omitted or return approximate value
  • Consider capping display at "100+" to prevent exact enumeration

Example

# First page
list.limit-50.resources.myapp.v1.resolvedb.net
-> {"items":[...],"cursor":"eyJsYXN0IjoiZm9vIn0","hasMore":true,"total":150}

# Next page (cursor must fit in 63-char DNS label)
list.cursor-eyJsYXN0IjoiZm9vIn0.resources.myapp.v1.resolvedb.net
-> {"items":[...],"cursor":"eyJsYXN0IjoiYmFyIn0","hasMore":true,"total":150}

# Last page
list.cursor-eyJsYXN0IjoiYmFyIn0.resources.myapp.v1.resolvedb.net
-> {"items":[...],"hasMore":false,"total":150}

DNS Label Constraints

Cursors must fit within DNS label limits:

  • Maximum 63 characters per label
  • URL-safe Base64 encoding (no +, /, or =)
  • For long cursors, use hash reference: cursor-h-<hash> where hash points to stored cursor state

Large Data (NULL Records)

For data >4KB, use NULL record type (up to 64KB per record):

get.bigdata.tenant.v1.resolvedb.net TYPE=NULL

Amplification Attack Mitigation (CRITICAL)

NULL records present significant DDoS amplification risk:

  • Minimum query size: ~32 bytes
  • Maximum response size: 65,536 bytes
  • Amplification factor: 2,048x

Mandatory Mitigations (per RFC 5358):

MitigationRequirementImplementation
Response Rate Limiting (RRL)REQUIREDMax 10 NULL responses/second per source /24
TCP FallbackREQUIREDResponses >4KB MUST use TC bit, require TCP
AuthenticationREQUIREDNULL records require auth- token or API key
Source ValidationRECOMMENDEDBCP 38/84 ingress filtering

Protocol Behavior:

# UDP query for large data:
Query:  get.bigdata.tenant.v1.resolvedb.net TYPE=NULL (UDP)
Response: v=rdb1;s=toolarge;err=E015;d=Use TCP for responses >4KB;tc=1

# New error code:
E015 | toolarge | Response requires TCP | Yes | Retry over TCP

Size Limits by Transport:

TransportMax ResponseBehavior
UDP4,096 bytesTC bit set if exceeded
TCP65,536 bytesFull response allowed
DoH65,536 bytesFull response allowed

Rate Limits for NULL Records:

TierNULL Records/secBurstNotes
Unauthenticated00NULL requires auth
Free15Strict limit
Pro1050Standard
Enterprise100500High volume

Chunking Protocol

# 1. Get manifest (includes per-chunk hashes for integrity)
get.manifest.bigfile.tenant.v1.resolvedb.net
-> {"chunks":5,"size":320000,"hash":"abc123def456789012345678901234567890123456789012345678901234","chunk_hashes":["hash0","hash1","hash2","hash3","hash4"]}

# 2. Retrieve chunks (can be parallel, format: chunk-index-total-hash)
# Hash reference MUST be at least 16 hex chars (64 bits)
get.chunk-0-5-abc123def4567890.bigfile.tenant.v1.resolvedb.net  TYPE=NULL
get.chunk-1-5-abc123def4567890.bigfile.tenant.v1.resolvedb.net  TYPE=NULL
...

# 3. Verify each chunk hash, then verify full content hash after reassembly

Chunk Integrity Verification

Clients MUST:

  1. Verify each chunk's SHA-256 hash matches chunk_hashes[index] before storing
  2. Verify reassembled content SHA-256 matches manifest hash
  3. Reject chunks with mismatched hashes (do not retry automatically - may indicate MITM)
  4. Complete all chunks within 5 minutes or restart (prevents resource exhaustion)

Encryption Wire Format

For encrypted responses (t=encrypted), the following wire format is used.

AES-256-GCM Structure

┌─────────────────────────────────────────────────────────────┐
│                    Encrypted Response                        │
├─────────────────────────────────────────────────────────────┤
│  Nonce (12 bytes)  │  Ciphertext (variable)  │  Tag (16 bytes) │
└─────────────────────────────────────────────────────────────┘
ComponentSizeDescription
Nonce12 bytesUnique per encryption (random or counter-based)
CiphertextVariableEncrypted payload
Auth Tag16 bytesGCM authentication tag

Key Derivation (CRITICAL)

Keys are derived using HKDF-SHA256 with mandatory context binding:

shared_secret = X25519(client_private, server_ephemeral_public)
               OR X25519(server_private, client_public)

encryption_key = HKDF-SHA256(
    ikm  = shared_secret,
    salt = "resolvedb-v1-encryption",
    info = context_info,  # MANDATORY - see below
    len  = 32
)

Context Binding Requirements (MANDATORY):

The context_info field MUST include all of the following to prevent key reuse attacks:

ComponentFormatPurpose
Query FQDNUTF-8 bytesPrevents cross-query key reuse
Client ephemeral pubkey32 bytesBinds to specific client
Server ephemeral pubkey32 bytesBinds to specific response
Timestamp8 bytes (big-endian Unix epoch)Prevents replay
Nonce8 bytes (random)Additional entropy

Context Construction:

context_info = concat(
    length_prefix(query_fqdn),         # 2-byte length + UTF-8 FQDN
    client_ephemeral_pubkey,           # 32 bytes
    server_ephemeral_pubkey,           # 32 bytes
    timestamp_be64,                    # 8 bytes (Unix timestamp, big-endian)
    random_nonce                       # 8 bytes (cryptographically random)
)

Security Rationale:

Without complete context binding:

  • Same FQDN from different clients could derive same key
  • Responses could be replayed to different sessions
  • Keys could be precomputed for known FQDNs

Implementation Check:

# CORRECT: Full context binding
context = (
    len(fqdn).to_bytes(2, 'big') + fqdn.encode() +
    client_pubkey +     # 32 bytes
    server_pubkey +     # 32 bytes
    timestamp_bytes +   # 8 bytes
    random_nonce        # 8 bytes
)

# WRONG: Incomplete binding
context = fqdn.encode()  # Missing keys, timestamp, nonce

Ephemeral Key Format

The k field in encrypted responses contains the server's ephemeral X25519 public key:

v=rdb1;s=ok;t=encrypted;e=aes256gcm;k=<base64-ephemeral-pubkey>;d=<base64-encrypted-payload>
FieldFormatDescription
kBase64 (32 bytes decoded)Server ephemeral X25519 public key
dBase64Nonce + Ciphertext + Tag concatenated

Complete Example

Response:

v=rdb1;s=ok;t=encrypted;e=aes256gcm;k=MCowBQYDK2VuAyEAe8RB0...;d=dGVzdCBub25jZQAAAA...

Decoding d:

Base64 decode -> raw_bytes
nonce      = raw_bytes[0:12]      # 12 bytes
ciphertext = raw_bytes[12:-16]    # variable length
tag        = raw_bytes[-16:]      # 16 bytes

Decryption:

from cryptography.hazmat.primitives.ciphers.aead import AESGCM

# Derive shared secret from client private key and server ephemeral public
shared = x25519(client_private_key, server_ephemeral_public)
key = hkdf_sha256(shared, salt=b"resolvedb-v1-encryption", info=query_fqdn, length=32)

# Decrypt
aesgcm = AESGCM(key)
plaintext = aesgcm.decrypt(nonce, ciphertext + tag, associated_data=None)

Multi-TLD Root Server Redundancy

All four TLDs serve as active authoritative nameservers with identical data:

TLDPrimaryCross-Backup
.comns1/ns2.resolvedb.comns-backup.resolvedb.net
.netns1/ns2.resolvedb.netns-backup.resolvedb.org
.orgns1/ns2.resolvedb.orgns-backup.resolvedb.io
.ions1/ns2.resolvedb.ions-backup.resolvedb.com

Client Failover

ROOT_SERVERS = ['resolvedb.com', 'resolvedb.net', 'resolvedb.org', 'resolvedb.io']

def query_with_redundancy(resource):
    # Sort by health/latency
    for tld in sorted_by_health(ROOT_SERVERS):
        try:
            result = dns_query(f"{resource}.{tld}")
            mark_healthy(tld)
            return result
        except DNSError:
            mark_unhealthy(tld)
            continue
    raise AllTLDsFailedError()

Benefits

  • TLD-level failure protection
  • DDoS mitigation (attack one TLD, others continue)
  • Load distribution across infrastructures
  • Regulatory compliance (different jurisdictions)
  • Performance optimization (clients choose fastest)

Security Protocol (RDBSP)

Layer 1: DNSSEC Foundation

  • ECDSA P-256 (Algorithm 13) for all zones
  • Automatic key rotation (ZSK: 30 days, KSK: 365 days)
  • NSEC3 with salt for authenticated denial (see parameters below)
  • Clients SHOULD verify AD flag

NSEC3 Parameters (RFC 5155, RFC 9276)

ParameterValueRationale
Hash AlgorithmSHA-1 (1)Required by RFC 5155
Iterations0-10Per RFC 9276 guidance (low for online signing)
Salt Length0-8 bytesRandom salt, rotate with ZSK
Opt-OutDisabledAll names authenticated

NSEC3PARAM Record:

resolvedb.net. NSEC3PARAM 1 0 10 <random-salt-hex>

Salt Rotation:

  • Rotate salt with each ZSK rotation (30 days)
  • Use cryptographically random salt (minimum 64 bits)
  • Zero-length salt acceptable per RFC 9276

Iteration Count Guidance (RFC 9276):

  • Online signing: 0-10 iterations (performance)
  • Offline signing: Up to 100 iterations acceptable
  • Higher iterations provide minimal security benefit but significant CPU cost

Layer 2: Content Integrity

  • SHA-256 hash verification (minimum 16 chars, full recommended)
  • Ed25519 signatures for authenticity
  • Unix timestamps for replay protection (5-second max tolerance)
  • Cryptographic nonces (MANDATORY for authenticated requests)

Replay Protection Requirements (CRITICAL)

Timestamp Tolerance:

ContextMax ToleranceRationale
Authenticated requests5 secondsLimits replay window
Unsigned public queries30 secondsAllows for clock skew
Encrypted responses5 secondsBound to ephemeral keys

Nonce Requirements:

For authenticated requests (auth-* prefix), clients MUST include a nonce:

get.auth-<jwt>.ts-<unix_timestamp>.nonce-<8-random-chars>.resource.namespace.v1.resolvedb.net
FieldFormatRequirements
ts-Unix timestampWithin 5 seconds of server time
nonce-8 alphanumeric charsCryptographically random, unique per request

Server-Side Tracking:

Servers MUST:

  1. Reject requests with ts more than 5 seconds from server time
  2. Track (nonce, ts) pairs for 10 seconds (2x tolerance window)
  3. Reject duplicate (nonce, ts) pairs with secviol status
  4. Use constant-time comparison for nonce matching

New Error Code:

CodeStatusDescriptionRetryableRecovery
E016secviolReplay attack detectedNoGenerate new nonce

Clock Synchronization:

Clients SHOULD:

  • Use NTP or similar for time synchronization
  • Include RTT estimate in tolerance calculations
  • Retry with fresh timestamp on E016 (but not same nonce)

Layer 3: Encryption Modes

Public (Integrity Only):

- Plaintext data
- SHA-256 hash
- Ed25519 signature
- DNSSEC transport

Symmetric (Shared Secret):

- AES-256-GCM encryption
- Pre-shared keys (out-of-band)
- Argon2id key derivation
- AEAD

Asymmetric (Public Key):

- X25519 key exchange
- ChaCha20-Poly1305 encryption
- Ephemeral keys (PFS)
- Public keys in TLSA records

Layer 4: Query Privacy (CRITICAL)

Transport Security Requirements:

Query TypeTransport RequirementEnforcement
auth-* prefixDoH/DoT REQUIREDServer MUST return secviol for plaintext
user.* namespaceDoH/DoT REQUIREDServer MUST return secviol for plaintext
public.* namespaceDoH/DoT RECOMMENDEDWarning logged, query processed
system.* namespacePlaintext ALLOWEDHealth checks allowed over UDP

Server Enforcement:

Servers MUST detect transport type and enforce requirements:

# Plaintext query to protected namespace:
v=rdb1;s=secviol;err=E014;d=Encrypted transport required (DoH/DoT)

New Error Code:

CodeStatusDescriptionRetryableRecovery
E014secviolEncrypted transport requiredYesRetry over DoH/DoT

Query Privacy Measures:

  • Query pattern obfuscation via QNAME minimization (RFC 9156)
  • Decoy queries for statistical privacy (implementation-specific)
  • Padding to fixed sizes to prevent length-based analysis

Plaintext DNS Exposure Warning:

Queries over plaintext DNS expose to all network observers:

  • User identifiers in namespace paths
  • Resource names being accessed
  • Access timing patterns
  • Query frequency

Even with hash IDs (a7f3b2 instead of acme-corp), traffic analysis can correlate patterns.

Authentication

# JWT in auth- parameter (hyphen prefix, not colon)
get.auth-<jwt>.resource.namespace.v1.resolvedb.net

Algorithm Requirements (CRITICAL)

Allowed Algorithms:

AlgorithmUse CaseStatus
EdDSA (Ed25519)Primary signing algorithmREQUIRED
ES256ECDSA P-256 (legacy compatibility)ALLOWED
RS256RSA 2048+ (legacy compatibility)ALLOWED

Forbidden Algorithms:

AlgorithmReasonAction
noneNo signatureMUST reject with secviol
HS256, HS384, HS512Symmetric key confusion riskMUST reject
PS256, PS384, PS512Implementation complexitySHOULD reject

Algorithm Confusion Prevention:

Implementations MUST:

  1. Explicit allowlist: Only process tokens with algorithms from the allowed list above
  2. Pre-parse validation: Check alg header BEFORE any signature verification
  3. Reject before decode: If alg is forbidden, reject immediately without attempting verification
  4. Case-sensitive matching: "alg": "None" and "alg": "NONE" MUST also be rejected
  5. Key type binding: RSA keys MUST only verify RS*/PS* algorithms; EC keys MUST only verify ES* algorithms

Implementation Pattern:

# BEFORE any JWT library processing:
header = base64url_decode(token.split('.')[0])
if header.get('alg') in ['none', 'None', 'NONE', 'HS256', 'HS384', 'HS512']:
    return error('secviol', 'E008', 'Forbidden algorithm')

JWT Claims Specification

Required Claims:

ClaimTypeDescription
substringSubject (user ID or service ID)
issstringIssuer (resolvedb.io or tenant issuer)
audstringAudience (must include resolvedb.io)
expintegerExpiration time (Unix timestamp)
iatintegerIssued at (Unix timestamp)
nbfintegerNot before (Unix timestamp)
jtistringJWT ID (unique token identifier for revocation)
tenantstringNamespace/tenant identifier
scopesarrayPermission scopes (e.g., ["read", "write"])

Optional Claims:

ClaimTypeDescription
rate_limit_tierstringOverride tier: free, pro, enterprise
metadataobjectArbitrary key-value metadata
noncestringReplay protection nonce

Example JWT Payload:

{
  "sub": "user-12345",
  "iss": "resolvedb.io",
  "aud": "resolvedb.io",
  "exp": 1704153600,
  "iat": 1704067200,
  "nbf": 1704067200,
  "jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "tenant": "myapp",
  "scopes": ["read", "write", "list"],
  "rate_limit_tier": "pro"
}

Token Transport Constraints

DNS labels are limited to 63 characters. JWT tokens typically exceed this limit.

Solutions:

  1. Token Hash Reference (REQUIRED): Store token server-side, reference by cryptographic hash

    get.auth-h-<32-hex-chars>.resource.namespace.v1.resolvedb.net

    Security Requirements:

    • Token references MUST use HMAC-SHA256 with a server-side secret key
    • Reference MUST be at least 128 bits (32 hex characters) to prevent brute-force
    • Format: auth-h-<first-32-hex-chars-of-HMAC-SHA256(server_secret, token)>
    • Server MUST maintain token-to-reference mapping with TTL matching token's exp claim
    • References MUST be invalidated when corresponding token is revoked
    • Server SHOULD rate-limit auth-h- queries to prevent enumeration attacks
  2. Short-Lived Tokens: Use compact tokens with minimal claims (max 5 minutes validity)

  3. Multi-Label Split: Spread token across labels - NOT RECOMMENDED due to:

    • Increased attack surface (multiple labels to intercept)
    • Complex reassembly logic prone to implementation errors
    • No integrity protection across labels

Recommended Pattern: Use the HTTP API to exchange a full JWT for a cryptographically-signed token reference, then use that reference in DNS queries. The reference exchange endpoint MUST require TLS 1.3+.

DNS Compliance (RFC 1035/1123)

Absolute Limits

  • Max FQDN: 253 characters (excluding trailing dot, per RFC 1035 Section 2.3.4)
  • Max label: 63 characters
  • Max labels: 127 levels
  • Valid chars: a-z, A-Z, 0-9, - (hyphen, not at label edges)

Important: Colons (:) are NOT valid DNS characters. UQRP uses hyphens (-) for encoding prefixes (e.g., b64- not b64:).

Case Normalization (CRITICAL)

Per RFC 1035 Section 2.3.3, DNS names are case-insensitive. Implementations MUST normalize consistently to prevent security issues.

Normalization Requirements:

ComponentNormalization PointRule
Query FQDNImmediately at parseLowercase before ANY processing
NamespaceImmediately at extractionLowercase before authorization check
Cache keyAfter normalizationUse normalized form only
Auth comparisonAll comparisonsCase-insensitive or pre-normalized

Security Rationale:

Without consistent normalization, attackers can exploit case differences:

# Attack: Cache poisoning via case confusion
1. Victim caches response for: get.data.VICTIM.v1.resolvedb.net
2. Cache key uses: get.data.victim.v1.resolvedb.net (normalized)
3. Attacker queries with different case: get.data.Victim.v1.resolvedb.net
4. If parser extracts "Victim" but cache uses "victim", cross-user data leak

# Defense: Normalize BEFORE any extraction
namespace = extracted_namespace.to_lowercase()  # FIRST

Implementation Pattern:

// CORRECT: Normalize immediately at parse
fn parse_query(qname: &str) -> Result<ParsedQuery> {
    let normalized = qname.to_lowercase(); // FIRST OPERATION
    let parts: Vec<&str> = normalized.split('.').collect();
    // All subsequent operations use normalized form
}

// WRONG: Normalize only at cache time
fn get_cache_key(qname: &str) -> String {
    qname.to_lowercase() // TOO LATE - parser may have used original case
}

Length Budget

Base domain:     resolvedb.net           (12 chars)
Tenant:          org1                    (4-10 chars)
Version:         v1                      (2 chars)
Separators:                              (3-10 chars)
Safety margin:                           (10 chars)
─────────────────────────────────────────────────────
Reserved:                                (~35 chars)
Available for data:                      (~218 chars)

Fallback Strategies

  1. Hash Reference: Store full data, query by hash
  2. Multi-Query: Split across queries
  3. Compression: Dictionary for common patterns
  4. Indirect: Short reference to full data

Rate Limits

TierQPS/IPQPS/TenantNULL Records
Free100N/A10/s
Pro1,00010,000100/s
Enterprise10,000100,0001,000/s

Rate Limit Normalization (CRITICAL)

Rate limiting MUST use normalized queries to prevent bypass via case variations.

Normalization Before Rate Check:

# These MUST count as the SAME query for rate limiting:
get.data.VICTIM.v1.resolvedb.net
get.data.victim.v1.resolvedb.net
get.data.Victim.v1.resolvedb.net
GET.data.victim.v1.resolvedb.net  # Operation case

Rate Limit Key Construction:

rate_key = hash(
    source_ip_prefix,              # /24 for IPv4, /48 for IPv6
    normalized_qname.to_lowercase(),
    qtype,
    qclass
)

Separate Buckets (Do NOT Combine):

BucketPurposeRationale
Standard queriesNormal operationsBase rate
NULL record queriesLarge dataAmplification risk
Authenticated queriesPer-tenantDifferent limits
Health checksSystem monitoringHigher allowance

Bypass Prevention:

Attackers may attempt bypass via:

  1. Case variations - Mitigated by normalization
  2. Multiple query types - Separate buckets prevent mixing
  3. Distributed sources - /24 aggregation limits effectiveness
  4. Authenticated token rotation - Per-tenant limits apply

Rate Limit Response:

v=rdb1;s=ratelimit;err=E010;retry=60;ttl=1;d=Rate limit exceeded

Pluggable Provider Protocol (PPP)

Services can be implemented via MCPs or custom backends:

class ResolveDBProvider:
    name: str
    version: str
    capabilities: ProviderCapabilities

    def can_handle(self, query: DNSQuery) -> bool
    def execute(self, query: DNSQuery) -> DNSResponse
    def health_check(self) -> HealthStatus

Service Discovery:

_services.registry.resolvedb.net    TXT  "weather.v1,stock.v1,news.v1"
_meta.weather.v1.registry           TXT  "provider=OpenWeather;sla=99.9"
_health.weather.v1.registry         TXT  "status=healthy;latency=15ms"

Location-Based Queries

Following the Privacy by Design principle, location-based queries REQUIRE explicit location parameters. The server does NOT infer location from the client's IP address.

Location Parameter Formats

# Named location (city, region)
get.city-newyork.weather.public.v1.resolvedb.net

# Coordinates via geo- prefix (decimals use 'd' separator)
# Format: lat-<lat>.lon-<lon> where decimals use 'd' separator
get.lat-40d7128.lon--74d0060.weather.public.v1.resolvedb.net
#                   ^^ double hyphen for negative longitude

# Coordinates via Base64-encoded JSON
# {"lat":40.7128,"lon":-74.0060} -> eyJsYXQiOjQwLjcxMjgsImxvbiI6LTc0LjAwNjB9
get.b64-eyJsYXQiOjQwLjcxMjgsImxvbiI6LTc0LjAwNjB9.weather.public.v1.resolvedb.net

Why Explicit Location?

Implicit (WRONG)Explicit (CORRECT)
Server infers from source IPClient provides location
Breaks through VPNs/proxiesWorks everywhere
Different results from different networksSame query = same result
Privacy leakPrivacy preserved
Cache fragmentation (ECS scopes)Fully cacheable

Note: Colons (:) are not valid in DNS labels per RFC 1035. All parameters requiring special characters must use Base64 encoding or DNS-safe character substitution.

EDNS Client Subnet (ECS) Handling (RFC 7871)

Privacy Implications:

ECS exposes client subnet information to authoritative servers. This creates privacy concerns:

  • Client location disclosed without explicit consent
  • Cached responses may leak location to subsequent queries
  • Third-party observers can correlate IP ranges to locations

Server Requirements:

RequirementImplementationRFC Reference
Scope Prefix HandlingREQUIREDRFC 7871 Section 7.3
Privacy Mode SupportREQUIREDRFC 7871 Section 12.3
Opt-Out MechanismREQUIREDClient can omit ECS

Scope Prefix Behavior:

Servers MUST include SCOPE PREFIX-LENGTH in ECS responses to indicate caching granularity:

# Query includes ECS with /24 prefix
# Server responds with /16 scope (less specific = broader caching)
Client: ECS 192.0.2.0/24
Server: ECS 192.0.0.0/16 SCOPE 16

# Cached response valid for all 192.0.x.x clients

Privacy Mode (ECS=0):

Clients MAY send ECS with SOURCE PREFIX-LENGTH=0 to indicate privacy preference:

  • Server MUST NOT use client subnet for response
  • Server MUST respond with SCOPE PREFIX-LENGTH=0
  • Response is cached globally (no location variance)
# Privacy mode query
Client: ECS 0.0.0.0/0 (SOURCE=0)
Server: ECS 0.0.0.0/0 SCOPE=0

GeoIP Privacy Considerations:

  1. geoip operation responses MUST use short TTL (30-60 seconds)
  2. GeoIP should NOT be served via shared recursive resolvers
  3. For accurate, privacy-preserving geolocation, clients should query authoritative directly over DoH
  4. The geoip operation SHOULD NOT cache at intermediate resolvers

Cache Scope Pollution Prevention:

Implementations MUST separate cache entries by ECS scope:

Cache Key = (QNAME, QTYPE, QCLASS, ECS_SCOPE_PREFIX)

# Different cache entries:
weather.public.v1.resolvedb.net:TXT:IN:192.0.0.0/16
weather.public.v1.resolvedb.net:TXT:IN:198.51.0.0/16
weather.public.v1.resolvedb.net:TXT:IN:GLOBAL  # ECS=0 response

TTL Cache Delegation

ResolveDB's core scalability advantage: leveraging the global DNS caching infrastructure to achieve massive query reduction.

RFC References: TTL semantics per RFC 1035 Section 3.2.1, negative caching per RFC 2308, stale serving per RFC 8767.

The Caching Multiplier

When ResolveDB returns a response with ttl=3600, every resolver in the DNS hierarchy caches that response independently:

┌─────────────────────────────────────────────────────────────────┐
│                    DNS CACHING HIERARCHY                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────┐                                               │
│  │   Browser    │  TTL respected per response                   │
│  │   DNS Cache  │  (Chrome, Firefox, Safari cache DNS)          │
│  └──────┬───────┘                                               │
│         │ cache miss                                            │
│         ▼                                                       │
│  ┌──────────────┐                                               │
│  │   OS DNS     │  System resolver cache                        │
│  │   Cache      │  (macOS, Windows, Linux systemd-resolved)     │
│  └──────┬───────┘                                               │
│         │ cache miss                                            │
│         ▼                                                       │
│  ┌──────────────┐                                               │
│  │  Corporate   │  Serves entire organization                   │
│  │  DNS Server  │  (1000s of users share this cache)            │
│  └──────┬───────┘                                               │
│         │ cache miss                                            │
│         ▼                                                       │
│  ┌──────────────┐                                               │
│  │  ISP         │  Serves millions of subscribers               │
│  │  Recursive   │  (Comcast, AT&T, regional ISPs)               │
│  └──────┬───────┘                                               │
│         │ cache miss                                            │
│         ▼                                                       │
│  ┌──────────────┐                                               │
│  │  Public DNS  │  Global scale (8.8.8.8, 1.1.1.1)             │
│  │  (optional)  │  Serves tens of millions                      │
│  └──────┬───────┘                                               │
│         │ cache miss                                            │
│         ▼                                                       │
│  ┌──────────────┐                                               │
│  │  ResolveDB   │  <--- ONLY SEES CACHE MISSES                  │
│  │ Authoritative│                                               │
│  └──────────────┘                                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Example: If 10,000 users behind a corporate DNS query weather.public.v1.resolvedb.net with ttl=3600:

  • Traditional API: 10,000 requests/hour to origin
  • ResolveDB: 1 request/hour (all others served from corporate DNS cache)

Key insight: ResolveDB query volume scales with:

  • (Number of UNIQUE queries) x (1 / TTL)

NOT with total number of clients or total request volume.

TTL Classes

Server assigns TTL based on data volatility classification:

ClassTTLUse Case
immutable604800 (7 days)Hash-addressed content (h-<hash>), versioned data
stable86400 (24 hours)Reference data, public datasets, documentation
standard3600 (1 hour)Default for most data, weather forecasts, news
dynamic300 (5 min)User data, frequently changing content
volatile30-60 secReal-time status, live feeds, health checks
nocache0Write confirmations, errors, rate limit responses

Operation TTL Defaults

OperationDefault TTLNotes
get3600Per-resource override based on data classification
put0Write confirmation, not cacheable
delete0Delete confirmation, not cacheable
info3600Metadata is stable
list300Listings change with additions
search60Results may be contextual/personalized
health30Must reflect current state
geoip300Location data is relatively stable
watch0Streaming endpoint, not DNS-cacheable

TTL Selection Guidelines

For API Consumers:

  1. Static configuration: Use stable (24h) or include version in query path
  2. User data: Use dynamic (5min) - accept predictable staleness window
  3. Real-time feeds: Use volatile (30-60s) or switch to streaming (watch)
  4. Content-addressed: Use immutable (7 days) for h-<hash> queries

Special Cases:

CaseTTL Behavior
Authenticated (auth- prefix)Short TTL (60-300s) - prevents stale sessions in shared caches
notfound (404)SOA MINIMUM (3600s) - negative cache prevents repeated queries
ratelimit (429)TTL=1 - signal immediate retry
error (500)TTL=0 - don't cache server failures
unavail (503)TTL=0 - transient, retry immediately
Chunked data (chunk- prefix)Same TTL across all chunks - prevents partial staleness

Authenticated Query Cache Exclusion (CRITICAL)

Authenticated queries MUST NOT be cached to prevent cross-user data leakage.

Detection Requirements:

Implementations MUST detect authenticated queries through BOTH methods:

Detection MethodCoverageFallback
Full query parsingPrimary - extracts auth_token from parsed queryRequired
String matchingSecondary - checks for .auth- in QNAMEBackup only

Fail-Secure Behavior:

fn is_authenticated_query(query: &DNSQuery) -> bool {
    // PRIMARY: Parse the query structure
    match parse_resolvedb_query(query) {
        Ok(parsed) => parsed.auth_token.is_some(),
        Err(_) => true,  // FAIL SECURE: If parsing fails, assume authenticated
    }
}

Security Rationale:

Without strict auth detection:

  1. Encoded auth tokens (b64-<token-with-auth-inside>) bypass string matching
  2. Malformed queries may cache and serve to unauthorized users
  3. Cross-user cache poisoning becomes possible

Cache Key Requirements for Authenticated Queries:

If authenticated responses MUST be cached (e.g., for performance):

  • Cache key MUST include full token hash or user identifier
  • Cache entries MUST be isolated per authenticated session
  • TTL MUST NOT exceed token expiration minus clock skew margin

Negative Caching (RFC 2308)

ResolveDB returns negative responses in two forms:

ResponseRCODEMeaningTTL Source
NXDOMAIN3Name does not existmin(SOA TTL, SOA MINIMUM) = 3600s
NODATA0 (empty answer)Name exists, but not for queried typeSame as NXDOMAIN

Both responses include the SOA record in the authority section (required for caching per RFC 2308 Section 3). Negative caching prevents repeated queries for non-existent resources and defends against DNS water torture attacks.

TTL=0 Behavior Notes

TTL=0 signals "do not cache" per RFC 1035, but resolver implementations vary:

ResolverTTL=0 Behavior
BINDCaches briefly (~1 second) for loop prevention
UnboundRespects TTL=0, no cache
Cloudflare (1.1.1.1)Respects TTL=0
Google (8.8.8.8)May serve stale data (RFC 8767)
Windows DNS ClientMinimum 1-second cache
Browser cachesOften apply minimum TTL regardless

For truly uncacheable operations (put, delete, errors), TTL=0 is correct but clients should not assume zero latency for repeated queries. Write confirmations should use response signatures for verification regardless of caching.

Resolver TTL Capping

Some public resolvers cap maximum TTL values:

ResolverMax TTL
Google Public DNS86400 (1 day)
Cloudflare604800 (7 days)
Most ISP resolvers86400-604800

The immutable class (7 days) may be capped to 1 day by some resolvers. For hash-addressed content (h-<hash>), this is acceptable since re-queries return identical data.

GeoIP/ECS Considerations

When responses vary by client location using EDNS Client Subnet (RFC 7871), caching becomes scope-limited. A response served to NYC clients is not cacheable for LA clients, reducing the caching multiplier but maintaining correctness.

Implementation Status

FeatureStatusNotes
TTL in response formatImplementedttl=<seconds> in TXT metadata
SOA MINIMUM for negative cacheImplemented3600s default
Cache respects response TTLImplementedExtracts from first answer, clamps to ttl_min/ttl_max
Per-operation TTL defaultsImplementedttl_for_operation() in constants.rs, determine_ttl() in authority.rs
TTL class constantsImplementedTTL_IMMUTABLE (7d), TTL_STABLE (24h), TTL_STANDARD (1h), TTL_DYNAMIC (5m), TTL_VOLATILE (30-60s)
Error-specific TTL (0 for failures)Implementeddetermine_ttl() returns TTL_NOCACHE for errors
Auth query TTL reductionImplementeddetermine_ttl() returns TTL_AUTH_MAX (300s), cache excludes auth- queries
Rate limit TTLImplementedTTL_RATELIMIT (1s) via RateAction::suggested_ttl()

Protocol Evolution

Version Negotiation

Negotiation Process:

  1. Client queries with preferred version: get.weather.public.v2.resolvedb.net
  2. If server doesn't support v2, respond with redirect: v=rdb1;s=redirect;supported=v1;d=get.weather.public.v1.resolvedb.net
  3. Client retries with supported version

Anti-Downgrade Protection (CRITICAL):

Attackers may force clients to use older, vulnerable protocol versions:

ProtectionImplementation
Maximum downgradeClients MUST NOT downgrade more than one major version
Minimum versionServers advertise min_version in health endpoint
Version pinningClients MAY pin to specific versions for security-critical operations

Server Health Includes Version Info:

_health.system.resolvedb.net TXT "v=rdb1;status=healthy;versions=v1,v2;min_version=v1;recommended=v2"

Client Behavior:

def negotiate_version(preferred: str, server_supported: list) -> str:
    # Check minimum version requirement
    if server_min_version > client_minimum_acceptable:
        raise VersionError("Server requires newer client")

    # Only downgrade one major version
    pref_major = int(preferred[1:])
    for v in sorted(server_supported, reverse=True):
        v_major = int(v[1:])
        if v_major >= pref_major - 1:  # Max 1 version downgrade
            return v

    raise VersionError("No acceptable version available")

Backward Compatibility Rules

Change TypeAllowedRequires
Adding optional fieldsYesMinor version bump
Adding new status codesYesClients treat unknown as error
Adding new operationsYesClients return E002 for unknown
Adding new encoding prefixesYesClients reject unknown prefixes
Removing required fieldsNONew major version
Changing field semanticsNONew major version
Changing status code meaningsNONew major version
Changing encoding prefix formatNONew major version

Deprecation

_deprecation.old-service.v1.public.resolvedb.net TXT "deprecated=true;sunset=2025-12-31;migrate=new-service.v2"

Deprecation Timeline:

  • Deprecation announcement: 6 months before sunset
  • Warning responses: 3 months before sunset (include deprecation=true in responses)
  • Sunset: Return redirect to new version

Domain Portfolio

DomainPurposeStatus
resolvedb.netDNS resolver (primary)Active
resolvedb.ioHTTP API & WebActive
resolvedb.comRoot redundancyReserved
resolvedb.orgRoot redundancyReserved
resolvedb.devDeveloper portal, sandboxPlanned
resolvedb.cloudCDN, blob storagePlanned
resolvedb.appDashboard, UIPlanned
resolvedb.caCanadian data residencyReserved

Current usage:

  • DNS queries: *.resolvedb.net (all DNS resolution)
  • HTTP API: api.resolvedb.io (write operations, management)
  • Web dashboard: resolvedb.io (future)