diff --git a/app/build.gradle b/app/build.gradle
index aa45df1..0c17793 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -101,4 +101,6 @@ dependencies {
def lifecycleVersion = "2.6.2"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
+
+ compileOnly(project(":stub"))
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8ecd7a9..5719c12 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,6 +11,9 @@
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
tools:node="remove" />
+
+
+
+
+
diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Asn1Utils.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Asn1Utils.java
index db8e00a..08517f1 100644
--- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Asn1Utils.java
+++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Asn1Utils.java
@@ -28,6 +28,7 @@
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1Set;
import org.bouncycastle.asn1.DEROctetString;
+import org.bouncycastle.asn1.DERPrintableString;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@@ -63,11 +64,16 @@ public static Long getLongFromAsn1(ASN1Encodable asn1Value) throws CertificatePa
public static byte[] getByteArrayFromAsn1(ASN1Encodable asn1Encodable)
throws CertificateParsingException {
- if (asn1Encodable == null || !(asn1Encodable instanceof DEROctetString)) {
+ if (asn1Encodable == null) {
throw new CertificateParsingException("Expected DEROctetString");
}
- ASN1OctetString derOctectString = (ASN1OctetString) asn1Encodable;
- return derOctectString.getOctets();
+ if (asn1Encodable instanceof DEROctetString) {
+ return ((ASN1OctetString) asn1Encodable).getOctets();
+ }
+ if (asn1Encodable instanceof DERPrintableString) {
+ return ((DERPrintableString) asn1Encodable).getOctets();
+ }
+ throw new CertificateParsingException("Expected DEROctetString");
}
public static ASN1Encodable getAsn1EncodableFromBytes(byte[] bytes)
diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Attestation.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Attestation.java
index d258260..fc0a8d6 100644
--- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Attestation.java
+++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/Attestation.java
@@ -34,6 +34,7 @@
* contents.
*/
public abstract class Attestation {
+ static final String KNOX_EXTENSION_OID = "1.3.6.1.4.1.236.11.3.23.7";
static final String EAT_OID = "1.3.6.1.4.1.11129.2.1.25";
static final String ASN1_OID = "1.3.6.1.4.1.11129.2.1.17";
static final String KEY_USAGE_OID = "2.5.29.15"; // Standard key usage extension.
@@ -64,10 +65,17 @@ public abstract class Attestation {
*/
public static Attestation loadFromCertificate(X509Certificate x509Cert) throws CertificateParsingException {
- if (x509Cert.getExtensionValue(EAT_OID) == null
+ if (x509Cert.getExtensionValue(KNOX_EXTENSION_OID) == null
+ && x509Cert.getExtensionValue(EAT_OID) == null
&& x509Cert.getExtensionValue(ASN1_OID) == null) {
throw new CertificateParsingException("No attestation extensions found");
}
+ if (x509Cert.getExtensionValue(KNOX_EXTENSION_OID) != null) {
+ if (x509Cert.getExtensionValue(EAT_OID) != null) {
+ throw new CertificateParsingException("Multiple attestation extensions found");
+ }
+ return new KnoxAttestation(x509Cert);
+ }
if (x509Cert.getExtensionValue(EAT_OID) != null) {
if (x509Cert.getExtensionValue(ASN1_OID) != null) {
throw new CertificateParsingException("Multiple attestation extensions found");
diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthResult.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthResult.java
new file mode 100644
index 0000000..9c5c7da
--- /dev/null
+++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthResult.java
@@ -0,0 +1,110 @@
+package io.github.vvb2060.keyattestation.attestation;
+
+import com.google.common.io.BaseEncoding;
+
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.ASN1Primitive;
+import org.bouncycastle.asn1.ASN1Sequence;
+import org.bouncycastle.asn1.ASN1SequenceParser;
+import org.bouncycastle.asn1.ASN1TaggedObject;
+
+import java.io.IOException;
+import java.security.cert.CertificateParsingException;
+
+public class AuthResult {
+ private static final int CALLER_AUTH_RESULT = 0;
+ private static final int CALLING_PACKAGE = 1;
+ private static final int CALLING_PACKAGE_SIGS = 2;
+ private static final int CALLING_PACKAGE_AUTH_RESULT = 3;
+
+ public static final int STATUS_NORMAL = 0;
+ public static final int STATUS_ABNORMAL = 1;
+ public static final int STATUS_NOT_SUPPORT = 2;
+
+ private int callerAuthResult;
+ private byte[] callingPackage;
+ private byte[] callingPackageSigs;
+ private int callingPackageAuthResult;
+
+ public AuthResult(ASN1Encodable asn1Encodable) throws CertificateParsingException {
+ if (!(asn1Encodable instanceof ASN1Sequence sequence)) {
+ throw new CertificateParsingException("Expected sequence for caller auth, found "
+ + asn1Encodable.getClass().getName());
+ }
+
+ ASN1SequenceParser parser = sequence.parser();
+ ASN1TaggedObject entry = parseAsn1TaggedObject(parser);
+
+ for (; entry != null; entry = parseAsn1TaggedObject(parser)) {
+ int tag = entry.getTagNo();
+ ASN1Primitive value = entry.getBaseObject().toASN1Primitive();
+
+ switch (tag) {
+ case CALLER_AUTH_RESULT:
+ callerAuthResult = Asn1Utils.getIntegerFromAsn1(value);
+ break;
+ case CALLING_PACKAGE:
+ callingPackage = Asn1Utils.getByteArrayFromAsn1(value);
+ break;
+ case CALLING_PACKAGE_SIGS:
+ callingPackageSigs = Asn1Utils.getByteArrayFromAsn1(value);
+ break;
+ case CALLING_PACKAGE_AUTH_RESULT:
+ callingPackageAuthResult = Asn1Utils.getIntegerFromAsn1(value);
+ break;
+ }
+ }
+ }
+
+ private static ASN1TaggedObject parseAsn1TaggedObject(ASN1SequenceParser parser)
+ throws CertificateParsingException {
+ ASN1Encodable asn1Encodable = parseAsn1Encodable(parser);
+ if (asn1Encodable == null || asn1Encodable instanceof ASN1TaggedObject) {
+ return (ASN1TaggedObject) asn1Encodable;
+ }
+ throw new CertificateParsingException(
+ "Expected tagged object, found " + asn1Encodable.getClass().getName());
+ }
+
+ private static ASN1Encodable parseAsn1Encodable(ASN1SequenceParser parser)
+ throws CertificateParsingException {
+ try {
+ return parser.readObject();
+ } catch (IOException e) {
+ throw new CertificateParsingException("Failed to parse ASN1 sequence", e);
+ }
+ }
+
+ public String statusToString(int status, boolean isCallingPackageAuthResult) {
+ switch (status) {
+ case STATUS_NORMAL:
+ return "Normal";
+ case STATUS_ABNORMAL:
+ return "Abnormal";
+ case STATUS_NOT_SUPPORT:
+ return "Not support";
+ default:
+ if (isCallingPackageAuthResult) {
+ return "Not support";
+ }
+ return Integer.toHexString(status);
+ }
+ }
+
+ @Override
+ public String toString() {
+ try {
+ StringBuilder sb = new StringBuilder("Caller auth result: ")
+ .append(statusToString(callerAuthResult, false)).append('\n')
+ .append("Calling package: ")
+ .append(new String(callingPackage)).append('\n')
+ .append("Calling package signatures: ")
+ .append(BaseEncoding.base64().encode(callingPackageSigs)).append(" (base64)").append('\n')
+ .append("Calling package auth result: ")
+ .append(statusToString(callingPackageAuthResult, true));
+ return sb.toString();
+ } catch (NullPointerException e) {
+ return "Not performed";
+ }
+ }
+}
diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthorizationList.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthorizationList.java
index 6e27a72..663d035 100644
--- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthorizationList.java
+++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthorizationList.java
@@ -226,6 +226,7 @@ public class AuthorizationList {
private Boolean rollbackResistant;
private Boolean rollbackResistance;
private RootOfTrust rootOfTrust;
+ private IntegrityStatus integrityStatus;
private Integer osVersion;
private Integer osPatchLevel;
private Integer vendorPatchLevel;
@@ -767,6 +768,14 @@ public RootOfTrust getRootOfTrust() {
return rootOfTrust;
}
+ public IntegrityStatus getIntegrityStatus() {
+ return integrityStatus;
+ }
+
+ void setIntegrityStatus(IntegrityStatus is) {
+ integrityStatus = is;
+ }
+
public Integer getOsVersion() {
return osVersion;
}
@@ -960,6 +969,15 @@ public String toString() {
s.append(rootOfTrust);
}
+ if (integrityStatus != null) {
+ s.append("\nIntegrity Status:\n");
+ s.append(integrityStatus);
+ if (integrityStatus.getAuthResult() != null) {
+ s.append("\nCaller Auth Status:\n");
+ s.append(integrityStatus.getAuthResult());
+ }
+ }
+
if (osVersion != null) {
s.append("\nOS Version: ").append(osVersion);
}
diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/CertificateInfo.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/CertificateInfo.java
index 212eb9d..a7f6c0f 100644
--- a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/CertificateInfo.java
+++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/CertificateInfo.java
@@ -10,11 +10,13 @@
import java.security.GeneralSecurityException;
import java.security.PublicKey;
+import java.security.cert.CertPath;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.LinkedList;
import java.util.List;
import co.nstant.in.cbor.CborDecoder;
@@ -28,6 +30,7 @@ public class CertificateInfo {
public static final int KEY_UNKNOWN = 0;
public static final int KEY_AOSP = 1;
public static final int KEY_GOOGLE = 2;
+ public static final int KEY_SAMSUNG = 3;
public static final int CERT_UNKNOWN = 0;
public static final int CERT_SIGN = 1;
@@ -35,6 +38,24 @@ public class CertificateInfo {
public static final int CERT_EXPIRED = 3;
public static final int CERT_NORMAL = 4;
+ private static final String SAMSUNG_SAKV1_ROOT_PUBLIC_KEY = "" +
+ "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBs9Qjr//REhkXW7jUqjY9KNwWac4r" +
+ "5+kdUGk+TZjRo1YEa47Axwj6AJsbOjo4QsHiYRiWTELvFeiuBsKqyuF0xyAAKvDo" +
+ "fBqrEq1/Ckxo2mz7Q4NQes3g4ahSjtgUSh0k85fYwwHjCeLyZ5kEqgHG9OpOH526" +
+ "FFAK3slSUgC8RObbxys=";
+
+ private static final String SAMSUNG_SAKV2_ROOT_PUBLIC_KEY = "" +
+ "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBhbGuLrpql5I2WJmrE5kEVZOo+dgA" +
+ "46mKrVJf/sgzfzs2u7M9c1Y9ZkCEiiYkhTFE9vPbasmUfXybwgZ2EM30A1ABPd12" +
+ "4n3JbEDfsB/wnMH1AcgsJyJFPbETZiy42Fhwi+2BCA5bcHe7SrdkRIYSsdBRaKBo" +
+ "ZsapxB0gAOs0jSPRX5M=";
+
+ private static final String SAMSUNG_SAKMV1_ROOT_PUBLIC_KEY = "" +
+ "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB9XeEN8lg6p5xvMVWG42P2Qi/aRKX" +
+ "2rPRNgK92UlO9O/TIFCKHC1AWCLFitPVEow5W+yEgC2wOiYxgepY85TOoH0AuEkL" +
+ "oiC6ldbF2uNVU3rYYSytWAJg3GFKd1l9VLDmxox58Hyw2Jmdd5VSObGiTFQ/SgKs" +
+ "n2fbQPtpGlNxgEfd6Y8=";
+
private static final String GOOGLE_ROOT_PUBLIC_KEY = "" +
"MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xU" +
"FmOr75gvMsd/dTEDDJdSSxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5j" +
@@ -59,6 +80,9 @@ public class CertificateInfo {
"MdsGUmX4RFlXYfC78hdLt0GAZMAoDo9Sd47b0ke2RekZyOmLw9vCkT/X11DEHTVm" +
"+Vfkl5YLCazOkjWFmwIDAQAB";
+ private static final byte[] sakV1Key = Base64.decode(SAMSUNG_SAKV1_ROOT_PUBLIC_KEY, 0);
+ private static final byte[] sakV2Key = Base64.decode(SAMSUNG_SAKV2_ROOT_PUBLIC_KEY, 0);
+ private static final byte[] sakmV1Key = Base64.decode(SAMSUNG_SAKMV1_ROOT_PUBLIC_KEY, 0);
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);
@@ -107,7 +131,11 @@ public Integer getCertsIssued() {
private void checkIssuer() {
var publicKey = cert.getPublicKey().getEncoded();
- if (Arrays.equals(publicKey, googleKey)) {
+ if (Arrays.equals(publicKey, sakV1Key)
+ || Arrays.equals(publicKey, sakV2Key)
+ || Arrays.equals(publicKey, sakmV1Key)) {
+ issuer = KEY_SAMSUNG;
+ } else if (Arrays.equals(publicKey, googleKey)) {
issuer = KEY_GOOGLE;
} else if (Arrays.equals(publicKey, aospEcKey)) {
issuer = KEY_AOSP;
@@ -207,4 +235,68 @@ public static AttestationResult parseCertificateChain(List cert
return AttestationResult.form(infoList);
}
+
+ private static List sortCerts(List certs) {
+ if (certs.size() < 2) {
+ return certs;
+ }
+
+ var issuer = certs.get(0).getIssuerX500Principal();
+ boolean okay = true;
+ for (var cert : certs) {
+ var subject = cert.getSubjectX500Principal();
+ if (issuer.equals(subject)) {
+ issuer = subject;
+ } else {
+ okay = false;
+ break;
+ }
+ }
+ if (okay) {
+ return certs;
+ }
+
+ var newList = new ArrayList(certs.size());
+ for (var cert : certs) {
+ boolean found = false;
+ var subject = cert.getSubjectX500Principal();
+ for (var c : certs) {
+ if (c == cert) continue;
+ if (c.getIssuerX500Principal().equals(subject)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ newList.add(cert);
+ }
+ }
+ if (newList.size() != 1) {
+ return certs;
+ }
+
+ var oldList = new LinkedList<>(certs);
+ oldList.remove(newList.get(0));
+ for (int i = 0; i < newList.size(); i++) {
+ issuer = newList.get(i).getIssuerX500Principal();
+ for (var it = oldList.iterator(); it.hasNext(); ) {
+ var cert = it.next();
+ if (cert.getSubjectX500Principal().equals(issuer)) {
+ newList.add(cert);
+ it.remove();
+ break;
+ }
+ }
+ }
+ if (!oldList.isEmpty()) {
+ return certs;
+ }
+ return newList;
+ }
+
+ public static AttestationResult parseCertificateChain(CertPath certPath) {
+ // noinspection unchecked
+ var certs = (List) certPath.getCertificates();
+ return parseCertificateChain(sortCerts(certs));
+ }
}
diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/IntegrityStatus.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/IntegrityStatus.java
new file mode 100644
index 0000000..3a1f802
--- /dev/null
+++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/IntegrityStatus.java
@@ -0,0 +1,117 @@
+package io.github.vvb2060.keyattestation.attestation;
+
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.ASN1Primitive;
+import org.bouncycastle.asn1.ASN1Sequence;
+import org.bouncycastle.asn1.ASN1SequenceParser;
+import org.bouncycastle.asn1.ASN1TaggedObject;
+
+import java.io.IOException;
+import java.security.cert.CertificateParsingException;
+
+public class IntegrityStatus {
+ private static final int TRUST_BOOT = 0;
+ private static final int WARRANTY = 1;
+ private static final int ICD = 2;
+ private static final int KERNEL_STATUS = 3;
+ private static final int SYSTEM_STATUS = 4;
+ private static final int AUTH_RESULT = 5;
+
+ public static final int STATUS_NORMAL = 0;
+ public static final int STATUS_ABNORMAL = 1;
+ public static final int STATUS_NOT_SUPPORT = 2;
+
+ private int trustBoot;
+ private int warranty;
+ private int icd;
+ private int kernelStatus;
+ private int systemStatus;
+ private AuthResult authResult;
+
+ public IntegrityStatus(ASN1Encodable asn1Encodable) throws CertificateParsingException {
+ if (!(asn1Encodable instanceof ASN1Sequence sequence)) {
+ throw new CertificateParsingException("Expected sequence for integrity status, found "
+ + asn1Encodable.getClass().getName());
+ }
+
+ ASN1SequenceParser parser = sequence.parser();
+ ASN1TaggedObject entry = parseAsn1TaggedObject(parser);
+
+ for (; entry != null; entry = parseAsn1TaggedObject(parser)) {
+ int tag = entry.getTagNo();
+ ASN1Primitive value = entry.getBaseObject().toASN1Primitive();
+
+ switch (tag) {
+ case TRUST_BOOT:
+ trustBoot = Asn1Utils.getIntegerFromAsn1(value);
+ break;
+ case WARRANTY:
+ warranty = Asn1Utils.getIntegerFromAsn1(value);
+ break;
+ case ICD:
+ icd = Asn1Utils.getIntegerFromAsn1(value);
+ break;
+ case KERNEL_STATUS:
+ kernelStatus = Asn1Utils.getIntegerFromAsn1(value);
+ break;
+ case SYSTEM_STATUS:
+ systemStatus = Asn1Utils.getIntegerFromAsn1(value);
+ break;
+ case AUTH_RESULT:
+ authResult = new AuthResult(value);
+ break;
+ }
+ }
+ }
+
+ private static ASN1TaggedObject parseAsn1TaggedObject(ASN1SequenceParser parser)
+ throws CertificateParsingException {
+ ASN1Encodable asn1Encodable = parseAsn1Encodable(parser);
+ if (asn1Encodable == null || asn1Encodable instanceof ASN1TaggedObject) {
+ return (ASN1TaggedObject) asn1Encodable;
+ }
+ throw new CertificateParsingException(
+ "Expected tagged object, found " + asn1Encodable.getClass().getName());
+ }
+
+ private static ASN1Encodable parseAsn1Encodable(ASN1SequenceParser parser)
+ throws CertificateParsingException {
+ try {
+ return parser.readObject();
+ } catch (IOException e) {
+ throw new CertificateParsingException("Failed to parse ASN1 sequence", e);
+ }
+ }
+
+ public AuthResult getAuthResult() {
+ return authResult;
+ }
+
+ public String statusToString(int status) {
+ switch (status) {
+ case STATUS_NORMAL:
+ return "Normal";
+ case STATUS_ABNORMAL:
+ return "Abnormal";
+ case STATUS_NOT_SUPPORT:
+ return "Not support";
+ default:
+ return Integer.toHexString(status);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("Trustboot: ")
+ .append(statusToString(trustBoot)).append('\n')
+ .append("Warranty bit: ")
+ .append(statusToString(warranty)).append('\n')
+ .append("ICD: ")
+ .append(statusToString(icd)).append('\n')
+ .append("Kernel status: ")
+ .append(statusToString(kernelStatus)).append('\n')
+ .append("System status: ")
+ .append(statusToString(systemStatus));
+ return sb.toString();
+ }
+}
diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/attestation/KnoxAttestation.java b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/KnoxAttestation.java
new file mode 100644
index 0000000..24e4fff
--- /dev/null
+++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/KnoxAttestation.java
@@ -0,0 +1,93 @@
+package io.github.vvb2060.keyattestation.attestation;
+
+import android.os.Build;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.ASN1Sequence;
+import org.bouncycastle.asn1.ASN1SequenceParser;
+import org.bouncycastle.asn1.ASN1TaggedObject;
+
+import java.io.IOException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+
+import io.github.vvb2060.keyattestation.AppApplication;
+
+public class KnoxAttestation extends Asn1Attestation {
+ static final String RO_PRODUCT_FIRST_API = "ro.product.first_api_level";
+ static final int KNOX_TEE_PROPERTIES_INTEGRITY_STATUS = 5;
+
+ IntegrityStatus mKnoxIntegrity;
+
+ /**
+ * Constructs a {@code KnoxAttestation} object from the provided {@link X509Certificate},
+ * extracting the attestation data from the attestation extension.
+ *
+ * @param x509Cert
+ * @throws CertificateParsingException if the certificate does not contain a properly-formatted
+ * attestation extension.
+ */
+ public KnoxAttestation(X509Certificate x509Cert) throws CertificateParsingException {
+ super(x509Cert);
+ ASN1Sequence knoxExtSeq = getKnoxExtensionSequence(x509Cert);
+
+ if (knoxExtSeq != null) {
+ for (int i = 0; i < knoxExtSeq.size(); i++) {
+ if (knoxExtSeq.getObjectAt(i) instanceof ASN1TaggedObject entry) {
+ if (entry.getTagNo() == KNOX_TEE_PROPERTIES_INTEGRITY_STATUS) {
+ mKnoxIntegrity = new IntegrityStatus(entry.getBaseObject().toASN1Primitive());
+ break;
+ }
+ }
+ }
+ }
+
+ teeEnforced.setIntegrityStatus(mKnoxIntegrity);
+ }
+
+ ASN1Sequence getKnoxExtensionSequence(X509Certificate x509Cert)
+ throws CertificateParsingException {
+ byte[] knoxExtensionSequence = x509Cert.getExtensionValue(Attestation.KNOX_EXTENSION_OID);
+ if (knoxExtensionSequence == null) {
+ Log.e(AppApplication.TAG, "getKnoxExtensionSequence : not include knox extension");
+ return null;
+ }
+
+ String value = bytesToHex(knoxExtensionSequence);
+
+ int lengthOfExtension = Integer.parseInt(value.substring(2, 4), 16);
+ int lengthOfValue = Integer.parseInt(value.substring(10, 12), 16);
+ String firstApiLevel = SystemProperties.get(RO_PRODUCT_FIRST_API);
+
+ if (!TextUtils.isEmpty(firstApiLevel)
+ && Integer.parseInt(firstApiLevel) < Build.VERSION_CODES.O) {
+ if (lengthOfExtension - 4 != lengthOfValue) {
+ byte[] copy = new byte[lengthOfValue + 6];
+ System.arraycopy(knoxExtensionSequence, 0,
+ copy, 0, lengthOfValue + 6);
+ System.arraycopy(Integer.toHexString(lengthOfValue + 4).getBytes(), 1,
+ copy, 1, 1);
+ System.arraycopy(Integer.toHexString(lengthOfValue + 2).getBytes(), 1,
+ copy, 3, 1);
+ knoxExtensionSequence = copy;
+ }
+ }
+
+ if (knoxExtensionSequence == null || knoxExtensionSequence.length == 0) {
+ throw new CertificateParsingException("Did not find extension with OID "
+ + KNOX_EXTENSION_OID);
+ }
+ return Asn1Utils.getAsn1SequenceFromBytes(knoxExtensionSequence);
+ }
+
+ private String bytesToHex(byte[] a) {
+ StringBuilder sb = new StringBuilder();
+ for (byte b : a) {
+ sb.append(String.format("%02x", Integer.valueOf(b & 255)));
+ }
+ return sb.toString();
+ }
+}
diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/home/CommonItemViewHolder.kt b/app/src/main/java/io/github/vvb2060/keyattestation/home/CommonItemViewHolder.kt
index fa85456..c6dcbfc 100644
--- a/app/src/main/java/io/github/vvb2060/keyattestation/home/CommonItemViewHolder.kt
+++ b/app/src/main/java/io/github/vvb2060/keyattestation/home/CommonItemViewHolder.kt
@@ -140,7 +140,8 @@ open class CommonItemViewHolder(itemView: View, binding: HomeCommonItemBindin
isClickable = false
iconRes = R.drawable.ic_untrustworthy_24
colorAttr = rikka.material.R.attr.colorWarning
- } else if (data.issuer == CertificateInfo.KEY_GOOGLE) {
+ } else if (data.issuer == CertificateInfo.KEY_GOOGLE
+ || data.issuer == CertificateInfo.KEY_SAMSUNG) {
isVisible = true
isClickable = false
iconRes = R.drawable.ic_trustworthy_24
diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeAdapter.kt b/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeAdapter.kt
index 260780a..4ef3897 100644
--- a/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeAdapter.kt
+++ b/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeAdapter.kt
@@ -202,6 +202,8 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() {
list.origin?.let { AuthorizationList.originToString(it) },
list.rollbackResistant?.toString(),
list.rootOfTrust?.toString(),
+ list.integrityStatus?.toString(),
+ list.integrityStatus?.authResult?.toString(),
list.osVersion?.toString(),
list.osPatchLevel?.toString(),
list.attestationApplicationId?.toString()?.trim(),
@@ -249,6 +251,8 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() {
R.string.authorization_list_origin,
R.string.authorization_list_rollbackResistant,
R.string.authorization_list_rootOfTrust,
+ R.string.authorization_list_integrityStatus,
+ R.string.authorization_list_authResult,
R.string.authorization_list_osVersion,
R.string.authorization_list_osPatchLevel,
R.string.authorization_list_attestationApplicationId,
@@ -295,6 +299,8 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() {
R.string.authorization_list_origin_description,
R.string.authorization_list_rollbackResistant_description,
R.string.authorization_list_rootOfTrust_description,
+ R.string.authorization_list_integrityStatus_description,
+ R.string.authorization_list_authResult_description,
R.string.authorization_list_osVersion_description,
R.string.authorization_list_osPatchLevel_description,
R.string.authorization_list_attestationApplicationId_description,
diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeFragment.kt b/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeFragment.kt
index f614dac..392f5d6 100644
--- a/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeFragment.kt
+++ b/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeFragment.kt
@@ -43,7 +43,11 @@ class HomeFragment : AppFragment(), HomeAdapter.Listener, MenuProvider {
}
private val save = registerForActivityResult(CreateDocument("application/x-pkcs7-certificates")) {
- viewModel.save(requireContext().contentResolver, it)
+ viewModel.save(requireContext().contentResolver, it, "PKCS7")
+ }
+
+ private val save2 = registerForActivityResult(CreateDocument("application/pkix-pkipath")) {
+ viewModel.save(requireContext().contentResolver, it, "PkiPath")
}
private val load = registerForActivityResult(OpenDocument()) {
@@ -150,13 +154,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
@@ -167,6 +176,7 @@ class HomeFragment : AppFragment(), HomeAdapter.Listener, MenuProvider {
isChecked = viewModel.preferShowAll
}
menu.findItem(R.id.menu_save).isVisible = viewModel.currentCerts != null
+ menu.findItem(R.id.menu_save2).isVisible = viewModel.currentCerts != null
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
@@ -175,6 +185,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
@@ -202,6 +218,9 @@ class HomeFragment : AppFragment(), HomeAdapter.Listener, MenuProvider {
R.id.menu_save -> {
save.launch("${Build.PRODUCT}-${AppApplication.TAG}.p7b")
}
+ R.id.menu_save2 -> {
+ save2.launch("${Build.PRODUCT}-${AppApplication.TAG}.pkipath")
+ }
R.id.menu_load -> {
load.launch(arrayOf("application/*"))
}
diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeViewModel.kt b/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeViewModel.kt
index f1a60ab..64be53f 100644
--- a/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeViewModel.kt
+++ b/app/src/main/java/io/github/vvb2060/keyattestation/home/HomeViewModel.kt
@@ -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
@@ -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
@@ -44,12 +47,21 @@ import java.security.cert.CertificateParsingException
import java.security.cert.X509Certificate
import java.security.spec.ECGenParameterSpec
import java.util.Date
+import javax.security.auth.x500.X500Principal
class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : ViewModel() {
val attestationResult = MutableLiveData>()
var currentCerts: List? = 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)
@@ -86,6 +98,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?) {
@@ -111,15 +124,34 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie
if (attestKeyAlias != null && !attestKey) {
builder.setAttestKeyAlias(attestKeyAlias)
}
+ if (attestKey) {
+ builder.setCertificateSubject(X500Principal("CN=App Attest Key"))
+ }
}
- 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: List
@@ -129,9 +161,9 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
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 chainAlias = if (useAttestKey) attestKeyAlias else alias
val certificates = keyStore.getCertificateChain(chainAlias)
?: throw CertificateException("Unable to get certificate chain")
@@ -181,7 +213,7 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie
return parseCertificateChain(certs)
}
- fun save(cr: ContentResolver, uri: Uri?) = AppApplication.executor.execute {
+ fun save(cr: ContentResolver, uri: Uri?, encoding: String) = AppApplication.executor.execute {
val certs = currentCerts
if (uri == null || certs == null) return@execute
var name = uri.toString()
@@ -195,7 +227,7 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie
try {
val cf = CertificateFactory.getInstance("X.509")
cr.openOutputStream(uri)?.use {
- it.write(cf.generateCertPath(certs).getEncoded("PKCS7"))
+ it.write(cf.generateCertPath(certs).getEncoded(encoding))
} ?: throw IOException("openOutputStream $uri failed")
AppApplication.mainHandler.post {
Toast.makeText(AppApplication.app, name, Toast.LENGTH_SHORT).show()
@@ -215,17 +247,16 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie
val certPath = BufferedInputStream(cr.openInputStream(uri)).use {
try {
it.mark(8192)
- cf.generateCertPath(it, "PKCS7")
+ cf.generateCertPath(it, "PkiPath")
} catch (_: CertificateException) {
it.reset()
- cf.generateCertPath(it, "PkiPath")
+ cf.generateCertPath(it, "PKCS7")
}
}
- val certs = certPath.certificates
- if (certs.isEmpty()) throw CertificateParsingException("No certificate found")
- @Suppress("UNCHECKED_CAST")
- val attestationResult = parseCertificateChain(certs as List)
- Resource.success(attestationResult)
+ if (certPath.certificates.isEmpty()) {
+ throw CertificateParsingException("No certificate found")
+ }
+ Resource.success(parseCertificateChain(certPath))
} catch (e: Throwable) {
val cause = if (e is AttestationException) e.cause else e
Log.w(AppApplication.TAG, "Load attestation error.", cause)
@@ -244,11 +275,12 @@ class HomeViewModel(pm: PackageManager, private val sp: SharedPreferences) : Vie
currentCerts = null
attestationResult.postValue(Resource.loading(null))
+ 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
diff --git a/app/src/main/java/io/github/vvb2060/keyattestation/util/SamsungUtils.kt b/app/src/main/java/io/github/vvb2060/keyattestation/util/SamsungUtils.kt
new file mode 100644
index 0000000..72434d0
--- /dev/null
+++ b/app/src/main/java/io/github/vvb2060/keyattestation/util/SamsungUtils.kt
@@ -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
+ }
+}
diff --git a/app/src/main/res/menu/home.xml b/app/src/main/res/menu/home.xml
index 63f468d..f5533c1 100644
--- a/app/src/main/res/menu/home.xml
+++ b/app/src/main/res/menu/home.xml
@@ -1,6 +1,12 @@