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 <jonathan@tailscale.com>
Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
pull/709/head
Patrick O'Doherty 2 months ago
parent 0b32dd75c5
commit 9b9000628a
No known key found for this signature in database

@ -150,10 +150,12 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
private fun initializeApp() { private fun initializeApp() {
// Check if a directory URI has already been stored. // Check if a directory URI has already been stored.
val storedUri = getStoredDirectoryUri() 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://")) { if (storedUri != null && storedUri.toString().startsWith("content://")) {
startLibtailscale(storedUri.toString()) startLibtailscale(storedUri.toString(), hardwareAttestation)
} else { } else {
startLibtailscale(this.filesDir.absolutePath) startLibtailscale(this.filesDir.absolutePath, hardwareAttestation)
} }
healthNotifier = HealthNotifier(Notifier.health, Notifier.state, applicationScope) healthNotifier = HealthNotifier(Notifier.health, Notifier.state, applicationScope)
connectivityManager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 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 * 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. * Tailscale because directFileRoot must be set before LocalBackend starts being used.
*/ */
fun startLibtailscale(directFileRoot: String) { fun startLibtailscale(directFileRoot: String, hardwareAttestation: Boolean) {
app = Libtailscale.start(this.filesDir.absolutePath, directFileRoot, this) app = Libtailscale.start(this.filesDir.absolutePath, directFileRoot, hardwareAttestation, this)
ShareFileHelper.init(this, app, directFileRoot, applicationScope) ShareFileHelper.init(this, app, directFileRoot, applicationScope)
Request.setApp(app) Request.setApp(app)
Notifier.setApp(app) Notifier.setApp(app)

@ -18,6 +18,9 @@ object MDMSettings {
// to the backend. // to the backend.
class NoSuchKeyException : Exception("no such key") class NoSuchKeyException : Exception("no such key")
// MDM restriction keys
const val KEY_HARDWARE_ATTESTATION = "HardwareAttestation"
val forceEnabled = BooleanMDMSetting("ForceEnabled", "Force Enabled Connection Toggle") val forceEnabled = BooleanMDMSetting("ForceEnabled", "Force Enabled Connection Toggle")
// Handled on the backed // Handled on the backed
@ -115,6 +118,11 @@ object MDMSettings {
.map { it.call(MDMSettings) as MDMSetting<*> } .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 } } val allSettingsByKey by lazy { allSettings.associateBy { it.key } }
fun update(app: App, restrictionsManager: RestrictionsManager?) { fun update(app: App, restrictionsManager: RestrictionsManager?) {

@ -360,4 +360,8 @@
<string name="taildrop_directory_picker_info">What is taildrop?</string> <string name="taildrop_directory_picker_info">What is taildrop?</string>
<string name="taildrop_directory_picker_button">Open Directory Picker</string> <string name="taildrop_directory_picker_button">Open Directory Picker</string>
<!-- Strings for Hardware Attestation MDM setting -->
<string name="enable_hardware_attestation">Enable hardware attestation</string>
<string name="use_hardware_backed_keys_to_bind_node_identity_to_the_device">Use hardware-backed keys to bind node identity to the device</string>
</resources> </resources>

@ -148,4 +148,11 @@
android:key="OnboardingFlow" android:key="OnboardingFlow"
android:restrictionType="choice" android:restrictionType="choice"
android:title="@string/onboarding_flow" /> android:title="@string/onboarding_flow" />
</restrictions>
<restriction
android:defaultValue="true"
android:description="@string/use_hardware_backed_keys_to_bind_node_identity_to_the_device"
android:key="HardwareAttestation"
android:restrictionType="bool"
android:title="@string/enable_hardware_attestation" />
</restrictions>

@ -1,11 +1,11 @@
module github.com/tailscale/tailscale-android module github.com/tailscale/tailscale-android
go 1.25.1 go 1.25.2
require ( require (
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da
golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab 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 ( require (

@ -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= 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 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= 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.20251010193330-005e264b5456 h1:ELfWhOfTpC6wEHvD74NUwhvwQtGaR+fSmU7ldTTgBzU=
tailscale.com v1.89.0-pre.0.20250929162250-7bcab4ab2841/go.mod h1:LHaTiwRgzebPDLgZ6RQQVzX+1SR5fbNl51fzm7UtMaw= tailscale.com v1.89.0-pre.0.20251010193330-005e264b5456/go.mod h1:gsjhGL2raodX0jQJ6uTD5dWJmc1DFtf5nQ1MRpzCReU=

@ -1 +1 @@
aa85d1541af0921f830f053f29d91971fa5838f6 a80a86e575c5b7b23b78540e947335d22f74d274

@ -60,7 +60,7 @@ type App struct {
backendMu sync.Mutex backendMu sync.Mutex
} }
func start(dataDir, directFileRoot string, appCtx AppContext) Application { func start(dataDir, directFileRoot string, hwAttestationPref bool, appCtx AppContext) Application {
defer func() { defer func() {
if p := recover(); p != nil { if p := recover(); p != nil {
log.Printf("panic in Start %s: %s", p, debug.Stack()) 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) os.Setenv("HOME", dataDir)
} }
return newApp(dataDir, directFileRoot, appCtx) return newApp(dataDir, directFileRoot, hwAttestationPref, appCtx)
} }
type backend struct { type backend struct {
@ -111,7 +111,7 @@ type backend struct {
type settingsFunc func(*router.Config, *dns.OSConfig) error 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) paths.AppSharedDir.Store(a.dataDir)
hostinfo.SetOSVersion(a.osVersion()) hostinfo.SetOSVersion(a.osVersion())
hostinfo.SetPackage(a.appCtx.GetInstallSource()) hostinfo.SetPackage(a.appCtx.GetInstallSource())
@ -139,6 +139,9 @@ func (a *App) runBackend(ctx context.Context) error {
} }
a.logIDPublicAtomic.Store(&b.logIDPublic) a.logIDPublicAtomic.Store(&b.logIDPublic)
a.backend = b.backend a.backend = b.backend
if hardwareAttestation {
a.backend.SetHardwareAttested()
}
defer b.CloseTUNs() defer b.CloseTUNs()
hc := localapi.HandlerConfig{ hc := localapi.HandlerConfig{

@ -11,8 +11,8 @@ import (
// Start starts the application, storing state in the given dataDir and using // Start starts the application, storing state in the given dataDir and using
// the given appCtx. // the given appCtx.
func Start(dataDir, directFileRoot string, appCtx AppContext) Application { func Start(dataDir, directFileRoot string, hwAttestationPref bool, appCtx AppContext) Application {
return start(dataDir, directFileRoot, appCtx) return start(dataDir, directFileRoot, hwAttestationPref, appCtx)
} }
// AppContext provides a context within which the Application is running. This // AppContext provides a context within which the Application is running. This

@ -91,5 +91,15 @@ func (k *hardwareAttestationKey) Close() error {
} }
func (k *hardwareAttestationKey) Clone() key.HardwareAttestationKey { func (k *hardwareAttestationKey) Clone() key.HardwareAttestationKey {
if k == nil {
return nil
}
return &hardwareAttestationKey{appCtx: k.appCtx, id: k.id, public: k.public} return &hardwareAttestationKey{appCtx: k.appCtx, id: k.id, public: k.public}
} }
func (k *hardwareAttestationKey) IsZero() bool {
if k == nil {
return true
}
return k.id == ""
}

@ -32,7 +32,7 @@ const (
customLoginServerPrefKey = "customloginserver" customLoginServerPrefKey = "customloginserver"
) )
func newApp(dataDir, directFileRoot string, appCtx AppContext) Application { func newApp(dataDir, directFileRoot string, hardwareAttestationPref bool, appCtx AppContext) Application {
a := &App{ a := &App{
directFileRoot: directFileRoot, directFileRoot: directFileRoot,
dataDir: dataDir, dataDir: dataDir,
@ -44,7 +44,9 @@ func newApp(dataDir, directFileRoot string, appCtx AppContext) Application {
a.policyStore = &syspolicyStore{a: a} a.policyStore = &syspolicyStore{a: a}
netmon.RegisterInterfaceGetter(a.getInterfaces) netmon.RegisterInterfaceGetter(a.getInterfaces)
rsop.RegisterStore("DeviceHandler", setting.DeviceScope, a.policyStore) rsop.RegisterStore("DeviceHandler", setting.DeviceScope, a.policyStore)
if appCtx.HardwareAttestationKeySupported() {
hwAttestEnabled := appCtx.HardwareAttestationKeySupported() && hardwareAttestationPref
if hwAttestEnabled {
key.RegisterHardwareAttestationKeyFns( key.RegisterHardwareAttestationKeyFns(
func() key.HardwareAttestationKey { return emptyHardwareAttestationKey(appCtx) }, func() key.HardwareAttestationKey { return emptyHardwareAttestationKey(appCtx) },
func() (key.HardwareAttestationKey, error) { return createHardwareAttestationKey(appCtx) }, func() (key.HardwareAttestationKey, error) { return createHardwareAttestationKey(appCtx) },
@ -63,7 +65,7 @@ func newApp(dataDir, directFileRoot string, appCtx AppContext) Application {
}() }()
ctx := context.Background() ctx := context.Background()
if err := a.runBackend(ctx); err != nil { if err := a.runBackend(ctx, hwAttestEnabled); err != nil {
fatalErr(err) fatalErr(err)
} }
}() }()

Loading…
Cancel
Save