diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index a18a74e2a3..ff4b5c2a0e 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -408,6 +408,27 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { app.notifyPolicyChanged() } + override fun getUserCACertsPEM(): ByteArray { + return try { + val ks = java.security.KeyStore.getInstance("AndroidCAStore") + ks.load(null) + val sb = StringBuilder() + for (alias in ks.aliases()) { + if (!alias.startsWith("user:")) continue + val cert = ks.getCertificate(alias) as? java.security.cert.X509Certificate ?: continue + val encoded = android.util.Base64.encodeToString(cert.encoded, android.util.Base64.NO_WRAP) + sb.append("-----BEGIN CERTIFICATE-----\n") + // Wrap base64 at 64 characters per line + encoded.chunked(64).forEach { sb.append(it).append("\n") } + sb.append("-----END CERTIFICATE-----\n") + } + sb.toString().toByteArray(Charsets.UTF_8) + } catch (e: Exception) { + Log.e(TAG, "Failed to read user CA certificates: ${e.message}") + ByteArray(0) + } + } + override fun hardwareAttestationKeySupported(): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE) diff --git a/libtailscale/backend.go b/libtailscale/backend.go index 031bb0ef84..a3720a838a 100644 --- a/libtailscale/backend.go +++ b/libtailscale/backend.go @@ -84,6 +84,25 @@ func start(dataDir, directFileRoot string, hwAttestationPref bool, appCtx AppCon os.Setenv("HOME", dataDir) } + // Load user-installed CA certificates from the Android trust store. + // Go's crypto/x509 on Android only reads system CAs from + // /system/etc/security/cacerts/ and ignores user-installed CAs. + // We bridge them from Java via AppContext and add them to SSL_CERT_DIR + // so Go's TLS stack trusts them (e.g. for custom Headscale CAs). + if userCACerts, err := appCtx.GetUserCACertsPEM(); err != nil { + log.Printf("failed to load user CA certs: %v", err) + } else if len(userCACerts) > 0 { + userCertsDir := filepath.Join(dataDir, "user-cacerts") + if err := os.MkdirAll(userCertsDir, 0700); err != nil { + log.Printf("failed to create user CA certs dir: %v", err) + } else if err := os.WriteFile(filepath.Join(userCertsDir, "user-certs.pem"), userCACerts, 0600); err != nil { + log.Printf("failed to write user CA certs: %v", err) + } else { + os.Setenv("SSL_CERT_DIR", "/system/etc/security/cacerts:"+userCertsDir) + log.Printf("loaded user-installed CA certificates into %s", userCertsDir) + } + } + return newApp(dataDir, directFileRoot, hwAttestationPref, appCtx) } diff --git a/libtailscale/interfaces.go b/libtailscale/interfaces.go index ecebba5b47..88d1c41e7e 100644 --- a/libtailscale/interfaces.go +++ b/libtailscale/interfaces.go @@ -66,6 +66,12 @@ type AppContext interface { // expressed as a JSON string. GetSyspolicyStringArrayJSONValue(key string) (string, error) + // GetUserCACertsPEM returns PEM-encoded user-installed CA certificates + // from the Android trust store. Returns empty bytes if none are installed. + // This is needed because Go's crypto/x509 on Android only reads system CAs + // from /system/etc/security/cacerts/ and ignores user-installed CAs. + GetUserCACertsPEM() ([]byte, error) + // Methods used to implement key.HardwareAttestationKey using the Android // KeyStore. HardwareAttestationKeySupported() bool