Sparkle Documentation

A software update framework for macOS

9,860 stars Objective-C 4 files ยท ~10,045 tokens #framework#macos#software-update#update
RAW Doc

Technical Documentation: sparkle-project/Sparkle

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (`Vendor/ed25519-sparkle/readme.md`)

Ed25519

This is a portable implementation of Ed25519 based
on the SUPERCOP "ref10" implementation. Additionally there is key exchanging
and scalar addition included to further aid building a PKI using Ed25519. All
code is licensed under the permissive zlib license.

All code is pure ANSI C without any dependencies, except for the random seed
generation which uses standard OS cryptography APIs (CryptGenRandom on
Windows, /dev/urandom on nix). If you wish to be entirely portable define
ED25519_NO_SEED. This disables the ed25519_create_seed function, so if your
application requires key generation you must supply your own seeding function
(which is simply a 256 bit (32 byte) cryptographic random number generator).

Performance

On a Windows machine with an Intel Pentium B970 @ 2.3GHz I got the following
speeds (running on only one a single core):

text
Seed generation: 64us (15625 per second)
Key generation: 88us (11364 per second)
Message signing (short message): 87us (11494 per second)
Message verifying (short message): 228us (4386 per second)
Scalar addition: 100us (10000 per second)
Key exchange: 220us (4545 per second)

The speeds on other machines may vary. Sign/verify times will be higher with
longer messages. The implementation significantly benefits from 64 bit
architectures, if possible compile as 64 bit.

Usage

Simply add all .c and .h files in the src/ folder to your project and include
ed25519.h in any file you want to use the API. If you prefer to use a shared
library, only copy ed25519.h and define ED25519_DLL before importing. A
windows DLL is pre-built.

There are no defined types for seeds, private keys, public keys, shared secrets
or signatures. Instead simple unsigned char buffers are used with the
following sizes:

c
unsigned char seed[32];
unsigned char signature[64];
unsigned char public_key[32];
unsigned char private_key[64];
unsigned char scalar[32];
unsigned char shared_secret[32];

API

c
int ed25519_create_seed(unsigned char *seed);

Creates a 32 byte random seed in seed for key generation. seed must be a
writable 32 byte buffer. Returns 0 on success, and nonzero on failure.

c
void ed25519_create_keypair(unsigned char *public_key, unsigned char *private_key,
                            const unsigned char *seed);

Creates a new key pair from the given seed. public_key must be a writable 32
byte buffer, private_key must be a writable 64 byte buffer and seed must be
a 32 byte buffer.

c
void ed25519_sign(unsigned char *signature,
                  const unsigned char *message, size_t message_len,
                  const unsigned char *public_key, const unsigned char *private_key);

Creates a signature of the given message with the given key pair. signature
must be a writable 64 byte buffer. message must have at least message_len
bytes to be read.

c
int ed25519_verify(const unsigned char *signature,
                   const unsigned char *message, size_t message_len,
                   const unsigned char *public_key);

Verifies the signature on the given message using public_key. signature
must be a readable 64 byte buffer. message must have at least message_len
bytes to be read. Returns 1 if the signature matches, 0 otherwise.

c
void ed25519_add_scalar(unsigned char *public_key, unsigned char *private_key,
                        const unsigned char *scalar);

Adds scalar to the given key pair where scalar is a 32 byte buffer (possibly
generated with ed25519_create_seed), generating a new key pair. You can
calculate the public key sum without knowing the private key and vice versa by
passing in NULL for the key you don't know. This is useful for enforcing
randomness on a key pair by a third party while only knowing the public key,
among other things. Warning: the last bit of the scalar is ignored - if
comparing scalars make sure to clear it with scalar[31] &= 127.

c
void ed25519_key_exchange(unsigned char *shared_secret,
                          const unsigned char *public_key, const unsigned char *private_key);

Performs a key exchange on the given public key and private key, producing a
shared secret. It is recommended to hash the shared secret before using it.
shared_secret must be a 32 byte writable buffer where the shared secret will
be stored.

Example

c
unsigned char seed[32], public_key[32], private_key[64], signature[64];
unsigned char other_public_key[32], other_private_key[64], shared_secret[32];
const unsigned char message[] = "TEST MESSAGE";

/* create a random seed, and a key pair out of that seed */
if (ed25519_create_seed(seed)) {
    printf("error while generating seed\n");
    exit(1);
}

ed25519_create_keypair(public_key, private_key, seed);

/* create signature on the message with the key pair */
ed25519_sign(signature, message, strlen(message), public_key, private_key);

/* verify the signature */
if (ed25519_verify(signature, message, strlen(message), public_key)) {
    printf("valid signature\n");
} else {
    printf("invalid signature\n");
}

/* create a dummy keypair to use for a key exchange, normally you'd only have
the public key and receive it through some communication channel */
if (ed25519_create_seed(seed)) {
    printf("error while generating seed\n");
    exit(1);
}

ed25519_create_keypair(other_public_key, other_private_key, seed);

/* do a key exchange with other_public_key */
ed25519_key_exchange(shared_secret, other_public_key, private_key);

/* 
    the magic here is that ed25519_key_exchange(shared_secret, public_key,
    other_private_key); would result in the same shared_secret
*/

License

All code is released under the zlib license. See license.txt for details.


Documentation Design Practices (`Documentation/Design Practices.md`)

Design Practices

XPC Services

XPC services in Sparkle are all optional, so the code involved in the services needs to be usable directly from the framework as well. For this to work well, if the class used in the XPC service takes a delegate, it must not be weakly referenced, so the retain cycle will have to be broken explicitly (via an explicit added invalidate method). dealloc also must not be implemented (do cleanup in custom invalidate method). As one may tell, two of the services are simply proxies (InstallerConnection, InstallerStatus) -- we now recommend most developers to set up a temporary exception with their bundle ID rather than use these two services.

The protocols used in XPC services must also not adopt other protocols (i.e, one protocol inheriting from another protocol). This is because the XPC protocol decoder on older supported systems doesn't properly handle this case, and won't be able to find that methods exist on a class.

Singletons

Singletons and other global mutable variables have been either removed entirely or completely avoided with an exception to backwards compatibility. They have no place in a well architectured framework.

SPUUpdater doesn't maintain singleton instances and can now be properly deconstructed. Note because a caller is not expected to explicitly invalidate an updater, this means that the updater needs to avoid getting into a retain cycle. Intermediate clasess were created for the update cycle and schedule timer to avoid just this.

One may argue that we shouldn't allow multiple live updaters running at the same bundle simultaneously, but I disagree and I think that is missing the point. It also does not account for updaters running external to the process. For example, it may be perfectly reasonable to start an update from sparkle-cli that defers the installation until quit, and have the application that is being updated be able to resume that installation and relaunch immediately.

The original SUUpdater may have also been created to assist plug-ins and other non-app bundles. My advice there is in order to be truly safe, you must not inject a framework like Sparkle into the host application anyway. An external tool that is bundled like sparkle-cli may be more appropriate to use here.

Extensibility

Sparkle 2.0 does not support subclassing classes internal (not exported) to Sparkle anymore. Doing so would be almost impossible to maintain into the future. Subclassing in general has been banned. Composition is preferred everywhere, even amongst the internal update drivers which were rewritten to follow a protocol oriented approach. The reason why composition is preferred is because it's easier to follow the flow of logic.

I hope the user driver API gives enough extensibility without someone wanting to create another fork.

Delegation

Newer classes, other than assisting backwards compatibility, that support delegation don't pass the delegator around anymore. Doing so has some bad consequences and makes code hard to maintain.

Optional delegate methods that have return types need to be optional or have known default values for primitive types.

You may notice that the delegate and user driver are not accessible as properties from SPUUpdater. This is intentional. The methods that belong to these types aren't meaningful to any caller except from internal classes.

Decoupling

Two software components should not directly know about each other. Preferably they wouldn't know about each other at all, but if they must, they can use the delegation pattern with a declared protocol.

See Documentation/graph-of-sparkle.png for a graph of how the code looks like currently. This was generated via objc_dep (great tool). Note that there are no red edges which would mean that two nodes know of each other.

Attributes & Code Size

Instance variables and instance variable access should be used for private members (declared in @implementation block) whenever possible over properties. Preferring instance variables over properties for internal usage can significantly reduce code size. Instance variables should also be ordered by having the larger sized data members declared first.

SPU_OBJC_DIRECT_MEMBERS should be used for any internal class in Sparkle and SPU_OBJC_DIRECT should be used for any other internal methods to Sparkle that doesn't need to utilize the Obj-C runtime to reduce code size. Note these attributes should not be used for any class or method that is exported to the developer (this includes private headers / APIs we carefully decided to expose too). For internal methods and classes that are also used by our Swift tools or unit tests, we may not expose them as direct specifically when building for those targets.

nonatomic should really be used wherever possible with regards to obj-c properties (atomic is a bad default). readonly should be used wherever possible as well, which also implies that only ivar access should be used in initializers. NS_ASSUME_NONNULL_BEGIN and NS_ASSUME_NONNULL_END should be used around new headers whenever possible. AppKit prevention guards should be used for any non-UI class whenever possible.

Sparkle has several feature flags in ConfigCommon.xcconfig (e.g. SPARKLE_BUILD_LEGACY_SUUPDATER, SPARKLE_BUILD_LEGACY_DSA_SUPPORT, SPARKLE_BUILD_UI_BITS, etc). This allows disabling any combination of these features and building Sparkle with a more minimal feature set. These flags (with the exception for stripping UI bits, localizations, or XPC Services) are for disabling features that are legacy or not recommended to use for most applications. Note when altering these flags, OTHER_SWIFT_FLAGS_COMMON may need to be updated appropriately too.


Documentation Installation (`Documentation/Installation.md`)

Details on Installer IPC & Security

Important components:

  • The bundle to update & replace
  • The application to listen for termination & to relaunch. This can be the same bundle as the one being updated, but this doesn't have to be the case (eg: the bundle being updated could be a plug-in that is hosted by another application).
  • The updater that lives in Sparkle.framework - this schedules & downloads updates and starts the installer. This updater's life can be tied to the application's lifetime, but this doesn't have to be the case.
  • The installer (Autoupdate) which does all the extraction, validation, and installation work. This program is submitted by the updater and is run as a launchd agent or daemon, depending on the type of installation and if elevated permissions are needed to perform the update.
  • The progress or agent application which hosts the installation status service and is also responsible for showing UI if the installation takes a sufficiently long period of time, as well as responsible for launching the new update.

After downloading the update:

Launching the Installer

SPUCoreBasedUpdateDriver invokes SPUInstallerDriver's extraction method which invokes SUInstallerLauncher's launch installer method. An XPC Service may be used to launch the installer if necessary for Sandboxed applications.

First, if the updater driver that invoked the launcher doesn't allow for interaction (in particular, the automatic/silent update driver does not allow interaction), then the launcher fails with a hint to try again with interaction enabled (in particular, with a UI based update driver). If this happens, the updater keeps a reference to the downloaded update and appcast item that is used for resuming with a UI based update driver later. The launcher in this case is telling that authorization can only be done if interaction is allowed from the driver, so the user can enter their password to start up the installer as root to install over a location that requires such privileges.

The launcher then looks for the installer and agent application. If we are inside the XPC Service, a relative path back to the framework auxiliary directory is computed otherwise the bundle's auxiliary directory path is retrieved. Once those paths are found, the launcher copies the progress agent application to a temporary cache location. The installer is not copied because it is a plain single executable that does not rely on external relative dependencies, whereas the agent application relies on bundle resources. This is important to consider because the old application may be moved around and removed when the installation process occurs. Leaving the installer inside its bundle also could leave it in a place the user doesn't necessarily have write privileges to.

The installer is first submitted. If it requires root privileges, it will ask for an admin user name and password. How it determines if root privileges are necessary is based on:

  • If the installation type from the appcast is a guided or regular package based installer, then root privileges are necessary because the system installer utility has to run as root.
  • Otherwise the installation type is a normal application update, and root privileges are only needed if write permission or changing the owner is currently insufficient.

The installer makes sure, after extraction, that the expected installation type from the appcast matches the type of installation found within the archive. So this hint from the appcast is not just trusted implicitly.

If the user cancels submitting the installer on authorization (if it's required), the launcher aborts the installation process silently. If everything goes alright, the progress agent application is submitted next. If either of the submissions fail, the installation fails and aborts, otherwise the launcher succeeds and its job is done. Note before the submissions are done, the jobs are removed from launchd, and the jobs are submitted in a way that act as "one-time" jobs. If they fail or succeed, the jobs aren't restarted, and the jobs aren't installed in a system location for launchd to attempt launching again.

The most important argument passed to the installer is the host bundle identifier of the bundle to update. This bundle identifier is used so that the installer and updater know what Mach service to connect to for IPC. Mostly everything else to the installer is sent via a XPC connection. The host bundle path is passed to the progress agent, which is just used for obtaining the bundle identifier, and for UI purposes (eg: displaying app icon and name when the app needs to show progress). The type of domain the installer is running in (user vs system) is also passed to the progress agent application so it can know which domain to use to connect to the installer.

Timeouts

Upon starting the progress agent, it tries to establish a connection to the installer and send a "hello" message expecting a reply back. If a reply isn't received back within a certain threshold, the agent application aborts (unless it's already planning to terminate in the near future after relaunching the new update).

If the installer doesn't receive the "hello" message from the agent in a certain threshold, the installer aborts. If the installer doesn't receive the installation input data from the data in a certain threshold also, it aborts there too. If the installer aborts and the agent is connected to the installer, the agent will abort as well.

When the updater attempts to set up a connection to the installer, after a certain threshold, if the installation hasn't progressed from not beginning (i.e, extraction hasn't begun), then the updater aborts the installation.

Installation Data

After the installer launches, the updater creates a connection to the installer and sends installation data to the installer using the SPUInstallationData message. This data includes the bundle application path to relaunch, the path to the bundle to update and replace, Ed(DSA) signature from the appcast item, decryption password for dmg if available, the path to the downloaded directory and name of the downloaded item, and the type of expected installation.

The installer takes note of all this information, but it moves the downloaded item into a update directory it chooses for itself. This is for two reasons:

  1. If the installer is running at a higher level than the process that submitted it (eg: sandboxed -> user, or user -> root), it make sense from a security perspective to move the file into its own support directory. This could prevent an attacker from manipulating the download while the installer is using it.
  2. The updater, or another updater running, or someone else probably won't accidently remove the downloaded item if the installer decides to 'own' it.

After the installer moves the downloaded item, it makes sure the item is not a symbolic link. If it is a non-regular file, the installer aborts, causing the updater to abort as well. Note this check is done after the move rather than before due to potential race conditions an attacker could create.

Update Extraction

After the installer receives the input installation data, it starts extracting the update. The installer first sends a SPUExtractionStarted message.

If the unarchiver requires EdDSA validation before extraction (binary delta archives do) or if the expected installation type is a package type, then before starting the unarchiver, validation is checked to make sure the signature for the archive is valid. If the signature is not valid, then a SPUArchiveExtractionFailed message is sent to the updater (see below on what happens after that). We can't require validation before extracting for normal application updates because they allow changes to EdDSA keys. Delta updates are fine however because if those fail, then a full update can be tried.

After extraction starts, one or more SPUExtractedArchiveWithProgress messages indicating the unarchival progress are sent back to the updater. On failure, the installer will send SPUArchiveExtractionFailed. In the updater, if the update is a delta update and extraction fails, then the full archive is downloaded and we go back to the "Sending Installation Data" section to re-send the data and begin extraction again. If the update is not a delta update and extraction fails on the other hand, then the updater aborts causing the installer to abort as well.

Post Validation

If the unarchiving succeeds, a SPUValidationStarted message is sent back to the updater, and the installer begins post validating the update.

If validation was already done before extraction, just the code signature is checked on the extracted bundle to make sure it's valid (if there's a code signature on the bundle). This is just a consistency check to make sure the developer didn't make a careless error.

If validation fails, the installer aborts, causing the updater to abort the update as well. Otherwise, the installer sends a message SPUInstallationStartedStage1 to the updater and begins the installation.

Retrieving Process Identifier

If the installer hasn't made a connection to the agent yet within the time threshold, the installer waits until a connection is made. Otherwise, the installer asks the agent for the process identifier of the application bundle that it wants to wait for termination. If the installer doesn't get a response within a certain threshold, the installer aborts.

When the installer gets the process identifier, it begins the installation. At this point on, the installer will try to perform the installation even if the connection to the updater or the connection to the agent invalidates.

Starting the Installation

The installer figures out what kind of installer to use (regular, guided pkg) based on the type of file inside the archive. If the type of file doesn't match the expected installation type from the input installation data, then the installer aborts causing the updater to abort as well.

Once the type of installer is found, the first stage of installation is performed:

  • Regular application installer 1st stage: Makes sure this update is not a downgrade and do all initial installation work if possible (such as clearing quarantine, changing owner/group, updating modification date, invoking GateKeeper scan). This initial work is not done if the bundle has to transfer to another volume; in this case, this work will be done in a later stage.
  • Guided Package installer 1st stage: Does nothing.

If the first stage fails, the installer aborts causing the updater to abort the update.

Otherwise a SPUInstallationFinishedStage1 message is sent back to the updater along with some data. This data includes whether the application bundle to relaunch is currently terminated, and whether the installation at later stages can be performed silently (that is, with no user interaction allowed). If we reach here, only the interactive package installer can't be performed silently (however interactive pkg installer is no longer supported).

The installer then listens and waits for the target application to relaunch terminates. If it is already terminated, then it resumes to stage 2 and 3 of the installation immediately on the assumption that the installer does not have permission to show UI interaction to the user. Thus if the installer has to show user interaction here and hasn't received an OK from the updater (it won't if the target application is already terminated), the install will fail. If the target is already terminated, the installer will also assume that the target should not be relaunched after installation.

Installation Waiting Period

The updater receives SPUInstallationFinishedStage1 message. The updater sends a message SPUSentUpdateAppcastItemData with the appcast data in case the updater may request for it later (due to installer resumability, discussed later). It also reads if the target has already been terminated (implying that the installer will continue installing the update immediately), and if the installation will be done silently.

For UI based update drivers, the updater tells the user driver to show that the application is ready to be relaunched - the user can continue to install & relaunch the app. The user driver is only alerted however if the installation isn't happening immediately (that is, if the target application to relaunch is still alive). The user driver can decide whether to a) install b) install & relaunch or c) delay installation. If installation is delayed, it can be resumed later, or if the target application terminates, the installer will try to continue installation if it is capable to without user interaction.

For automatic based drivers, if the update is not going to be installed immediately and if it can be installed silently, the updater's delegate has a choice to handle the immediate installation of the update. If the delegate handles the installation, it can invoke a block that will trigger the automatic update driver to tell the installer to resume to stage 2 as detailed in step the "Continue to Installation" section - except without displaying any user interface and by relaunching the application afterwards. If the delegate handles the immediate installation, the automatic update driver will not abort, it will just leave the driver running until the installer requests for the app to be terminated later. This means the update can't be resumed later and the user driver won't be involved.

Otherwise if the updater delegate doesn't handle immediate installation for automatic based drivers (assuming still the update is not going to be installed immediately), the update driver is aborted; the installer will still wait for the target to terminate however. If the update cannot be silently installed or if the update is marked as critical from the appcast, the update procedure is actually 'resumed' as a scheduled UI based update driver immediately. The update driver can also be 'resumed' later when the user initiates for an update manually or when a long duration (I think a week) has passed by without the user terminating the application. Note automatic based drivers are unable to do a resume, so only UI based ones can.

If an update driver is resumed (which cannot happen if the target application is already terminated by the way), then the updater first requests the installer for the appcast item data that the installer received before. The updater does this by creating a temporary distinct connection for the purpose of querying for the installation status. The connection will give up if a short timeout passes. If the updater fails to retrieve resume data, it assumes that there's no update to resume and will start back from the beginning. The updater can use this data for showing release notes, etc. Note the updater and target application don't have to live in the same process, and the updater could choose to terminate and resume later as a new process - so having the installer keep the appcast item data is nice.

Afterwards the resumed update driver then allows the user driver to decide whether to a) install the update now, b) install & relaunch the update, or to c) delay the update installation and abort the update driver. Note we are now back to the same options discussed earlier.

Continue to Installation

If the user driver decides to install the update, it sends a SPUResumeInstallationToStage2 message to the installer and supplies whether the update should be relaunched, and whether user interface can be displayed. The user driver specifies that the user interface can be displayed as long as the updater delegate allows interaction. If the updater's delegate handles immediate installation in the automatic based update driver, UI cannot be displayed there too.

The installer receives SPUResumeInstallationToStage2 and reads whether it should relaunch the target application and whether it can show UI (thus be allowed to show user interaction). The installer then resumes to stage 2 of the installation if it has not been performed already (that is if the target app already terminated). Note if the installer doesn't receive this message before the target application terminates, then the installer will not relaunch or show UI and resume stage 2 & 3 by itself. Again not displaying UI is only an issue for interactive based package installers which are no longer supported.

If the 2nd stage succeeds, the installer sends a SPUInstallationFinishedStage2 message back to the updater, including if the target application has already terminated at this time. If the target application has not already been terminated, the installer sends a request to the progress agent to terminate the application. By terminate, we mean sending an Apple quit event, allowing the application or user to possibly cancel or delay termination.

The updater receives a SPUInstallationFinishedStage2 message, and reads if the target application had already been terminated. If the target application has not already terminated, the updater tells the user driver to show that the application is being sent a termination request.

In the case termination of the application is delayed or canceled, the user driver may re-try sending a SPUResumeInstallationToStage2 message to the installer. This time the installer will recognize it already completed stage 2. If it hasn't proceeded onto stage 3 it will send another quit event to the application. This re-try can be done multiple times.

Showing Progress

When the target application is terminated, if the updater allowed the installer to show UI progress and the installation type doesn't show progress on its own (only interactive package installer shows progress on its own, but these are no longer supported), then the installer sends a SPUUpdaterAlivePing message to the updater. If the updater is still alive by now and receives the message, the updater will then send back a SPUUpdaterAlivePong message. This lets the installer know that the updater is still active after the target application is terminated, and whether the installer should later be responsible for displaying updater progress or not if a short time passes by, and the installation is still not finished. If the updater is still not alive, then the installer should be responsible for showing progress if otherwise allowed.

If the installer decides to show progress, it sends a message to the progress agent to show progress UI. Under most circumstances, the installation will finish faster than this point is reached however (exceptions may be guided package installers and updates over a remote network mount). If the connection to the updater is still connected after this short time passes (eg: like with sparkle-cli), then it's the updater's job to show progress instead.

Finishing the Installation

The installer starts stage 3 of the installation after the target application is terminated as well. The third stage does the final installation for updating and replacing the new updated bundle.

If the third stage fails, then the installer aborts, causing the updater driver to abort if it's still running. The target application is not ever relaunched on failure.

Otherwise if the third stage succeeds, the installer sends a message to the agent to stop showing progress. The agent uses this hint to acknowledge that it should stop broadcasting the status info service for the updater. If the connection to the updater is still alive, a SPUInstallationFinishedStage3 message is sent back to the updater and the updater driver silently aborts the update.

The installer then sends a message to the agent to relaunch the new application if the installer was requested to relaunch it. This also signals to the agent that it will terminate shortly. If the installer decides not to relaunch the update, the agent will terminate when its connection to the installer invalidates. The installer lastly does cleanup work by removing its update directory it was using and exits.

When the progress agent exits, it cleans up by removing itself from disk. The installer doesn't do that because the installer isn't copied to another location in the first place.

Notes:

  • Performing an update as the root user is not currently supported. The command line driver (minorly) and the agent application link to AppKit, and running the agent as a different user (i.e, dropping from root -> logged in user) is not particularly the most elegant idea.

  • We use IPC in such a way that the installer process does not trust the updater process, which is why the installer does extraction, validation, and installation -- all in a single process. The launcher portion of code (which could be running as a XPC service) also does not trust the updater for determining the paths to the installer and agent tool. This portion of code does not check the code signing signature of the installer or agent though for reasons explained in the code.

  • We have timeouts and ping/pong messages in various cases because we don't want to trust the other end very much for communicating back quickly. On older systems in my testing (eg: 10.8), connection invalidation events don't even get sent to the other end when interacting with launchd, possibly due a bug that is outputted in Console.app.

  • When we terminate the application before doing final installation work, we send an Apple quit event to all running application instances we find under the agent's GUI login session. This does not include running instances from other logged in sessions -- that may require authenticating all the time, so trade off is not worthwhile. Also currently the installer only handles a single process identifier to watch for until termination, and certainly doesn't handle the case if more instances are launched afterwards. Note all of this only applies to instances being launched from the same bundle path, and the consequence may rarely be updating a bundle when a running instance is still left alive.

  • SMJobBless() would be interesting to explore and perhaps more suitable for sparkle-cli than ordinary application updaters because using this API is less appropriate for one-off installations. Furthermore, the installer would have to change to persist rather than be a one-time install, and may have to handle multiple connections for installing multiple bundles simultaneously.


Documentation Security (`Documentation/Security.md`)

Security

First, some references I've found to be quite useful:

Sparkle 2.0 puts a huge emphasis on splitting Sparkle into several components to achieve privilege separation.

  • User Driver (Application; AppKit permitted)
  • Updater Scheduler (Framework)
  • XPC Services (Instead of some portions of Framework)
  • Progress Agent (Application; AppKit permitted)
  • Installer (Agent and Daemon safe)

These achieve privilege separation, because at least theoretically, they can all be placed into different processes from one another (except for the user driver & updater scheduler).

For XPC Services, it's significant to understand they can be used independent of sandboxing (but this is strongly discouraged). Besides privilege separation, they also have an impact on fault tolerance and termination.

We have code that detects whether or not XPC services are available and enabled by the main application bundle. This is simpler and more efficient than attempting to create a connection and wait for a timeout. We don't have any checks for seeing if the "current process" is sandboxed; doing so is a rather brittle. The XPC Services are important, not the sandboxing.

I said above that XPC services are looked up in the main bundle. One may think this assumption doesn't hold for helpers or plug-ins inside applications. My response to that is they probably don't need the services, and they probably shouldn't be injecting Sparkle inside the host application anyway due to unintended conflicts and other consequences. A more appropriate approach may be to bundle a separate tool such as the sparkle command line utility.

The InstallerLauncher XPC service needs a JoinExistingSession key set to YES otherwise authorization will not work properly, and even less so on older systems. It took me forever to debug this, so it's worth mentioning.

When developing Sparkle 2.0, I still had a huge hole in the privilege separation even after supporting XPC Services and sandboxing. I fixed that by removing references to AuthorizationExecuteWithPrivileges and submitting a launchd agent/daemon for the installer. This is overlooked, but absolutely essential for our model to be secure.

When communicating to a launchd job, we've several options to use for IPC such as BSD Sockets, Mach ports, XPC (note: XPC is an IPC API and is != "XPC Services"), and file IO. The technote I linked above describes why Mach ports have user namespace issues and why BSD sockets may be preferred over them. However, now, XPC which allows you to choose which domain to look up is the simplest, best, and most modern choice. It's crucial in any event we don't have a daemon (running in system domain) try to connect to something in the user domain. Our launchd tasks should also be connected to rather than the other way around. As for communication via file IO, I just wouldn't go anywhere close to trusting such a model mixing in system and user running processes.

For sending Obj-C objects across the wire, we use SPUSecureCoding because Cocoa doesn't support sending objects securely out of the box unless working with XPC Services. It's very important the objects implement NSSecureCoding and whitelist types that are expected to be decoded, as well as whitelist types inside collections before decoding them.

The installer handles extraction, validation, and installation of the update. This was the main reason why other sandboxed-capable forks were rejected from being secure. The point to be stressed anyway is that these all need to be done in the same process; they can't be handed off by some other process. This is also why removing references to AuthorizationExecuteWithPrivileges is crucial as well.

Note that the installer does not handle downloading. That is handled by the updater framework. This is ideal because downloading can be done without disrupting the user, allowing it to be done silently without presenting an authorization dialog. The downloading portion of code can also be stuck into a XPC service with just an entitlement for allowing incoming connections. This also means for secure installations the installer cannot know (or trust) what protocol the update was downloaded from (i.e: http vs https). A good installer will not care, which is why applying updates without a EdDSA signature/key is now deprecated (reminder that Apple's code signature checks are not intended for complete integrity).

The XPC Services and Autoupdate, if code signed, also check if the clients are code signed with the same team ID and may reject connections or reject policies if this is not the case. This is a "backup" check because we still do not want endpoints to open up and allow arbitrary operations across different levels of security boundaries, even if apps are not code signed.


--- METRICS ---