Skip to content

Commit

Permalink
Initial implementation of KeyboxImitationHooks
Browse files Browse the repository at this point in the history
Squashed:

  Author: Adithya R <[email protected]>
  Date:   Mon, 23 Sep 2024 17:07:15 +0530

      KeyboxImitationHooks: Few minor improvements

      - Avoid logspam
      - Bail out if current process is unknown
      - Print current process name with debug logs

      Change-Id: I2af79186c9de085c96c6be0f242327f5cfafe34f

Co-authored-by: chiteroman <[email protected]>
Co-authored-by: Fabian Leutenegger <[email protected]>
Change-Id: I163df27cc743f33ae1b30161f65076b744e16754
Signed-off-by: Adithya R <[email protected]>
  • Loading branch information
3 people authored and fazilsheik96 committed Feb 27, 2025
1 parent 26dccd1 commit 5608ba0
Show file tree
Hide file tree
Showing 7 changed files with 418 additions and 3 deletions.
56 changes: 56 additions & 0 deletions core/java/com/android/internal/util/IKeyboxProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* SPDX-FileCopyrightText: 2024 Paranoid Android
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.internal.util;

/**
* Interface for keybox providers.
*
* This interface defines the methods that a keybox provider must implement
* to provide access to EC and RSA keys and certificate chains.
*
* @hide
*/
public interface IKeyboxProvider {

/**
* Checks if a valid keybox is available.
*
* @return true if a valid keybox is available, false otherwise
* @hide
*/
boolean hasKeybox();

/**
* Retrieves the EC private key.
*
* @return the EC private key as a String
* @hide
*/
String getEcPrivateKey();

/**
* Retrieves the RSA private key.
*
* @return the RSA private key as a String
* @hide
*/
String getRsaPrivateKey();

/**
* Retrieves the EC certificate chain.
*
* @return an array of Strings representing the EC certificate chain
* @hide
*/
String[] getEcCertificateChain();

/**
* Retrieves the RSA certificate chain.
*
* @return an array of Strings representing the RSA certificate chain
* @hide
*/
String[] getRsaCertificateChain();
}
114 changes: 114 additions & 0 deletions core/java/com/android/internal/util/KeyProviderManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Paranoid Android
* SPDX-License-Identifier: Apache-2.0
*/
package com.android.internal.util;

import android.app.ActivityThread;
import android.content.Context;
import android.util.Log;

import com.android.internal.R;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
* Manager class for handling keybox providers.
* @hide
*/
public final class KeyProviderManager {

private static final String TAG = "KeyProviderManager";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

private static final IKeyboxProvider PROVIDER = new DefaultKeyboxProvider();

private KeyProviderManager() {
}

public static IKeyboxProvider getProvider() {
return PROVIDER;
}

public static boolean isKeyboxAvailable() {
if (!PropImitationHooks.sEnableKeyboxImitation) {
dlog("Key attestation spoofing is disabled by user");
return false;
}
return PROVIDER.hasKeybox();
}

private static void dlog(String msg) {
if (DEBUG) Log.d(TAG, msg);
}

private static class DefaultKeyboxProvider implements IKeyboxProvider {
private final Map<String, String> keyboxData = new HashMap<>();

private DefaultKeyboxProvider() {
Context context = getApplicationContext();
if (context == null) {
Log.e(TAG, "Failed to get application context");
return;
}

String[] keybox = context.getResources().getStringArray(R.array.config_certifiedKeybox);

Arrays.stream(keybox)
.map(entry -> entry.split(":", 2))
.filter(parts -> parts.length == 2)
.forEach(parts -> keyboxData.put(parts[0], parts[1]));

if (!hasKeybox()) {
Log.w(TAG, "Incomplete keybox data loaded");
}
}

private static Context getApplicationContext() {
try {
return ActivityThread.currentApplication().getApplicationContext();
} catch (Exception e) {
Log.e(TAG, "Error getting application context", e);
return null;
}
}

@Override
public boolean hasKeybox() {
return Arrays.asList("EC.PRIV", "EC.CERT_1", "EC.CERT_2", "EC.CERT_3",
"RSA.PRIV", "RSA.CERT_1", "RSA.CERT_2", "RSA.CERT_3")
.stream()
.allMatch(keyboxData::containsKey);
}

@Override
public String getEcPrivateKey() {
return keyboxData.get("EC.PRIV");
}

@Override
public String getRsaPrivateKey() {
return keyboxData.get("RSA.PRIV");
}

@Override
public String[] getEcCertificateChain() {
return getCertificateChain("EC");
}

@Override
public String[] getRsaCertificateChain() {
return getCertificateChain("RSA");
}

private String[] getCertificateChain(String prefix) {
return new String[]{
keyboxData.get(prefix + ".CERT_1"),
keyboxData.get(prefix + ".CERT_2"),
keyboxData.get(prefix + ".CERT_3")
};
}
}
}
215 changes: 215 additions & 0 deletions core/java/com/android/internal/util/KeyboxImitationHooks.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Paranoid Android
* SPDX-License-Identifier: Apache-2.0
*/

