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 11 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
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 @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,13 +30,32 @@ 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;
public static final int CERT_REVOKED = 2;
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" +
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -207,4 +235,68 @@ public static AttestationResult parseCertificateChain(List<X509Certificate> cert

return AttestationResult.form(infoList);
}

private static List<X509Certificate> sortCerts(List<X509Certificate> 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<X509Certificate>(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<X509Certificate>) certPath.getCertificates();
return parseCertificateChain(sortCerts(certs));
}
}
Loading