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) } }()