pairing

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 1, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package pairing implements Astarte's Pairing API surface (docs/DESIGN.md §4.4 flows A–C): device registration with show-once credentials secrets, CSR-based credential issuance through the embedded per-realm CA, broker discovery, and certificate verification — all wire-compatible with upstream so official device SDKs and astartectl run unmodified.

Index

Constants

View Source
const (
	DefaultRegisterRate     = 5.0
	DefaultRegisterBurst    = 20
	DefaultCredentialsRate  = 1.0
	DefaultCredentialsBurst = 5
)

Default rate-limit parameters (docs/DESIGN.md §4.5). Registration is operator-driven (JWT-protected, fleet provisioning bursts); credentials requests are device-driven and rare (a renewal per device per cert TTL).

View Source
const (
	// CauseExpired marks a certificate outside its validity window.
	CauseExpired = "EXPIRED"
	// CauseInvalid marks a certificate that does not parse or does not
	// chain to the realm CA.
	CauseInvalid = "INVALID"
	// CauseRevoked marks a certificate superseded by a newer issuance
	// (latest-serial enforcement, docs/DESIGN.md §4.3).
	CauseRevoked = "REVOKED"
)

Certificate verification causes on the wire (upstream CertificateValidationError enum subset, docs/DESIGN.md §4.4).

View Source
const (

	// DefaultVersion is reported by the info endpoint when Config.Version
	// is empty.
	DefaultVersion = "0.1.0-astrate"
)

Variables

View Source
var (
	// ErrInvalidHWID reports a hw_id that is not a 22-character unpadded
	// base64url 128-bit device ID (422 upstream changeset shape).
	ErrInvalidHWID = errors.New("pairing: invalid hw_id")
	// ErrInvalidPayloadFormat reports an initial_payload_format outside
	// {bson, json} (Astrate extension, docs/DESIGN.md §3.5.4).
	ErrInvalidPayloadFormat = errors.New("pairing: invalid initial_payload_format")
	// ErrAlreadyRegistered reports re-registration of a device that has
	// already requested credentials (upstream error_name
	// "already_registered", 422).
	ErrAlreadyRegistered = errors.New("pairing: device already registered")
	// ErrRegistrationLimitReached reports the realm's
	// device_registration_limit being hit (upstream error_name
	// "device_registration_limit_reached", 422).
	ErrRegistrationLimitReached = errors.New("pairing: device registration limit reached")
	// ErrUnauthorized is the uniform device-authentication failure: unknown
	// device, unregistered device, or wrong credentials secret all produce
	// this same error (docs/DESIGN.md §4.4: no oracle distinguishing them).
	ErrUnauthorized = errors.New("pairing: unauthorized")
	// ErrInhibited reports a device blocked by credentials_inhibited (403).
	ErrInhibited = errors.New("pairing: credentials request inhibited")
	// ErrInvalidCSR reports an unusable certificate signing request (422).
	ErrInvalidCSR = errors.New("pairing: invalid CSR")
)

Service-level sentinel errors; the HTTP layer maps them onto upstream statuses and envelopes.

Functions

func ProvisionCA

func ProvisionCA(realmName string, lifetime time.Duration, sealer *store.KeySealer) (certPEM string, sealedKey []byte, err error)

ProvisionCA generates a realm CA and seals its private key, producing the (ca_certificate, ca_private_key) pair stored on the realm row. Called by housekeeping at realm creation; lifetime zero selects the 10-year default.

Types

type API

type API struct {
	// contains filtered or unexported fields
}

API is the /pairing/v1 HTTP surface (docs/DESIGN.md §4.4, §3.7). Agent endpoints are guarded by realm JWTs carrying a_pa; device endpoints authenticate with the device's credentials secret as a bearer token.

func NewAPI

func NewAPI(svc *Service, mw *auth.Middleware, cfg APIConfig) *API

NewAPI wires the pairing service to its HTTP surface. mw provides the realm-JWT middleware (M3).

func (*API) Mount

func (a *API) Mount(mux *http.ServeMux)

Mount registers the pairing routes on mux. Paths are wire-frozen (docs/DESIGN.md §4.4): they are exactly what the official SDKs and astartectl call.

type APIConfig

type APIConfig struct {
	RegisterRate     float64
	RegisterBurst    int
	CredentialsRate  float64
	CredentialsBurst int
}

APIConfig tunes the HTTP layer's rate limits; zero values select the defaults above.

type Config

type Config struct {
	// BrokerURL is handed to devices by the info endpoint
	// (e.g. "mqtts://host:8883").
	BrokerURL string
	// CertTTL is the client certificate validity; zero selects
	// ca.DefaultCertTTL (30 days).
	CertTTL time.Duration
	// EnforceLatestCert enables the always-online-CRL behaviour
	// (docs/DESIGN.md §4.3): verify reports REVOKED for certificates whose
	// serial differs from the device's latest issuance.
	EnforceLatestCert bool
	// Version is reported by the info endpoint; empty selects
	// DefaultVersion.
	Version string
	// BcryptCost hashes credentials secrets; zero selects
	// bcrypt.DefaultCost (10, docs/DESIGN.md §4.1).
	BcryptCost int
}

