simulator

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: 41 Imported by: 0

Documentation

Overview

Package simulator drives an MDM server the way an Apple device does.

Why

Servers need to be tested end to end without hardware. The simulator performs check-in (Authenticate, TokenUpdate, CheckOut, bootstrap tokens, GetToken, DeclarativeManagement), polls the server URL with Idle, answers commands with typed responses, can inject NotNow and Error replies, enrolls through SCEP and the OTA profile service, and runs a declarative management client: it synchronises tokens, declaration items, and declarations, evaluates activation predicates with ddm/predicate, and builds status reports with Apple's reason codes so the server's grading is observable. Faults (dropped status, stale token, failed fetch) model the device behaviours the references tripped over. It never imports the server-side engine, so it stays an independent client.

References

Index

Constants

View Source
const (
	WellKnownPath         = "/.well-known/com.apple.remotemanagement"
	ContentTypeDeviceInfo = "application/pkcs7-signature"
	AuthCallbackScheme    = "apple-remotemanagement-user-login"
	AuthResultsHost       = "authentication-results"
	AuthResultsTokenParam = "access-token"

	ContentTypeAspenConfig = "application/x-apple-aspen-config"
)

Account-driven enrollment constants from Apple's flow description.

View Source
const (
	// HeaderDeviceInfo carries the CMS-signed MachineInfo on the web view's
	// first request.
	HeaderDeviceInfo = "x-apple-aspen-deviceinfo"
	// ErrorCodeSoftwareUpdateRequired is the 403 body code that makes
	// Setup Assistant update before enrolling.
	ErrorCodeSoftwareUpdateRequired = "com.apple.softwareupdate.required"
)

Automated Device Enrollment constants from Apple's documentation.

View Source
const (
	ContentTypeCheckin = "application/x-apple-aspen-mdm-checkin"
	ContentTypeConnect = "application/x-apple-aspen-mdm"
)

Content types Apple devices send.

Variables

View Source
var (
	// ErrDDMNotSettled reports that the declarations token kept changing for
	// MaxRounds rounds, so the sync never converged.
	ErrDDMNotSettled = errors.New("simulator: declarations token did not settle")
	// ErrDDMBadResponse reports a DDM response that does not decode, is
	// missing a required key, or carries a body where none is expected.
	ErrDDMBadResponse = errors.New("simulator: malformed declarative management response")
	// ErrDDMFault is returned when an injected DDMFaults fault fires.
	ErrDDMFault = errors.New("simulator: injected declarative management fault")
)

Errors from the DDM client.

View Source
var ErrACME = errors.New("simulator: ACME enrollment")

ErrACME is an ACME enrollment that did not complete.

View Source
var ErrADE = errors.New("simulator: automated device enrollment")

ErrADE reports a failure in the ADE flow.

View Source
var ErrAccountDriven = errors.New("simulator: account-driven enrollment")

ErrAccountDriven reports a failure in the account-driven flow.

View Source
var ErrDigestChallenge = errors.New("simulator: malformed digest challenge")

ErrDigestChallenge is returned when a DigestChallenge cannot be parsed.

View Source
var ErrProfile = errors.New("simulator: enrollment profile")

ErrProfile is returned when an enrollment profile cannot be followed.

View Source
var ErrTooManyCommands = errors.New("simulator: command loop exceeded the limit")

ErrTooManyCommands stops a Connect loop that never drains.

Functions

func AccessTokenFromRedirect

func AccessTokenFromRedirect(location string) (string, error)

AccessTokenFromRedirect extracts the access token from the apple-remotemanagement-user-login://authentication-results redirect an apple-as-web page ends with.

func DigestResponse

func DigestResponse(challenge, username, password, uri string, random io.Reader) (string, error)

DigestResponse answers a DigestChallenge the way a macOS client does for RFC 2617 Digest with qop=auth: it parses realm and nonce from the challenge, draws a client nonce from random (crypto/rand when nil), and returns the parameter list the client places in the second UserAuthenticate message's DigestResponse. The request method is POST because the check-in endpoint is always posted to.

func HA1

func HA1(username, realm, password string) string

HA1 returns the RFC 2617 HA1 value MD5(username:realm:password) as lower case hex. Servers that use service.HA1Verifier store this per user.

Types

type ACMEFaults

type ACMEFaults struct {
	// WrongKey attests one key and asks the server to certify another,
	// which is what an attacker replaying somebody else's attestation
	// would do.
	WrongKey bool
	// StaleFreshness answers with an attestation minted for a different
	// challenge, which is a replay from an earlier order.
	StaleFreshness bool
	// NoAttestation sends a well formed statement carrying no chain, as a
	// device that cannot attest does.
	NoAttestation bool
	// ForeignCA attests with an authority the server does not trust.
	ForeignCA *attesttest.CA
}

ACMEFaults are ways a device can answer a challenge wrongly. Each one is a real attack or a real bug, not an arbitrary corruption.

type ACMEOptions

type ACMEOptions struct {
	// Attestation mints the attestation. Nil means the device produces
	// none, which is what hardware without a Secure Enclave does and what
	// a profile with Attest false asks for.
	Attestation *attesttest.CA
	// Properties describe the device in the attestation. Empty identity
	// fields are filled from the device's own serial number and UDID.
	Properties attest.Properties
	// Faults make the device misbehave, for the tests that check a server
	// refuses what it should.
	Faults ACMEFaults
}

ACMEOptions control how the simulated device answers a device-attest-01 challenge.

The client itself is golang.org/x/crypto/acme rather than anything from this repository. Testing a server against its own client proves that the two agree, not that the server implements RFC 8555, so the simulator drives it with an independent implementation and only the attestation, which is Apple's own extension, is ours.

type ADEOptions

type ADEOptions struct {
	// Language is the LANGUAGE key (default "en").
	Language string
	// CanRequestSoftwareUpdate sets MDM_CAN_REQUEST_SOFTWARE_UPDATE.
	CanRequestSoftwareUpdate bool
	// SoftwareUpdateDeviceID is SOFTWARE_UPDATE_DEVICE_ID (default the
	// product name).
	SoftwareUpdateDeviceID string
	// Signer signs the MachineInfo; nil uses the device Identity (the
	// built-in Apple certificate in real life).
	Signer *Identity
	// Parse configures profile parsing.
	Parse profile.ParseOptions
	// WebView, when the profile URL is a configuration_web_url, plays the
	// person in the web view: it receives the first response (already
	// carrying x-apple-aspen-deviceinfo) and must return the final
	// application/x-apple-aspen-config response by following the
	// redirects and signing in. Nil means the token-based POST lane only.
	WebView func(ctx context.Context, first *http.Response) (*http.Response, error)
}

ADEOptions drive ADEEnroll.

type AccountDrivenOptions

type AccountDrivenOptions struct {
	// UserIdentifier is what the person types (user@domain); the domain
	// hosts the well-known document unless DiscoveryURL overrides it.
	UserIdentifier string
	// DiscoveryURL, when set, replaces https://<domain> as the well-known
	// host (tests point it at a local server).
	DiscoveryURL string
	// ModelFamily is sent as model-family (default from the device model).
	ModelFamily string
	// Language is the LANGUAGE the body carries.
	Language string
	// Authenticate turns the 401 challenge into the bearer token the
	// device presents on the second attempt. Tests drive the web view or
	// the OAuth 2 flow here.
	Authenticate func(ctx context.Context, c AuthChallenge) (string, error)
	// Signer signs the enrollment body; nil uses the device Identity.
	Signer *Identity
	// Parse configures profile parsing (signature requirements).
	Parse profile.ParseOptions
}

AccountDrivenOptions drive AccountDrivenEnroll.

type AccountDrivenResult

type AccountDrivenResult struct {
	Servers   []DiscoveryServer
	Chosen    DiscoveryServer
	Challenge AuthChallenge
	// Profile is the enrollment profile the device installed.
	Profile []byte
}

AccountDrivenResult reports what the flow saw.

type AuthChallenge

type AuthChallenge struct {
	Method           string
	URL              string
	AuthorizationURL string
	TokenURL         string
	RedirectURL      string
	ClientID         string
	Scope            string
}

AuthChallenge is the parsed WWW-Authenticate Bearer challenge.

func ParseAuthChallenge

func ParseAuthChallenge(header string) (AuthChallenge, error)

ParseAuthChallenge reads the 401 header the device receives.

type DDMDeclaration

type DDMDeclaration struct {
	Kind        schemaddm.Kind
	Type        string
	Identifier  string
	ServerToken string
	Payload     map[string]any
	// Graded state from the last DDMStatusReport.
	Active  bool
	Valid   string
	Reasons []DDMReason
}

DDMDeclaration is a declaration the client holds, with its graded state.

type DDMFaults

