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..b20a330 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 @@ -25,6 +25,7 @@ import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1OctetString; import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1PrintableString; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.ASN1Set; import org.bouncycastle.asn1.DEROctetString; @@ -131,6 +132,15 @@ public static String getStringFromAsn1OctetStreamAssumingUTF8(ASN1Encodable enco return new String(octetString.getOctets(), StandardCharsets.UTF_8); } + public static String getStringFromASN1PrintableString(ASN1Encodable encodable) + throws CertificateParsingException { + if (!(encodable instanceof ASN1PrintableString printableString)) { + throw new CertificateParsingException( + "Expected printable string, found " + encodable.getClass().getName()); + } + return printableString.getString(); + } + public static Date getDateFromAsn1(ASN1Primitive value) throws CertificateParsingException { return new Date(getLongFromAsn1(value)); } 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 00cd1b7..2cbd20e 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 @@ -36,6 +36,7 @@ public abstract class Attestation { 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 KNOX_OID = "1.3.6.1.4.1.236.11.3.23.7"; static final String KEY_USAGE_OID = "2.5.29.15"; // Standard key usage extension. static final String CRL_DP_OID = "2.5.29.31"; // Standard CRL Distribution Points extension. @@ -82,6 +83,9 @@ public static Attestation loadFromCertificate(X509Certificate x509Cert) throws C Log.w(AppApplication.TAG, "CRL Distribution Points extension found in leaf certificate."); } + if (x509Cert.getExtensionValue(KNOX_OID) != null) { + return new KnoxAttestation(x509Cert); + } return new Asn1Attestation(x509Cert); } 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..6e117a0 --- /dev/null +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/AuthResult.java @@ -0,0 +1,63 @@ +package io.github.vvb2060.keyattestation.attestation; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1TaggedObject; + +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; + + private int callerAuthResult = IntegrityStatus.STATUS_NOT_SUPPORT; + private String callingPackage; + private String callingPackageSigs; + private int callingPackageAuthResult = IntegrityStatus.STATUS_NOT_SUPPORT; + + public AuthResult(ASN1Encodable asn1Encodable) throws CertificateParsingException { + if (!(asn1Encodable instanceof ASN1Sequence sequence)) { + throw new CertificateParsingException("Expected sequence for caller auth, found " + + asn1Encodable.getClass().getName()); + } + for (var entry : sequence) { + if (!(entry instanceof ASN1TaggedObject taggedObject)) { + throw new CertificateParsingException( + "Expected tagged object, found " + entry.getClass().getName()); + } + int tag = taggedObject.getTagNo(); + var value = taggedObject.getBaseObject().toASN1Primitive(); + switch (tag) { + case CALLER_AUTH_RESULT -> callerAuthResult = Asn1Utils.getIntegerFromAsn1(value); + case CALLING_PACKAGE -> + callingPackage = Asn1Utils.getStringFromASN1PrintableString(value); + case CALLING_PACKAGE_SIGS -> + callingPackageSigs = Asn1Utils.getStringFromASN1PrintableString(value); + case CALLING_PACKAGE_AUTH_RESULT -> + callingPackageAuthResult = Asn1Utils.getIntegerFromAsn1(value); + default -> throw new CertificateParsingException("invalid tag no: " + tag); + } + } + } + + @Override + public String toString() { + return "Caller Auth Result: " + IntegrityStatus.statusToString(callerAuthResult) + + "\nCalling Package: " + callingPackage + + "\nCalling Package Signatures: " + callingPackageSigs + + "\nCalling Package Auth Result: " + IntegrityStatus.statusToString(callingPackageAuthResult); + } + + public static AuthResult parse(ASN1Encodable asn1Encodable) throws CertificateParsingException { + var auth = new AuthResult(asn1Encodable); + if (auth.callerAuthResult == IntegrityStatus.STATUS_NOT_SUPPORT && + auth.callingPackage == null && + auth.callingPackageSigs == null && + auth.callingPackageAuthResult == IntegrityStatus.STATUS_NOT_SUPPORT) { + return null; + } + return auth; + } +} 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 c02967e..0b18006 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 @@ -5,7 +5,6 @@ import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1OctetString; -import org.json.JSONObject; import java.io.ByteArrayInputStream; import java.security.GeneralSecurityException; 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..25960c4 --- /dev/null +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/IntegrityStatus.java @@ -0,0 +1,71 @@ +package io.github.vvb2060.keyattestation.attestation; + +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1TaggedObject; + +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 = STATUS_NOT_SUPPORT; + private int warranty = STATUS_NOT_SUPPORT; + private int icd = STATUS_NOT_SUPPORT; + private int kernelStatus = STATUS_NOT_SUPPORT; + private int systemStatus = STATUS_NOT_SUPPORT; + 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()); + } + for (var entry : sequence) { + if (!(entry instanceof ASN1TaggedObject taggedObject)) { + throw new CertificateParsingException( + "Expected tagged object, found " + entry.getClass().getName()); + } + int tag = taggedObject.getTagNo(); + var value = taggedObject.getBaseObject().toASN1Primitive(); + switch (tag) { + case TRUST_BOOT -> trustBoot = Asn1Utils.getIntegerFromAsn1(value); + case WARRANTY -> warranty = Asn1Utils.getIntegerFromAsn1(value); + case ICD -> icd = Asn1Utils.getIntegerFromAsn1(value); + case KERNEL_STATUS -> kernelStatus = Asn1Utils.getIntegerFromAsn1(value); + case SYSTEM_STATUS -> systemStatus = Asn1Utils.getIntegerFromAsn1(value); + case AUTH_RESULT -> authResult = AuthResult.parse(value); + default -> throw new CertificateParsingException("invalid tag no: " + tag); + } + } + } + + public static String statusToString(int status) { + return switch (status) { + case STATUS_NORMAL -> "Normal"; + case STATUS_ABNORMAL -> "Abnormal"; + case STATUS_NOT_SUPPORT -> "Not support"; + default -> Integer.toHexString(status); + }; + } + + @Override + public String toString() { + return "TrustBoot: " + statusToString(trustBoot) + + "\nWarranty: " + statusToString(warranty) + + "\nICD: " + statusToString(icd) + + "\nKernel Status: " + statusToString(kernelStatus) + + "\nSystem Status: " + statusToString(systemStatus) + + "\nCaller auth(with PROCA) Status: \n" + + (authResult == null ? "Not performed" : authResult.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..be56686 --- /dev/null +++ b/app/src/main/java/io/github/vvb2060/keyattestation/attestation/KnoxAttestation.java @@ -0,0 +1,69 @@ +package io.github.vvb2060.keyattestation.attestation; + +import com.google.common.io.BaseEncoding; + +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1TaggedObject; + +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; + +// https://docs.samsungknox.com/dev/knox-attestation/ +public class KnoxAttestation extends Asn1Attestation { + private static final int CHALLENGE = 0; + private static final int INTEGRITY = 5; + private static final int ATTESTATION_RECORD_HASH = 6; + + private String challenge; + private IntegrityStatus knoxIntegrity; + private byte[] recordHash; + + public KnoxAttestation(X509Certificate x509Cert) throws CertificateParsingException { + super(x509Cert); + ASN1Sequence knoxExtSeq = getKnoxExtensionSequence(x509Cert); + for (var entry : knoxExtSeq) { + if (!(entry instanceof ASN1TaggedObject taggedObject)) { + throw new CertificateParsingException( + "Expected tagged object, found " + entry.getClass().getName()); + } + int tag = taggedObject.getTagNo(); + var value = taggedObject.getBaseObject().toASN1Primitive(); + switch (tag) { + case CHALLENGE -> challenge = Asn1Utils.getStringFromASN1PrintableString(value); + case INTEGRITY -> knoxIntegrity = new IntegrityStatus(value); + case ATTESTATION_RECORD_HASH -> recordHash = Asn1Utils.getByteArrayFromAsn1(value); + default -> throw new CertificateParsingException("invalid tag no: " + tag); + } + } + } + + ASN1Sequence getKnoxExtensionSequence(X509Certificate x509Cert) + throws CertificateParsingException { + byte[] knoxExtensionSequence = x509Cert.getExtensionValue(KNOX_OID); + if (knoxExtensionSequence == null || knoxExtensionSequence.length == 0) { + throw new CertificateParsingException("Did not find extension with OID " + KNOX_OID); + } + return Asn1Utils.getAsn1SequenceFromBytes(knoxExtensionSequence); + } + + public String getKnoxChallenge() { + return challenge; + } + + public IntegrityStatus getKnoxIntegrity() { + return knoxIntegrity; + } + + public byte[] getRecordHash() { + return recordHash; + } + + @Override + public String toString() { + return super.toString() + + "\n\nExtension type: " + getClass().getSimpleName() + + "\nChallenge: " + challenge + + "\nIntegrity status: " + knoxIntegrity + + "\nAttestation record hash: " + BaseEncoding.base16().encode(recordHash); + } +} 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 7598a5a..4af1866 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 @@ -7,6 +7,7 @@ import io.github.vvb2060.keyattestation.attestation.Attestation import io.github.vvb2060.keyattestation.attestation.AttestationResult import io.github.vvb2060.keyattestation.attestation.AuthorizationList import io.github.vvb2060.keyattestation.attestation.CertificateInfo +import io.github.vvb2060.keyattestation.attestation.KnoxAttestation import io.github.vvb2060.keyattestation.lang.AttestationException import rikka.recyclerview.IdBasedRecyclerViewAdapter @@ -136,6 +137,28 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { } } + if (attestation is KnoxAttestation) { + id = ID_KNOX_START + addItem(SubtitleViewHolder.CREATOR, SubtitleData( + R.string.knox, + R.string.knox_description), id++) + + addItem(CommonItemViewHolder.COMMON_CREATOR, CommonData( + R.string.knox_challenge, + R.string.knox_challenge_description, + attestation.knoxChallenge), id++) + + addItem(CommonItemViewHolder.COMMON_CREATOR, CommonData( + R.string.knox_integrity, + R.string.knox_integrity_description, + attestation.knoxIntegrity.toString()), id++) + + addItem(CommonItemViewHolder.COMMON_CREATOR, CommonData( + R.string.knox_record_hash, + R.string.knox_record_hash_description, + BaseEncoding.base16().encode(attestation.recordHash)), id++) + } + notifyDataSetChanged() } @@ -178,6 +201,7 @@ class HomeAdapter(listener: Listener) : IdBasedRecyclerViewAdapter() { private const val ID_CERT_INFO_START = 2000L private const val ID_DESCRIPTION_START = 3000L private const val ID_AUTHORIZATION_LIST_START = 4000L + private const val ID_KNOX_START = 5000L private const val ID_ERROR_MESSAGE = 100000L private fun createAuthorizationItems(list: AuthorizationList): Array { diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index afc04d9..539fd39 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -201,4 +201,19 @@ + + + +
  • 请求应用的软件包名称、版本号和开发者密钥。
  • +
  • 设备当前状态和预期环境的签名信息。
  • +
  • 硬件保险丝读数显示设备上是否加载过不受信任的固件。
  • + + ]]>
    + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 36f2dbc..55b8941 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -272,4 +272,24 @@ + + Samsung Knox Attestation + + @string/attestation_challenge + @string/attestation_challenge_description + Integrity status + +
  • the requesting app’s package name, version code, and developer key.
  • +
  • signed info about the device’s current state and expected environment.
  • +
  • hardware fuse readings indicating if untrusted firmware was ever loaded onto the device.
  • + + ]]>
    + Attestation record hash +