resolve

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package resolve turns a user@domain into an identity key and the overlay host that serves it, using only the two documents a domain publishes about itself: its BRC-180 manifest and its BRC-169 handle-resolution endpoint.

The domain is the trust anchor and nothing else is. A manifest says what a domain CLAIMS to host and a resolution answer says what it CLAIMS a handle's key is; both are secured by control of the domain and by nothing stronger. Every later step a reader takes (signature, sequence, proof, pin) exists because this step proves so little, so this package is deliberately literal: it fetches exactly the documents the specifications name, refuses anything that does not match what was asked, and never probes.

HTTP here is the one place a reader reaches beyond the host and header service it was configured with, so the client policy is part of the security posture rather than a convenience: system roots only, HTTPS only, no cross-origin redirects, bounded bodies. NewHTTPClient is that policy in one place.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotHTTPS refuses a URL that is not https. RFC 7033 and BRC-169 both
	// require HTTPS for discovery, and a manifest that points resolution at
	// http:// has asked the client to accept a key that anyone on the path
	// could have written.
	ErrNotHTTPS = errors.New("not an https URL")
	// ErrCrossOriginRedirect refuses a redirect that leaves the origin the
	// request was made to. Following one would let a domain hand its
	// authority to a host the user never named.
	ErrCrossOriginRedirect = errors.New("redirect to another origin refused")
	// ErrBodyTooLarge refuses a response above the bound.
	ErrBodyTooLarge = errors.New("response body exceeds the bound")
	// ErrNoHandles means the manifest has no metanet.handles object, so the
	// domain does not offer handle resolution. BRC-169 section 5.1 forbids
	// probing the well-known path in that case.
	ErrNoHandles = errors.New("domain does not offer handle resolution (no metanet.handles in its manifest)")
	// ErrNotFound is a 404: no such handle is registered at the domain.
	ErrNotFound = errors.New("handle is not registered at this domain")
	// ErrRevoked means the handle was registered and has been released or
	// revoked. The error may carry a forwarding record; see RevokedError.
	ErrRevoked = errors.New("handle revoked")
	// ErrCertificateUnverified is what VerifyHandleCertificate returns until
	// BRC-52 verification is implemented.
	ErrCertificateUnverified = errors.New("handle certificate not verified (verification is not implemented in this build)")
)

Functions

func NewHTTPClient

func NewHTTPClient(timeout time.Duration) *http.Client

NewHTTPClient returns a client that implements the discovery policy: system TLS roots only, no way to skip verification, redirects followed only within the origin of the first request, and no proxy.

No proxy is a choice, not an omission. HTTP_PROXY in the environment would route discovery through a host that appears nowhere in the caller's configuration or its trace, and a run should reach exactly what it was configured to reach.

func VerifyHandleCertificate

func VerifyHandleCertificate(h *Handle, m *Manifest) error

VerifyHandleCertificate is the seam for BRC-169 section 4.1. It would check that h.Certificate is a BRC-52 certificate whose subject equals h.IdentityKey, whose certifier is m.Trust.PublicKey, whose type is the handle certificate type of section 4.5, whose fields bind the handle and domain, and whose revocation outpoint is unspent (section 4.2) at the freshness the pending action needs; on success it would set h.CertificateChecked.

It is the seam the BRC-52 verifier fills, exported so that a caller can gate an action on it and gain the check without changing its own code. Until the verifier is in place it always refuses: it returns ErrCertificateUnverified unconditionally and never sets CertificateChecked. Meanwhile an identity key from ResolveHandle is attested by the domain's resolution endpoint alone, which is exactly the trust a key pin is there to bound.

Types

type Acct

type Acct struct {
	// Handle is lowercase, with any +tag stripped.
	Handle string
	// Domain is the lowercase FQDN. Never an alias; see ParseAcct.
	Domain string
	// Tag is the "+tag" subhandle if one was given, without the "+". It is
	// kept because it is the recipient's routing hint (section 3) and
	// stripped from Handle because resolution is on the bare handle (5.2).
	Tag string
}

Acct is a parsed recipient: the fully qualified form of BRC-169 section 2.1 with the ecosystem already known to be a domain.

func ParseAcct

func ParseAcct(s string) (Acct, error)

ParseAcct accepts user@domain, @user@domain and acct:user@domain, lowercases per section 2.1 rule 2, strips and keeps a +tag, and validates the grammar.

The ecosystem MUST be a domain. Section 2.1 rule 5 makes the dot the discriminator, so a dotless ecosystem is an alias, and aliases are not supported here: resolving one means trusting a directory to say which domain "example" is, which is a trust step this package has no way to check. The error says so rather than guessing at a hostname the user did not type.

Example

ParseAcct accepts the three written forms of a BRC-169 address, lowercases it, and keeps a +tag apart from the handle it routes to.

package main

import (
	"fmt"

	"github.com/lightwebinc/bcommon/resolve"
)

