Documentation
¶
Overview ¶
Package simulator exercises modeled Apple MDM and enrollment flows as a device-side test client.
Design ¶
The client supports check-in, command polling/results, SCEP, OTA, ADE, account-driven authentication, user channels, Shared iPad and ACME. Declarative synchronization tracks tokens and versions, evaluates the supported predicate subset and emits full/incremental status reports. Fault options cover NotNow, command errors, stale tokens, failed fetches and dropped reports.
The simulator uses independent client paths where available, including golang.org/x/crypto/acme. It does not reproduce every device behavior; tests against it do not replace physical-device and live-service validation.
References ¶
- Decision record 0024: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0024-simulator-ddm-client-and-predicates.md
- E2E scenarios: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/testing/e2e-scenarios.md
- Apple: https://developer.apple.com/documentation/devicemanagement/check-in
- Apple: https://developer.apple.com/documentation/devicemanagement/commands-and-queries
- Apple: https://developer.apple.com/documentation/devicemanagement/integrating-declarative-management
- Schema: third_party/device-management/declarative/protocol/*.yaml, declarative/status/**
Index ¶
- Constants
- Variables
- func AccessTokenFromRedirect(location string) (string, error)
- func DigestResponse(challenge, username, password, uri string, random io.Reader) (string, error)
- func HA1(username, realm, password string) string
- type ACMEFaults
- type ACMEOptions
- type ADEOptions
- type AccountDrivenOptions
- type AccountDrivenResult
- type AuthChallenge
- type DDMDeclaration
- type DDMFaults
- type DDMReason
- type DDMState
- type DDMSyncResult
- type Device
- func (d *Device) ACMEEnroll(ctx context.Context, p *enroll.ACME, o ACMEOptions) error
- func (d *Device) ADEEnroll(ctx context.Context, profileURL string, opts ADEOptions) error
- func (d *Device) AccountDrivenEnroll(ctx context.Context, opts AccountDrivenOptions) (*AccountDrivenResult, error)
- func (d *Device) AccountTokens() (secrets.Secret, secrets.Secret)
- func (d *Device) ApplyProfile(ctx context.Context, data []byte, o profile.ParseOptions) error
- func (d *Device) Authenticate(ctx context.Context) error
- func (d *Device) CheckOut(ctx context.Context) error
- func (d *Device) Commands() []*mdm.Command
- func (d *Device) Connect(ctx context.Context) ([]*mdm.Command, error)
- func (d *Device) DDM() *DDMState
- func (d *Device) DDMStatusReport(full bool) []byte
- func (d *Device) DeclarativeManagement(ctx context.Context, endpoint string, data []byte) ([]byte, error)
- func (d *Device) DevicePropertiesAttestation(nonce []byte) ([][]byte, error)
- func (d *Device) Enroll(ctx context.Context) error
- func (d *Device) ExpireAttestationCache()
- func (d *Device) GetBootstrapToken(ctx context.Context) ([]byte, error)
- func (d *Device) GetToken(ctx context.Context, serviceType string) ([]byte, error)
- func (d *Device) MachineInfo(opts ADEOptions) map[string]any
- func (d *Device) OAuth2CodeFlow(ctx context.Context, c AuthChallenge, loginHint string, ...) (string, error)
- func (d *Device) OTAEnroll(ctx context.Context, profileServiceURL, challenge string, deviceID *Identity, ...) error
- func (d *Device) PostDDMStatus(ctx context.Context, full bool) error
- func (d *Device) Reenroll(ctx context.Context, id *Identity) error
- func (d *Device) RefreshAccountToken(ctx context.Context) (string, error)
- func (d *Device) Replies() []Reply
- func (d *Device) ReturnToService(ctx context.Context) (*checkin.ReturnToServiceResponse, error)
- func (d *Device) SetBootstrapToken(ctx context.Context, token []byte) error
- func (d *Device) SharedIPadUser(shortName, longName string) *User
- func (d *Device) SignedMachineInfo(opts ADEOptions) ([]byte, error)
- func (d *Device) SyncDDM(ctx context.Context) (DDMSyncResult, error)
- func (d *Device) TokenUpdate(ctx context.Context) error
- func (d *Device) User(userID, shortName, longName string) *User
- type DiscoveryServer
- type HTTPError
- type Identity
- type Option
- func WithACME(o ACMEOptions) Option
- func WithClient(c *http.Client) Option
- func WithDDM(props map[string]any) Option
- func WithDDMFaults(f DDMFaults) Option
- func WithDDMMaxRounds(n int) Option
- func WithDDMTestStatusItems(on bool) Option
- func WithIdentity(id *Identity) Option
- func WithResponder(r Responder) Option
- func WithTopic(t string) Option
- func WithURLs(checkin, server string) Option
- type Reply
- type Responder
- type SoftwareUpdateRequired
- type User
- func (u *User) Authenticate(ctx context.Context, digestResponse string) ([]byte, error)
- func (u *User) CheckOut(ctx context.Context) error
- func (u *User) Connect(ctx context.Context) ([]*mdm.Command, error)
- func (u *User) DDM() *DDMState
- func (u *User) DDMStatusReport(full bool) []byte
- func (u *User) PostDDMStatus(ctx context.Context, full bool) error
- func (u *User) SyncDDM(ctx context.Context) (DDMSyncResult, error)
- func (u *User) TokenUpdate(ctx context.Context) error
Constants ¶
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.
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.
const ( ContentTypeCheckin = "application/x-apple-aspen-mdm-checkin" ContentTypeConnect = "application/x-apple-aspen-mdm" )
Content types Apple devices send.
Variables ¶
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.
var ErrACME = errors.New("simulator: ACME enrollment")
ErrACME is an ACME enrollment that did not complete.
var ErrADE = errors.New("simulator: automated device enrollment")
ErrADE reports a failure in the ADE flow.
var ErrAccountDriven = errors.New("simulator: account-driven enrollment")
ErrAccountDriven reports a failure in the account-driven flow.
var ErrDigestChallenge = errors.New("simulator: malformed digest challenge")
ErrDigestChallenge is returned when a DigestChallenge cannot be parsed.
var ErrProfile = errors.New("simulator: enrollment profile")
ErrProfile is returned when an enrollment profile cannot be followed.
var ErrTooManyCommands = errors.New("simulator: command loop exceeded the limit")
ErrTooManyCommands stops a Connect loop that never drains.
Functions ¶
func AccessTokenFromRedirect ¶
AccessTokenFromRedirect extracts the access token from the apple-remotemanagement-user-login://authentication-results redirect an apple-as-web page ends with.
func DigestResponse ¶
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.
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 configures the simulator's device-attest-01 response. The client uses golang.org/x/crypto/acme for the base ACME exchange and supplies Apple's attestation extension separately.
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 (*Device) ACMEEnroll ¶
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 ¶
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. BYOD uses a fresh EnrollmentID; ADDE uses the device UDID and ordinary device channel.
func (*Device) AccountTokens ¶ added in v0.2.0
AccountTokens returns redacted copies of the current OAuth credentials.
func (*Device) ApplyProfile ¶
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 ¶
Authenticate sends the Authenticate check-in message.
func (*Device) Connect ¶
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) DDMStatusReport ¶
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 ¶
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) 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 ¶
GetBootstrapToken retrieves the escrowed bootstrap token (nil when the server has none).
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 ¶
PostDDMStatus builds and posts a status report on the device channel.
func (*Device) RefreshAccountToken ¶ added in v0.2.0
RefreshAccountToken rotates the retained OAuth refresh credential.
func (*Device) ReturnToService ¶
ReturnToService asks the server for its return-to-service configuration, as a supervised Automated Device Enrollment device does when a user triggers return to service or its idle timeout expires. A real device only sends this when its enrollment profile has put it in return-to-service mode; the simulator sends it on demand so a server can be exercised without one.
The response says whether to erase and re-enrol, and carries the bootstrap token the device needs to preserve apps across the erasure.
func (*Device) SetBootstrapToken ¶
SetBootstrapToken escrows a bootstrap token.
func (*Device) SharedIPadUser ¶
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 ¶
TokenUpdate sends TokenUpdate, generating push magic and token on first use.
type DiscoveryServer ¶
DiscoveryServer is one entry of the well-known document.
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 WithDDM ¶
WithDDM sets the activation properties the client evaluates @property references against. Properties from management.properties declarations the server sends are merged on top.
func WithDDMMaxRounds ¶
WithDDMMaxRounds bounds the SyncDDM convergence loop (default 5).
func WithDDMTestStatusItems ¶
WithDDMTestStatusItems includes Apple's "test.*" status items in the reported client capabilities.
func WithIdentity ¶
WithIdentity sets the identity used to sign requests.
func WithResponder ¶
WithResponder sets how commands are answered.
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 ¶
AcknowledgeAll answers every command with Acknowledged and a zero typed response when the schema knows the command.
type SoftwareUpdateRequired ¶
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 ¶
Authenticate sends UserAuthenticate and returns the response body.
func (*User) CheckOut ¶
TokenUpdate sends the user channel TokenUpdate. CheckOut sends CheckOut on the user channel: the server disables this user only (decision record 0029).
func (*User) DDMStatusReport ¶
DDMStatusReport builds the user channel's status report; see Device.DDMStatusReport.
func (*User) PostDDMStatus ¶
PostDDMStatus builds and posts a status report on the user channel.