Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a77e254e authored by Raphael Kim's avatar Raphael Kim
Browse files

Implement secure channel

Bug: 253307662
Test: Manual test on CtsPermissionsSyncTestApp
Change-Id: I3d493ae33ef99270d31ad6d39f2b85c5dbedd862
parent 82701133
Loading
Loading
Loading
Loading
+100 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.server.companion.securechannel;

import static android.security.attestationverification.AttestationVerificationManager.PARAM_CHALLENGE;
import static android.security.attestationverification.AttestationVerificationManager.PROFILE_PEER_DEVICE;
import static android.security.attestationverification.AttestationVerificationManager.TYPE_CHALLENGE;

import android.annotation.NonNull;
import android.content.Context;
import android.os.Bundle;
import android.security.attestationverification.AttestationProfile;
import android.security.attestationverification.AttestationVerificationManager;
import android.security.attestationverification.VerificationToken;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;

/**
 * Helper class to perform attestation verification synchronously.
 */
class AttestationVerifier {
    private static final long ATTESTATION_VERIFICATION_TIMEOUT_SECONDS = 10; // 10 seconds
    private static final String PARAM_OWNED_BY_SYSTEM = "android.key_owned_by_system";

    private final Context mContext;

    AttestationVerifier(Context context) {
        this.mContext = context;
    }

    /**
     * Synchronously verify remote attestation as a suitable peer device on current thread.
     *
     * The peer device must be owned by the Android system and be protected with appropriate
     * public key that this device can verify as attestation challenge.
     *
     * @param remoteAttestation the full certificate chain containing attestation extension.
     * @param attestationChallenge attestation challenge for authentication.
     * @return true if attestation is successfully verified; false otherwise.
     */
    @NonNull
    public int verifyAttestation(
            @NonNull byte[] remoteAttestation,
            @NonNull byte[] attestationChallenge
    ) throws SecureChannelException {
        Bundle requirements = new Bundle();
        requirements.putByteArray(PARAM_CHALLENGE, attestationChallenge);
        requirements.putBoolean(PARAM_OWNED_BY_SYSTEM, true); // Custom parameter for CDM

        // Synchronously execute attestation verification.
        AtomicInteger verificationResult = new AtomicInteger(0);
        CountDownLatch verificationFinished = new CountDownLatch(1);
        BiConsumer<Integer, VerificationToken> onVerificationResult = (result, token) -> {
            verificationResult.set(result);
            verificationFinished.countDown();
        };

        mContext.getSystemService(AttestationVerificationManager.class).verifyAttestation(
                new AttestationProfile(PROFILE_PEER_DEVICE),
                /* localBindingType */ TYPE_CHALLENGE,
                requirements,
                remoteAttestation,
                Runnable::run,
                onVerificationResult
        );

        boolean finished;
        try {
            finished = verificationFinished.await(
                    ATTESTATION_VERIFICATION_TIMEOUT_SECONDS,
                    TimeUnit.SECONDS
            );
        } catch (InterruptedException e) {
            throw new SecureChannelException("Attestation verification was interrupted", e);
        }

        if (!finished) {
            throw new SecureChannelException("Attestation verification timed out.");
        }

        return verificationResult.get();
    }
}
+130 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.server.companion.securechannel;

import static android.security.keystore.KeyProperties.DIGEST_SHA256;
import static android.security.keystore.KeyProperties.KEY_ALGORITHM_EC;
import static android.security.keystore.KeyProperties.PURPOSE_SIGN;
import static android.security.keystore.KeyProperties.PURPOSE_VERIFY;

import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore2.AndroidKeyStoreSpi;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;

/**
 * Utility class to help generate, store, and access key-pair for the secure channel. Uses
 * Android Keystore.
 */
final class KeyStoreUtils {
    private static final String TAG = "CDM_SecureChannelKeyStore";
    private static final String ANDROID_KEYSTORE = AndroidKeyStoreSpi.NAME;

    private KeyStoreUtils() {}

    /**
     * Load Android keystore to be used by the secure channel.
     *
     * @return loaded keystore instance
     */
    static KeyStore loadKeyStore() throws GeneralSecurityException {
        KeyStore androidKeyStore = KeyStore.getInstance(ANDROID_KEYSTORE);

        try {
            androidKeyStore.load(null);
        } catch (IOException e) {
            // Should not happen
            throw new KeyStoreException("Failed to load Android Keystore.", e);
        }

        return androidKeyStore;
    }

    /**
     * Fetch the certificate chain encoded as byte array in the form of concatenated
     * X509 certificates.
     *
     * @param alias unique alias for the key-pair entry
     * @return a single byte-array containing the entire certificate chain
     */
    static byte[] getEncodedCertificateChain(String alias) throws GeneralSecurityException {
        KeyStore ks = loadKeyStore();

        Certificate[] certificateChain = ks.getCertificateChain(alias);

        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        for (Certificate certificate : certificateChain) {
            buffer.writeBytes(certificate.getEncoded());
        }
        return buffer.toByteArray();
    }

    /**
     * Generate a new attestation key-pair.
     *
     * @param alias unique alias for the key-pair entry
     * @param attestationChallenge challenge value to check against for authentication
     */
    static void generateAttestationKeyPair(String alias, byte[] attestationChallenge)
            throws GeneralSecurityException {
        KeyGenParameterSpec parameterSpec =
                new KeyGenParameterSpec.Builder(alias, PURPOSE_SIGN | PURPOSE_VERIFY)
                        .setAttestationChallenge(attestationChallenge)
                        .setDigests(DIGEST_SHA256)
                        .build();

        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(
                /* algorithm */ KEY_ALGORITHM_EC,
                /* provider */ ANDROID_KEYSTORE);
        keyPairGenerator.initialize(parameterSpec);
        keyPairGenerator.generateKeyPair();
    }

    /**
     * Check if alias exists.
     *
     * @param alias unique alias for the key-pair entry
     * @return true if given alias already exists in the keystore
     */
    static boolean aliasExists(String alias) {
        try {
            KeyStore ks = loadKeyStore();
            return ks.containsAlias(alias);
        } catch (GeneralSecurityException e) {
            return false;
        }

    }

    static void cleanUp(String alias) {
        try {
            KeyStore ks = loadKeyStore();

            if (ks.containsAlias(alias)) {
                ks.deleteEntry(alias);
            }
        } catch (Exception ignored) {
            // Do nothing;
        }
    }
}
+543 −0

File added.

Preview size limit exceeded, changes collapsed.

+34 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.server.companion.securechannel;

/**
 * Catch-all exception for any error in the secure channel.
 */
public class SecureChannelException extends RuntimeException {
    /**
     *
     * @param message
     */
    public SecureChannelException(String message) {
        super(message);
    }

    public SecureChannelException(String message, Throwable t) {
        super(message, t);
    }
}