In the first two lessons we configured a proxy, routed Android emulator traffic through Charles, and learned why HTTPS traffic is not automatically readable. Now we go one level deeper: system-level CA certificates.
This topic matters because Android can physically contain your certificate file and still refuse to trust it as a real system CA. For a clean lab, you need to understand not only where the file goes, but also the filename hash, Linux permissions, owner, and SELinux label.
The practical problem looks like this:
- the certificate file exists in
/system/etc/security/cacerts/; - Chrome still complains about the certificate;
- Android Settings does not clearly show the certificate;
- Charles may show either encrypted
CONNECTtunnels or real decryptedGET https://...requests; - you are not sure whether the problem is the certificate, Chrome cache, Android version, or SELinux.
This lesson gives you a direct troubleshooting workflow.
Use this only on your own emulator, your own device, your own apps, or systems where you have explicit permission to test.
1. User CA vs system CA
Android has two important trust locations:
| Store | Typical installation method | Main limitation |
|---|---|---|
| User CA store | Install certificate through Android Settings | Many apps ignore it unless their network security config allows user CAs. |
| System CA store | Place the certificate in the system certificate directory | Requires root, correct filename, permissions, and SELinux context. |
For browser demos, a user certificate may be enough. For mobile app traffic analysis, especially with apps that rely on the default system trust store, a system certificate is usually more useful.
That does not mean system CA installation bypasses SSL pinning. If an app pins its backend certificate or public key, the system CA may be installed correctly and the app can still reject the connection. In that case you need the pinning bypass workflow from the earlier lessons.
2. The target directory
On many Android versions, system CA certificates are stored in:
/system/etc/security/cacerts/
A manually installed proxy CA should look like the existing files in that directory. It should not be called charles.pem, proxy.crt, or certificate.cer. Android expects a hash-based filename.
For example:
c79a2ee1.0
The .0 suffix matters. If the file is named only c79a2ee1, Android will not treat it the same way.
Check that the file exists:
adb shell ls -l /system/etc/security/cacerts/ | grep c79a2ee1
Expected idea:
-rw-r--r-- 1 root root ... c79a2ee1.0
If you see the hash without .0, rename or recreate the file correctly.
3. Create the correct hash filename
The safest way is to generate the filename from the certificate itself.
Assume your exported Charles, Burp, or mitmproxy CA certificate is named:
Certificates.pem
First confirm that OpenSSL can parse it:
openssl x509 -in Certificates.pem -noout -subject -issuer -dates
If OpenSSL cannot read it, Android will not trust it either. Convert it to PEM first if needed.
Now generate the old subject hash used by Android certificate stores:
HASH=$(openssl x509 -inform PEM -subject_hash_old -in Certificates.pem -noout)
echo $HASH
cp Certificates.pem ${HASH}.0
For example, if OpenSSL prints:
c79a2ee1
then the final file must be:
c79a2ee1.0
You can verify the copied file again:
openssl x509 -in c79a2ee1.0 -noout -subject -issuer -dates
openssl x509 -subject_hash_old -in c79a2ee1.0 -noout
The second command must print the same hash as the filename, without the .0 suffix.
4. Push the certificate into the system store
A typical emulator workflow is:
adb root
adb remount
adb push ${HASH}.0 /system/etc/security/cacerts/
adb shell "chown root:root /system/etc/security/cacerts/${HASH}.0"
adb shell "chmod 644 /system/etc/security/cacerts/${HASH}.0"
adb shell "restorecon /system/etc/security/cacerts/${HASH}.0"
adb reboot
The important details are:
- owner should be
root:root; - permissions should usually be
644; - SELinux label should match the Android certificate directory;
- reboot after installation.
Do not treat chmod as the whole solution. On Android, classic Linux permissions are only one layer.
5. Verify permissions and owner
After reboot, check the file:
adb shell ls -l /system/etc/security/cacerts/c79a2ee1.0
A normal result should look like this:
-rw-r--r-- 1 root root ... c79a2ee1.0
If it does not, fix it:
adb shell "chown root:root /system/etc/security/cacerts/c79a2ee1.0"
adb shell "chmod 644 /system/etc/security/cacerts/c79a2ee1.0"
adb reboot
This confirms the regular Linux side: owner, group, and file mode.
6. Verify the SELinux context
This is the part that is easy to miss.
Run:
adb shell ls -lZ /system/etc/security/cacerts/c79a2ee1.0
A good result can look like this:
-rw-r--r-- 1 root root u:object_r:system_security_cacerts_file:s0 ... /system/etc/security/cacerts/c79a2ee1.0
The key part is:
u:object_r:system_security_cacerts_file:s0
That label means Android sees the file as a system security CA certificate file, not just as a random file copied into a system folder.
This is why SELinux is not just “another chmod”. chmod and chown answer one question:
What do traditional Linux permissions allow?
SELinux answers another question:
Does Android security policy allow this object to be treated as this type of system file?
A file can have correct -rw-r--r-- root root permissions and still be ignored or blocked if the SELinux context is wrong.
Compare your certificate with existing certificates:
adb shell ls -lZ /system/etc/security/cacerts/ | head
If your context is wrong, try restoring it:
adb shell restorecon /system/etc/security/cacerts/c79a2ee1.0
adb reboot
If restorecon does not fix it in your lab emulator, you can compare against the working context and set it manually:
adb shell "chcon u:object_r:system_security_cacerts_file:s0 /system/etc/security/cacerts/c79a2ee1.0"
adb reboot
Use manual chcon as a lab troubleshooting step, not as a production habit.
7. If Chrome complains about the certificate
Chrome warnings do not always mean the system CA file is missing. Work through the checks in order.
Check the hash
Make sure the filename matches the certificate:
openssl x509 -subject_hash_old -in c79a2ee1.0 -noout
If the command prints anything other than c79a2ee1, the file is named incorrectly.
Clear Chrome state
Chrome can keep old state, failed sessions, or cached certificate decisions. Reset it in the emulator:
adb shell am force-stop com.android.chrome
adb shell pm clear com.android.chrome
Then open the test site again, for example:
https://www.example.com
Look at what Charles shows
This is the practical test.
If Charles only shows something like:
CONNECT www.example.com:443
then the proxy sees the tunnel, but not decrypted HTTPS content.
If Charles shows a real request like:
GET https://www.example.com/ → 200
then HTTPS decryption is working for that request. In a lab demo, that is stronger evidence than whether Android Settings displays the certificate nicely.
8. Android 14 and 15 emulator warning
Recent Android versions can be less friendly for this lab. Depending on the emulator image, Conscrypt/APEX behavior and certificate store handling may differ from older tutorials. A file copied into /system/etc/security/cacerts/ may not appear in Settings, and in some images it may not work the way older Android versions worked.
For teaching and repeatable demos, I prefer:
- Android 11, 12, or 13;
- Google APIs image;
- not a Google Play image;
- rooted emulator access;
- clean boot after certificate changes.
This is not a weak setup. It is a controlled lab setup. The goal is to teach traffic analysis, certificates, and debugging without fighting unnecessary platform restrictions during the first lesson.
9. Quick troubleshooting checklist
Use this checklist when the certificate file exists but Android or Chrome still behaves strangely.
File name
adb shell ls -l /system/etc/security/cacerts/ | grep c79a2ee1
Must be:
c79a2ee1.0
not just:
c79a2ee1
Certificate format
openssl x509 -in c79a2ee1.0 -noout -subject -issuer -dates
OpenSSL must parse it successfully.
Hash match
openssl x509 -subject_hash_old -in c79a2ee1.0 -noout
The output must match the filename before .0.
Permissions
adb shell ls -l /system/etc/security/cacerts/c79a2ee1.0
Expected:
-rw-r--r-- 1 root root
SELinux context
adb shell ls -lZ /system/etc/security/cacerts/c79a2ee1.0
A good context is:
u:object_r:system_security_cacerts_file:s0
Chrome reset
adb shell am force-stop com.android.chrome
adb shell pm clear com.android.chrome
Trust dump
You can also inspect Android trust information:
adb shell dumpsys trust
adb shell dumpsys trust | grep -i charles
Do not rely on only one signal. Use filename, OpenSSL parsing, permissions, SELinux context, Chrome behavior, and Charles output together.
10. The important mental model
System CA installation on Android is not only “copy a file into a folder”. A correct setup has several gates:
- The certificate must be a valid PEM certificate.
- The filename must be the old OpenSSL subject hash plus
.0. - The file must live in the system CA directory.
- The owner should be
root:root. - The permissions should be readable by the system, usually
644. - The SELinux context must identify it as a system CA certificate file.
- The app or browser must actually use the Android system trust store.
- SSL pinning can still block interception even when the CA is installed correctly.
The most useful command for explaining the hidden Android-specific part is:
adb shell ls -lZ /system/etc/security/cacerts/c79a2ee1.0
When you see:
u:object_r:system_security_cacerts_file:s0
you can explain it like this:
chmod and chown are not the full story on Android. Android also uses SELinux labels. Even if the file has correct Linux permissions, the system may still ignore or block it if the SELinux context is wrong. In this case, the certificate has the
system_security_cacerts_filecontext, so Android treats it as a real system CA certificate file.
That is the practical meaning of SELinux in this workflow.