port

package
v0.1.6 Latest Latest
Warning

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

Go to latest
Published: Jun 26, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package port defines the hexagonal-architecture port interfaces that the domain and service layers depend on. Adapters in internal/adapter provide concrete implementations. This package has no imports from framework, database, or transport libraries.

Index

Constants

View Source
const (
	AlgorithmECDSAP256 = "ECDSA_P256"
	AlgorithmECDSAP384 = "ECDSA_P384"
	AlgorithmRSA2048   = "RSA_2048"
	AlgorithmRSA4096   = "RSA_4096"
)

Algorithm names understood by KeyManager implementations.

Variables

View Source
var ErrOrderFailed = errors.New("certificate order failed")

ErrOrderFailed is returned by ServerCertificateIssuer.FinalizeOrder when the provider reported a terminal order failure (e.g. an ACME order moved to `invalid`). The order cannot be retried; the operator must submit a new registration or renewal. Wrap with %w so errors.Is matches.

View Source
var ErrOrderPending = errors.New("certificate order pending")

ErrOrderPending is returned by ServerCertificateIssuer.FinalizeOrder when the provider accepted the finalize request but has not produced a certificate yet (e.g. an ACME order sitting in `processing`). The caller persists the order as ISSUING and re-drives the finalize on a subsequent verify-acme call. Wrap with %w so errors.Is matches.

Functions

This section is empty.

Types

type AgentStore

type AgentStore interface {
	// Save inserts or updates an AgentRegistration. If agent.ID == 0 the
	// implementation must assign a new ID and set it on the aggregate.
	Save(ctx context.Context, agent *domain.AgentRegistration) error

	// FindByID returns the agent with the given database ID or
	// domain.ErrNotFound.
	FindByID(ctx context.Context, id int64) (*domain.AgentRegistration, error)

	// FindByAgentID returns the agent with the given stable agent UUID.
	FindByAgentID(ctx context.Context, agentID string) (*domain.AgentRegistration, error)

	// FindByAnsName returns the agent with the given versioned ANS name.
	FindByAnsName(ctx context.Context, ansName domain.AnsName) (*domain.AgentRegistration, error)

	// ExistsByAnsName returns true if any registration uses the given name.
	ExistsByAnsName(ctx context.Context, ansName domain.AnsName) (bool, error)

	// FindAllByAgentHost returns every registration (any version, any status)
	// for the given FQDN, newest first.
	FindAllByAgentHost(ctx context.Context, host string) ([]*domain.AgentRegistration, error)

	// FindExistingByFQDN returns ACTIVE or PENDING_* registrations for the
	// given FQDN, newest first. Used to check for supersession candidates.
	FindExistingByFQDN(ctx context.Context, fqdn string) ([]*domain.AgentRegistration, error)

	// ListByOwner returns a cursor page of agents owned by ownerID.
	ListByOwner(
		ctx context.Context,
		ownerID string,
		filter ListFilter,
	) (*CursorPage[*domain.AgentRegistration], error)

	// ExpireLapsedPendingValidation atomically transitions to EXPIRED
	// every registration that is still PENDING_VALIDATION with a
	// PENDING certificate order whose challenge window lapsed at or
	// before now, returning the number transitioned. The agent-expiry
	// sweeper uses it to honor the spec's "PENDING_VALIDATION
	// registrations are not cancellable and will auto-expire".
	//
	// The transition is a single guarded write — not a read-then-save
	// — so a verify-acme that advances the same row (to PENDING_DNS,
	// or to a non-PENDING order state) between scans cannot be
	// clobbered: such a row simply no longer matches. In-flight
	// (order ISSUING) and terminally-failed (order FAILED)
	// registrations are excluded; they leave PENDING_VALIDATION
	// through the cancel route instead, per domain.Cancel's
	// eligibility rule.
	ExpireLapsedPendingValidation(ctx context.Context, now time.Time) (int64, error)

	// Delete removes the registration with the given ID. Used only for
	// administrative cleanup; normal lifecycle uses Revoke.
	Delete(ctx context.Context, id int64) error
}

AgentStore persists AgentRegistration aggregates and supports all query patterns required by the V2 management and registration APIs.

type AuthProvider

type AuthProvider interface {
	// Authenticate extracts and validates credentials from the request,
	// returning the authenticated Identity on success. An error is returned
	// if credentials are missing, malformed, or invalid.
	Authenticate(ctx context.Context, r *http.Request) (*Identity, error)

	// Middleware returns an HTTP middleware that authenticates every
	// incoming request and stores the Identity in the request context.
	// On authentication failure the middleware must return 401.
	Middleware() func(http.Handler) http.Handler
}

