Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions android/src/main/java/com/tailscale/ipn/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions libtailscale/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
6 changes: 6 additions & 0 deletions libtailscale/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down