Code signing in Android's security model


Baca Juga

In the previous post we introduced code signing as implemented in Android and saw that it is practically identical to JAR signing. Android requires all installed packages to be signed and makes heavy use of the attached code signing certificates in its security model. This is where the major differences with other platforms that use code signing lie, so we will explore the topic in more detail.

Java access control

Before we start digging into Android's security model, let's go through a quick overview of the corresponding features of the Java platform. Java was initially designed to support running potentially untrusted code, downloaded from a public network (mostly applets). The initial applet sandbox model was extended to a more flexible, policy-based scheme where different permissions can be granted based on the code's origin and author. Code origin refers to the place where classes are loaded from, typically a local file or a remote URL, while authorship is asserted via code signatures and is represented by the signer's certificate chain. Combined those two properties define a code source. Each code source is granted a set of permissions based on a policy, the default implementation being to read rules from a policy file (created with the policytool). At runtime a security manager (if installed) enforces access control by comparing code elements on the stack with the current policy. It throws a SecurityException if the permissions required to access a resource have not been granted to the requesting code source. Java code that runs (or is started in) the browser, such as applets or Java Web Start applications, is automatically run with a security manager installed, while for local applications you need to explicitly set the java.security.manager in order to install one. In practice, a security manager for local code is only used with some applications servers, and it is usually disabled by default. A wide range of permissions are supported by the platform, the major ones being file and socket-oriented, as well as different types of runtime permissions which control operations ranging from class and library loading to managing the current security manager. By defining multiple code sources and assigning each one specific permissions one can implement fine grained access control for both local and remote code.

As we mentioned though, unless you are in the browser plugin or application server development business chances are you hadn't heard about any of this until the beginning of this year. Just when everyone thought that Java applets were for all intents and purposes dead, they made somewhat of a comeback as a malware distribution medium. A series of vulnerabilities were discovered in the Oracle Java implementation that allow applets to escape the sandbox they run in and reset the security manager, effectively granting themselves full privileges. The exploits used to achieve this employ techniques ranging from reflection recursion to direct memory manipulation to bypass runtime security checks. Oracle has responded by releasing a series of patches, changing the default applet execution policy and introducing more visible warnings to let users know that potentially harmful code is being executed. Naturally, different ways to bypass this are being discovered to catch up.

In short, Java has had full-featured code access control for some time, even though the most widely used implementation appears to be lacking in enforcing it. But let's (finally!) get back to Android now. As the Java code access control mechanism can use code signer identity to define code sources and grant permissions, and Android code is required to be signed, one might expect that our favourite mobile OS would be making use of the Java's security model in some form, just as it does with JAR files. As it turns out, this is not the case. Access control related classes are part of the Java API, and are indeed available in Android. However, looking at the implementation reveals that they are practically empty, with just enough code to compile. In addition, they feature a prominent 'Legacy security code; do not use.' notice. So why bother reviewing all of the above then? Even though Android's access control model is very different from the legacy Java one, it does borrow some of the same ideas, and a comparison is helpful when discussing the design decisions made.

Android security architecture basics

Before we discuss the role of code signing in Android's security model, let's say a few words about Android's general security architecture. As we know, Android is Linux-based and relies heavily on traditional UNIX features to implement its security architecture. Each application runs in a separate process with a distinct identity (user ID, UID). By default apps cannot modify each other's resources and this is enforced by Linux which doesn't allow different processes to access memory or files they don't own (unless access is explicitly granted by the owner, a.k.a discretionary access control). Additionally, each app (UID) is granted a set of logical permissions at install time, and cannot perform operations (call APIs) that require permissions it doesn't have. This is the biggest difference compared to the 'standard' Java permission model: code from different sources running in a single process cannot have different permissions, since permissions are granted at the UID level. Most permissions cannot be dynamically granted after the package has been installed, however as of 4.2 a number of 'development' permissions (e.g., READ_LOGS, WRITE_SECURE_SETTINGS) have been introduced that can be granted or revoked on demand using the pm grant/revoke command (or matching system APIs). The system will show a confirmation dialog showing permissions requested by an app before installing. With the exception of the new 'development' permissions, all requested permissions are permanently granted if the the user allows the install. For a certain messaging app it looks like this in Jelly Bean:



Android permissions are typically implemented by mapping them to Linux groups that have the necessary read/write access to relevant system resources (files or sockets) and thus are ultimately enforced by the Linux kernel. Some permissions are enforced by system daemons or services by explicitly checking if the calling UID is whitelisted to perform a particular operation. The network access permission (INTERNET) is somewhat of a hybrid: it is mapped to a group (inet), but since network access is not associated with one particular socket, the kernel checks whether processes trying to open a socket are members of the inet group on each related system call (known as 'paranoid network security').

Each permission has an associated 'protection level' that indicates how the system proceeds when deciding whether to grant or deny the permission. The two levels most relevant to our discussion are signature and signatureOrSystem. The former is granted only to apps signed with the same certificate as the package declaring the permission, while the latter is granted to apps that are in the Android system image, even if the signer is different.

Besides the built-in permissions, custom permissions can also be defined by declaring them in the app manifest file. Those can be enforced statically by the system or dynamically by app components. Permissions attached to components (activities, services, broadcast receivers or content providers) defined in AndroidManifest.xml are automatically enforced by the system. Components can also make use of framework APIs to check whether the calling UID has been granted a required permissions on a case-by-case basis (e.g., only for write operations, etc.). We will introduce other permission related details as necessary later, but you can refer to this Marakana presentation for a more complete and thorough discussion of Android permissions (and more). Of course, some official documentation is also available.

The role of code signing

As we saw in the previous article, Android code signing is based on Java JAR signing. Consequently, it uses public key cryptography and X.509 certificates as do a lot of other code signing schemes. However, this is where the similarities end. In practically all other platforms that use code signing (for example Java ME), code signing certificate needs to be issued by a CA that the platform trusts. While there is no lack of CAs that issue code signing certificates, in reality it is quite difficult to obtain a certificate that will be trusted by all targeted devices. Android solves this problem quite simply: it doesn't care about the actual signing certificate. Thus you do not need to have it issued by a CA (although you could, and most will happily take your money), and virtually all code signing certificates used in Android are self-signed. Additionally, you don't need to assert your identity in any way: you can use pretty much anything as the subject name (the Google Play store does have a few checks to weed out some common names, but not the OS itself). Signing certificates are treated as binary blobs by Android, and the fact that they are in X.509 format is merely a consequence of using the JAR format. Android doesn't validate certificates as such: if the certificate is not self-signed, the signing CA's certificate does not have to be present, yet alone trusted; it will also happily install apps with an expired signing certificate. If you are coming from a traditional PKI background, this may sound like heresy, but try to keep an open mind and note that Android does not make use of PKI for code signing.

So what are code signing certificates used for then? Two things: making sure updates for an app are coming from the same author (same origin policy), and establishing trust relationships between applications. Both are implemented by comparing the signing certificate of the currently installed target app with the certificate of the update or related application. Comparison boils down to calling Arrays.equals() on the binary (DER) representation of both certificates. This method naturally knows nothing about CAs or expiration dates. One consequence of this is that once an app (identified by a unique package name) is installed, updates need to use the exact same signing certificates (with one exception, see next section). While multiple signatures on Android apps are not common, if the original application was signed by more than one signer, any updates need to be signed by the same signers, each using its original signing certificate. This means that if your signing certificate(s) expires, you cannot update your app and need to release a new one instead. This would result in not only losing any existing user base or ratings, but more importantly losing access to the legacy app's data and settings (again, there are some exceptions). The solution to this problem is quite simple: don't let your certificate expire. The currently recommended validity period is at least 25 years, and the Google Play Store requires validity until at least October 2033 (Y2K33?). While technically this only amounts to putting off the problem, proper certificate migration support might eventually be added to the platform. Unfortunately, this means that if your signing key is lost or compromised, you are currently out of luck.

Let's examine the major uses of code signing in Android in detail.

Application authenticity and identity

In Android all apps are managed by the system PacakgeManagerService, no matter if they are pre-installed, downloaded from an app market or side loaded. It keeps a database of currently installed apps, including their signing certificate(s), granted permissions and additional metadata in the /data/system/packages.xml file. A typical entry for a user-installed app might look like this:

<package codepath="/data/app/com.chrome.beta-2.apk" 
flags="572996" ft="13e20480558"
installer="com.android.vending"
it="13ca981cbe3" name="com.chrome.beta"
nativelibrarypath="/data/app-lib/com.chrome.beta-2"
userid="10092" ut="13e204816ce" version="1453060">
<sigs count="1">
<cert index="8">
</cert>
</sigs>
<perms>
<item name="android.permission.NFC"/>
...
<item name="com.android.browser.permission.READ_HISTORY_BOOKMARKS"/>
</perms>
</package>

As you can see above, a package entry specifies the package name, the location of the APK and associated libraries, assigned UID and some additional install metadata such as install and update time. This is followed by the number of signatures and the signing certificate as a hexadecimal string. Since a hex-encoded certificate will usually take up around 2K, the actual certificate contents is listed only once. All subsequent packages signed with the same certificate only refer to it by index, as is the case above. The PackageManagerService uses the <cert/> values in packages.xml to decide whether an update is signed with the same certificate as the original app. The certificate is followed by the list of permissions the package has been granted. All of this information is cached on memory (keyed by package name) at runtime for performance reasons.

Just like user-installed apps, pre-installed apps (usually found in /system/app) can be updated without a full-blown system update, usually via the Play Store or a similar app distribution service. As the /system partition is mounted read-only though, updates are installed in /data, while the original app remains as is. In addition to a <package/> entry, such an app will also have a <updated-package> entry that might look like this:

<updated-package name="com.google.android.youtube" 
codePath="/system/app/YouTube.apk"
ft="13cd6667b50" it="13ae93df638" ut="13cd6667b50"
version="4216"
nativeLibraryPath="/data/app-lib/com.google.android.youtube-1"
userId="10067">
<perms>
<item name="android.permission.NFC" />
...
</perms>
</updated-package>

The update (in /data/app) inherits the original app's permissions and UID. System apps receive another special treatment as well: if an updated APK is installed over the original one (in /system/app) it is allowed to be signed with a different certificate. The rationale behind this is that if the installer has enough privileges to write to /system, it can be trusted to change the signing certificate as well. The UID, and any files and permissions are retained. Again, there is an exception though: if the package is part of a shared user (discussed in the next section), the signature cannot be updated, because that would affect other apps as well. In the reverse case, when a new system app signed by a different certificate than that of the currently installed non-system app (with the same package name), the non-system app will be deleted first.

Speaking of system apps, most of those are signed by a number of so called 'platform keys'. There are four different keys in the current AOSP tree, named platform, shared, media and testkey (releasekey for release builds). All packages considered part of the core platform (System UI, Settings, Phone, Bluetooth etc.) are signed with the platform key, launcher and contacts related packages -- with the shared key, the gallery app and media related providers -- with the media key, and everything else (including packages that don't explicitly specify the signing key) -- with the testkey. One thing to note is that the keys distributed with AOSP are in no way special, even though they have 'Google' in the certificate DN. Using them to sign your apps will not give you any specific privileges, you will need the actual keys Google or your carrier/device manufacturer uses. Even though the associated certificates may happen to have the same DN as the ones in AOSP, they are different and very unlikely to be publicly accessible. Custom ROMs are often an exception though, and some, including CyanogenMod, use the AOSP keys, or publicly available keys, as is (there are plans to change this for CyanogenMod though). Sharing the signing key allows packages to work together and establish trust relationships, which we will discuss next.

Inter-application trust relationships

Signature permissions

As we mentioned above, Android permissions (system or custom) can be declared with the signature protection level. With this level, the permission is only granted if the requesting app is signed by the same signer as the package declaring the permission. This can be thought of as a limited form of mandatory access control (MAC). For custom (app-declared) permission, permissions are declared in the package's AndroidManifest.xml file, and are added to the system when it is installed. Just as other package data, permissions are saved in the /data/system/packages.xml file, as children of the <permissions/> element. Here's how the declaration of a custom permission used by some Google apps looks like:

<permissions>
..
<item name="com.google.android.googleapps.permission.ACCESS_GOOGLE_PASSWORD"
package="com.google.android.gsf.login"
protection="2" />
...
</permissions>

The entry has the permission name, declaring package and protection level (2 corresponds to signature) as attributes. When installing a package that requests this permission, the PackageManagerService will perform binary comparison (just as when upgrading packages) of its signing certificate against the certificate of the Google Login Service (the declaring package, com.google.android.gsf.login) in order to decide whether to grant the permission. A noteworthy detail is that the system cannot grant a permission it doesn't know about. That is, if app A declares permission 'foo' and app B uses it, app B needs to be installed after app A, otherwise you will get a warning at install time and the permission won't be granted. Since app installation order typically cannot be guaranteed, the usual workaround for this situation is to declare the permission in both apps. Permissions can also be added and removed dynamically using the PackageManger.addPermission() API (know as 'dynamic permissions'). However, packages can only add permissions to a permission tree they define (i.e., you cannot add permissions to another app).

That mostly explains custom permissions, but what about built-in, system permissions with signature protection level? They work exactly as custom permissions, except that the package that defines them is special. They are defined in the android package, sometimes also referred as 'the framework' or 'the platform'. The core android framework is the set of classes shared by system services, some of them exposed via the public SDK. Those are packaged in JAR files found in /system/framework. Interestingly, those JAR files are not signed: while Android borrows the JAR format to implement code signing, only APK files are signed, not actual JARs. The only APK file in the framework directory is framework-res.apk. As the name implies, it packages framework resources (animation, drawables, layouts, etc.), but no actual code. Most importantly, it defines the android package and system permissions. Thus any app trying to request a system-level signature permission needs to be signed with the same certificate as the framework resource package. Not surprisingly, it is signed by the platform key discussed in the previous section (usually found in build/target/product/security/platform.pk8|.x509.pem). The associated certificate may looks something like this for an AOSP build:

Version: 3 (0x2)
Serial Number: 12941516320735154170 (0xb3998086d056cffa)
Signature Algorithm: md5WithRSAEncryption
Issuer: C=US, ST=California, L=Mountain View, O=Android, OU=Android,
CN=Android/emailAddress=android@android.com
Validity
Not Before: Apr 15 22:40:50 2008 GMT
Not After : Sep 1 22:40:50 2035 GMT
Subject: C=US, ST=California, L=Mountain View, O=Android, OU=Android,
CN=Android/emailAddress=android@android.com

Shared user ID

Android provides an even stronger inter-app trust relationship than using signature permissions:  the ability for different apps to run as the same UID, and optionally in the same process. It is usually referred to as 'shared user ID'. This feature is extensively used by core framework services and system applications, and while the Android team does not recommend that third-party application use it, it is available to user applications as well. It is enabled by adding the android:sharedUserId attribute to AndroidManifest.xml's root element. The 'user ID' specified in the manifest needs to be in Java package format (containing at least one '.') and is used as an identifier, much like package names for applications. If the specified shared UID does not exist it is simply created, but if another package with the same shared UID is already installed, the signing certificate is compared to that of the existing package, and if they do not match, a INSTALL_FAILED_SHARED_USER_INCOMPATIBLE error is returned and installation fails. Adding the sharedUserId to the new version of an already installed app will cause it to change its UID, which would result in losing access to its own files (that was the case in some previous Android versions). Therefore, this is disallowed by the system, and it will reject the update with the INSTALL_FAILED_UID_CHANGED error. In short, if you plan to use shared UID for your apps, you have to design for it from the start, and have them use it since the very first release.

A shared UID is a first class object in the system's packages.xml and is treated much like apps are: it has associated signing certificate(s) and permissions. Android has 5 built-in shared UIDs, automatically added when the system is bootstrapped:
  • android.uid.system (SYSTEM_UID, 1000)
  • android.uid.phone (PHONE_UID, 1001)
  • android.uid.bluetooth (BLUETOOH_UID, 1002)
  • android.uid.log (LOG_UID, 1007)
  • android.uid.nfc (NFC_UID, 1027)

Here's how the system shared UID is defined:

<shared-user name="android.uid.system" userId="1000">
<sigs count="1">
<cert index="4" />
</sigs>
<perms>
<item name="android.permission.MASTER_CLEAR" />
<item name="android.permission.CLEAR_APP_USER_DATA" />
<item name="android.permission.MODIFY_NETWORK_ACCOUNTING" />
...
<shared-user/>

As you can see, apart from having a bunch of scary permissions (about 60 on a 4.2 device), the declaration is very similar to the package declarations we showed previously. Conversely, packages that are a part of a shared UID, do not have an associated granted permission list. They inherit the permissions of the shared UID, which are a union of the permissions requested by all currently installed packages with the same shared UID. A side effect of this is, that if a package is part of a shared UID, it can access APIs it hasn't explicitly requested permissions for, as long as some package with the same shared UID has already requested them. Permissions are dynamically removed from the <shared-user/> declaration as packages are installed or uninstalled though, so the set of available permissions is neither guaranteed nor constant. Here's how the declaration of a system app (KeyChain) that runs under a shared ID looks like. It references the shared UID with the sharedUserId attribute and lacks explicit permission declarations:

<package name="com.android.keychain" 
codePath="/system/app/KeyChain.apk"
nativeLibraryPath="/data/app-lib/KeyChain"
flags="540229" ft="13cd65721a0"
it="13c2d4721f0" ut="13cd65721a0"
version="17"
sharedUserId="1000">
<sigs count="1">
<cert index="4" />
</sigs>
</package>

The shared UID is not just a package management construct, it actually maps to a shared Linux UID at runtime as well. Here is an example of two system apps running under the system UID:

system    5901  9852  845708 40972 ffffffff 00000000 S com.android.settings
system 6201 9852 824756 22256 ffffffff 00000000 S com.android.keychain

The ultimate trust level on Android is, of course, running in the same process. Since apps that are part of the same shared UID already have the same Linux UID and can access the same system resources, this is not a problem. It can be requested by specifying the same process name in the process attribute of the <application/> element in the manifest for all apps that need to run in one process. While the obvious result of this is that the apps can share memory and communicate directly instead of using RPC, some system services allow special access to components running in the same process (for example direct access to cached passwords or getting authentication tokens without showing UI prompts). Google apps take advantage of this by requesting to run in the same process as the login service in order to be able to sync data in the background, without user interaction (e.g., Play Services and the Google location service). Naturally, they are signed withe same certificate and part of the com.google.uid.shared shared UID.

Summary

Android uses the Java JAR format for code signing, and signatures can be added to both application packages (APKs) and system update packages (OTA updates). While JAR signing is based on X.509 certificates and PKI, Android does not use or validate the signer certificates as such. They are treated as binary blobs and an exact byte match is required in order for the system to consider two packages signed by the same signer. Package signature matching is at the heart of the Android security model, used both to guarantee that package updates come from the same origin and when establishing inter-application trust relationships. Inter-app trust relationships can be created either using signature-level permissions (built-in or custom), or by allowing apps to share the same system UID and, optionally, process. 


Subscribe to receive free email updates:

0 Response to "Code signing in Android's security model"

Post a Comment