Documentation
¶
Overview ¶
Package sqlstore is the SQL acme.Store: one implementation over database/sql for the SQLite, PostgreSQL, and MySQL dialects.
Why ¶
The ACME server keeps no state of its own, so every property a device depends on is a property of the store, and a deployment that survives a restart needs those properties from a database rather than from a map. Phase 7 of the plan of record delivers the server against acme/inmem (decision record 0031); this package is the same contract where the records outlive the process. It reuses storage/sqlcommon's dialects and migration runner but owns its own migration set, recorded in acme_schema_migrations, so the ACME tables can share a database with the MDM, DDM, and DEP schemas or live apart.
Nothing here is sealed. An account key is a public JWK, an attestation object is a signed statement the device sent in clear, and an issued certificate is published to whoever asks; there is no secret at rest for a keyring to protect, so Options carries none and the storage/crypt dependency the sibling stores have is absent.
Each record is stored as the JSON of the whole value beside indexed copies of the fields that are looked up or filtered, so a record gains a field without a migration. Two rules are the store's own rather than SQL's: a duplicate account key and a second claim on a client identifier are decided by a unique index, never by reading first and writing after, because a read-then-write cannot be correct under concurrency; and a nonce is taken with DELETE ... RETURNING where the engine has it, and on MySQL by a read and a delete in one transaction whose row count says who won, so exactly one of two concurrent takers takes it. The contract suite in acme/acmetest runs against all three dialects.
References ¶
- Decision record 0031: docs/research/decisions/0031-acme-server-and-state-store.md
- Decision record 0012: docs/research/decisions/0012-sql-storage-backends.md (dialects and migrations)
- Plan of record: docs/research/implementation_plan.md (section 9, phase 7)
- RFC 8555 (ACME): https://www.rfc-editor.org/rfc/rfc8555
- draft-ietf-acme-device-attest: https://datatracker.ietf.org/doc/draft-acme-device-attest/
- Migrations: acme/sqlstore/migrations/{sqlite,postgres,mysql}/0001_init.sql
Index ¶
- Constants
- Variables
- func Migrate(ctx context.Context, db *sql.DB, d sqlcommon.Dialect) ([]int, error)
- func MigrationSet(d sqlcommon.Dialect) (sqlcommon.MigrationSet, error)
- func Rollback(ctx context.Context, db *sql.DB, d sqlcommon.Dialect, target int) ([]int, error)
- func Version(ctx context.Context, db *sql.DB, d sqlcommon.Dialect) (int, error)
- type Options
- type Store
- func (s *Store) AccountByThumbprint(ctx context.Context, thumbprint string) (*acme.Account, error)
- func (s *Store) DB() *sql.DB
- func (s *Store) GetAccount(ctx context.Context, id string) (*acme.Account, error)
- func (s *Store) GetAuthorization(ctx context.Context, id string) (*acme.Authorization, error)
- func (s *Store) GetCertificate(ctx context.Context, id string) (*acme.Certificate, error)
- func (s *Store) GetChallenge(ctx context.Context, id string) (*acme.Challenge, error)
- func (s *Store) GetOrder(ctx context.Context, id string) (*acme.Order, error)
- func (s *Store) ListCertificates(ctx context.Context, q acme.CertificateQuery, p storage.Page) (storage.Result[acme.Certificate], error)
- func (s *Store) ListOrders(ctx context.Context, accountID string, p storage.Page) (storage.Result[acme.Order], error)
- func (s *Store) Prune(ctx context.Context, before time.Time) (int, error)
- func (s *Store) PutNonce(ctx context.Context, n acme.Nonce) error
- func (s *Store) TakeNonce(ctx context.Context, value string) (*acme.Nonce, error)
- func (s *Store) Update(ctx context.Context, fn func(acme.Tx) error) error
Constants ¶
const MigrationsTable = "acme_schema_migrations"
MigrationsTable records the applied versions of the ACME schema, separate from the storage, DDM, and DEP tables so the version sequences never mix.
Variables ¶
var ErrUnsupportedDialect = errors.New("sqlstore: unsupported dialect")
ErrUnsupportedDialect is returned for a dialect without embedded migrations (anything but sqlite, postgres, and mysql).
Functions ¶
func MigrationSet ¶
func MigrationSet(d sqlcommon.Dialect) (sqlcommon.MigrationSet, error)
MigrationSet returns the ACME migrations for the dialect.
Types ¶
type Options ¶
type Options struct {
// SkipMigrate leaves the schema alone; the caller has run Migrate.
SkipMigrate bool
}
Options tune Open.
There is no Keyring here, unlike the DEP and MDM stores. Nothing this package holds is a secret at rest: an account key is a public JWK, the attestation object is a signed statement the device sent in clear, an issued certificate is published, and a nonce is worthless the moment it is taken. A keyring would add a strict-mode failure path guarding nothing.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store implements acme.Store over a *sql.DB it does not own: closing the pool is the caller's job.
func Open ¶
Open wraps an opened pool for the dialect and, unless o.SkipMigrate, applies pending migrations.
func (*Store) AccountByThumbprint ¶
AccountByThumbprint implements acme.Reader.
func (*Store) GetAccount ¶
GetAccount implements acme.Reader.
func (*Store) GetAuthorization ¶
GetAuthorization implements acme.Reader.
func (*Store) GetCertificate ¶
GetCertificate implements acme.Reader.
func (*Store) GetChallenge ¶
GetChallenge implements acme.Reader.
func (*Store) ListCertificates ¶
func (s *Store) ListCertificates( ctx context.Context, q acme.CertificateQuery, p storage.Page, ) (storage.Result[acme.Certificate], error)
ListCertificates implements acme.Reader.
func (*Store) ListOrders ¶
func (s *Store) ListOrders( ctx context.Context, accountID string, p storage.Page, ) (storage.Result[acme.Order], error)
ListOrders implements acme.Reader.
func (*Store) TakeNonce ¶
TakeNonce implements acme.Store. Removing the row is what takes the nonce, so the winner of a race is whichever caller's DELETE removed a row and the loser sees ErrNotFound, which is also how the server detects a replay: the first use removed it.