service

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Overview

Package service implements the MDM server behaviour behind the check-in and command endpoints.

Why

The HTTP layer decodes and authenticates; this package decides. It owns the enrollment lifecycle (Authenticate, TokenUpdate, CheckOut, user channels), identity pinning and re-enrollment policy, command delivery with NotNow backoff, the optional message handlers (GetToken, UserAuthenticate, DeclarativeManagement through service.DMHandler), and the hooks and events that let integrators observe or veto every step. Storage is behind the storage interfaces so the same core runs on every backend, and the DDM engine plugs in through the handler and hook seams rather than being imported.

References

Index

Constants

View Source
const (
	DefaultUserAuthRealm        = "mdm"
	DefaultUserAuthChallengeTTL = 5 * time.Minute
)

Defaults for DigestUserAuth.

View Source
const ContentTypePlist = "application/xml; charset=utf-8"

ContentTypePlist is the content type for plist response bodies.

Variables

View Source
var (
	ErrUnknownEnrollment = errors.New("service: unknown enrollment")
	ErrCertRequired      = errors.New("service: identity certificate required")
	ErrCertMismatch      = errors.New("service: identity certificate does not match the enrollment")
	ErrReenrollDenied    = errors.New("service: re-enrollment with a new identity denied")
	ErrNoHandler         = errors.New("service: no handler configured")
	ErrInvalidMessage    = errors.New("service: invalid message")
	ErrHookVeto          = errors.New("service: rejected by hook")
	// ErrCertReused is returned when an identity certificate presented on
	// Authenticate, or on a retroactive pin, appears in another
	// enrollment's certificate history (decision record 0014).
	ErrCertReused = errors.New("service: identity certificate already used by another enrollment")
)

Sentinel errors.

View Source
var (
	// ErrUserNotManaged is returned by DigestUserAuth.Manage to answer 410
	// for the current login session.
	ErrUserNotManaged = errors.New("service: user not managed")
	// ErrUserAuthRequired is returned (CodeForbidden) for a user channel
	// TokenUpdate without a completed UserAuthenticate when
	// Config.RequireUserAuth is set.
	ErrUserAuthRequired = errors.New("service: UserAuthenticate required before TokenUpdate")
	// ErrNoChallenge is used internally when the second message arrives
	// without an outstanding challenge; it surfaces as an empty AuthToken.
	ErrNoChallenge = errors.New("service: no outstanding challenge")
	// ErrBadDigest reports a DigestResponse that could not be parsed or
	// that does not match the challenge. DigestUserAuth treats it as a
	// rejected login rather than a server failure.
	ErrBadDigest = errors.New("service: malformed digest response")
)

UserAuthenticate errors (decision record 0016).

View Source
var ErrUnsupportedTarget = errors.New("service: request type not supported on this enrollment")

ErrUnsupportedTarget marks an Enqueue target the request type does not support per Apple's schema metadata.

Functions

func AllowCertReuse

func AllowCertReuse(context.Context, *mdm.Request, []storage.CertAssociation) error

AllowCertReuse accepts a certificate that appears only in other enrollments' history. It never overrides a live pin: a hash that another enrollment currently pins still fails with ErrCertMismatch.

func AllowReenroll

func AllowReenroll(context.Context, *mdm.Request, *storage.Enrollment) error

AllowReenroll accepts every re-enrollment with a new identity.

func DenyCertReuse

DenyCertReuse rejects every certificate that another enrollment pinned before, with ErrCertReused.

func DenyReenroll

func DenyReenroll(context.Context, *mdm.Request, *storage.Enrollment) error

DenyReenroll rejects re-enrollment with a new identity.

Types

type Call

type Call struct {
	// Op is "checkin:<MessageType>", "connect", "enqueue", "export", or
	// "import".
	Op       string
	Request  *mdm.Request
	Checkin  *mdm.Checkin
	Response *mdm.Response
	Command  *mdm.Command
}

Call describes one service operation for hooks.

type CertReusePolicy

type CertReusePolicy func(ctx context.Context, r *mdm.Request, previous []storage.CertAssociation) error

CertReusePolicy decides whether an Authenticate may use an identity certificate that another enrollment has pinned before. previous lists the other enrollments' history rows (never the requesting device).

type CheckinResult

type CheckinResult struct {
	Body        []byte
	ContentType string
	// Status overrides the HTTP status when non-zero.
	Status int
}