type DDMFaults struct {
	// DropStatus makes PostDDMStatus build the report but never send it.
	DropStatus bool
	// StaleToken makes SyncDDM forget the stored token and every
	// ServerToken, so the manifest and all declarations are fetched again.
	StaleToken bool
	// FailFetch makes the next declaration fetch fail with ErrDDMFault once.
	FailFetch bool
}

DDMFaults injects client misbehaviour for server tests.

type DDMReason

type DDMReason struct {
	Code        string         `json:"code"`
	Description *string        `json:"description,omitempty"`
	Details     map[string]any `json:"details,omitempty"`
}

DDMReason is one entry in a declaration's status reasons.

type DDMState

type DDMState struct {
	// DeclarationsToken is the token of the manifest last synchronized.
	DeclarationsToken string
	// TokenHint is the token carried by the last DeclarativeManagement
	// command's Data.
	TokenHint string
	// Items is the manifest last fetched; nil before the first sync.
	Items *ddmproto.DeclarationItemsResponse
	// Declarations is keyed by "<kind>/<identifier>".
	Declarations map[string]*DDMDeclaration
	// Properties are the @property values in effect: WithDDM properties with
	// every management.properties declaration merged on top.
	Properties map[string]any
	LastSync   time.Time
	LastReport time.Time
}

DDMState is one channel's declarative management state.

type DDMSyncResult

type DDMSyncResult struct {
	// Rounds is how many tokens fetches it took to settle.
	Rounds int
	// Fetched and Removed list "<kind>/<identifier>" keys.
	Fetched []string
	Removed []string
	// Token is the declarations token in effect after the sync.
	Token string
	// Changed reports whether any declaration or the token changed.
	Changed bool
}

DDMSyncResult describes one SyncDDM call.

type Device

type Device struct {
	UDID         string
	SerialNumber string
	Model        string
	ModelName    string
	DeviceName   string
	ProductName  string
	OSVersion    string
	BuildVersion string
	Topic        string

	CheckinURL string
	ServerURL  string
	// EnrollmentID, when set, makes this a User Enrollment: check-ins carry
	// EnrollmentID (and EnrollmentUserID on the user channel) instead of
	// the UDID (decision record 0028).
	EnrollmentID string
	Client       *http.Client
	Identity     *Identity
	Responder    Responder
	// MaxCommandsPerConnect bounds one Connect loop (default 100).
	MaxCommandsPerConnect int

	PushMagic   string
	PushToken   []byte
	UnlockToken []byte
	// contains filtered or unexported fields
}

Device is a simulated enrolled device.

func New

func New(udid string, opts ...Option) *Device

New creates a device with sensible defaults for the given UDID.

func (*Device) ACMEEnroll

func (d *Device) ACMEEnroll(ctx context.Context, p *enroll.ACME, o ACMEOptions) error

ACMEEnroll obtains an identity from the ACME server named in an enrollment profile's ACME payload and adopts it as the device identity.

func (*Device) ADEEnroll

func (d *Device) ADEEnroll(ctx context.Context, profileURL string, opts ADEOptions) error

ADEEnroll runs Automated Device Enrollment against the DEP profile's URL: it posts the signed MachineInfo (token-based lane) or, with opts.WebView, opens the web view lane with the x-apple-aspen-deviceinfo header; a 403 with the software update body becomes *SoftwareUpdateRequired; the returned profile is applied and the device enrols.

func (*Device) AccountDrivenEnroll

func (d *Device) AccountDrivenEnroll(ctx context.Context, opts AccountDrivenOptions) (*AccountDrivenResult, error)

AccountDrivenEnroll runs Apple's account-driven enrollment: service discovery, the first signed POST, the 401 challenge, authentication through opts.Authenticate, the second POST with the bearer, then ApplyProfile and Enroll as a User Enrollment identified by a fresh EnrollmentID.

func (*Device) ApplyProfile

func (d *Device) ApplyProfile(ctx context.Context, data []byte, o profile.ParseOptions) error

ApplyProfile configures the device from an enrollment profile the way a device installing it would: URLs and topic from the MDM payload and, for a SCEP identity, a fresh RSA key enrolled at the SCEP URL. The device is then ready for Enroll.

func (*Device) Authenticate

func (d *Device) Authenticate(ctx context.Context) error

Authenticate sends the Authenticate check-in message.

func (*Device) CheckOut

func (d *Device) CheckOut(ctx context.Context) error

CheckOut sends CheckOut.

func (*Device) Commands

func (d *Device) Commands() []*mdm.Command

Commands returns every command received so far.

func (*Device) Connect

func (d *Device) Connect(ctx context.Context) ([]*mdm.Command, error)

