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 CONNECT tunnels or real decrypted GET 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:

StoreTypical installation methodMain limitation
User CA storeInstall certificate through Android SettingsMany apps ignore it unless their network security config allows user CAs.
System CA storePlace the certificate in the system certificate directoryRequires 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:

  1. The certificate must be a valid PEM certificate.
  2. The filename must be the old OpenSSL subject hash plus .0.
  3. The file must live in the system CA directory.
  4. The owner should be root:root.
  5. The permissions should be readable by the system, usually 644.
  6. The SELinux context must identify it as a system CA certificate file.
  7. The app or browser must actually use the Android system trust store.
  8. 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_file context, so Android treats it as a real system CA certificate file.

That is the practical meaning of SELinux in this workflow.