CheckinResult is the response to a check-in message. Most messages return an empty body.

type Code

type Code int

Code classifies service errors so transports can map them to responses without inspecting messages.

const (
	CodeInternal          Code = iota // storage or handler failure
	CodeBadRequest                    // malformed or inconsistent message
	CodeForbidden                     // identity mismatch or policy veto
	CodeUnknownEnrollment             // no enrollment for the identity presented
	CodeNotImplemented                // no handler configured for the message
	CodeGone                          // the server declines to manage this user or enrollment (HTTP 410)
)

Error codes.

func CodeOf

func CodeOf(err error) Code

CodeOf returns the Code of err, or CodeInternal for other errors.

type Config

type Config struct {
	Store storage.Store
	// Bus receives events; nil disables publishing.
	Bus *event.Bus
	// Clock defaults to the real clock.
	Clock clock.Clock
	Hooks []Hook
	// Logger defaults to slog.Default.
	Logger *slog.Logger
	// Pinning defaults to PinEnforce.
	Pinning PinMode
	// Reenroll defaults to AllowReenroll.
	Reenroll ReenrollPolicy
	// CertReuse defaults to DenyCertReuse. It is consulted when an
	// Authenticate presents a certificate whose hash appears in another
	// enrollment's history and is ignored under PinOff. AllowCertReuse only
	// permits certificates that are in history but not currently pinned: a
	// live pin held by another enrollment still yields ErrCertMismatch with
	// CodeForbidden, because the pin exists to stop a second device using
	// the identity.
	CertReuse CertReusePolicy
	// RequireUserAuth makes a user channel's TokenUpdate depend on a
	// completed UserAuthenticate session (a token issued by DigestUserAuth):
	// without one the TokenUpdate is CodeForbidden (decision record 0029).
	// Shared iPad and User Enrollment user channels are exempt because
	// Apple never sends UserAuthenticate for them.
	RequireUserAuth bool
	// ValidateTargets checks every Enqueue target against the request
	// type's support metadata (channel, Shared iPad, User Enrollment) from
	// schema/commands and reports unsupported targets in
	// EnqueueResult.Skipped instead of queuing them. Default true.
	ValidateTargets *bool
	// Optional message handlers.
	DeclarativeManagement DMHandler
	GetToken              GetTokenHandler
	UserAuthenticate      UserAuthenticateHandler
}

Config builds a Core.

type Core

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

Core is the service implementation.

func New

func New(cfg Config) (*Core, error)

New validates the configuration and builds a Core.

func (*Core) Checkin

func (c *Core) Checkin(ctx context.Context, r *mdm.Request, ck *mdm.Checkin) (*CheckinResult, error)

Checkin handles one check-in message.

func (*Core) Connect

func (c *Core) Connect(ctx context.Context, r *mdm.Request, resp *mdm.Response) (*mdm.Command, error)

Connect handles one request on the server URL: it records the device's response (unless Idle) and returns the next command to deliver, or nil when the queue is empty. A NotNow response skips other NotNow commands for this connection, as Apple recommends.

func (*Core) Enqueue

Enqueue queues a command for enrollments and publishes CommandQueued for each that accepted it.

func (*Core) ExportEnrollments

func (c *Core) ExportEnrollments(ctx context.Context, p storage.Page) (storage.Result[storage.EnrollmentExport], error)

ExportEnrollments pages through enrollment records for migration. Device channels precede the user channels that belong to them, so a page can be replayed into ImportEnrollment in order (decision record 0017).

func (*Core) ImportEnrollment

func (c *Core) ImportEnrollment(ctx context.Context, rec storage.EnrollmentExport) error

ImportEnrollment writes one exported record and publishes EnrollmentImported with actor "admin". The record is written exactly as given: a disabled enrollment stays disabled and the command queue is not touched.

type DMHandler

type DMHandler func(ctx context.Context, r *mdm.Request, ck *mdm.Checkin, m *checkin.DeclarativeManagement) (DMResponse, error)

DMHandler serves declarative management check-in messages. ck is the check-in as received, including the raw plist bytes, so an adapter that forwards the message to another process can send it unchanged (decision record 0023); m is the typed message inside ck.

type DMResponse

type DMResponse struct {
	Body        []byte
	ContentType string
	// Status overrides the HTTP status when non-zero (for example 404 for
	// an unknown declaration).
	Status int
}

DMResponse is what a DeclarativeManagement handler returns to the device.

