service

package
v0.9.1 Latest Latest
Warning

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

Go to latest
Published: Sep 14, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package service implements enrollment lifecycle, authorization and command delivery behind MDM endpoints.

Design

Core handles Authenticate, TokenUpdate, CheckOut, certificate pins/reuse, user channels and command results. Storage interfaces support interchangeable backends. Hooks can observe or veto operations, and typed events report outcomes. Optional handlers implement GetToken, UserAuthenticate, DeclarativeManagement and ReturnToService.

Certificate status checking, when configured, precedes hooks and device side effects independently of pin mode. Both the reusable service and reference server deny changed identities during re-enrollment by default. Command-target checks use available OS/channel metadata and recorded capability observations; unknown supervision, ADE and user-approved MDM state cannot satisfy a command requirement.

An unconfigured ReturnToService handler answers disabled. An enabled response receives the stored bootstrap token if available and not supplied by policy; without one, the device can erase fully without app preservation.

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 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 = dmhook.Call

Call describes one service operation for hooks. It is an alias of dmhook.Call so a hook can be written without importing this package.

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)
	CodeUnavailable                   // required recording is unavailable; no local mutation committed
)

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
	// EnableReplacements enables authorized profile updates on stores implementing
	// storage.ReplacementStore. Ordinary re-enrollment policy remains independent.
	EnableReplacements bool
	// Bus receives events; nil disables publishing.
	Bus event.Publisher
	// Clock defaults to the real clock.
	Clock clock.Clock
	Hooks []Hook
	// CertificateStatus is optional and independent of Pinning. It runs before
	// hooks or side effects on all device requests, including Authenticate and DDM.
	CertificateStatus func(context.Context, *x509.Certificate) error
	// Logger defaults to slog.Default.
	Logger *slog.Logger
	// Pinning defaults to PinEnforce.
	Pinning PinMode
	// Reenroll defaults to DenyReenroll.
	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 and populated fields' support metadata (channel, Shared iPad,
	// User Enrollment) from schema/commands and reports unsupported targets in
	// EnqueueResult.Skipped instead of queuing them. Default true.
	// Required fields and value constraints are checked even when false.
	ValidateTargets *bool
	// Optional message handlers.
	DeclarativeManagement DMHandler
	GetToken              GetTokenHandler
	UserAuthenticate      UserAuthenticateHandler
	// ReturnToService is optional. With no handler the service answers
	// Enabled false, which is a valid answer meaning "do not erase": the
	// safe reading of an unconfigured server.
	ReturnToService ReturnToServiceHandler
}

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 processes local enrollment mutations in the event transaction. DDM adapters and token/return-to-service integrations own their own boundaries; they may call remote services and must not hold this database transaction.

func (*Core) Connect

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

Connect commits the response, next-command transition and required events before the transport can deliver that command to the device.

func (*Core) Enqueue

Enqueue validates and queues commands together with their required events.

func (*Core) ExportEnrollments

func (c *Core) ExportEnrollments(ctx context.Context, p paging.Page) (paging.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.Publisher
	// 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 supplies tokens for GetToken requests. For com.apple.maid, TokenData must contain a UTF-8 JWT signed with RS256 by the RSA private key corresponding to the certificate registered with Apple Business Manager or Apple School Manager. The caller supplies iss (the AccountDetail server_uuid), iat, a unique jti, and service_type="com.apple.maid". The service transports the token unchanged; it neither issues tokens nor verifies registration with Apple.

type Hook

type Hook = dmhook.Hook

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. It is an alias of dmhook.Hook.

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 ReturnToServiceHandler

type ReturnToServiceHandler func(ctx context.Context, r *mdm.Request, m *checkin.ReturnToService) (*checkin.ReturnToServiceResponse, error)

ReturnToServiceHandler selects the configuration returned to a device requesting Return to Service. Deployment policy decides whether to enable erasure and re-enrollment.

For an enabled response, the service fills an omitted BootstrapToken from storage when available and preserves a supplied token. Without a token, Apple devices can erase fully without app preservation. A nil response is treated as disabled.

Policy can set the optional ShouldRetryEnrollment field for iOS 27 or later. Nil omits the option and retains Apple's false default; explicit true requests a retry after failure. The caller selects eligible targets.

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