Config carries the service's operational knobs.

type Info

type Info struct {
	Version   string
	Status    string
	BrokerURL string
	CACertPEM string
}

Info is the flow C device-info document.

type Limiter

type Limiter struct {
	// contains filtered or unexported fields
}

Limiter is a keyed token-bucket rate limiter for the pairing endpoints (docs/DESIGN.md §4.4–4.5): one bucket per key (client IP, device address), refilled at rate tokens/second up to burst.

func NewLimiter

func NewLimiter(rate float64, burst int) *Limiter

NewLimiter builds a Limiter refilling rate tokens per second with the given burst capacity. Non-positive parameters are clamped to minimal sane values (1 token/minute, burst 1).

func (*Limiter) Allow

func (l *Limiter) Allow(key string) bool

Allow reports whether one request under key may proceed, consuming a token when it does.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service implements pairing flows A–C over a Store and the per-realm CA.

func New

func New(st Store, sealer *store.KeySealer, cfg Config) *Service

New builds a pairing Service. The sealer opens realms' AES-GCM-sealed CA private keys (docs/DESIGN.md §4.3); it is owned by the caller and shared with housekeeping (which seals new realm CAs).

func (*Service) Credentials

func (s *Service) Credentials(ctx context.Context, realmName, deviceIDStr, secret, csrPEM string, ip netip.Addr) (string, error)

Credentials implements flow B, the SDK hot path: it authenticates the device by credentials secret (uniform ErrUnauthorized on any mismatch), rejects inhibited devices, signs the CSR with the realm CA, and records the new certificate's serial/AKI plus the requesting IP (which also flips the device status registered → confirmed).

func (*Service) Info

func (s *Service) Info(ctx context.Context, realmName, deviceIDStr, secret string) (*Info, error)

Info implements the flow C GET: broker discovery plus device status, authenticated by credentials secret. Status strings are upstream parity: "pending" until the first credentials request, then "confirmed"; "inhibited" when blocked (inhibited devices may still read info — only new credentials and connections are blocked).

func (*Service) Register

func (s *Service) Register(ctx context.Context, realmName, hwID, initialFormat string) (string, error)

Register implements flow A: it validates hw_id, enforces the realm's device registration limit, generates a 44-character credentials secret, stores its bcrypt hash, and returns the secret (shown exactly once). Re-registering a device that has not yet requested credentials rotates the secret; afterwards it fails with ErrAlreadyRegistered. initialFormat is the Astrate initial_payload_format extension ("", "bson" or "json").

func (*Service) Unregister

func (s *Service) Unregister(ctx context.Context, realmName, deviceIDStr string) error

Unregister implements the flow A DELETE: the device becomes registrable again, its data is kept (store.UnregisterDevice clears only the credential trail). store.ErrNotFound is returned for unknown devices.

func (*Service) VerifyCredentials

func (s *Service) VerifyCredentials(ctx context.Context, realmName, deviceIDStr, secret, clientCrtPEM string) (*VerifyResult, error)

VerifyCredentials implements the flow C verify endpoint, authenticated by credentials secret. Classification precedence: certificates outside their validity window report EXPIRED; certificates that fail to parse or to chain to the realm CA report INVALID; valid-but-superseded certificates report REVOKED when EnforceLatestCert is on.

type Store

type Store interface {
	GetRealmByName(ctx context.Context, name string) (*store.Realm, error)
	RegisterDevice(ctx context.Context, realmID int16, id deviceid.ID, secretHash string) error
	UnregisterDevice(ctx context.Context, realmID int16, id deviceid.ID) error
	GetDevice(ctx context.Context, realmID int16, id deviceid.ID) (*store.Device, error)
	SetDeviceCredentials(ctx context.Context, realmID int16, id deviceid.ID, certSerial, certAKI string, requestIP netip.Addr) error
	SetPayloadFormatHint(ctx context.Context, realmID int16, id deviceid.ID, hint string) error
	CountDevices(ctx context.Context, realmID int16) (int64, error)
}

Store is the persistence surface the pairing service consumes (hexagonal-lite, docs/DESIGN.md §1.3). *store.Store satisfies it; tests use an in-memory fake.

type VerifyResult

type VerifyResult struct {
	Valid     bool
	Timestamp time.Time
	Until     time.Time
	Cause     string
}

VerifyResult is the flow C credentials/verify outcome. With Valid true, Until carries the certificate expiry; otherwise Cause carries one of the Cause* constants. Timestamp is the verification instant.

Directories

Path Synopsis
Package ca implements Astrate's embedded per-realm certificate authority (docs/DESIGN.md §4.3), replacing upstream Astarte's CFSSL sidecar.
Package ca implements Astrate's embedded per-realm certificate authority (docs/DESIGN.md §4.3), replacing upstream Astarte's CFSSL sidecar.

Jump to

Keyboard shortcuts

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