AuthProvider authenticates incoming HTTP requests and exposes middleware that enforces authentication on protected routes. Implementations must be safe for concurrent use.

type ByocCertificateStore

type ByocCertificateStore interface {
	Save(ctx context.Context, agentID string, cert *domain.ByocServerCertificate) error
	FindByAgentID(ctx context.Context, agentID string) ([]*domain.ByocServerCertificate, error)
	FindLatestValidByAgentID(ctx context.Context, agentID string) (*domain.ByocServerCertificate, error)
}

ByocCertificateStore persists operator-provided server certificates. Unlike identity certs, BYOC certs are never issued by us — we only validate and store them.

type CertificateStore

type CertificateStore interface {
	// SaveIdentityCertificate persists a newly issued identity certificate.
	SaveIdentityCertificate(
		ctx context.Context,
		agentID string,
		cert *domain.StoredCertificate,
	) error

	// FindIdentityCertificatesByAgent returns all identity certificates
	// for the given agent, newest first.
	FindIdentityCertificatesByAgent(
		ctx context.Context,
		agentID string,
	) ([]*domain.StoredCertificate, error)

	// UpdateCertificateStatus updates the status of an existing certificate.
	UpdateCertificateStatus(
		ctx context.Context,
		cert *domain.StoredCertificate,
	) error

	// SaveCSR persists a new or updated CSR (identity or server).
	// Upserts on csr_id; status transitions replace the previous row.
	SaveCSR(
		ctx context.Context,
		agentID string,
		csr *domain.AgentCSR,
	) error

	// FindCSRByID returns the CSR with the given ID, scoped to the
	// given agent. Used by the CSR-status handler and the registration
	// flow's post-rotation refresh.
	FindCSRByID(
		ctx context.Context,
		agentID, csrID string,
	) (*domain.AgentCSR, error)

	// FindLatestPendingCSRByType returns the most recent PENDING CSR
	// of the given type (IDENTITY or SERVER) for an agent, or
	// (nil, nil) when none exists. Used by VerifyACME to pull the
	// CSRs submitted at register time for signing once domain
	// control has been proven.
	FindLatestPendingCSRByType(
		ctx context.Context,
		agentID string,
		csrType domain.CSRType,
	) (*domain.AgentCSR, error)
}

CertificateStore persists identity certificates and CSRs issued by the system's private CA. BYOC server certificates are handled by ByocCertificateStore.

type CertificateValidator

type CertificateValidator interface {
	// ValidateServerCertificate verifies a BYOC server certificate against
	// the expected FQDN. It checks PEM parsing, chain validity, CN/SAN
	// matching, validity period, signature algorithm, and key strength.
	ValidateServerCertificate(
		ctx context.Context,
		leafPEM, chainPEM, expectedFQDN string,
	) (*ValidatedCert, error)

	// ValidateIdentityCSR verifies an identity CSR before it is signed.
	// It checks PEM parsing, signature, key strength, and that the URI SAN
	// matches the expected ANS name.
	ValidateIdentityCSR(
		ctx context.Context,
		csrPEM string,
		expectedAnsName string,
	) error

	// ValidateServerCSR verifies a server CSR before issuance. Server
	// CSRs must carry the agent's FQDN as a DNS SAN (TLS server-auth
	// convention). Falls back to matching against the CSR subject CN
	// for SDK-compat when no SAN is present.
	ValidateServerCSR(
		ctx context.Context,
		csrPEM string,
		expectedFQDN string,
	) error
}

CertificateValidator validates operator-provided server certificates and identity CSRs without issuing anything. It is used at the edges of the system (registration, renewal) to prevent malformed or mismatched certificates from entering the domain model.

type CursorPage

type CursorPage[T any] struct {
	Items         []T
	NextCursor    string
	HasMore       bool
	ReturnedCount int
}

CursorPage is a generic cursor-paginated result set. NextCursor is opaque and should be passed unchanged as the Cursor field in the next ListFilter. An empty NextCursor combined with HasMore=false signals the end of the result set.

type DIDDocument added in v0.1.6

type DIDDocument struct {
	ID              string
	AssertionMethod []VerificationMethod
}

DIDDocument is the subset of a resolved DID document the identity control proof needs: the document's id and its assertionMethod verification methods. Per DID Core, a key listed under assertionMethod is authorized to make assertions as the DID — key possession IS what DID control means.

