Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Samsung Knox TEE integrity status #15

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,6 @@ dependencies {
def lifecycleVersion = "2.6.2"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"

compileOnly(project(":stub"))
}
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
tools:node="remove" />

<uses-permission
android:name="com.samsung.android.security.permission.SAMSUNG_KEYSTORE_PERMISSION" />

<application
android:name=".AppApplication"
android:icon="@drawable/ic_launcher"
Expand All @@ -19,6 +22,11 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="AllowBackup">

<uses-library
android:name="samsungkeystoreutils"
android:required="false" />

<activity
android:name=".home.HomeActivity"
android:exported="true">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,30 @@ public class CertificateInfo {
"MdsGUmX4RFlXYfC78hdLt0GAZMAoDo9Sd47b0ke2RekZyOmLw9vCkT/X11DEHTVm" +
"+Vfkl5YLCazOkjWFmwIDAQAB";

private static final String KNOX_SAKV1_ROOT_PUBLIC_KEY = "" +
"MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBs9Qjr//REhkXW7jUqjY9KNwWac4r" +
"5+kdUGk+TZjRo1YEa47Axwj6AJsbOjo4QsHiYRiWTELvFeiuBsKqyuF0xyAAKvDo" +
"fBqrEq1/Ckxo2mz7Q4NQes3g4ahSjtgUSh0k85fYwwHjCeLyZ5kEqgHG9OpOH526" +
"FFAK3slSUgC8RObbxys=";

private static final String KNOX_SAKV2_ROOT_PUBLIC_KEY = "" +
"MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBhbGuLrpql5I2WJmrE5kEVZOo+dgA" +
"46mKrVJf/sgzfzs2u7M9c1Y9ZkCEiiYkhTFE9vPbasmUfXybwgZ2EM30A1ABPd12" +
"4n3JbEDfsB/wnMH1AcgsJyJFPbETZiy42Fhwi+2BCA5bcHe7SrdkRIYSsdBRaKBo" +
"ZsapxB0gAOs0jSPRX5M=";

private static final String KNOX_SAKMV1_ROOT_PUBLIC_KEY = "" +
"MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB9XeEN8lg6p5xvMVWG42P2Qi/aRKX" +
"2rPRNgK92UlO9O/TIFCKHC1AWCLFitPVEow5W+yEgC2wOiYxgepY85TOoH0AuEkL" +
"oiC6ldbF2uNVU3rYYSytWAJg3GFKd1l9VLDmxox58Hyw2Jmdd5VSObGiTFQ/SgKs" +
"n2fbQPtpGlNxgEfd6Y8=";

private static final byte[] googleKey = Base64.decode(GOOGLE_ROOT_PUBLIC_KEY, 0);
private static final byte[] aospEcKey = Base64.decode(AOSP_ROOT_EC_PUBLIC_KEY, 0);
private static final byte[] aospRsaKey = Base64.decode(AOSP_ROOT_RSA_PUBLIC_KEY, 0);
private static final byte[] knoxSakv1Key = Base64.decode(KNOX_SAKV1_ROOT_PUBLIC_KEY, 0);
private static final byte[] knoxSakv2Key = Base64.decode(KNOX_SAKV2_ROOT_PUBLIC_KEY, 0);
private static final byte[] knoxSakmv1Key = Base64.decode(KNOX_SAKMV1_ROOT_PUBLIC_KEY, 0);
private static final Set<PublicKey> oemKeys = getOemPublicKey();

private final X509Certificate cert;
Expand Down Expand Up @@ -125,7 +139,9 @@ private void checkIssuer() {
issuer = KEY_AOSP;
} else if (Arrays.equals(publicKey, aospRsaKey)) {
issuer = KEY_AOSP;
} else if (Arrays.equals(publicKey, knoxSakv2Key)) {
} else if (Arrays.equals(publicKey, knoxSakv1Key)
|| Arrays.equals(publicKey, knoxSakv2Key)
|| Arrays.equals(publicKey, knoxSakmv1Key)) {
issuer = KEY_KNOX;
} else if (oemKeys != null) {
for (var key : oemKeys) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,18 @@ class HomeFragment : AppFragment(), HomeAdapter.Listener, MenuProvider {
}

override fun onPrepareMenu(menu: Menu) {
menu.findItem(R.id.menu_use_sak).apply {
isVisible = viewModel.hasSAK
isChecked = viewModel.preferSAK
}
menu.findItem(R.id.menu_use_strongbox).apply {
isVisible = viewModel.hasStrongBox
isChecked = viewModel.preferStrongBox
}
menu.findItem(R.id.menu_use_attest_key).apply {
isVisible = viewModel.hasAttestKey
isChecked = viewModel.preferAttestKey
isEnabled = !viewModel.preferSAK
isChecked = !viewModel.preferSAK && viewModel.preferAttestKey
}
menu.findItem(R.id.menu_incluid_props).apply {
isVisible = viewModel.hasDeviceIds
Expand All @@ -171,6 +176,12 @@ class HomeFragment : AppFragment(), HomeAdapter.Listener, MenuProvider {

override fun onMenuItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_use_sak -> {
val status = !item.isChecked
item.isChecked = status
viewModel.preferSAK = status
viewModel.load()
}
R.id.menu_use_strongbox -> {
val status = !item.isChecked
item.isChecked = status
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import android.widget.Toast
import androidx.core.content.edit
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.samsung.android.security.keystore.AttestParameterSpec
import com.samsung.android.security.keystore.AttestationUtils
import io.github.vvb2060.keyattestation.AppApplication
import io.github.vvb2060.keyattestation.attestation.AttestationResult
import io.github.vvb2060.keyattestation.attestation.CertificateInfo.parseCertificateChain
Expand All @@ -30,6 +32,7 @@ import io.github.vvb2060.keyattestation.lang.AttestationException.Companion.CODE
import io.github.vvb2060.keyattestation.lang.AttestationException.Companion.CODE_UNAVAILABLE_TRANSIENT
import io.github.vvb2060.keyattestation.lang.AttestationException.Companion.CODE_UNKNOWN
import io.github.vvb2060.keyattestation.util.Resource
import io.github.vvb2060.keyattestation.util.SamsungUtils
import java.io.BufferedInputStream
import java.io.ByteArrayInputStream
import java.io.IOException
Expand All @@ -53,6 +56,14 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie
val attestationResult = MutableLiveData<Resource<AttestationResult>>()
var currentCerts: List<X509Certificate>? = null

val hasSAK = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
SamsungUtils.isSecAttestationSupported()
var preferSAK = sp.getBoolean("prefer_sak", hasSAK)
set(value) {
field = value
sp.edit { putBoolean("prefer_sak", value) }
}

val hasStrongBox = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
pm.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
var preferStrongBox = sp.getBoolean("prefer_strongbox", true)
Expand Down Expand Up @@ -84,6 +95,7 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie

@Throws(GeneralSecurityException::class)
private fun generateKey(alias: String,
useSAK: Boolean,
useStrongBox: Boolean,
includeProps: Boolean,
attestKeyAlias: String?) {
Expand Down Expand Up @@ -112,24 +124,40 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie
builder.setAttestKeyAlias(attestKeyAlias)
}
}
val keyPairGenerator = KeyPairGenerator.getInstance(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && useSAK) {
val spec = AttestParameterSpec.Builder(alias, now.toString().toByteArray())
.setAlgorithm(KeyProperties.KEY_ALGORITHM_EC)
.setKeyGenParameterSpec(builder.build())
.setVerifiableIntegrity(true)
.setPackageName(AppApplication.app.packageName)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && includeProps) {
spec.setDevicePropertiesAttestationIncluded(true)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && attestKey) {
spec.setCertificateSubject(X500Principal("CN=App Attest Key"))
}
AttestationUtils().generateKeyPair(spec.build())
} else {
val keyPairGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
keyPairGenerator.initialize(builder.build())
keyPairGenerator.generateKeyPair()
keyPairGenerator.initialize(builder.build())
keyPairGenerator.generateKeyPair()
}
}

@Throws(AttestationException::class)
private fun doAttestation(useStrongBox: Boolean,
private fun doAttestation(useSAK: Boolean,
useStrongBox: Boolean,
includeProps: Boolean,
useAttestKey: Boolean): AttestationResult {
val certs = ArrayList<Certificate>()
val alias = if (useStrongBox) "${AppApplication.TAG}_strongbox" else AppApplication.TAG
val attestKeyAlias = if (useAttestKey) "${alias}_persistent" else null
try {
if (useAttestKey && !keyStore.containsAlias(attestKeyAlias)) {
generateKey(attestKeyAlias!!, useStrongBox, includeProps, attestKeyAlias)
generateKey(attestKeyAlias!!, useSAK, useStrongBox, includeProps, attestKeyAlias)
}
generateKey(alias, useStrongBox, includeProps, attestKeyAlias)
generateKey(alias, useSAK, useStrongBox, includeProps, attestKeyAlias)

val certChain = keyStore.getCertificateChain(alias)
?: throw CertificateException("Unable to get certificate chain")
Expand Down Expand Up @@ -239,11 +267,12 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie
}
}

val useSAK = hasSAK && preferSAK
val useStrongBox = hasStrongBox && preferStrongBox
val includeProps = hasDeviceIds && preferIncludeProps
val useAttestKey = hasAttestKey && preferAttestKey
val useAttestKey = hasAttestKey && preferAttestKey && !useSAK
val result = try {
val attestationResult = doAttestation(useStrongBox, includeProps, useAttestKey)
val attestationResult = doAttestation(useSAK, useStrongBox, includeProps, useAttestKey)
Resource.success(attestationResult)
} catch (e: Throwable) {
val cause = if (e is AttestationException) e.cause else e
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.github.vvb2060.keyattestation.util

import android.content.pm.PackageManager
import android.os.SystemProperties
import android.util.Log
import androidx.core.content.ContextCompat
import io.github.vvb2060.keyattestation.AppApplication

object SamsungUtils {
private const val SAMSUNG_KEYSTORE_PERMISSION =
"com.samsung.android.security.permission.SAMSUNG_KEYSTORE_PERMISSION"

fun isSecAttestationSupported(): Boolean {
if (!isSamsungKeystoreLibrarySupported()) {
Log.w(AppApplication.TAG, "This device has no samsungkeystoreutils library, " +
"skipping SAK.")
return false
}

if (!isSAKSupported()) {
Log.w(AppApplication.TAG, "This device has no SAK support, " +
"skipping SAK.")
return false
}

if (!isKeystorePermissionGranted()) {
Log.e(AppApplication.TAG, "SAMSUNG_KEYSTORE_PERMISSION has not been granted to the app, " +
"skipping SAK.")
return false
}

return true
}

private fun isSamsungKeystoreLibrarySupported(): Boolean {
val pm: PackageManager = AppApplication.app.packageManager
val systemSharedLibraries = pm.systemSharedLibraryNames

if (systemSharedLibraries != null) {
for (lib in systemSharedLibraries) {
if (lib != null && lib.lowercase() == "samsungkeystoreutils") {
return true
}
}
}

return false
}

private fun isSAKSupported(): Boolean {
return SystemProperties.get("ro.security.keystore.keytype", "").lowercase()
.contains("sak")
}

private fun isKeystorePermissionGranted(): Boolean{
return ContextCompat.checkSelfPermission(
AppApplication.app, SAMSUNG_KEYSTORE_PERMISSION) ==
PackageManager.PERMISSION_GRANTED
}
}
6 changes: 6 additions & 0 deletions app/src/main/res/menu/home.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

<item
android:id="@+id/menu_use_sak"
android:showAsAction="never"
android:checkable="true"
android:title="@string/use_sak" />

<item
android:id="@+id/menu_use_strongbox"
android:showAsAction="never"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Key Attestation</string>
<string name="use_sak">Use Samsung attestation</string>
<string name="use_strongbox">Use StrongBox</string>
<string name="use_attest_key">Use app generated attest key</string>
<string name="attest_device_props">Attest device props</string>
Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ dependencyResolutionManagement {
}
rootProject.name = "KeyAttestation"
include(":app")
include(":stub")
1 change: 1 addition & 0 deletions stub/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
20 changes: 20 additions & 0 deletions stub/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
plugins {
id 'com.android.library'
}

android {
compileSdk 34
namespace 'io.github.vvb2060.keyattestation.stub'
defaultConfig {
minSdk 24
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}

dependencies {
implementation 'androidx.annotation:annotation:1.6.0'
}
2 changes: 2 additions & 0 deletions stub/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
78 changes: 78 additions & 0 deletions stub/src/main/java/android/os/SystemProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package android.os;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class SystemProperties {
@NonNull
public static String get(@NonNull String key) {
throw new RuntimeException("Stub!");
}

@NonNull
public static String get(@NonNull String key, @Nullable String def) {
throw new RuntimeException("Stub!");
}

public static int getInt(@NonNull String key, int def) {
throw new RuntimeException("Stub!");
}

public static long getLong(@NonNull String key, long def) {
throw new RuntimeException("Stub!");
}

public static boolean getBoolean(@NonNull String key, boolean def) {
throw new RuntimeException("Stub!");
}

public static void set(@NonNull String key, @Nullable String val) {
throw new RuntimeException("Stub!");
}

public static void addChangeCallback(@NonNull Runnable callback) {
throw new RuntimeException("Stub!");
}

public static void removeChangeCallback(@NonNull Runnable callback) {
throw new RuntimeException("Stub!");
}

public static void reportSyspropChanged() {
throw new RuntimeException("Stub!");
}

public static @NonNull String digestOf(@NonNull String... keys) {
throw new RuntimeException("Stub!");
}

private SystemProperties() {
throw new RuntimeException("Stub!");
}

@Nullable public static Handle find(@NonNull String name) {
throw new RuntimeException("Stub!");
}

public static final class Handle {
@NonNull public String get() {
throw new RuntimeException("Stub!");
}

public int getInt(int def) {
throw new RuntimeException("Stub!");
}

public long getLong(long def) {
throw new RuntimeException("Stub!");
}

public boolean getBoolean(boolean def) {
throw new RuntimeException("Stub!");
}

private Handle(long nativeHandle) {
throw new RuntimeException("Stub!");
}
}
}
Loading
Loading