type DigestUserAuth

type DigestUserAuth struct {
	Store    storage.UserAuthStore
	Verifier UserVerifier
	// Realm defaults to DefaultUserAuthRealm.
	Realm string
	// ChallengeTTL defaults to DefaultUserAuthChallengeTTL.
	ChallengeTTL time.Duration
	// Manage decides whether the user is managed at all. Returning
	// ErrUserNotManaged answers 410 (CodeGone) for this login session; any
	// other error is CodeInternal. Nil manages everyone.
	Manage func(ctx context.Context, r *mdm.Request, m *checkin.UserAuthenticate) error
	// Clock defaults to the real clock.
	Clock clock.Clock
	// Bus receives UserAuthenticated and UserAuthFailed; nil disables it.
	Bus *event.Bus
	// Rand defaults to crypto/rand.
	Rand io.Reader
}

DigestUserAuth implements the two-message UserAuthenticate handshake: the first message is answered with a one-shot DigestChallenge, the second is verified and answered with an AuthToken. A wrong or expired digest clears the challenge and answers an empty AuthToken, which is how Apple documents a rejected password.

func (*DigestUserAuth) Handle

Handle satisfies UserAuthenticateHandler.

type Error

type Error struct {
	Code Code
	Err  error
}

Error is the typed error every service method returns.

func (*Error) Error

func (e *Error) Error() string

Error implements error.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap implements errors.Unwrap.

type GetTokenHandler

type GetTokenHandler func(ctx context.Context, r *mdm.Request, m *checkin.GetToken) (*checkin.GetTokenResponse, error)

GetTokenHandler serves GetToken requests.

type Hook

type Hook interface {
	Before(ctx context.Context, c *Call) (context.Context, error)
	After(ctx context.Context, c *Call, err error)
}

Hook observes and may veto operations. Before runs before storage is touched; an error aborts the operation with CodeForbidden. After runs with the operation's result.

type PinMode

type PinMode int

PinMode controls identity certificate pinning.

const (
	// PinEnforce rejects requests whose certificate does not match the
	// pinned one, and requires a certificate on every request.
	PinEnforce PinMode = iota
	// PinWarn logs mismatches but allows the request.
	PinWarn
	// PinOff disables pinning entirely.
	PinOff
)

Pin modes.

type ReenrollPolicy

type ReenrollPolicy func(ctx context.Context, r *mdm.Request, existing *storage.Enrollment) error

ReenrollPolicy decides whether an Authenticate from an enrollment whose pinned certificate differs from the one presented is accepted.

type UserAuthenticateHandler

type UserAuthenticateHandler func(ctx context.Context, r *mdm.Request, m *checkin.UserAuthenticate) (*mdm.UserAuthenticateResponse, error)

UserAuthenticateHandler serves UserAuthenticate. Returning a response with an empty DigestChallenge accepts the user without authentication, which is the default behaviour.

type UserVerifier

type UserVerifier interface {
	Verify(ctx context.Context, r *mdm.Request, in VerifyInput) (bool, error)
}

UserVerifier checks a DigestResponse against the deployment's password store. It returns false for a wrong password or unknown user and an error only when the check itself could not run.

func HA1Verifier

func HA1Verifier(ha1 func(ctx context.Context, username, realm string) (string, error)) UserVerifier

HA1Verifier implements RFC 2617 Digest (MD5, qop=auth) given the HA1 value MD5(username:realm:password) from the deployment. The lookup returning ("", nil) means the user is unknown and the login is rejected; an error from the lookup is returned as such.

type UserVerifierFunc

type UserVerifierFunc func(ctx context.Context, r *mdm.Request, in VerifyInput) (bool, error)

UserVerifierFunc adapts a function to UserVerifier.

func (UserVerifierFunc) Verify

func (f UserVerifierFunc) Verify(ctx context.Context, r *mdm.Request, in VerifyInput) (bool, error)

Verify implements UserVerifier.

type VerifyInput

type VerifyInput struct {
	// UserID is the GUID from the UserAuthenticate message.
	UserID string
	// Realm is the realm the challenge was issued for.
	Realm string
	// Challenge is the full DigestChallenge string that was issued.
	Challenge string
	// DigestResponse is the client's Authorization-style parameter list.
	DigestResponse string
}

VerifyInput is what a UserVerifier needs to check one DigestResponse.

Jump to

Keyboard shortcuts

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