func (*DIDDocument) FindAssertionMethod added in v0.1.6

func (d *DIDDocument) FindAssertionMethod(kid string) *VerificationMethod

FindAssertionMethod returns the assertionMethod entry with the given id, or nil.

type DIDResolver added in v0.1.6

type DIDResolver interface {
	// Resolve returns the DID document for the given canonical
	// did:web identifier. Implementations return domain errors with
	// the DID_* codes (DID_RESOLUTION_FAILED,
	// DID_DOCUMENT_ID_MISMATCH, DID_REDIRECT_DOMAIN_MISMATCH).
	Resolve(ctx context.Context, did string, hints []KeyHint) (*DIDDocument, error)
}

DIDResolver fetches the DID document for a did:web identifier — the authoritative key source for did:web control proofs. It is the only outbound I/O in the identity proof gate, which makes it the port:

  • The "web" adapter performs a hardened HTTPS fetch (WebPKI, SSRF dialer guards, timeout, size cap, bounded same-site redirects) of the document at the DID's resolution URL. Hints are ignored — the resolved document is always the key source.

  • The "noop" adapter performs no I/O and synthesizes a document from the hints (the kid → JWK pairs embedded in the submitted proofs' `jwk` headers). Signature verification still genuinely runs against those keys, so sealed events stay self-verifying even from quickstart runs — only the binding "the live did.json really lists this key" is waived. Mirrors the noop DNS verifier: real crypto, waived external-world binding. NOT for production.

did:key never reaches this port — its key decodes from the DID string with zero I/O.

type DNSVerifier

type DNSVerifier interface {
	// VerifyRecords performs DNS lookups for each expected record and
	// returns a per-record report plus an overall pass/fail summary.
	// A VerificationResult is always returned even on partial failures;
	// a non-nil error indicates a systemic failure (e.g., DNS unreachable).
	VerifyRecords(
		ctx context.Context,
		fqdn string,
		expected []domain.ExpectedDNSRecord,
	) (*VerificationResult, error)
}

DNSVerifier checks that the operator's DNS zone contains the records the domain expects. It does NOT create, update, or delete records — ANS is verification-only. Operators manage their own DNS.

type EndpointStore

type EndpointStore interface {
	Save(ctx context.Context, endpoints *domain.AgentEndpoints) error
	FindByAgentID(ctx context.Context, agentID string) (*domain.AgentEndpoints, error)
	FindByAgentIDs(ctx context.Context, agentIDs []string) (map[string]*domain.AgentEndpoints, error)
}

EndpointStore persists AgentEndpoints collections.

type EventBus

type EventBus interface {
	// Publish delivers the event to all handlers registered for its type.
	// Returns an error only if the bus itself is broken (e.g., shut down);
	// per-handler errors are surfaced through the adapter's logging.
	Publish(ctx context.Context, event domain.Event) error

	// Subscribe registers a handler for a specific event type.
	// Subscriptions made after Close are rejected.
	Subscribe(eventType domain.EventType, handler EventHandler) error
}

EventBus delivers domain events from producers (services) to consumers (other services, TL client, outbound integrations). Subscriptions are registered at startup; Publish is called by aggregates after state transitions.

Semantics:

  • Publish is at-least-once.
  • Handlers for the same event type are called in registration order.
  • The bus must NOT panic on handler errors; it logs and continues.

type EventHandler

type EventHandler func(ctx context.Context, event domain.Event) error

EventHandler processes a single domain event. A non-nil error causes the bus implementation to apply its retry policy (or log and discard, depending on the adapter).

type FinalizeOrderRequest added in v0.1.6

type FinalizeOrderRequest struct {
	// OrderRef is the provider-opaque handle returned by CreateOrder
	// (an ACME order URL, an internal id, …).
	OrderRef string
	// CSRPEM is the operator-submitted server CSR. Per RFC 8555 the
	// CSR is presented at finalize time, not at order creation.
	CSRPEM string
	// FQDN is the agent host the certificate must cover.
	FQDN string
	// Verified lists the challenge types whose artifacts the RA's own
	// pre-flight check found published. ACME implementations MUST only
	// answer a verified challenge: telling the provider to validate an
	// unsatisfied challenge invalidates the authorization — and with it
	// the whole order. Self-signed implementations may ignore it (the
	// RA's gate is authoritative there).
	Verified []domain.ChallengeType
}

FinalizeOrderRequest carries everything an issuer needs to complete a previously created order.

type HTTPChallengeVerifier added in v0.1.6

type HTTPChallengeVerifier interface {
	// VerifyHTTPChallenge fetches `http://<fqdn><path>` and reports
	// whether the response body matches the expected content (the key
	// authorization for account-bound challenges, the raw token
	// otherwise). Returns (false, nil) when the artifact is missing or
	// mismatched; a non-nil error indicates a systemic failure that
	// prevented checking at all (which callers treat the same as
	// not-published, since an unreachable host cannot prove control).
	VerifyHTTPChallenge(ctx context.Context, fqdn, path, expectedContent string) (bool, error)
}

HTTPChallengeVerifier checks that the domain owner has published an HTTP-01 challenge artifact at the expected path under their FQDN. Like the DNSVerifier, it is verification-only: ANS never serves challenge files on the owner's behalf — the owner publishes the artifact themselves from the challenge info relayed in the pending response.

type Identity

type Identity struct {
	// Subject uniquely identifies the authenticated principal
	// (e.g., an OIDC "sub" claim, a user ID, or an API key owner).
	Subject string

	// Scopes lists the permissions granted to this identity.
	Scopes []string

	// IsAdmin is true if this identity has administrative privileges.
	IsAdmin bool
}

Identity represents the authenticated principal for a request. It is produced by an AuthProvider after successful authentication and attached to the request context for downstream handlers.

type IdentityCertificateAuthority

type IdentityCertificateAuthority interface {
	// IssueIdentityCertificate signs the given identity CSR and returns
	// the resulting certificate plus chain.
	IssueIdentityCertificate(
		ctx context.Context,
		csrPEM string,
		ansName string,
	) (*IssuedCert, error)

	// RevokeCertificate revokes a previously issued certificate at the
	// CA, so private CRL/OCSP distribution reflects the revocation —
	// the RA's own database flip and transparency-log emit are the
	// caller's responsibility. Must be idempotent (see
	// RevokeCertificateRequest).
	RevokeCertificate(
		ctx context.Context,
		req RevokeCertificateRequest,
	) error

	// GetCACertificate returns the CA's root certificate PEM.
	// Clients use this to build trust chains.
	GetCACertificate(ctx context.Context) (string, error)
}

IdentityCertificateAuthority issues identity certificates from a PRIVATE trust root — identity certs are never publicly issued. The CA binds the versioned ANS name as a URI SAN, creating the verifiable link between an agent and its declared version.

Because the trust root is private, no domain-control challenge lifecycle exists on this port: domain ownership is proven by the server-certificate flow (the verify-acme gate, plus the public provider's own validation on the ACME path) BEFORE the RA asks this port to sign anything. Issuance is therefore a plain CSR-in / cert-out call.

The in-process default is the file-backed SelfCA. Cloud private CAs (AWS Private CA, GCP Private CA Service, Vault PKI) slot in at this boundary; adapter notes for them:

  • All three accept CSRs. AWS PCA issues asynchronously — the adapter does a short bounded poll (IssueCertificate → GetCertificate), the same in-call pattern the ACME server issuer uses. No pending/order state is needed: no third party or operator action intervenes, so a retryable error suffices and the caller's idempotent re-entry re-drives.
  • The URI SAN is the load-bearing field. The ansName parameter is passed alongside the CSR so adapters can either verify the CSR's URI SAN (the SelfCA approach) or stamp it API-side. The provider must be configured to permit URI SANs and a ClientAuth EKU (PCA: a CSR/API-passthrough template; CAS: pool issuance policy; Vault: role allowed_uri_sans).
  • Issuance should be idempotent under retry: derive the provider's idempotency token (PCA IdempotencyToken, CAS request_id) from a hash of the CSR PEM, which is stable per CSR row.

type IdentityLinkStore added in v0.1.6

type IdentityLinkStore interface {
	// Link upserts a live link for the pair. Returns true when a new
	// link was created, false when the pair was already live
	// (idempotent — an already-linked agent in a batch is not an
	// error, and is excluded from the sealed batch event).
	Link(ctx context.Context, identityID, agentID string, now time.Time) (bool, error)

	// Unlink flips the live link to UNLINKED. Not-found error when no
	// live link exists.
	Unlink(ctx context.Context, identityID, agentID string, now time.Time) error

	// ListLiveByIdentity returns the identity's live links.
	ListLiveByIdentity(ctx context.Context, identityID string) ([]*domain.IdentityLink, error)

	// ListLiveByAgent returns the agent's live links.
	ListLiveByAgent(ctx context.Context, agentID string) ([]*domain.IdentityLink, error)
}

IdentityLinkStore persists identity↔agent link rows. Rows are read-side caches of the sealed IDENTITY_LINKED / IDENTITY_UNLINKED events; UNLINKED rows are history and never block re-linking.

type IdentityStore added in v0.1.6

type IdentityStore interface {
	// Save upserts the aggregate. The storage layer enforces the two
	// uniqueness rules with partial indexes: one live (non-REVOKED)
	// row per (provider, kind, value), and one VERIFIED row per
	// (kind, value) across all owners — first to prove wins; a save
	// that loses the race maps to a conflict error.
	Save(ctx context.Context, identity *domain.VerifiedIdentity) error

	// FindByID returns the identity with the given identityId.
	FindByID(ctx context.Context, identityID string) (*domain.VerifiedIdentity, error)

	// FindLive returns the owner's non-REVOKED row for (kind, value),
	// or a not-found error. Drives the idempotent re-add: a re-POST
	// of the same value while PENDING_CONTROL returns the same
	// identity with a fresh challenge.
	FindLive(ctx context.Context, providerID string, kind domain.IdentifierKind, value string) (*domain.VerifiedIdentity, error)

	// ExistsVerified reports whether ANY owner holds a VERIFIED row
	// for (kind, value) — early duplicate feedback at register time.
	// The authoritative guard is the proven-uniqueness index at
	// verify time.
	ExistsVerified(ctx context.Context, kind domain.IdentifierKind, value string) (bool, error)

	// ListByOwner returns the principal's identities, newest first,
	// cursor-paginated with the same opaque-cursor convention as the
	// agent list (§5.6.1 pagination inherits, never invents).
	ListByOwner(ctx context.Context, providerID string, limit int, cursor string) (*CursorPage[*domain.VerifiedIdentity], error)

	// ConsumeChallenge atomically consumes the live challenge nonce:
	// a conditional update that succeeds only while the stored nonce
	// matches, is unconsumed, and is unexpired. Exactly one of two
	// concurrent verify-control calls can win (the TOCTOU guard);
	// the loser receives an invalid-state error. MUST be called
	// inside the verify-control success transaction.
	ConsumeChallenge(ctx context.Context, identityID, nonce string, now time.Time) error

	// ClaimChallenge takes the short-TTL provisional claim on the
	// live nonce before the seal-before-success TL round trip
	// (design §5.6.1): a conditional update that succeeds only while
	// the nonce matches, is unconsumed, and is not already claimed
	// by an attempt fresher than staleBefore. Serializes concurrent
	// verify-control attempts so at most one can seal; the loser
	// gets an invalid-state error. A claim is NOT consumption.
	ClaimChallenge(ctx context.Context, identityID, nonce string, now, staleBefore time.Time) error

	// ReleaseChallenge releases a provisional claim after a failed
	// attempt so the registrant can retry until the nonce expires
	// (§3.2 failed-attempts-don't-consume). Best-effort: releasing
	// an already-consumed or superseded nonce is a no-op.
	ReleaseChallenge(ctx context.Context, identityID, nonce string) error

	// StageChallenge persists a freshly issued challenge (and any
	// staged pending_value) onto an EXISTING row, conditionally on
	// the status and nonce observed at load time and on no live seal
	// claim — the optimistic-concurrency guard that stops a re-add /
	// rotate from clobbering a concurrently committed verify or
	// revoke (their commits race the resolver fetch between load and
	// persist). Never writes status, value, or verified_at: issuing
	// a challenge is not a state transition. A failed condition maps
	// to a precise conflict error.
	StageChallenge(ctx context.Context, identity *domain.VerifiedIdentity, expectedStatus domain.IdentityStatus, expectedNonce string, staleBefore time.Time) error

	// MarkRevoked flips a VERIFIED row to REVOKED conditionally —
	// the seal-before-success Phase C commit for Revoke. The status
	// condition (not a blind save) means a verify or rotate that
	// committed during the revoke's TL round trip is never clobbered
	// with stale column values. Zero rows → conflict.
	MarkRevoked(ctx context.Context, identityID string, now time.Time) error
}

IdentityStore persists VerifiedIdentity aggregates (the "who" — owned by a providerId, independent of any agent).

type IssuedCert

type IssuedCert struct {
	CertPEM      string
	ChainPEM     string
	SerialNumber string
	// CertificateRef is the issuer's opaque handle for the issued
	// certificate — an ACME certificate URL, a cloud CA resource name
	// (GCP CAS) or ARN (AWS PCA), empty for the in-process self-signed
	// CAs. Persisted alongside the certificate because some providers
	// revoke by handle rather than by serial.
	CertificateRef string
	ExpiresAt      time.Time
	IssuedAt       time.Time
}

IssuedCert is returned by a certificate issuer after signing a CSR.

type KeyHint added in v0.1.6

type KeyHint struct {
	Kid          string
	PublicKeyJWK json.RawMessage
}

KeyHint is one kid → public-JWK pair the caller extracted from a submitted proof's protected header. Hints exist for the noop resolver (below); the web resolver ignores them.

type KeyManager

type KeyManager interface {
	// Sign produces a signature over data using the key identified by keyID.
	// The returned signature format is adapter-defined (typically DER for
	// ECDSA, PKCS1v15 or PSS for RSA); downstream code must match the format.
	Sign(ctx context.Context, keyID string, data []byte) ([]byte, error)

	// Verify returns true if signature is valid for data under the given key.
	// A verify error (e.g., key not found) is distinct from a valid "false".
	Verify(ctx context.Context, keyID string, data, signature []byte) (bool, error)

	// GetPublicKey returns the public half of the named key. The concrete type
	// depends on the algorithm (e.g., *ecdsa.PublicKey for ECDSA_P256).
	GetPublicKey(ctx context.Context, keyID string) (crypto.PublicKey, error)

	// CreateKey generates a new key pair under the given algorithm and
	// returns its identifier. The identifier is opaque to the caller.
	CreateKey(ctx context.Context, algorithm string) (keyID string, err error)

	// ListKeys returns all known key IDs. Useful for key rotation and audit.
	ListKeys(ctx context.Context) ([]string, error)
}

KeyManager abstracts cryptographic key storage and signing operations. The default local-dev adapter stores keys on disk; production adapters wrap AWS KMS, GCP KMS, or HashiCorp Vault Transit. Callers never see raw private keys — the interface only exposes sign/verify/public-key.

type ListFilter

type ListFilter struct {
	Statuses  []domain.RegistrationStatus // nil or empty means "all statuses"
	AgentHost string                      // exact match if non-empty
	Limit     int                         // 1-100; 0 means default (20)
	Cursor    string                      // opaque; empty for first page
}

ListFilter controls cursor-paginated listing of agents owned by a caller.

type ProfileEmitter added in v0.1.6

type ProfileEmitter interface {
	// ID returns the wire-format identifier (e.g. "ANS_DNSAID", "ANS_TXT").
	// Persisted on agent rows; surfaced on the V2 register schema; used
	// as the registry key.
	ID() domain.DiscoveryProfile

	// Records returns the DNS records this profile needs an operator to
	// publish for reg. Includes both per-profile discovery records and any
	// family-level trust attestation records the profile requires (e.g.
	// _ans-badge for the ANS family, TLSA for any HTTPS-endpoint binding).
	// The service walker dedupes across profiles, so a family's shared
	// records emit once even when multiple sibling profiles request them.
	//
	// A profile that emits a transparency-log-relative trust record (the
	// ANS family's _ans-badge) is configured with the deployment TL URL
	// at construction — see ans.NewTXTProfile / ans.NewDNSAIDProfile — so this
	// stays a pure function of reg with no per-call deployment input.
	Records(reg *domain.AgentRegistration) []domain.ExpectedDNSRecord
}

ProfileEmitter is one named DNS discovery family the RA can emit for an agent registration. Implementations live under internal/adapter/discovery/ <vendor>/. Today the bundled set is the ANS family (ANS_DNSAID, ANS_TXT); additional families plug in as new vendor packages without touching the service or domain layers.

Records is a pure function: no I/O, no context, no error. The service layer composes per-profile outputs by walking a ProfileRegistry and concatenating each profile's records, deduping by (Name, Type, Value) so per-family trust records (e.g. _ans-badge, TLSA) emitted by multiple profiles in the same family land once.

type ProfileRegistry added in v0.1.6

type ProfileRegistry interface {
	// Get returns the profile registered under id, or (nil, false) when no
	// such profile is wired. A miss is informational, not an error: the
	// service skips unknown stored profiles (e.g. post-decommission rows)
	// and logs a WARN, rather than failing the registration.
	Get(id domain.DiscoveryProfile) (ProfileEmitter, bool)

	// IDs returns every registered profile's ID in registry-wired
	// insertion order. Order is stable across calls and process restarts
	// for a given wiring — the service walker iterates IDs() and gates
	// each by membership in reg.DiscoveryProfiles, so wiring order
	// determines emission order on the wire (TL canonical bytes).
	IDs() []domain.DiscoveryProfile
}

ProfileRegistry is the lookup surface the service uses to compose per-profile outputs. Implementations are immutable post-construction so reads are safe under concurrent registration / verify-dns load without locking. The bundled implementation lives at internal/adapter/discovery/registry/registry.go.

type RecordVerification

type RecordVerification struct {
	Record domain.ExpectedDNSRecord
	Found  bool
	// Actual carries the live answer DNS returned for this record. When
	// records exist for the name/type but none matched the expected
	// value, Actual is the first live answer (so the verify-dns 422 can
	// show the operator what is actually in their zone). When nothing
	// answered at all, Actual is empty. The service layer partitions on
	// exactly this: !Found && Actual == "" is MISSING, !Found && Actual
	// != "" is a value MISMATCH. When Found is true, Actual MAY differ
	// benignly from the expected value (e.g. an SVCB subset match where
	// the live record carries coexistence extras) and is informational
	// only — Found is the verdict.
	Actual string
	Error  string // Lookup error, if any.
	// DNSSECVerified is true when the response carried an
	// authenticated-data (AD) bit from a validating resolver. Set
	// on TLSA, SVCB, and HTTPS responses; surfaced to the TL
	// attestation so a downstream verifier can trust the cert /
	// capability / service binding without re-querying DNS. The
	// service layer enforces a hard-fail rule when AD=true and the
	// record's value disagrees with the expected one (the threat
	// shape: an attacker rewrote a record in a DNSSEC-signed zone).
	DNSSECVerified bool
}

RecordVerification is the result of checking a single DNS record.

type RenewalStore

type RenewalStore interface {
	Save(ctx context.Context, renewal *domain.ServerCertificateRenewal) error
	FindByAgentID(ctx context.Context, agentID string) (*domain.ServerCertificateRenewal, error)
	FindPendingByAgentID(ctx context.Context, agentID string) (*domain.ServerCertificateRenewal, error)
	Delete(ctx context.Context, id int64) error
	ListPendingExpired(ctx context.Context) ([]*domain.ServerCertificateRenewal, error)
}

RenewalStore persists server certificate renewal aggregates. At most one renewal per agent may exist in a non-terminal state.

type RevocationStore

type RevocationStore interface {
	Save(ctx context.Context, revocation *domain.AgentRevocation) error
	FindByAgentID(ctx context.Context, agentID string) (*domain.AgentRevocation, error)
}

RevocationStore persists AgentRevocation audit records.

type RevokeCertificateRequest added in v0.1.6

type RevokeCertificateRequest struct {
	SerialNumber   string
	CertificateRef string
	Reason         domain.RevocationReason
}

RevokeCertificateRequest identifies a certificate to revoke at its issuer. SerialNumber (lowercase hex) is always populated; CertificateRef carries the provider handle captured at issuance when one exists. Implementations use whichever their API keys on — serial for the self-signed CA, AWS PCA, and Vault; resource name for GCP CAS.

Revocation MUST be idempotent: revoking an already-revoked certificate returns nil. The caller performs CA revocation before committing its own transaction, so a crash between the two is healed by a retried call.

type ServerCertificateIssuer added in v0.1.6

type ServerCertificateIssuer interface {
	// CreateOrder opens an issuance order for the agent's FQDN and
	// returns the provider's domain-control challenges. The CSR is NOT
	// required yet (matching ACME, where the CSR is presented at
	// finalize). The returned order is persisted on the registration
	// or renewal aggregate.
	CreateOrder(ctx context.Context, fqdn string) (*domain.CertificateOrder, error)

	// FinalizeOrder completes the order: the issuer validates domain
	// control by its own rules (or trusts the RA's gate, for the
	// self-signed CA), signs the CSR, and returns the leaf + chain.
	// Returns ErrOrderPending while an asynchronous provider is still
	// processing, and ErrOrderFailed when the order is terminally
	// dead.
	FinalizeOrder(ctx context.Context, req FinalizeOrderRequest) (*IssuedCert, error)

	// GetCACertificate returns the server CA's root certificate PEM.
	// Distinct from the identity CA's root — operators publish this
	// separately so relying parties building TLS trust stores can
	// trust it without trusting the identity CA. Publicly trusted
	// providers (ACME) return their chain root; it is informational
	// there since relying parties already hold it in system stores.
	GetCACertificate(ctx context.Context) (string, error)
}

ServerCertificateIssuer issues server-auth TLS certificates through a certificate-order lifecycle:

  1. CreateOrder — called at registration / renewal-submission time. The returned domain-control challenges are relayed verbatim to the domain owner in the pending response. ANS never publishes challenge artifacts on the owner's behalf.
  2. FinalizeOrder — called from verify-acme after the RA confirmed at least one challenge artifact is published. Synchronous issuers return the certificate immediately; asynchronous ones return ErrOrderPending and the order is re-driven on the next verify-acme call.

The local implementation is the file-backed self-signed CA under `internal/adapter/cert`, which self-issues its challenge tokens and finalizes in-process. External providers slot in at this boundary without touching the service layer — e.g. an ACME adapter (Let's Encrypt) maps CreateOrder to new-order (relaying the provider's token + key authorization and the computed DNS digest), and FinalizeOrder to challenge-answer → poll → finalize → download. Proprietary CAs (managed-CA APIs, AWS Private CA, GCP CAS) map the same way, returning no challenges from CreateOrder when they perform no domain validation of their own.

The server-cert trust root is distinct from the identity CA so the two can be rotated and key-managed independently.

type TileStorage

type TileStorage interface {
	// WriteTile persists the tile bytes at (level, index), overwriting any
	// existing tile at the same address. Data is opaque to this layer.
	WriteTile(ctx context.Context, level, index uint64, data []byte) error

	// ReadTile returns the tile bytes at (level, index). A missing tile
	// must return domain.ErrNotFound so callers can distinguish
	// "gap in the tree" from "storage broken".
	ReadTile(ctx context.Context, level, index uint64) ([]byte, error)
}

TileStorage persists RFC 9162 Merkle tree tiles for the Transparency Log. A tile is a fixed-size slice of the Merkle tree at a given level and index; together they let clients reconstruct inclusion and consistency proofs.

Addressing follows the Tessera convention:

  • level 0 is the leaf layer
  • level N each covers 2^8 nodes at level N-1
  • index is the sequential tile number within its level

Implementations must be safe for concurrent reads; writes are serialized by the Tessera driver and need not be concurrency-safe at this layer.

type UnitOfWork

type UnitOfWork interface {
	Run(ctx context.Context, fn func(ctx context.Context) error) error
}

UnitOfWork groups multiple store writes into a single atomic commit. Service-layer methods that touch more than one aggregate (or one aggregate plus the outbox) hand a closure to `Run`; the adapter manages begin / commit / rollback. The closure receives a scoped `context.Context` that downstream stores use to find the active transaction — callers should pass that scoped context to every store call inside the closure.

Implementations must:

  • Begin a transaction (or equivalent atomic batch) before invoking fn.
  • Roll back if fn returns a non-nil error and propagate that error.
  • Commit if fn returns nil; surface any commit error to the caller.
  • Be safe to call concurrently from multiple goroutines, each with its own scoped ctx.

SQL adapters back this with `BEGIN`/`COMMIT`/`ROLLBACK`. Cloud stores without true ACID transactions (DynamoDB, Spanner) can implement the same contract via TransactWriteItems / batched mutations as long as the all-or-nothing semantics hold.

type ValidatedCert

type ValidatedCert struct {
	LeafPEM      string
	ChainPEM     string
	CN           string
	SANs         []string
	IssuerDN     string
	ValidFrom    time.Time
	ValidTo      time.Time
	Fingerprint  string // SHA-256, lowercase hex
	SerialNumber string
}

ValidatedCert is the result of successfully parsing and validating a BYOC server certificate. It carries all the fields the domain needs without exposing x509 internals.

type VerificationMethod added in v0.1.6

type VerificationMethod struct {
	ID                 string
	Controller         string
	Type               string
	PublicKeyJwk       json.RawMessage
	PublicKeyMultibase string
	Raw                json.RawMessage
}

VerificationMethod is one assertionMethod entry. Exactly one of PublicKeyJwk / PublicKeyMultibase carries the key material.

Raw is the verification-method object EXACTLY as the DID document served it — member-for-member, values untouched. Sealed identity events quote Raw verbatim: nothing derived, re-encoded, or normalized ever enters a seal. The typed fields beside it exist for the RA's checks (membership, controller, key parsing) only.

type VerificationResult

type VerificationResult struct {
	// AllRequired is true when every record marked Required was found
	// with a matching value.
	AllRequired bool
	Results     []RecordVerification
}

VerificationResult aggregates per-record results from a DNS verification run.

Jump to

Keyboard shortcuts

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