From 2c9fddab4ff55a05ffef9fd04894229340df4273 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Thu, 17 Dec 2020 10:23:02 +0100 Subject: [PATCH] cmd/tailscale: warn when debug signed and Google Sign-In fails Fixes tailscale/tailscale#1036 Signed-off-by: Elias Naur --- .../src/main/java/com/tailscale/ipn/App.java | 15 ++++++- cmd/tailscale/main.go | 40 ++++++++++++++++++- cmd/tailscale/ui.go | 4 +- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.java b/android/src/main/java/com/tailscale/ipn/App.java index 61c4d88..103362a 100644 --- a/android/src/main/java/com/tailscale/ipn/App.java +++ b/android/src/main/java/com/tailscale/ipn/App.java @@ -8,11 +8,14 @@ import android.app.Application; import android.app.Activity; import android.app.Fragment; import android.app.FragmentTransaction; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; -import android.content.BroadcastReceiver; +import android.content.pm.PackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.Signature; import android.provider.Settings; import android.net.ConnectivityManager; import android.net.Uri; @@ -189,6 +192,16 @@ public class App extends Application { }); } + // getPackageSignatureFingerprint returns the first package signing certificate, if any. + byte[] getPackageCertificate() throws Exception { + PackageInfo info; + info = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES); + for (Signature signature : info.signatures) { + return signature.toByteArray(); + } + return null; + } + static native void onVPNPrepared(); private static native void onConnectivityChanged(boolean connected); } diff --git a/cmd/tailscale/main.go b/cmd/tailscale/main.go index 3ae0939..364ce20 100644 --- a/cmd/tailscale/main.go +++ b/cmd/tailscale/main.go @@ -5,6 +5,8 @@ package main import ( + "crypto/sha1" + "fmt" "log" "sort" "strings" @@ -105,6 +107,10 @@ const serverOAuthID = "744055068597-hv4opg0h7vskq1hv37nq3u26t8c15qk0.apps.google const enabledKey = "ipn_enabled" +// releaseCertFingerprint is the SHA-1 fingerprint of the Google Play Store signing key. +// It is used to check whether the app is signed for release. +const releaseCertFingerprint = "86:9D:11:8B:63:1E:F8:35:C6:D9:C2:66:53:BC:28:22:2F:B8:C1:AE" + // backendEvents receives events from the UI (Activity, Tile etc.) to the backend. var backendEvents = make(chan UIEvent) @@ -527,6 +533,12 @@ func (a *App) runUI() error { TokenType: ipn.GoogleIDTokenType, }, }) + } else { + // Warn about possible debug certificate. + if !a.isReleaseSigned() { + ui.ShowMessage("Google Sign-In failed because the app is not signed for Play Store") + w.Invalidate() + } } case <-a.updates: a.mu.Lock() @@ -560,7 +572,7 @@ func (a *App) runUI() error { } } case <-onVPNRevoked: - ui.NotifyRevoked() + ui.ShowMessage("VPN access denied or another VPN service is always-on") w.Invalidate() case e := <-w.Events(): switch e := e.(type) { @@ -596,6 +608,32 @@ func (a *App) runUI() error { } } +// isReleaseSigned reports whether the app is signed with a release +// signature. +func (a *App) isReleaseSigned() bool { + var cert []byte + err := jni.Do(a.jvm, func(env jni.Env) error { + cls := jni.GetObjectClass(env, a.appCtx) + m := jni.GetMethodID(env, cls, "getPackageCertificate", "()[B") + str, err := jni.CallObjectMethod(env, a.appCtx, m) + if err != nil { + return err + } + cert = jni.GetByteArrayElements(env, jni.ByteArray(str)) + return nil + }) + if err != nil { + fatalErr(err) + } + h := sha1.New() + h.Write(cert) + fingerprint := h.Sum(nil) + hex := fmt.Sprintf("%x", fingerprint) + // Strip colons and convert to lower case to ease comparing. + wantFingerprint := strings.ReplaceAll(strings.ToLower(releaseCertFingerprint), ":", "") + return hex == wantFingerprint +} + // attachPeer registers an Android Fragment instance for // handling onActivityResult callbacks. func (a *App) attachPeer(act jni.Object) { diff --git a/cmd/tailscale/ui.go b/cmd/tailscale/ui.go index b4fdecb..d369bb1 100644 --- a/cmd/tailscale/ui.go +++ b/cmd/tailscale/ui.go @@ -306,8 +306,8 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat return ui.events } -func (ui *UI) NotifyRevoked() { - ui.message.text = "VPN access denied or another VPN service is always-on" +func (ui *UI) ShowMessage(msg string) { + ui.message.text = msg ui.message.t0 = time.Now() }