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). + +