From 9b9000628ad76e86b09c320a37419a4724f3fa2a Mon Sep 17 00:00:00 2001 From: Patrick O'Doherty Date: Mon, 6 Oct 2025 15:08:05 -0700 Subject: [PATCH] android,libtailscale: allow toggling HW attestation via MDM Previously hardware attestation was enabled on all supported devices. We now gate this functionality behind an MDM setting (whose default value is true) to allow disabling this in deployments where it might cause issues. Updates tailscale/corp#31269 OSS and Version updated to 1.89.254-t005e264b5-g0b32dd75c Signed-off-by: Jonathan Nobels Signed-off-by: Patrick O'Doherty --- android/src/main/java/com/tailscale/ipn/App.kt | 10 ++++++---- .../src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt | 8 ++++++++ android/src/main/res/values/strings.xml | 4 ++++ android/src/main/res/xml/app_restrictions.xml | 9 ++++++++- go.mod | 4 ++-- go.sum | 4 ++-- go.toolchain.rev | 2 +- libtailscale/backend.go | 9 ++++++--- libtailscale/interfaces.go | 4 ++-- libtailscale/keystore.go | 10 ++++++++++ libtailscale/tailscale.go | 8 +++++--- 11 files changed, 54 insertions(+), 18 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index e8851d4..7a6088c 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -150,10 +150,12 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { private fun initializeApp() { // Check if a directory URI has already been stored. val storedUri = getStoredDirectoryUri() + val rm = getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager + val hardwareAttestation = rm.applicationRestrictions.getBoolean(MDMSettings.KEY_HARDWARE_ATTESTATION, false) if (storedUri != null && storedUri.toString().startsWith("content://")) { - startLibtailscale(storedUri.toString()) + startLibtailscale(storedUri.toString(), hardwareAttestation) } else { - startLibtailscale(this.filesDir.absolutePath) + startLibtailscale(this.filesDir.absolutePath, hardwareAttestation) } healthNotifier = HealthNotifier(Notifier.health, Notifier.state, applicationScope) connectivityManager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager @@ -202,8 +204,8 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { * Called when a SAF directory URI is available (either already stored or chosen). We must restart * Tailscale because directFileRoot must be set before LocalBackend starts being used. */ - fun startLibtailscale(directFileRoot: String) { - app = Libtailscale.start(this.filesDir.absolutePath, directFileRoot, this) + fun startLibtailscale(directFileRoot: String, hardwareAttestation: Boolean) { + app = Libtailscale.start(this.filesDir.absolutePath, directFileRoot, hardwareAttestation, this) ShareFileHelper.init(this, app, directFileRoot, applicationScope) Request.setApp(app) Notifier.setApp(app) diff --git a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt index 34b341f..096e99b 100644 --- a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt +++ b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt @@ -18,6 +18,9 @@ object MDMSettings { // to the backend. class NoSuchKeyException : Exception("no such key") + // MDM restriction keys + const val KEY_HARDWARE_ATTESTATION = "HardwareAttestation" + val forceEnabled = BooleanMDMSetting("ForceEnabled", "Force Enabled Connection Toggle") // Handled on the backed @@ -115,6 +118,11 @@ object MDMSettings { .map { it.call(MDMSettings) as MDMSetting<*> } } + val hardwareAttestation = BooleanMDMSetting( + KEY_HARDWARE_ATTESTATION, + "Use hardware-backed keys to bind node identity to the device", + ) + val allSettingsByKey by lazy { allSettings.associateBy { it.key } } fun update(app: App, restrictionsManager: RestrictionsManager?) { diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 97d7edc..001774a 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -360,4 +360,8 @@ What is taildrop? Open Directory Picker + + Enable hardware attestation + Use hardware-backed keys to bind node identity to the device + diff --git a/android/src/main/res/xml/app_restrictions.xml b/android/src/main/res/xml/app_restrictions.xml index b47cc58..b313f78 100644 --- a/android/src/main/res/xml/app_restrictions.xml +++ b/android/src/main/res/xml/app_restrictions.xml @@ -148,4 +148,11 @@ android:key="OnboardingFlow" android:restrictionType="choice" android:title="@string/onboarding_flow" /> - \ No newline at end of file + + + diff --git a/go.mod b/go.mod index 2d2d417..4d56063 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ module github.com/tailscale/tailscale-android -go 1.25.1 +go 1.25.2 require ( github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab - tailscale.com v1.89.0-pre.0.20250929162250-7bcab4ab2841 + tailscale.com v1.89.0-pre.0.20251010193330-005e264b5456 ) require ( diff --git a/go.sum b/go.sum index 911c721..6f8c0c4 100644 --- a/go.sum +++ b/go.sum @@ -235,5 +235,5 @@ howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.89.0-pre.0.20250929162250-7bcab4ab2841 h1:BfBXlsl/ffzlJoTCQL78hVmGGdRm//h/75lKIWOX79o= -tailscale.com v1.89.0-pre.0.20250929162250-7bcab4ab2841/go.mod h1:LHaTiwRgzebPDLgZ6RQQVzX+1SR5fbNl51fzm7UtMaw= +tailscale.com v1.89.0-pre.0.20251010193330-005e264b5456 h1:ELfWhOfTpC6wEHvD74NUwhvwQtGaR+fSmU7ldTTgBzU= +tailscale.com v1.89.0-pre.0.20251010193330-005e264b5456/go.mod h1:gsjhGL2raodX0jQJ6uTD5dWJmc1DFtf5nQ1MRpzCReU= diff --git a/go.toolchain.rev b/go.toolchain.rev index 1fd4f3d..d5de795 100644 --- a/go.toolchain.rev +++ b/go.toolchain.rev @@ -1 +1 @@ -aa85d1541af0921f830f053f29d91971fa5838f6 +a80a86e575c5b7b23b78540e947335d22f74d274 diff --git a/libtailscale/backend.go b/libtailscale/backend.go index e7cbb78..864136c 100644 --- a/libtailscale/backend.go +++ b/libtailscale/backend.go @@ -60,7 +60,7 @@ type App struct { backendMu sync.Mutex } -func start(dataDir, directFileRoot string, appCtx AppContext) Application { +func start(dataDir, directFileRoot string, hwAttestationPref bool, appCtx AppContext) Application { defer func() { if p := recover(); p != nil { log.Printf("panic in Start %s: %s", p, debug.Stack()) @@ -84,7 +84,7 @@ func start(dataDir, directFileRoot string, appCtx AppContext) Application { os.Setenv("HOME", dataDir) } - return newApp(dataDir, directFileRoot, appCtx) + return newApp(dataDir, directFileRoot, hwAttestationPref, appCtx) } type backend struct { @@ -111,7 +111,7 @@ type backend struct { type settingsFunc func(*router.Config, *dns.OSConfig) error -func (a *App) runBackend(ctx context.Context) error { +func (a *App) runBackend(ctx context.Context, hardwareAttestation bool) error { paths.AppSharedDir.Store(a.dataDir) hostinfo.SetOSVersion(a.osVersion()) hostinfo.SetPackage(a.appCtx.GetInstallSource()) @@ -139,6 +139,9 @@ func (a *App) runBackend(ctx context.Context) error { } a.logIDPublicAtomic.Store(&b.logIDPublic) a.backend = b.backend + if hardwareAttestation { + a.backend.SetHardwareAttested() + } defer b.CloseTUNs() hc := localapi.HandlerConfig{ diff --git a/libtailscale/interfaces.go b/libtailscale/interfaces.go index 14c5694..7155f06 100644 --- a/libtailscale/interfaces.go +++ b/libtailscale/interfaces.go @@ -11,8 +11,8 @@ import ( // Start starts the application, storing state in the given dataDir and using // the given appCtx. -func Start(dataDir, directFileRoot string, appCtx AppContext) Application { - return start(dataDir, directFileRoot, appCtx) +func Start(dataDir, directFileRoot string, hwAttestationPref bool, appCtx AppContext) Application { + return start(dataDir, directFileRoot, hwAttestationPref, appCtx) } // AppContext provides a context within which the Application is running. This diff --git a/libtailscale/keystore.go b/libtailscale/keystore.go index b803de9..dfde42a 100644 --- a/libtailscale/keystore.go +++ b/libtailscale/keystore.go @@ -91,5 +91,15 @@ func (k *hardwareAttestationKey) Close() error { } func (k *hardwareAttestationKey) Clone() key.HardwareAttestationKey { + if k == nil { + return nil + } return &hardwareAttestationKey{appCtx: k.appCtx, id: k.id, public: k.public} } + +func (k *hardwareAttestationKey) IsZero() bool { + if k == nil { + return true + } + return k.id == "" +} diff --git a/libtailscale/tailscale.go b/libtailscale/tailscale.go index 76dc979..63b4032 100644 --- a/libtailscale/tailscale.go +++ b/libtailscale/tailscale.go @@ -32,7 +32,7 @@ const ( customLoginServerPrefKey = "customloginserver" ) -func newApp(dataDir, directFileRoot string, appCtx AppContext) Application { +func newApp(dataDir, directFileRoot string, hardwareAttestationPref bool, appCtx AppContext) Application { a := &App{ directFileRoot: directFileRoot, dataDir: dataDir, @@ -44,7 +44,9 @@ func newApp(dataDir, directFileRoot string, appCtx AppContext) Application { a.policyStore = &syspolicyStore{a: a} netmon.RegisterInterfaceGetter(a.getInterfaces) rsop.RegisterStore("DeviceHandler", setting.DeviceScope, a.policyStore) - if appCtx.HardwareAttestationKeySupported() { + + hwAttestEnabled := appCtx.HardwareAttestationKeySupported() && hardwareAttestationPref + if hwAttestEnabled { key.RegisterHardwareAttestationKeyFns( func() key.HardwareAttestationKey { return emptyHardwareAttestationKey(appCtx) }, func() (key.HardwareAttestationKey, error) { return createHardwareAttestationKey(appCtx) }, @@ -63,7 +65,7 @@ func newApp(dataDir, directFileRoot string, appCtx AppContext) Application { }() ctx := context.Background() - if err := a.runBackend(ctx); err != nil { + if err := a.runBackend(ctx, hwAttestEnabled); err != nil { fatalErr(err) } }()