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..44bbfd6
--- /dev/null
+++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthResult.java
@@ -0,0 +1,106 @@
+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() {
+ 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();
+ }
+}
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/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/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/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5e0f1d7..2964876 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -115,6 +115,8 @@ This software is open source under %2$s (%1$s).
Origin
Rollback resistant
Root of trust
+ Integrity status
+ Caller auth status
OS version
OS patch level
Attestation application ID
@@ -211,6 +213,12 @@ This software is open source under %2$s (%1$s).
+
+