package com.android.internal.util;

import android.app.Application;
import android.security.KeyChain;
import android.security.keystore.KeyProperties;
import android.system.keystore2.KeyEntryResponse;
import android.text.TextUtils;
import android.util.Log;

import com.android.internal.org.bouncycastle.asn1.ASN1Boolean;
import com.android.internal.org.bouncycastle.asn1.ASN1Encodable;
import com.android.internal.org.bouncycastle.asn1.ASN1EncodableVector;
import com.android.internal.org.bouncycastle.asn1.ASN1Enumerated;
import com.android.internal.org.bouncycastle.asn1.ASN1ObjectIdentifier;
import com.android.internal.org.bouncycastle.asn1.ASN1OctetString;
import com.android.internal.org.bouncycastle.asn1.ASN1Sequence;
import com.android.internal.org.bouncycastle.asn1.ASN1TaggedObject;
import com.android.internal.org.bouncycastle.asn1.DEROctetString;
import com.android.internal.org.bouncycastle.asn1.DERSequence;
import com.android.internal.org.bouncycastle.asn1.DERTaggedObject;
import com.android.internal.org.bouncycastle.asn1.x509.Extension;
import com.android.internal.org.bouncycastle.cert.X509CertificateHolder;
import com.android.internal.org.bouncycastle.cert.X509v3CertificateBuilder;
import com.android.internal.org.bouncycastle.operator.ContentSigner;
import com.android.internal.org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

import java.io.ByteArrayOutputStream;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.concurrent.ThreadLocalRandom;

