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 e8851d4317..7a6088c16b 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 34b341fc76..096e99be8f 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 97d7edc514..001774ae19 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 b47cc58cd1..b313f7820f 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 2d2d417524..4d56063cb7 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 911c721b54..6f8c0c407d 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 1fd4f3df25..d5de795585 100644 --- a/go.toolchain.rev +++ b/go.toolchain.rev @@ -1 +1 @@ -aa85d1541af0921f830f053f29d91971fa5838f6 +a80a86e575c5b7b23b78540e947335d22f74d274 diff --git a/libtailscale/backend.go b/libtailscale/backend.go index e7cbb78f05..864136c33e 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 14c5694bec..7155f06f82 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 b803de9444..dfde42af1a 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 76dc979572..63b403229f 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) } }()