Connect polls the server URL with Idle and answers every command it is handed until the server returns an empty body. It returns the commands processed in this connection.

func (*Device) DDM

func (d *Device) DDM() *DDMState

DDM returns a snapshot of the device channel's DDM state.

func (*Device) DDMStatusReport

func (d *Device) DDMStatusReport(full bool) []byte

DDMStatusReport builds the device channel's status report from state. A full report carries every declaration, the device items, and the client capabilities; an incremental one carries only declarations whose graded state changed since the last posted report (plus the device items and capabilities before any report has been posted).

func (*Device) DeclarativeManagement

func (d *Device) DeclarativeManagement(ctx context.Context, endpoint string, data []byte) ([]byte, error)

DeclarativeManagement sends a DDM check-in for an endpoint.

func (*Device) DevicePropertiesAttestation

func (d *Device) DevicePropertiesAttestation(nonce []byte) ([][]byte, error)

DevicePropertiesAttestation answers the DevicePropertiesAttestation query of a DeviceInformation command with a chain describing this device.

Apple's device caches the attestation it generated and returns that one for up to seven days, whatever freshness code the server asked for, so a server cannot treat a mismatched freshness code on this path as evidence of a replay the way it can on the ACME path. The simulator reproduces that: once an attestation has been minted it is returned again until ExpireAttestationCache is called.

func (*Device) Enroll

func (d *Device) Enroll(ctx context.Context) error

Enroll performs Authenticate followed by TokenUpdate.

func (*Device) ExpireAttestationCache

func (d *Device) ExpireAttestationCache()

ExpireAttestationCache makes the next DevicePropertiesAttestation mint a fresh attestation, as if Apple's seven day window had passed.

func (*Device) GetBootstrapToken

func (d *Device) GetBootstrapToken(ctx context.Context) ([]byte, error)

GetBootstrapToken retrieves the escrowed bootstrap token (nil when the server has none).

func (*Device) GetToken

func (d *Device) GetToken(ctx context.Context, serviceType string) ([]byte, error)

GetToken requests a token for a service type.

func (*Device) MachineInfo

func (d *Device) MachineInfo(opts ADEOptions) map[string]any

MachineInfo builds the plist Apple documents for the ADE enrollment request, from the device's fields.

func (*Device) OAuth2CodeFlow

func (d *Device) OAuth2CodeFlow(ctx context.Context, c AuthChallenge, loginHint string, authorize func(ctx context.Context, authorizationURL string) (location string, err error)) (string, error)

OAuth2CodeFlow performs the apple-oauth2 authorization code grant the way the device does: it builds the authorization request with state and login_hint, hands it to authorize (which plays the person signing in and returns the 308 Location), checks the echoed state, and exchanges the code at the token endpoint. It returns the access token.

func (*Device) OTAEnroll

func (d *Device) OTAEnroll(ctx context.Context, profileServiceURL, challenge string, deviceID *Identity, o profile.ParseOptions) error

OTAEnroll runs the over-the-air profile-service flow: phase 1 signed with the device certificate, SCEP enrollment from the returned profile, phase 2 signed with the new identity, then ApplyProfile on the final enrollment profile. deviceID plays the Apple-issued device certificate.

func (*Device) PostDDMStatus

func (d *Device) PostDDMStatus(ctx context.Context, full bool) error

PostDDMStatus builds and posts a status report on the device channel.

func (*Device) Reenroll

func (d *Device) Reenroll(ctx context.Context, id *Identity) error

Reenroll switches to a new identity and enrols again.

func (*Device) Replies

func (d *Device) Replies() []Reply

Replies returns every reply sent so far, aligned with Commands.

func (*Device) SetBootstrapToken

func (d *Device) SetBootstrapToken(ctx context.Context, token []byte) error

SetBootstrapToken escrows a bootstrap token.

func (*Device) SharedIPadUser

func (d *Device) SharedIPadUser(shortName, longName string) *User

SharedIPadUser is the user channel of the person logged in to a Shared iPad: Apple sends the sentinel UserID and identifies the user by UserShortName (decision record 0029).

func (*Device) SignedMachineInfo

func (d *Device) SignedMachineInfo(opts ADEOptions) ([]byte, error)

SignedMachineInfo is the CMS-signed MachineInfo blob.

func (*Device) SyncDDM

func (d *Device) SyncDDM(ctx context.Context) (DDMSyncResult, error)

SyncDDM runs the synchronization loop on the device channel.

func (*Device) TokenUpdate

func (d *Device) TokenUpdate(ctx context.Context) error

