Skip to content

Commit 9e4c757

Browse files
committed
libtailscale: bridge user-installed CA certificates from Android to Go TLS
Go's crypto/x509 on Android only reads system CAs from /system/etc/security/cacerts/ and does not read user-installed CAs from the Android trust store. This causes TLS connections to fail with "x509: certificate signed by unknown authority" when connecting to servers using custom/self-signed CAs (e.g. Headscale with a private CA). Add GetUserCACertsPEM() to the AppContext gomobile interface, implemented in App.kt using KeyStore.getInstance("AndroidCAStore"). At startup, user CA certs are written to the app's data directory and SSL_CERT_DIR is set to include both the system and user cert directories, allowing Go's TLS stack to trust user-installed certificates. Fixes tailscale/tailscale#8085 Signed-off-by: Logan Rupe <logan@coldtap.io>
1 parent 5819e29 commit 9e4c757

3 files changed

Lines changed: 46 additions & 0 deletions

File tree

android/src/main/java/com/tailscale/ipn/App.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,27 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
408408
app.notifyPolicyChanged()
409409
}
410410

411+
override fun getUserCACertsPEM(): ByteArray {
412+
return try {
413+
val ks = java.security.KeyStore.getInstance("AndroidCAStore")
414+
ks.load(null)
415+
val sb = StringBuilder()
416+
for (alias in ks.aliases()) {
417+
if (!alias.startsWith("user:")) continue
418+
val cert = ks.getCertificate(alias) as? java.security.cert.X509Certificate ?: continue
419+
val encoded = android.util.Base64.encodeToString(cert.encoded, android.util.Base64.NO_WRAP)
420+
sb.append("-----BEGIN CERTIFICATE-----\n")
421+
// Wrap base64 at 64 characters per line
422+
encoded.chunked(64).forEach { sb.append(it).append("\n") }
423+
sb.append("-----END CERTIFICATE-----\n")
424+
}
425+
sb.toString().toByteArray(Charsets.UTF_8)
426+
} catch (e: Exception) {
427+
Log.e(TAG, "Failed to read user CA certificates: ${e.message}")
428+
ByteArray(0)
429+
}
430+
}
431+
411432
override fun hardwareAttestationKeySupported(): Boolean {
412433
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
413434
packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)

libtailscale/backend.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,25 @@ func start(dataDir, directFileRoot string, hwAttestationPref bool, appCtx AppCon
8484
os.Setenv("HOME", dataDir)
8585
}
8686

87+
// Load user-installed CA certificates from the Android trust store.
88+
// Go's crypto/x509 on Android only reads system CAs from
89+
// /system/etc/security/cacerts/ and ignores user-installed CAs.
90+
// We bridge them from Java via AppContext and add them to SSL_CERT_DIR
91+
// so Go's TLS stack trusts them (e.g. for custom Headscale CAs).
92+
if userCACerts, err := appCtx.GetUserCACertsPEM(); err != nil {
93+
log.Printf("failed to load user CA certs: %v", err)
94+
} else if len(userCACerts) > 0 {
95+
userCertsDir := filepath.Join(dataDir, "user-cacerts")
96+
if err := os.MkdirAll(userCertsDir, 0700); err != nil {
97+
log.Printf("failed to create user CA certs dir: %v", err)
98+
} else if err := os.WriteFile(filepath.Join(userCertsDir, "user-certs.pem"), userCACerts, 0600); err != nil {
99+
log.Printf("failed to write user CA certs: %v", err)
100+
} else {
101+
os.Setenv("SSL_CERT_DIR", "/system/etc/security/cacerts:"+userCertsDir)
102+
log.Printf("loaded user-installed CA certificates into %s", userCertsDir)
103+
}
104+
}
105+
87106
return newApp(dataDir, directFileRoot, hwAttestationPref, appCtx)
88107
}
89108

libtailscale/interfaces.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ type AppContext interface {
6666
// expressed as a JSON string.
6767
GetSyspolicyStringArrayJSONValue(key string) (string, error)
6868

69+
// GetUserCACertsPEM returns PEM-encoded user-installed CA certificates
70+
// from the Android trust store. Returns empty bytes if none are installed.
71+
// This is needed because Go's crypto/x509 on Android only reads system CAs
72+
// from /system/etc/security/cacerts/ and ignores user-installed CAs.
73+
GetUserCACertsPEM() ([]byte, error)
74+
6975
// Methods used to implement key.HardwareAttestationKey using the Android
7076
// KeyStore.
7177
HardwareAttestationKeySupported() bool

0 commit comments

Comments
 (0)