From df38cfa5f6d21887dd9261994027c0744f352858 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 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 ++++++++- libtailscale/backend.go | 9 ++++++--- libtailscale/interfaces.go | 4 ++-- libtailscale/keystore.go | 4 ++++ libtailscale/tailscale.go | 8 +++++--- 8 files changed, 43 insertions(+), 13 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/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..54498cf 100644 --- a/libtailscale/keystore.go +++ b/libtailscale/keystore.go @@ -93,3 +93,7 @@ func (k *hardwareAttestationKey) Close() error { func (k *hardwareAttestationKey) Clone() key.HardwareAttestationKey { return &hardwareAttestationKey{appCtx: k.appCtx, id: k.id, public: k.public} } + +func (k* hardwareAttestationKey) IsZero() bool { + 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) } }()