TokenUpdate sends TokenUpdate, generating push magic and token on first use.

func (*Device) User

func (d *Device) User(userID, shortName, longName string) *User

User returns a user channel for the device.

type DiscoveryServer

type DiscoveryServer struct {
	Version string
	BaseURL string
}

DiscoveryServer is one entry of the well-known document.

type HTTPError

type HTTPError struct {
	Status int
	Body   []byte
}

HTTPError reports a non-200 response from the server.

func (*HTTPError) Error

func (e *HTTPError) Error() string

Error implements error.

type Identity

type Identity struct {
	Cert *x509.Certificate
	Key  crypto.Signer
}

Identity is the device's MDM identity certificate and key.

type Option

type Option func(*Device)

Option configures a Device.

func WithACME

func WithACME(o ACMEOptions) Option

WithACME sets how the device answers an ACME device-attest-01 challenge when it applies a profile whose identity comes from ACME.

func WithClient

func WithClient(c *http.Client) Option

WithClient sets the HTTP client.

func WithDDM

func WithDDM(props map[string]any) Option

WithDDM sets the activation properties the client evaluates @property references against. Properties from management.properties declarations the server sends are merged on top.

func WithDDMFaults

func WithDDMFaults(f DDMFaults) Option

WithDDMFaults injects client faults.

func WithDDMMaxRounds

func WithDDMMaxRounds(n int) Option

WithDDMMaxRounds bounds the SyncDDM convergence loop (default 5).

func WithDDMTestStatusItems

func WithDDMTestStatusItems(on bool) Option

WithDDMTestStatusItems includes Apple's "test.*" status items in the reported client capabilities.

func WithIdentity

func WithIdentity(id *Identity) Option

WithIdentity sets the identity used to sign requests.

func WithResponder

func WithResponder(r Responder) Option

WithResponder sets how commands are answered.

func WithTopic

func WithTopic(t string) Option

WithTopic sets the push topic (default com.apple.mgmt.External.simulator).

func WithURLs

func WithURLs(checkin, server string) Option

WithURLs sets the check-in and server URLs.

type Reply

type Reply struct {
	Status     mdm.Status
	ErrorChain []mdm.ErrorChainItem
	// Payload is merged into the response plist; nil sends the envelope only.
	Payload commands.Response
}

Reply is how the device answers one command.

func AcknowledgeAll

func AcknowledgeAll(cmd *mdm.Command) Reply

AcknowledgeAll answers every command with Acknowledged and a zero typed response when the schema knows the command.

type Responder

type Responder func(cmd *mdm.Command) Reply

Responder decides the reply for a command.

type SoftwareUpdateRequired

type SoftwareUpdateRequired struct {
	OSVersion    string
	BuildVersion string
	Message      string
}

SoftwareUpdateRequired is the typed form of Apple's 403 response: the device must update to OSVersion (and BuildVersion when given) first.

func (*SoftwareUpdateRequired) Error

func (e *SoftwareUpdateRequired) Error() string

type User

type User struct {
	Device    *Device
	UserID    string
	ShortName string
	LongName  string
	PushMagic string
	PushToken []byte
	// contains filtered or unexported fields
}

User is a user channel of a device.

func (*User) Authenticate

func (u *User) Authenticate(ctx context.Context, digestResponse string) ([]byte, error)

Authenticate sends UserAuthenticate and returns the response body.

func (*User) CheckOut

func (u *User) CheckOut(ctx context.Context) error

TokenUpdate sends the user channel TokenUpdate. CheckOut sends CheckOut on the user channel: the server disables this user only (decision record 0029).

func (*User) Connect

func (u *User) Connect(ctx context.Context) ([]*mdm.Command, error)

Connect polls the server URL on the user channel.

func (*User) DDM

func (u *User) DDM() *DDMState

DDM returns a snapshot of the user channel's DDM state.

func (*User) DDMStatusReport

func (u *User) DDMStatusReport(full bool) []byte

DDMStatusReport builds the user channel's status report; see Device.DDMStatusReport.

func (*User) PostDDMStatus

func (u *User) PostDDMStatus(ctx context.Context, full bool) error

PostDDMStatus builds and posts a status report on the user channel.

func (*User) SyncDDM

func (u *User) SyncDDM(ctx context.Context) (DDMSyncResult, error)

SyncDDM runs the synchronization loop on the user channel.

func (*User) TokenUpdate

func (u *User) TokenUpdate(ctx context.Context) error

Jump to

Keyboard shortcuts

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