/**
* @hide
*/
public class KeyboxImitationHooks {

private static final String TAG = "KeyboxImitationHooks";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

private static final ASN1ObjectIdentifier KEY_ATTESTATION_OID = new ASN1ObjectIdentifier(
"1.3.6.1.4.1.11129.2.1.17");

private static volatile String sProcessName;

private static PrivateKey parsePrivateKey(String encodedKey, String algorithm)
throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(encodedKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
return KeyFactory.getInstance(algorithm).generatePrivate(keySpec);
}

private static byte[] parseCertificate(String encodedCert) {
return Base64.getDecoder().decode(encodedCert);
}

private static byte[] getCertificateChain(String algorithm) throws Exception {
IKeyboxProvider provider = KeyProviderManager.getProvider();
String[] certChain = KeyProperties.KEY_ALGORITHM_EC.equals(algorithm)
? provider.getEcCertificateChain()
: provider.getRsaCertificateChain();

ByteArrayOutputStream certificateStream = new ByteArrayOutputStream();
for (String cert : certChain) {
certificateStream.write(parseCertificate(cert));
}
return certificateStream.toByteArray();
}

private static PrivateKey getPrivateKey(String algorithm) throws Exception {
IKeyboxProvider provider = KeyProviderManager.getProvider();
String privateKeyEncoded = KeyProperties.KEY_ALGORITHM_EC.equals(algorithm)
? provider.getEcPrivateKey()
: provider.getRsaPrivateKey();

return parsePrivateKey(privateKeyEncoded, algorithm);
}

private static X509CertificateHolder getCertificateHolder(String algorithm) throws Exception {
IKeyboxProvider provider = KeyProviderManager.getProvider();
String certChain = KeyProperties.KEY_ALGORITHM_EC.equals(algorithm)
? provider.getEcCertificateChain()[0]
: provider.getRsaCertificateChain()[0];

return new X509CertificateHolder(parseCertificate(certChain));
}

private static byte[] modifyLeafCertificate(X509Certificate leafCertificate,
String keyAlgorithm) throws Exception {
X509CertificateHolder certificateHolder = new X509CertificateHolder(
leafCertificate.getEncoded());
Extension keyAttestationExtension = certificateHolder.getExtension(KEY_ATTESTATION_OID);
ASN1Sequence keyAttestationSequence = ASN1Sequence.getInstance(
keyAttestationExtension.getExtnValue().getOctets());
ASN1Encodable[] keyAttestationEncodables = keyAttestationSequence.toArray();
ASN1Sequence teeEnforcedSequence = (ASN1Sequence) keyAttestationEncodables[7];
ASN1EncodableVector teeEnforcedVector = new ASN1EncodableVector();

ASN1Sequence rootOfTrustSequence = null;
for (ASN1Encodable teeEnforcedEncodable : teeEnforcedSequence) {
ASN1TaggedObject taggedObject = (ASN1TaggedObject) teeEnforcedEncodable;
if (taggedObject.getTagNo() == 704) {
rootOfTrustSequence = (ASN1Sequence) taggedObject.getObject();
continue;
}
teeEnforcedVector.add(teeEnforcedEncodable);
}

if (rootOfTrustSequence == null) throw new Exception("Root of trust not found");

PrivateKey privateKey = getPrivateKey(keyAlgorithm);
X509CertificateHolder providerCertHolder = getCertificateHolder(keyAlgorithm);

X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(
providerCertHolder.getSubject(),
certificateHolder.getSerialNumber(),
certificateHolder.getNotBefore(),
certificateHolder.getNotAfter(),
certificateHolder.getSubject(),
certificateHolder.getSubjectPublicKeyInfo()
);

ContentSigner contentSigner = new JcaContentSignerBuilder(
leafCertificate.getSigAlgName()).build(privateKey);

byte[] verifiedBootKey = new byte[32];
ThreadLocalRandom.current().nextBytes(verifiedBootKey);

DEROctetString verifiedBootHash = (DEROctetString) rootOfTrustSequence.getObjectAt(3);
if (verifiedBootHash == null) {
byte[] randomHash = new byte[32];
ThreadLocalRandom.current().nextBytes(randomHash);
verifiedBootHash = new DEROctetString(randomHash);
}

ASN1Encodable[] rootOfTrustEncodables = {
new DEROctetString(verifiedBootKey),
ASN1Boolean.TRUE,
new ASN1Enumerated(0),
verifiedBootHash
};

ASN1Sequence newRootOfTrustSequence = new DERSequence(rootOfTrustEncodables);
ASN1TaggedObject rootOfTrustTaggedObject = new DERTaggedObject(704, newRootOfTrustSequence);
teeEnforcedVector.add(rootOfTrustTaggedObject);

ASN1Sequence newTeeEnforcedSequence = new DERSequence(teeEnforcedVector);
keyAttestationEncodables[7] = newTeeEnforcedSequence;
ASN1Sequence newKeyAttestationSequence = new DERSequence(keyAttestationEncodables);
ASN1OctetString newKeyAttestationOctetString = new DEROctetString(
newKeyAttestationSequence);
Extension newKeyAttestationExtension = new Extension(KEY_ATTESTATION_OID, false,
newKeyAttestationOctetString);

certificateBuilder.addExtension(newKeyAttestationExtension);

for (ASN1ObjectIdentifier extensionOID :
certificateHolder.getExtensions().getExtensionOIDs()) {
if (KEY_ATTESTATION_OID.getId().equals(extensionOID.getId())) continue;
certificateBuilder.addExtension(certificateHolder.getExtension(extensionOID));
}

return certificateBuilder.build(contentSigner).getEncoded();
}

public static KeyEntryResponse onGetKeyEntry(KeyEntryResponse response) {
if (response == null || response.metadata == null || response.metadata.certificate == null)
return response;

final String processName = Application.getProcessName();
if (TextUtils.isEmpty(processName)) {
Log.e(TAG, "Null process name");
return response;
}
sProcessName = processName;

// If no keybox is found, don't continue spoofing
if (!KeyProviderManager.isKeyboxAvailable()) {
dlog("Key attestation spoofing is unavailable");
return response;
}

try {
X509Certificate certificate = KeyChain.toCertificate(response.metadata.certificate);
if (certificate.getExtensionValue(KEY_ATTESTATION_OID.getId()) == null) {
dlog("Key attestation OID not found, skipping modification");
return response;
}

String keyAlgorithm = certificate.getPublicKey().getAlgorithm();
response.metadata.certificate = modifyLeafCertificate(certificate, keyAlgorithm);
response.metadata.certificateChain = getCertificateChain(keyAlgorithm);
dlog("Succesfully modified certificate chain");
} catch (Exception e) {
elog("Error in onGetKeyEntry", e);
}

return response;
}

private static void dlog(String msg) {
if (DEBUG) Log.d(TAG, "[" + sProcessName + "] " + msg);
}

private static void elog(String msg, Exception e) {
Log.e(TAG, "[" + sProcessName + "] " + msg, e);
}
}
Loading

0 comments on commit 5608ba0

Please sign in to comment.