Implement Key Attestation for K-Anon

Test: atest com.android.adservices.service.kanon.KeyAttestationTest,
atest
com.android.adservices.service.kanon.KeyAttestationCertificateChainRecordTest

Change-Id: I827ef5eb9c648158dc7f2cc297672941cd807888
diff --git a/adservices/service-core/java/com/android/adservices/service/kanon/KeyAttestation.java b/adservices/service-core/java/com/android/adservices/service/kanon/KeyAttestation.java
new file mode 100644
index 0000000..ef3df8c
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/kanon/KeyAttestation.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.adservices.service.kanon;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import android.security.keystore.StrongBoxUnavailableException;
+
+import com.android.adservices.LoggerFactory;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.spec.ECGenParameterSpec;
+import java.util.ArrayList;
+
+/** Provides method to generate secure hardware key backed X.509 certificate chain */
+public class KeyAttestation {
+
+    private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
+
+    private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
+
+    private static final String PA_KANON_KEY_ALIAS = "PaKanonKeyAttestation";
+
+    private static final String ELLIPTIC_CURVE_KEY_GENERATION_NAME = "secp256r1";
+
+    private final boolean mUseStrongBox;
+
+    private final KeyStore mKeyStore;
+    private final KeyPairGenerator mKeyPairGenerator;
+
+    @VisibleForTesting
+    KeyAttestation(Boolean useStrongBox, KeyStore keyStore, KeyPairGenerator keyPairGenerator) {
+        this.mUseStrongBox = useStrongBox;
+        this.mKeyStore = keyStore;
+        this.mKeyPairGenerator = keyPairGenerator;
+    }
+
+    /** Creates an instance of {@link KeyAttestation} */
+    public static KeyAttestation create(Context context)
+            throws KeyStoreException, NoSuchAlgorithmException, NoSuchProviderException {
+        return new KeyAttestation(
+                context.getPackageManager()
+                        .hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE),
+                KeyStore.getInstance(ANDROID_KEY_STORE),
+                KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEY_STORE));
+    }
+
+    /**
+     * Given a challenge, return a {@link KeyAttestationCertificateChainRecord}. The attestation is
+     * performed using a 256-bit Elliptical Curve (EC) key-pair generated by the secure hardware.
+     */
+    public KeyAttestationCertificateChainRecord generateAttestationRecord(final byte[] challenge)
+            throws IllegalStateException {
+        KeyPair kp = generateHybridKey(challenge, PA_KANON_KEY_ALIAS);
+        if (kp == null) {
+            sLogger.e("Received a null key pair. Can't generate certificate.");
+            throw new IllegalStateException("Unable to generate a key pair");
+        }
+        return getAttestationRecordFromKeyAlias(PA_KANON_KEY_ALIAS);
+    }
+
+    @VisibleForTesting
+    KeyPair generateHybridKey(final byte[] challenge, final String keyAlias) {
+        try {
+            mKeyPairGenerator.initialize(
+                    new KeyGenParameterSpec.Builder(
+                                    /* keystoreAlias= */ keyAlias,
+                                    /* purposes= */ KeyProperties.PURPOSE_SIGN)
+                            .setDigests(KeyProperties.DIGEST_SHA256)
+                            .setAlgorithmParameterSpec(
+                                    new ECGenParameterSpec(ELLIPTIC_CURVE_KEY_GENERATION_NAME))
+                            .setAttestationChallenge(challenge)
+                            .setIsStrongBoxBacked(mUseStrongBox)
+                            .build());
+
+            return mKeyPairGenerator.generateKeyPair();
+        } catch (InvalidAlgorithmParameterException e) {
+            sLogger.e("Failed to generate EC key for attestation: " + e.getMessage());
+            throw new IllegalStateException("Failed to generate a key Pair", e);
+        } catch (StrongBoxUnavailableException e) {
+            sLogger.e(
+                    "Strong box not available on device but isStrongBox is set to true: "
+                            + e.getMessage());
+            throw new IllegalStateException("Failed to generate a key Pair", e);
+        }
+    }
+
+    @VisibleForTesting
+    KeyAttestationCertificateChainRecord getAttestationRecordFromKeyAlias(String keyAlias) {
+        try {
+            mKeyStore.load(null);
+            Certificate[] certificateChain = mKeyStore.getCertificateChain(keyAlias);
+            if (certificateChain == null) {
+                throw new IllegalStateException("Certificate chain was null");
+            }
+            ArrayList<byte[]> attestationRecord = new ArrayList<>();
+            for (Certificate certificate : certificateChain) {
+                attestationRecord.add(certificate.getEncoded());
+            }
+
+            return KeyAttestationCertificateChainRecord.create(attestationRecord);
+        } catch (CertificateException
+                | IOException
+                | KeyStoreException
+                | NoSuchAlgorithmException e) {
+            sLogger.e("Exception when" + "loading certs for attestation: " + e.getMessage());
+            return KeyAttestationCertificateChainRecord.create(new ArrayList<>());
+        }
+    }
+}
diff --git a/adservices/service-core/java/com/android/adservices/service/kanon/KeyAttestationCertificateChainRecord.java b/adservices/service-core/java/com/android/adservices/service/kanon/KeyAttestationCertificateChainRecord.java
new file mode 100644
index 0000000..b4f2619
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/kanon/KeyAttestationCertificateChainRecord.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.adservices.service.kanon;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+
+import dagger.internal.Preconditions;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+@AutoValue
+public abstract class KeyAttestationCertificateChainRecord {
+
+    private static final int BYTES_TO_ENCODE_CERTIFICATE_LENGTH = 4;
+
+    abstract ImmutableList<byte[]> getCertificateChain();
+
+    /** Create a KeyAttestation Record object with the given certificate chain */
+    public static KeyAttestationCertificateChainRecord create(List<byte[]> certificateChain) {
+        Preconditions.checkNotNull(certificateChain);
+        return new AutoValue_KeyAttestationCertificateChainRecord(
+                ImmutableList.copyOf(certificateChain));
+    }
+
+    /**
+     * Encode in a format K-Anon server can understand
+     *
+     * <p>The format is 4 bytes for the size of first certificate followed by the certificate, next
+     * 4 bytes for the size of the second certificate followed by the second certificate, and so on.
+     */
+    public byte[] encode() throws IOException {
+        List<byte[]> certificateChain = getCertificateChain();
+
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+        for (byte[] certificate : certificateChain) {
+            outputStream.write(
+                    ByteBuffer.allocate(BYTES_TO_ENCODE_CERTIFICATE_LENGTH)
+                            .putInt(certificate.length)
+                            .array());
+            outputStream.write(certificate);
+        }
+
+        return outputStream.toByteArray();
+    }
+}
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/kanon/KeyAttestationCertificateChainRecordTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/kanon/KeyAttestationCertificateChainRecordTest.java
new file mode 100644
index 0000000..28ce9d9
--- /dev/null
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/kanon/KeyAttestationCertificateChainRecordTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.adservices.service.kanon;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.io.BaseEncoding;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public final class KeyAttestationCertificateChainRecordTest {
+
+    private static final int BYTES_TO_ENCODE_CERTIFICATE_LENGTH = 4;
+    private static final byte[] FAKE_CERT_1 =
+            BaseEncoding.base16().lowerCase().decode("aabbccddee");
+    private static final byte[] FAKE_CERT_2 = BaseEncoding.base16().lowerCase().decode("112233");
+    private static final byte[] FAKE_CERT_3 =
+            BaseEncoding.base16().lowerCase().decode("11223322aabbddeeff4e");
+    private static final List<byte[]> AS_LIST =
+            Arrays.asList(FAKE_CERT_1, FAKE_CERT_2, FAKE_CERT_3);
+
+    @Test
+    public void test_serialize() throws IOException {
+        byte[] encoded = KeyAttestationCertificateChainRecord.create(AS_LIST).encode();
+
+        assertEquals(
+                12 + FAKE_CERT_1.length + FAKE_CERT_2.length + FAKE_CERT_3.length, encoded.length);
+
+        byte[] length1 = new byte[BYTES_TO_ENCODE_CERTIFICATE_LENGTH];
+        System.arraycopy(encoded, 0, length1, 0, BYTES_TO_ENCODE_CERTIFICATE_LENGTH);
+        assertThat(ByteBuffer.wrap(length1).getInt()).isEqualTo(FAKE_CERT_1.length);
+        byte[] cert1 = new byte[FAKE_CERT_1.length];
+        System.arraycopy(encoded, 4, cert1, 0, FAKE_CERT_1.length);
+        assertArrayEquals(FAKE_CERT_1, cert1);
+
+        byte[] length2 = new byte[BYTES_TO_ENCODE_CERTIFICATE_LENGTH];
+        System.arraycopy(
+                encoded, 4 + FAKE_CERT_1.length, length2, 0, BYTES_TO_ENCODE_CERTIFICATE_LENGTH);
+        assertThat(ByteBuffer.wrap(length2).getInt()).isEqualTo(FAKE_CERT_2.length);
+        byte[] cert2 = new byte[FAKE_CERT_2.length];
+        System.arraycopy(encoded, 8 + FAKE_CERT_1.length, cert2, 0, FAKE_CERT_2.length);
+        assertArrayEquals(FAKE_CERT_2, cert2);
+
+        byte[] length3 = new byte[BYTES_TO_ENCODE_CERTIFICATE_LENGTH];
+        System.arraycopy(
+                encoded,
+                8 + FAKE_CERT_1.length + FAKE_CERT_2.length,
+                length3,
+                0,
+                BYTES_TO_ENCODE_CERTIFICATE_LENGTH);
+        assertThat(ByteBuffer.wrap(length3).getInt()).isEqualTo(FAKE_CERT_3.length);
+        byte[] cert3 = new byte[FAKE_CERT_3.length];
+        System.arraycopy(
+                encoded,
+                12 + FAKE_CERT_1.length + FAKE_CERT_2.length,
+                cert3,
+                0,
+                FAKE_CERT_3.length);
+        assertArrayEquals(FAKE_CERT_3, cert3);
+        assertThat(FAKE_CERT_1).isEqualTo(cert1);
+    }
+
+    @Test
+    public void test_passNull_throwsException() throws IOException {
+        assertThrows(
+                NullPointerException.class,
+                () -> KeyAttestationCertificateChainRecord.create(null).encode());
+    }
+
+    @Test
+    public void test_passEmpty_returnsEmptyByteArray() throws IOException {
+        byte[] encoded = KeyAttestationCertificateChainRecord.create(new ArrayList<>()).encode();
+        assertEquals(0, encoded.length);
+    }
+}
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/kanon/KeyAttestationTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/kanon/KeyAttestationTest.java
new file mode 100644
index 0000000..f04d176
--- /dev/null
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/kanon/KeyAttestationTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.adservices.service.kanon;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.spy;
+
+import android.security.keystore.KeyProperties;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.cert.CertificateException;
+
+public final class KeyAttestationTest {
+    private static final byte[] CHALLENGE =
+            ("AHXUDhoSEFikqOefmo8xE7kGp/xjVMRDYBecBiHGxCN8rTv9W0Z4L/14d0OLB"
+                            + "vC1VVzXBAnjgHoKLZzuJifTOaBJwGNIQ2ejnx3n6ayoRchDNCgpK29T+EAhBWzH")
+                    .getBytes();
+
+    private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
+
+    private static final String KEY_ALIAS = "PaKanonKeyAttestation";
+
+    private KeyAttestation mKeyAttestation;
+
+    private KeyStore mSpyKeyStore;
+
+    private KeyPairGenerator mSpyKeyPairGenerator;
+
+    @Before
+    public void setUp() throws Exception {
+        mSpyKeyStore = spy(KeyStore.getInstance(ANDROID_KEY_STORE));
+        mSpyKeyPairGenerator =
+                spy(
+                        KeyPairGenerator.getInstance(
+                                KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEY_STORE));
+        mKeyAttestation =
+                new KeyAttestation(/* useStrongBox= */ false, mSpyKeyStore, mSpyKeyPairGenerator);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
+        keyStore.load(null);
+        if (keyStore.containsAlias(KEY_ALIAS)) {
+            keyStore.deleteEntry(KEY_ALIAS);
+        }
+
+        Mockito.reset(mSpyKeyStore, mSpyKeyPairGenerator);
+    }
+
+    @Test
+    public void testGenerateAttestationRecord_success() throws Exception {
+        KeyAttestationCertificateChainRecord record =
+                mKeyAttestation.generateAttestationRecord(CHALLENGE);
+
+        assertThat(record.encode().length).isGreaterThan(4);
+    }
+
+    @Test
+    public void testGenerateAttestationRecord_nullKey_throwsException() {
+        doReturn(null).when(mSpyKeyPairGenerator).generateKeyPair();
+
+        assertThrows(
+                IllegalStateException.class,
+                () -> mKeyAttestation.generateAttestationRecord(CHALLENGE));
+    }
+
+    @Test
+    public void testGenerateHybridKey_success() {
+        KeyPair keyPair = mKeyAttestation.generateHybridKey(CHALLENGE, KEY_ALIAS);
+
+        assertThat(keyPair).isNotNull();
+        assertThat(keyPair.getPublic()).isNotNull();
+        assertThat(keyPair.getPrivate()).isNotNull();
+    }
+
+    @Test
+    public void testGenerateHybridKey_initFailure() throws Exception {
+        doThrow(new InvalidAlgorithmParameterException("Invalid Parameters"))
+                .when(mSpyKeyPairGenerator)
+                .initialize(any());
+
+        assertThrows(
+                IllegalStateException.class,
+                () -> mKeyAttestation.generateHybridKey(CHALLENGE, KEY_ALIAS));
+    }
+
+    @Test
+    public void testGetAttestationRecordFromKeyAlias_success() throws Exception {
+        KeyPair unused = mKeyAttestation.generateHybridKey(CHALLENGE, KEY_ALIAS);
+
+        KeyAttestationCertificateChainRecord record =
+                mKeyAttestation.getAttestationRecordFromKeyAlias(KEY_ALIAS);
+
+        assertThat(record.encode().length).isGreaterThan(4);
+    }
+
+    @Test
+    public void testGetAttestationRecordFromKeyAlias_certFailure() throws Exception {
+        doThrow(new CertificateException("Cert Exception")).when(mSpyKeyStore).load(any());
+
+        KeyAttestationCertificateChainRecord record =
+                mKeyAttestation.getAttestationRecordFromKeyAlias(KEY_ALIAS);
+
+        assertThat(record.encode().length).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetAttestationRecordFromKeyAlias_keyStoreFailure() throws Exception {
+        doThrow(new KeyStoreException("Key Store Exception"))
+                .when(mSpyKeyStore)
+                .getCertificateChain(any());
+
+        KeyAttestationCertificateChainRecord record =
+                mKeyAttestation.getAttestationRecordFromKeyAlias(KEY_ALIAS);
+
+        assertThat(record.encode().length).isEqualTo(0);
+    }
+
+    @Test
+    public void testGetAttestationRecordFromKeyAlias_keyStoreReturnsNullChain_Failure()
+            throws Exception {
+        doReturn(null).when(mSpyKeyStore).getCertificateChain(any());
+
+        assertThrows(
+                IllegalStateException.class,
+                () -> mKeyAttestation.getAttestationRecordFromKeyAlias(KEY_ALIAS));
+    }
+}