How setupProtocol in ipatool Handles Plist-Encoded HTTPS Requests to Apple Servers

The setupProtocol type in ipatool implements a two-step SAP (Signing Apple Protocol) bootstrap that wraps the standard net/http client to exchange XML-plist messages with Apple's signing services, extracting binary certificates and setup buffers via the howett.net/plist library.

The majd/ipatool repository provides tools for interacting with Apple's infrastructure, and the setupProtocol struct—defined in internal/sap/protocol.go—serves as the low-level transport layer responsible for plist-encoded HTTPS communication. This component manages the initial handshake required to establish a trusted signing session with Apple's servers, handling both certificate retrieval and encrypted buffer exchange using Apple's proprietary XML property list format.

The Two-Step SAP Bootstrap Workflow

The setupProtocol type orchestrates a mandatory bootstrap sequence required by Apple's signing architecture. According to the source code in internal/sap/protocol.go, the implementation follows a strict two-phase pattern to initialize the SAP runtime.

Step 1: Fetching the SAP Certificate

The first phase retrieves the cryptographic certificate needed for subsequent signing operations. The certificate(ctx, endpoint) method constructs a GET request with the User-Agent header set to apphttp.DefaultUserAgent (defined in pkg/http/constants.go) to mimic official Apple configuration tools.

// Conceptual flow based on internal/sap/protocol.go
func (p *setupProtocol) certificate(ctx context.Context, endpoint string) ([]byte, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
    req.Header.Set("User-Agent", apphttp.DefaultUserAgent)
    
    body, err := p.send(req) // Enforces 1MiB maxSetupBody limit
    if err != nil {
        return nil, fmt.Errorf("failed to fetch certificate: %w", err)
    }
    
    return plistBytes(body, setupCertificateKey) // Extracts "sign-sap-setup-cert"
}

The response body passes through plistBytes(body, setupCertificateKey), which unmarshals the XML-plist using howett.net/plist and extracts the binary data associated with the sign-sap-setup-cert key. The implementation caps the response size at 1 MiB (maxSetupBody) to prevent memory exhaustion from malformed server responses.

Step 2: Exchanging the Setup Buffer

The second phase transmits the SAP runtime's initialization payload and receives the server's response. The exchange(ctx, endpoint, input) method marshals a map[string]any containing the sign-sap-setup-buffer key into an XML-plist format before transmission.

// Based on internal/sap/protocol.go implementation
func (p *setupProtocol) exchange(ctx context.Context, endpoint string, input []byte) ([]byte, error) {
    payload := map[string]any{setupBufferKey: input} // "sign-sap-setup-buffer"
    plistData, _ := plist.Marshal(payload, plist.XMLFormat)
    
    req, _ := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(plistData))
    req.Header.Set("Content-Type", "application/x-plist")
    
    body, err := p.send(req)
    if err != nil {
        return nil, fmt.Errorf("failed to exchange setup message: %w", err)
    }
    
    return plistBytes(body, setupBufferKey) // Extracts "sign-sap-setup-buffer"
}

This method sends a POST request with Content-Type: application/x-plist and returns the binary contents of the sign-sap-setup-buffer key from the server's response plist.

Internal Plumbing and Safety Mechanisms

The send() Helper Function

Both steps rely on the unexported send(request) method to execute HTTP requests. This helper enforces several critical safety checks:

  • Validates that the server returns HTTP 200 OK
  • Applies a 1 MiB maximum body size limit via maxSetupBody
  • Returns raw response bytes for plist parsing

Errors at this layer receive context-rich wrapping (e.g., "apple returned ...") to aid debugging when Apple's servers respond with non-200 status codes.

Plist Handling with howett.net/plist

The plistBytes helper function centralizes XML-plist parsing. As implemented in internal/sap/protocol.go, this function:

  1. Unmarshals XML data into a map[string]any using plist.Unmarshal
  2. Validates the presence of the expected key (either sign-sap-setup-cert or sign-sap-setup-buffer)
  3. Type-asserts the value to []byte and returns the binary payload

If the expected key is missing or contains non-binary data, the function returns a descriptive error stating "Apple plist is missing ...".

Practical Usage Example

While higher-level components like NewSigner in internal/sap/signer_local.go automatically invoke setupProtocol, the following example demonstrates manual interaction:

ctx := context.Background()
proto := internal.sap.setupProtocol{
    client: &http.Client{Timeout: 30 * time.Second},
}

// Example: Fetching the SAP certificate
cert, err := proto.certificate(ctx, "https://example.com/sap/certificate")
if err != nil {
    log.Fatalf("failed to get SAP certificate: %v", err)
}
fmt.Printf("Got %d-byte certificate\n", len(cert))

// Example: Exchanging a setup buffer
requestBuf := []byte{0x01, 0x02, 0x03} // Binary payload from the SAP runtime
reply, err := proto.exchange(ctx, "https://example.com/sap/setup", requestBuf)
if err != nil {
    log.Fatalf("SAP exchange failed: %v", err)
}
fmt.Printf("Received %d-byte reply\n", len(reply))

Key Implementation Files

The SAP protocol implementation spans three primary files in the majd/ipatool repository:

  • internal/sap/protocol.go: Contains the core setupProtocol type with certificate(), exchange(), send(), and plistBytes() functions
  • internal/sap/signer_local.go: Implements the high-level Signer that consumes setupProtocol during session initialization via NewSigner
  • pkg/http/constants.go: Defines DefaultUserAgent and other HTTP-related constants required for Apple server compatibility

Summary

  • setupProtocol wraps the standard net/http client to handle Apple's SAP bootstrap sequence over HTTPS
  • The implementation performs a two-step handshake: first fetching a certificate via GET, then exchanging binary buffers via POST
  • All communication uses XML-plist encoding with Content-Type: application/x-plist, processed through the howett.net/plist library
  • Safety limits include a 1 MiB response body cap (maxSetupBody) and strict HTTP 200 validation in the send() method
  • Binary payloads extract from specific plist keys: sign-sap-setup-cert and sign-sap-setup-buffer
  • The User-Agent header mimics official Apple tools to ensure server acceptance

Frequently Asked Questions

What is the SAP protocol used by ipatool?

The SAP (Signing Apple Protocol) is Apple's proprietary authentication mechanism required to establish trusted signing sessions. According to the majd/ipatool source code, setupProtocol implements the initial bootstrap phase of SAP, which involves retrieving a server certificate and exchanging encrypted setup buffers before any actual code signing can occur.

Why does setupProtocol use XML-plist instead of JSON?

Apple's signing infrastructure mandates XML property lists (plists) for configuration and metadata exchange. The setupProtocol type in internal/sap/protocol.go uses the howett.net/plist library to marshal Go maps into XML-plist format and unmarshal responses, specifically targeting keys like sign-sap-setup-cert that contain binary certificate data.

How does ipatool prevent memory exhaustion during SAP setup?

The implementation enforces a 1 MiB size limit on all SAP setup responses through the maxSetupBody constant. The send() method in internal/sap/protocol.go reads the response body with io.LimitReader, ensuring that malformed or malicious server responses cannot cause out-of-memory errors during the plist unmarshaling process.

Can I use setupProtocol independently of the Signer implementation?

While technically possible (as shown in the code examples), setupProtocol is designed as an internal unexported type consumed by NewSigner in internal/sap/signer_local.go. Direct usage requires manual management of the SAP runtime state and proper binary payload construction, which the higher-level Signer abstraction handles automatically during NewSigner initialization.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →