Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions android/src/main/java/com/tailscale/ipn/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?) {
Expand Down
4 changes: 4 additions & 0 deletions android/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,8 @@
<string name="taildrop_directory_picker_info">What is taildrop?</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>
9 changes: 8 additions & 1 deletion android/src/main/res/xml/app_restrictions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,11 @@
android:key="OnboardingFlow"
android:restrictionType="choice"
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>
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
2 changes: 1 addition & 1 deletion go.toolchain.rev
Original file line number Diff line number Diff line change
@@ -1 +1 @@
aa85d1541af0921f830f053f29d91971fa5838f6
a80a86e575c5b7b23b78540e947335d22f74d274
9 changes: 6 additions & 3 deletions libtailscale/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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 {
Expand All @@ -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())
Expand Down Expand Up @@ -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{
Expand Down
4 changes: 2 additions & 2 deletions libtailscale/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions libtailscale/keystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == ""
}
8 changes: 5 additions & 3 deletions libtailscale/tailscale.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) },
Expand All @@ -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)
}
}()
Expand Down