func main() {
	for _, s := range []string{"Alice@Example.COM", "@alice+news@example.com", "acct:alice@example.com", "alice@example"} {
		a, err := resolve.ParseAcct(s)
		if err != nil {
			fmt.Println(s, "=>", err)
			continue
		}
		fmt.Printf("%s => %s (tag %q)\n", s, a, a.Tag)
	}
}
Output:
Alice@Example.COM => alice@example.com (tag "")
@alice+news@example.com => alice@example.com (tag "news")
acct:alice@example.com => alice@example.com (tag "")
alice@example => resolve: "alice@example": ecosystem "example" has no dot, so it is an alias (BRC-169 section 2.1 rule 5); aliases are not supported, name the domain in full

func (Acct) String

func (a Acct) String() string

String returns handle@domain, the paymail-style form, which is how an address is written on a command line and keyed in a pin file.

type Handle

type Handle struct {
	Acct
	IdentityKey [33]byte
	// Certificate is the BRC-52 handle certificate exactly as served, and
	// unverified.
	Certificate json.RawMessage
	Messagebox  string
	// TTL is the caching bound in seconds (section 5.4): binding upward,
	// advisory downward.
	TTL     int
	Revoked bool
	// CertificateChecked reports whether Certificate has been verified
	// against the domain's certifier key. Never true in this build.
	CertificateChecked bool
}

Handle is a resolved handle: what the domain says the identity key is.

CertificateChecked is false in every Handle this build returns. The field exists so that a caller cannot mistake "the domain said so" for "the certifier signed it"; see VerifyHandleCertificate.

func ResolveHandle

func ResolveHandle(ctx context.Context, c *http.Client, m *Manifest, a Acct) (*Handle, error)

ResolveHandle queries the domain's handle-resolution endpoint (section 5.2) and returns what it claims about a.

The echo check is the one that matters. The endpoint must echo the handle and domain it resolved, and a static file that returns the same document for every handle passes every other check in this function: valid JSON, valid key, valid version. Requiring the echo to equal what was asked is how that file is caught here rather than trusted.

type Handles

type Handles struct {
	Version    string   `json:"version"`
	Resolve    string   `json:"resolve"`
	Search     string   `json:"search"`
	Reverse    string   `json:"reverse"`
	Messagebox string   `json:"messagebox"`
	Aliases    []string `json:"aliases"`
}

Handles is metanet.handles.

type Manifest

type Manifest struct {
	// Overlays maps a topic manager or lookup service name to its base URL
	// (BRC-180). Nil when the domain declares no overlays, which is a valid
	// manifest and not an error.
	Overlays map[string]string
	// Handles is the metanet.handles object (BRC-169 section 5.1). Nil when
	// the domain does not offer handle resolution.
	Handles *Handles
	// Trust is the metanet.trust anchor (BRC-68). Its PublicKey is the
	// certifier key handle certificates are checked against.
	Trust *Trust
	// Raw is the document as served.
	Raw json.RawMessage
}

Manifest is the part of a domain's /manifest.json this package reads: the BRC-180 overlay map, the BRC-169 handles object and the BRC-68 trust anchor. Unknown keys are ignored, as both specifications require, and Raw keeps the whole document for a caller that needs one this struct does not carry.

func FetchManifest

func FetchManifest(ctx context.Context, c *http.Client, domain string) (*Manifest, error)

FetchManifest reads https://<domain>/manifest.json.

The path is fixed by BRC-68 and the scheme by BRC-180; there is no fallback to http and no alternative location, because a manifest reached any other way would not be secured by control of the domain, which is the only thing securing it.

func ParseManifest

func ParseManifest(body []byte) (*Manifest, error)

ParseManifest decodes a manifest document.

func (*Manifest) Overlay

func (m *Manifest) Overlay(name string) (string, bool)

Overlay returns the base URL the domain declares for a service. ok is false when the entry is absent, and BRC-180 is explicit about what that means: the domain does not offer the service, and the client must not go looking.

Example

A manifest names one base URL per service. An absent entry means the domain does not offer the service, and a reader must not go looking.

package main

import (
	"fmt"

	"github.com/lightwebinc/bcommon/resolve"
)

func main() {
	m, err := resolve.ParseManifest([]byte(`{"metanet":{"overlays":{"ls_example":"https://overlay.example.com"}}}`))
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(m.Overlay("ls_example"))
	fmt.Println(m.Overlay("ls_other"))
}
Output:
https://overlay.example.com true
 false

type RevokedError

type RevokedError struct {
	Acct       Acct
	Status     int
	Forwarding json.RawMessage
}

RevokedError is ErrRevoked with the forwarding record, if the domain served one (section 4.3). errors.Is(err, ErrRevoked) matches it.

func (*RevokedError) Error

func (e *RevokedError) Error() string

func (*RevokedError) Is

func (e *RevokedError) Is(target error) bool

Is makes errors.Is(err, ErrRevoked) true for a RevokedError.

type Trust

type Trust struct {
	Name      string `json:"name"`
	PublicKey string `json:"publicKey"`
}

Trust is metanet.trust.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL