Android test-keys: system UID with AOSP's public platform key
Contents
My HTC Desire Z, a 2010 slider phone, runs CyanogenMod 7, a custom build of Android 2.3, and I wanted my own small apps on it to do system things: toggle GPS from a background service, take screenshots without the su toast popping up each time. The usual route is root, and root on this ROM announces itself every time, like a butler who insists on reading your name out at your own front door. This part is the other route, which the ROM leaves open by default.
Custom ROM security works like regular Android security. You're running the same permission model, the same signature checks, the same wall between system apps and user-installed apps. You just built it from source. That doesn't change anything fundamental.
Except: a custom ROM built without its own keys ships signed with keys that are publicly available in the AOSP (Android Open Source Project) source tree, the default when nothing else is configured, and the explicit condition under which Android marks a build test-keys. Sign an APK with those keys, declare android:sharedUserId="android.uid.system", and your app runs as UID 1000 — the system user — on any ROM signed that way, CyanogenMod 7 on this phone among them.
The permission model still applies. It's just that the wall is made of publicly-known material. I keep a list of locks that turned out to be decorative, and this one went straight to the top: the lock is real, it is checked every time, and the key sits in a public git repo next to the instructions for cutting it.
What test-keys means#
Every Android build has a ro.build.tags property. Stock OEM builds say release-keys — the manufacturer generated their own signing keys, kept them private, and only their apps get system privileges. CyanogenMod 7 on this phone says test-keys, and so does any build signed with the AOSP defaults, the keys in the public git repo, such as an unofficial ROM build.
$ adb shell getprop ro.build.tags
test-keys
The Android security model gates sensitive permissions behind signature checks. An app requesting android:sharedUserId="android.uid.system" only gets UID 1000 if its APK signature matches the platform certificate. On a release-keys build, that certificate is secret. On a test-keys build, it's build/target/product/security/platform.x509.pem.
The same file for everyone.
What UID 1000 gives you#
System-level permissions reserved for the platform:
READ_FRAME_BUFFER— raw display accessWRITE_SECURE_SETTINGS— toggle GPS, modify any system settingDEVICE_POWER— reboot, wake locksSTATUS_BAR— control notifications programmaticallyMASTER_CLEAR— factory reset via broadcast- Access to
/data/system/and protected broadcasts
No su. No Superuser prompt. No toast notification telling the user "an app just escalated." Your APK simply is the system, as far as Android cares.
The conversion pipeline from hell#
Getting from "public key on a website" to "signed APK" requires a key format pilgrimage that tells you everything about Java's relationship with the rest of the PKI universe.
The keys come as platform.pk8 (DER-encoded PKCS#8 private key) and platform.x509.pem (certificate). The googlesource web interface serves raw files base64-wrapped via ?format=TEXT, so step zero is decoding:
# Fetch and decode — both are base64-wrapped by googlesource's ?format=TEXT
curl -s 'https://android.googlesource.com/platform/build/+/refs/heads/main/target/product/security/platform.pk8?format=TEXT' | base64 -d > platform.pk8
curl -s 'https://android.googlesource.com/platform/build/+/refs/heads/main/target/product/security/platform.x509.pem?format=TEXT' | base64 -d > platform.x509.pem
Now the conversion dance:
# pk8 → PEM private key
openssl pkcs8 -inform DER -nocrypt -in platform.pk8 -out platform.pem
# PEM + cert → PKCS12 bundle
openssl pkcs12 -export -in platform.x509.pem -inkey platform.pem \
-out platform.p12 -name platform -password pass:android
# PKCS12 → Java Keystore
keytool -importkeystore -srckeystore platform.p12 -srcstoretype PKCS12 \
-srcstorepass android -destkeystore platform.jks \
-deststoretype JKS -deststorepass android
Three format conversions to get from "here's a key" to "Java can use it." The keytool step throws DerInputStream.getLength(): lengthTag=109, too big with Java 6 — the error message doesn't tell you it's a JRE version problem. Java 8's keytool works. This cost me an hour, and the error message has never once apologised.
Signing#
jarsigner -keystore platform.jks -storepass android \
-sigalg SHA1withRSA -digestalg SHA1 \
app.apk platform
The -sigalg SHA1withRSA -digestalg SHA1 flags are mandatory for Android 2.3. Without them, the PackageManager silently refuses to install. No error. The APK just doesn't appear. The system considered your app unworthy of acknowledgment.
The framebuffer lie#
Here's where Android's permission model gets philosophical. READ_FRAME_BUFFER sounds like it grants access to /dev/graphics/fb0. It does not. Android's platform.xml gives the permission no group at all, and on this phone the package manager wouldn't even grant it to my app while it was still debug-signed: logcat said Not granting permission android.permission.READ_FRAME_BUFFER. Whether the platform-signed build got it, I never checked. The framebuffer device is owned by GID 1003 (graphics), and nothing connects the permission to that group.
Running as UID 1000 (system) doesn't help either — system isn't in the graphics group on CyanogenMod 7. The fix was chmod 664 /dev/graphics/fb0 in an init.d script. A static permission change on a device node, because the entire signature-permission-GID pipeline promised something it couldn't deliver.
The permission exists. It is declared, and it does nothing for its stated purpose. I had just made my app the system itself, and the system still couldn't read its own screen.
The security theater angle#
On a test-keys ROM, any app can claim android.uid.system. There is no secondary check. No certificate pinning against a known-good hash. No prompt to the user. The entire permission enforcement model — the thing that distinguishes "system app" from "third-party app" — reduces to: does your signature match this public key?
For tinkerers, this is liberation. Toggle GPS from a background service. Take screenshots without root toasts. Read the settings database directly. Every permission wall evaporates.
For security, it means anyone on a test-keys ROM is running with the castle gate welded open. Any malicious APK signed with these keys escalates to system silently. The only protection is that most malware authors don't bother — the target audience is too small.
Google knows. Google publishes the keys. Custom ROMs built without their own keys ship with them. The workflow to exploit this is six shell commands, and everyone carries on as if the permission model still applies.
On my one phone I walked through that gate on purpose, with my own APK and the public key. Even then the screen stayed shut until a chmod opened it, so the gate is welded open and some of the rooms behind it are still locked with string. I'd still rather the next owner of a test-keys ROM knew the gate was open before someone with worse intentions finds it.
Filed under: android, security, hardware
Sources#
- AOSP platform signing keys — the public keys themselves, in the official source tree
- Android signature permissions documentation — protection levels including
signatureandsignatureOrSystem - CyanogenMod vendor config — AOSP
config.mk: definesBUILD_KEYS := test-keyswhenDEFAULT_SYSTEM_DEV_CERTIFICATEpoints to the public testkey — the canonical source of when and why a build gets taggedtest-keys