Documentation
¶
Overview ¶
Package storage defines MDM persistence contracts and shared sentinel errors.
Design ¶
Store composes enrollment, command queue, push, certificate, bootstrap-token, user-authentication and migration interfaces. Shared contracts define lifecycle cleanup, NotNow backoff, certificate history and cursor behavior independently of a database. Pagination values come from paging.
storage/inmem supplies process-local storage; server/sqlstore supplies SQLite, PostgreSQL and MySQL implementations. storage/crypt handles selected secret values. Account-driven associations and revocation use separate state interfaces and are not included in enrollment export/import.
References ¶
- Decision record 0005: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0005-storage-interfaces.md
- Decision record 0013: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0013-secrets-at-rest.md
- Decision record 0014: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0014-cert-association-history.md
- Decision record 0015: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0015-push-cert-store.md
- Decision record 0016: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0016-user-authenticate-state.md
- Decision record 0017: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0017-enrollment-export-import.md
- Decision record 0044: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0044-repository-layout.md
- Threat model: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/security/threat-model.md (Storage rows)
- End-to-end scenarios: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/testing/e2e-scenarios.md (E2E-003, E2E-005)
- Apple: https://developer.apple.com/documentation/devicemanagement/check-in
- Apple: https://developer.apple.com/documentation/devicemanagement/sending-mdm-commands-to-a-device
- Apple: https://developer.apple.com/documentation/devicemanagement/handling-notnow-status-responses
- Schema: third_party/device-management/mdm/checkin/authenticate.yaml, tokenupdate.yaml, setbootstraptoken.yaml
Index ¶
- Constants
- Variables
- func AdvanceReplacement(r **Replacement, e *Enrollment, c ReplacementChange) (bool, error)
- func ApplyReplacementToken(e *Enrollment, t ReplacementToken, at time.Time)
- func CheckAuthenticate(id mdm.EnrollmentID, e *Enrollment, c AuthenticateChange) (bool, error)
- func NotNowBackoff(attempt int) time.Duration
- type AuthenticateChange
- type BootstrapTokenStore
- type Capabilities
- type Capability
- type CertAssociation
- type CertAuthStore
- type ClearFilter
- type CommandQuery
- type CommandQueue
- type DeviceInfo
- type EnqueueOptions
- type EnqueueResult
- type Enrollment
- type EnrollmentExport
- type EnrollmentQuery
- type EnrollmentStore
- type MigrationStore
- type PushCert
- type PushCertStore
- type PushStore
- type QueuedCommand
- type Replacement
- type ReplacementChange
- type ReplacementStore
- type ReplacementToken
- type State
- type Store
- type UserAuthState
- type UserAuthStore
Constants ¶
const ( ReplacementPending = "pending" ReplacementCommitted = "committed" ReplacementFailed = "failed" ReplacementCancelled = "cancelled" ReplacementExpired = "expired" )
Variables ¶
var ( ErrNotFound = errors.New("storage: not found") ErrDisabled = errors.New("storage: enrollment disabled") ErrConflict = errors.New("storage: conflict") ErrInvalid = errors.New("storage: invalid argument") )
Errors shared by every backend.
var ErrUserChannelRequired = errors.New("storage: user channel required")
ErrUserChannelRequired is wrapped in ErrInvalid by UserAuthStore methods called with a device channel.
Functions ¶
func AdvanceReplacement ¶ added in v0.3.0
func AdvanceReplacement(r **Replacement, e *Enrollment, c ReplacementChange) (bool, error)
AdvanceReplacement is the shared state machine used under each backend's transaction lock. true means the backend must atomically commit candidate state.
func ApplyReplacementToken ¶ added in v0.3.0
func ApplyReplacementToken(e *Enrollment, t ReplacementToken, at time.Time)
ApplyReplacementToken updates only fields owned by TokenUpdate, retaining escrow and unrelated enrollment state. Call after validating the message.
func CheckAuthenticate ¶ added in v0.3.2
func CheckAuthenticate(id mdm.EnrollmentID, e *Enrollment, c AuthenticateChange) (bool, error)
CheckAuthenticate checks an enrollment under the backend's write lock. A true result denotes an idempotent retry whose existing state must be preserved.
func NotNowBackoff ¶
NotNowBackoff is the default retry delay after the nth NotNow (1-based): 30s, 1m, 2m, 4m, ... capped at 1h.
Types ¶
type AuthenticateChange ¶ added in v0.3.2
type AuthenticateChange struct {
ExpectedHash string
Hash string
AllowReuse bool
Message *checkin.Authenticate
Raw []byte
At time.Time
}
AuthenticateChange commits a policy-authorized Authenticate and its certificate pin together. ExpectedHash is the pin observed during policy evaluation; an empty value permits only an unpinned enrollment. AllowReuse permits historical reuse, never a certificate currently pinned by another device.
type BootstrapTokenStore ¶
type BootstrapTokenStore interface {
// StoreBootstrapToken escrows token for the device channel of id and
// records at as Enrollment.BootstrapTokenAt.
StoreBootstrapToken(ctx context.Context, id mdm.EnrollmentID, token []byte, at time.Time) error
// BootstrapToken returns the token or ErrNotFound.
BootstrapToken(ctx context.Context, id mdm.EnrollmentID) ([]byte, error)
}
BootstrapTokenStore escrows macOS bootstrap tokens (device channel).
type Capabilities ¶ added in v0.3.2
type Capabilities struct {
Supervised, DEP, UserApproved Capability
Source string
ObservedAt time.Time
}
Capabilities records device-reported management properties, distinct from ownership admission.
func CapabilitiesFromResult ¶ added in v0.3.2
func CapabilitiesFromResult( old Capabilities, id mdm.EnrollmentID, requestType string, r *mdm.Response, at time.Time, ) Capabilities
CapabilitiesFromResult extracts evidence only from an acknowledged, tracked device-channel command. requestType must come from the stored queue entry.
type Capability ¶ added in v0.3.2
type Capability uint8
Capability is a three-state observation. Unknown never grants eligibility.
const ( CapabilityUnknown Capability = iota CapabilityFalse CapabilityTrue )
Capability values distinguish unknown from explicitly false.
type CertAssociation ¶
type CertAssociation struct {
ID mdm.EnrollmentID
Hash string
At time.Time
}
CertAssociation is one row of the append-only pin history: a device channel pinned a certificate hash at a time (decision record 0014).
type CertAuthStore ¶
type CertAuthStore interface {
// AssociateCert pins hash to the device channel of id at the given time
// and appends the pair to the history. ErrConflict when the hash is
// currently pinned to a different enrollment, including when two
// callers race to pin the same hash.
AssociateCert(ctx context.Context, id mdm.EnrollmentID, hash string, at time.Time) error
// CertHash returns the pinned hash for the device channel of id, or
// "" when none.
CertHash(ctx context.Context, id mdm.EnrollmentID) (string, error)
// EnrollmentByCertHash resolves a hash to the device-channel enrollment
// that currently pins it.
EnrollmentByCertHash(ctx context.Context, hash string) (mdm.EnrollmentID, error)
// CertHistory returns every hash ever pinned to the device channel of
// id, oldest first. It is empty, not ErrNotFound, for an enrollment
// that never pinned; ErrNotFound for an unknown enrollment.
CertHistory(ctx context.Context, id mdm.EnrollmentID) ([]CertAssociation, error)
// CertHashHistory returns every enrollment that ever pinned hash,
// oldest first; empty when the hash was never seen.
CertHashHistory(ctx context.Context, hash string) ([]CertAssociation, error)
}
CertAuthStore pins identity certificates to device-channel enrollments and keeps the history of every pin.
type ClearFilter ¶
type ClearFilter struct {
States []State // default: every non-terminal state
RequestType string
Before time.Time // enqueued before this time
}
ClearFilter selects commands for Clear. Zero values mean "any".
type CommandQuery ¶
CommandQuery filters Commands.
type CommandQueue ¶
type CommandQueue interface {
// Enqueue queues cmd for each enrollment. Disabled or unknown
// enrollments are reported in Skipped, not as an error.
Enqueue(ctx context.Context, ids []mdm.EnrollmentID, cmd *mdm.Command, o EnqueueOptions) (EnqueueResult, error)
// Next returns the next command to deliver, in enqueue order: pending
// and sent commands, plus NotNow commands whose backoff elapsed unless
// skipNotNow is set (the device just said NotNow). It marks the command
// sent. nil, nil when the queue is empty.
Next(ctx context.Context, id mdm.EnrollmentID, skipNotNow bool, now time.Time) (*mdm.Command, error)
// StoreResult records the device's response for the command it names.
// Unknown CommandUUIDs return ErrNotFound.
StoreResult(ctx context.Context, id mdm.EnrollmentID, resp *mdm.Response, now time.Time) error
// Commands pages through an enrollment's commands, newest first.
Commands(ctx context.Context, id mdm.EnrollmentID, q CommandQuery, p paging.Page) (paging.Result[QueuedCommand], error)
// Clear marks matching non-terminal commands cleared and returns how
// many. Backends may apply it in batches without one enclosing
// transaction: on error the count is what was applied so far and the
// caller may simply retry.
Clear(ctx context.Context, id mdm.EnrollmentID, f ClearFilter) (int64, error)
}
CommandQueue persists commands per enrollment.
type DeviceInfo ¶
type DeviceInfo struct {
SerialNumber string
Model string
ModelName string
DeviceName string
ProductName string
OSVersion string
BuildVersion string
IMEI string
MEID string
Topic string
}
DeviceInfo is the subset of Authenticate worth indexing.
func DeviceInfoFromAuthenticate ¶
func DeviceInfoFromAuthenticate(m *checkin.Authenticate) DeviceInfo
DeviceInfoFromAuthenticate extracts the indexed fields.
type EnqueueOptions ¶
type EnqueueOptions struct {
// DedupeKey skips enrollments that already have a non-terminal command
// with the same key (for example one DeclarativeManagement kick).
DedupeKey string
// Now stamps EnqueuedAt; zero means time.Now().
Now time.Time
}
EnqueueOptions tune Enqueue.
type EnqueueResult ¶
type EnqueueResult struct {
Queued []mdm.EnrollmentID
Skipped map[mdm.EnrollmentID]error
}
EnqueueResult reports per-enrollment outcomes.
type Enrollment ¶
type Enrollment struct {
Capabilities Capabilities
ID mdm.EnrollmentID
// Enabled becomes true on TokenUpdate and false on CheckOut or a new
// Authenticate; only enabled enrollments receive commands and pushes.
Enabled bool
Push mdm.Push
Device DeviceInfo
// User channel fields from TokenUpdate: names, whether the user is
// logged in without console access, and the EnrollmentUserID of a
// User Enrollment's user channel (decision record 0029).
UserShortName string
UserLongName string
NotOnConsole bool
EnrollmentUserID string
// UnlockToken from TokenUpdate (macOS), if the device sent one.
UnlockToken []byte
// AuthenticateRaw is the last Authenticate plist as received.
AuthenticateRaw []byte
// TokenUpdateRaw is the last TokenUpdate plist as received, kept so an
// enrollment can be replayed into another server (decision record 0017).
TokenUpdateRaw []byte
EnrolledAt time.Time
TokenUpdatedAt time.Time
LastSeenAt time.Time
DisabledAt time.Time
// CertHash is the pinned identity certificate fingerprint (device channels).
CertHash string
// CertHashAt is when CertHash was pinned (zero when none).
CertHashAt time.Time
// BootstrapTokenAt is when the escrowed bootstrap token was stored
// (zero when none). The token itself is read through BootstrapTokenStore.
BootstrapTokenAt time.Time
}
Enrollment is one channel of one enrollment as the server knows it.
type EnrollmentExport ¶
type EnrollmentExport struct {
Enrollment
BootstrapToken []byte
CertHistory []CertAssociation
}
EnrollmentExport is everything one enrollment channel needs to move to another backend (decision record 0017). Empty byte fields are nil.
type EnrollmentQuery ¶
type EnrollmentQuery struct {
Channel mdm.Channel
Enabled *bool
ParentID string
// Serial matches Device.SerialNumber exactly. The SQL backends index
// this column, so it is a lookup rather than a scan.
Serial string
}
EnrollmentQuery filters List. Zero values mean "any".
type EnrollmentStore ¶
type EnrollmentStore interface {
// AuthenticateEnrollment atomically commits authentication, pinning, history,
// and enrollment reset. Same-certificate retries preserve existing state.
AuthenticateEnrollment(ctx context.Context, id mdm.EnrollmentID, change AuthenticateChange) error
// UpsertAuthenticate records an Authenticate message. It creates the
// record or resets an existing one: push info, unlock token, bootstrap
// token, certificate association, and the pending command queue are
// cleared so a re-enrollment never inherits the previous identity's
// state. The enrollment stays disabled until TokenUpdate.
UpsertAuthenticate(ctx context.Context, id mdm.EnrollmentID, msg *checkin.Authenticate, raw []byte, at time.Time) error
// StoreTokenUpdate records push info, the raw plist, and enables the
// enrollment. An unlock token in msg replaces the stored one; a missing
// one keeps it.
StoreTokenUpdate(ctx context.Context, id mdm.EnrollmentID, push mdm.Push, msg *checkin.TokenUpdate, raw []byte, at time.Time) error
// Disable marks the enrollment as checked out. Disabling a device
// channel also disables the user channels whose parent it is, because a
// checked-out device cannot carry a user channel. Records are kept.
Disable(ctx context.Context, id mdm.EnrollmentID, at time.Time) error
// Get returns the record or ErrNotFound.
Get(ctx context.Context, id mdm.EnrollmentID) (*Enrollment, error)
// EnrollmentByID resolves a globally unique raw ID to its stored identity.
// Authorization must use the returned identity, never the caller's channel.
EnrollmentByID(ctx context.Context, id string) (*Enrollment, error)
// List pages through enrollments ordered by id.
List(ctx context.Context, q EnrollmentQuery, p paging.Page) (paging.Result[Enrollment], error)
// TouchLastSeen records device activity.
TouchLastSeen(ctx context.Context, id mdm.EnrollmentID, at time.Time) error
}
EnrollmentStore persists enrollment records.
type MigrationStore ¶
type MigrationStore interface {
// Export pages through every enrollment with device channels before the
// user channels that belong to them.
Export(ctx context.Context, p paging.Page) (paging.Result[EnrollmentExport], error)
// Import writes rec exactly as given (Enabled, timestamps, pin, tokens,
// history) in one transaction, upserting by id. ErrInvalid for a user
// channel whose parent is absent or for history rows naming another
// enrollment; ErrConflict when CertHash is currently pinned elsewhere.
// The command queue is not touched.
Import(ctx context.Context, rec EnrollmentExport) error
}
MigrationStore exports and imports enrollment records between backends.
type PushCert ¶
type PushCert struct {
Topic string
CertPEM []byte
KeyPEM []byte
NotAfter time.Time
// Version increments on every StorePushCert for the topic, so caches
// can detect a renewal with one cheap read.
Version int64
UpdatedAt time.Time
}
PushCert is a stored APNs push certificate for one topic (decision record 0015). KeyPEM is empty in listings.
func ValidatePushCert ¶
ValidatePushCert checks a PEM certificate and key pair the way every backend's StorePushCert must: the key matches the certificate, the subject carries an APNs topic, the topic matches when one is given, and the certificate is valid at the given time. It returns the record to store, with copies of the PEM bytes and Version unset.
type PushCertStore ¶
type PushCertStore interface {
// StorePushCert validates the PEM pair (key matches certificate, topic
// in the subject UID, not expired at the given time) and upserts it.
// An empty topic accepts the certificate's own topic; otherwise the two
// must match. ErrInvalid for anything that fails validation. The
// returned record carries the new Version and no KeyPEM.
StorePushCert(ctx context.Context, topic string, certPEM, keyPEM []byte, at time.Time) (PushCert, error)
// PushCert returns the certificate and key for topic, or ErrNotFound.
PushCert(ctx context.Context, topic string) (*PushCert, error)
// PushCerts lists every stored certificate by topic, without keys.
PushCerts(ctx context.Context) ([]PushCert, error)
// PushCertVersion returns the current Version for topic, or ErrNotFound.
PushCertVersion(ctx context.Context, topic string) (int64, error)
}
PushCertStore keeps push certificates and their private keys.
type PushStore ¶
type PushStore interface {
// PushInfo returns push details for the enabled enrollments among ids.
PushInfo(ctx context.Context, ids []mdm.EnrollmentID) (map[mdm.EnrollmentID]mdm.Push, error)
}
PushStore returns what the push layer needs.
type QueuedCommand ¶
type QueuedCommand struct {
Command mdm.Command
State State
DedupeKey string
EnqueuedAt time.Time
LastSentAt time.Time
NotNowUntil time.Time
// Attempts counts deliveries; NotNowCount counts NotNow answers and
// drives the backoff.
Attempts int
NotNowCount int
CompletedAt time.Time
Result *mdm.Response
}
QueuedCommand is a command with its delivery state for one enrollment.
type Replacement ¶ added in v0.3.0
type Replacement struct {
ID, Method, OldHash, CandidateHash, SecretHash, PublicKeyHash string
State string
ExpiresAt, CompletedAt time.Time
Command mdm.Command
Delivered, Acknowledged, Authenticated bool
AuthenticateRaw []byte
Tokens []ReplacementToken
}
Replacement contains private handshake state. Administrative APIs must expose a redacted view, not this record (the command contains issuance credentials).
func CloneReplacement ¶ added in v0.3.0
func CloneReplacement(r *Replacement) *Replacement
CloneReplacement isolates callback/transport buffers from stored state.
type ReplacementChange ¶ added in v0.3.0
type ReplacementChange struct {
Op, ID, Hash, Method, SecretHash, PublicKeyHash string
At time.Time
Begin *Replacement
Raw []byte
Token *ReplacementToken
Response *mdm.Response
}
type ReplacementStore ¶ added in v0.3.0
type ReplacementStore interface {
TransitionReplacement(context.Context, mdm.EnrollmentID, ReplacementChange) (*Replacement, error)
}
ReplacementStore is an optional extension. Every transition locks the device enrollment and its replacement together. A successful terminal transition commits the candidate pin, token and certificate history in the same transaction; it never resets the enrollment, user channels, escrow or command queue.
type ReplacementToken ¶ added in v0.3.0
type ReplacementToken struct {
ID mdm.EnrollmentID
Message *checkin.TokenUpdate
Raw []byte
}
type State ¶
type State string
State of a queued command.
const ( StatePending State = "pending" // never delivered StateSent State = "sent" // delivered, awaiting a result StateNotNow State = "not-now" // device answered NotNow; retry after NotNowUntil StateAcknowledged State = "acknowledged" // terminal StateError State = "error" // terminal: Error or CommandFormatError StateCleared State = "cleared" // terminal: removed by Clear )
Command states.
type Store ¶
type Store interface {
EnrollmentStore
CommandQueue
PushStore
CertAuthStore
BootstrapTokenStore
PushCertStore
UserAuthStore
MigrationStore
}
Store is everything the service layer needs from one backend.
type UserAuthState ¶
type UserAuthState struct {
ID mdm.EnrollmentID
// Challenge is the outstanding DigestChallenge, "" once answered or
// cleared.
Challenge string
ChallengeAt time.Time
// AuthToken is the issued token, "" until the digest was accepted.
AuthToken string
TokenAt time.Time
// AuthenticateRaw is the first UserAuthenticate plist; DigestRaw the
// second one carrying DigestResponse.
AuthenticateRaw []byte
DigestRaw []byte
}
UserAuthState is the UserAuthenticate handshake state of one user channel (decision record 0016). The user's own enrollment row may not exist yet: the handshake precedes the user channel's TokenUpdate.
type UserAuthStore ¶
type UserAuthStore interface {
// StoreUserAuthChallenge records a new challenge and clears any token.
StoreUserAuthChallenge(ctx context.Context, id mdm.EnrollmentID, challenge string, raw []byte, at time.Time) error
// StoreUserAuthToken records the issued token and clears the challenge.
// ErrNotFound when no challenge was issued for the user.
StoreUserAuthToken(ctx context.Context, id mdm.EnrollmentID, token string, raw []byte, at time.Time) error
// UserAuth returns the state or ErrNotFound.
UserAuth(ctx context.Context, id mdm.EnrollmentID) (*UserAuthState, error)
// ClearUserAuth removes the state; absent state is not an error.
ClearUserAuth(ctx context.Context, id mdm.EnrollmentID) error
}
UserAuthStore persists UserAuthenticate challenges and tokens per user channel. Every method returns ErrInvalid for a device channel and ErrNotFound when the parent device enrollment does not exist. The state is removed when the device re-enrolls.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
acme
|
|
|
acmetest
Package acmetest defines ACME store contracts, fixtures and controlled failures.
|
Package acmetest defines ACME store contracts, fixtures and controlled failures. |
|
inmem
Package inmem implements a mutex-protected in-memory acme.Store.
|
Package inmem implements a mutex-protected in-memory acme.Store. |
|
Package crypt seals byte values with AES-256-GCM using named keys from secrets.Provider.
|
Package crypt seals byte values with AES-256-GCM using named keys from secrets.Provider. |
|
ddm
|
|
|
ddmtest
Package ddmtest defines transactional declaration-store contracts and fixtures.
|
Package ddmtest defines transactional declaration-store contracts and fixtures. |
|
inmem
Package inmem implements a mutex-protected in-memory ddm.Store.
|
Package inmem implements a mutex-protected in-memory ddm.Store. |
|
dep
|
|
|
inmem
Package inmem implements a mutex-protected in-memory device enrollment service store.
|
Package inmem implements a mutex-protected in-memory device enrollment service store. |
|
Package inmem implements a mutex-protected MDM store for tests and development.
|
Package inmem implements a mutex-protected MDM store for tests and development. |
|
Package storagetest defines the contract suites every MDM storage backend runs.
|
Package storagetest defines the contract suites every MDM storage backend runs. |