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
- Variables
- func ProvisionCA(realmName string, lifetime time.Duration, sealer *store.KeySealer) (certPEM string, sealedKey []byte, err error)
- type API
- type APIConfig
- type Config
- type Info
- type Limiter
- type Service
- func (s *Service) Credentials(ctx context.Context, realmName, deviceIDStr, secret, csrPEM string, ...) (string, error)
- func (s *Service) Info(ctx context.Context, realmName, deviceIDStr, secret string) (*Info, error)
- func (s *Service) Register(ctx context.Context, realmName, hwID, initialFormat string) (string, error)
- func (s *Service) Unregister(ctx context.Context, realmName, deviceIDStr string) error
- func (s *Service) VerifyCredentials(ctx context.Context, realmName, deviceIDStr, secret, clientCrtPEM string) (*VerifyResult, error)
- type Store
- type VerifyResult
Constants ¶
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).
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).
const ( // DefaultVersion is reported by the info endpoint when Config.Version // is empty. DefaultVersion = "0.1.0-astrate" )
Variables ¶
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") // 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.
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 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 ¶
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).
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 ¶
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 ¶
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 ¶
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 ¶
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. |