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

Commit 22c0a868 authored by Eran Messeri's avatar Eran Messeri Committed by Android (Google) Code Review
Browse files

Merge "Keystore: Support Ed25519 keys" into tm-dev

parents 9c9916e4 143fa393
Loading
Loading
Loading
Loading
+46 −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 android.security.keystore2;

import android.annotation.NonNull;
import android.security.KeyStoreSecurityLevel;
import android.system.keystore2.Authorization;
import android.system.keystore2.KeyDescriptor;

import java.security.PrivateKey;
import java.security.interfaces.EdECKey;
import java.security.spec.NamedParameterSpec;

/**
 * EdEC private key (instance of {@link PrivateKey} and {@link EdECKey}) backed by keystore.
 *
 * @hide
 */
public class AndroidKeyStoreEdECPrivateKey extends AndroidKeyStorePrivateKey implements EdECKey {
    public AndroidKeyStoreEdECPrivateKey(
            @NonNull KeyDescriptor descriptor, long keyId,
            @NonNull Authorization[] authorizations,
            @NonNull String algorithm,
            @NonNull KeyStoreSecurityLevel securityLevel) {
        super(descriptor, keyId, authorizations, algorithm, securityLevel);
    }

    @Override
    public NamedParameterSpec getParams() {
        return NamedParameterSpec.ED25519;
    }
}
+145 −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 android.security.keystore2;

import android.annotation.NonNull;
import android.security.KeyStoreSecurityLevel;
import android.system.keystore2.KeyDescriptor;
import android.system.keystore2.KeyMetadata;

import java.math.BigInteger;
import java.security.interfaces.EdECPublicKey;
import java.security.spec.EdECPoint;
import java.security.spec.NamedParameterSpec;
import java.util.Arrays;
import java.util.Objects;

/**
 * {@link EdECPublicKey} backed by keystore.
 *
 * @hide
 */
public class AndroidKeyStoreEdECPublicKey extends AndroidKeyStorePublicKey
        implements EdECPublicKey {
    /**
     * DER sequence, as defined in https://datatracker.ietf.org/doc/html/rfc8410#section-4 and
     * https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.
     * SEQUENCE (2 elem)
     *  SEQUENCE (1 elem)
     *    OBJECT IDENTIFIER 1.3.101.112 curveEd25519 (EdDSA 25519 signature algorithm)
     *    as defined in https://datatracker.ietf.org/doc/html/rfc8410#section-3
     *  BIT STRING (256 bit) as defined in
     *  https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.2
     */
    private static final byte[] DER_KEY_PREFIX = new byte[] {
            0x30,
            0x2a,
            0x30,
            0x05,
            0x06,
            0x03,
            0x2b,
            0x65,
            0x70,
            0x03,
            0x21,
            0x00,
    };
    private static final int ED25519_KEY_SIZE_BYTES = 32;

    private byte[] mEncodedKey;
    private EdECPoint mPoint;

    public AndroidKeyStoreEdECPublicKey(
            @NonNull KeyDescriptor descriptor,
            @NonNull KeyMetadata metadata,
            @NonNull String algorithm,
            @NonNull KeyStoreSecurityLevel iSecurityLevel,
            @NonNull byte[] encodedKey) {
        super(descriptor, metadata, encodedKey, algorithm, iSecurityLevel);
        mEncodedKey = encodedKey;

        int preambleLength = matchesPreamble(DER_KEY_PREFIX, encodedKey);
        if (preambleLength == 0) {
            throw new IllegalArgumentException("Key size is not correct size");
        }

        mPoint = pointFromKeyByteArray(
                Arrays.copyOfRange(encodedKey, preambleLength, encodedKey.length));
    }

    @Override
    AndroidKeyStorePrivateKey getPrivateKey() {
        return new AndroidKeyStoreEdECPrivateKey(
                getUserKeyDescriptor(),
                getKeyIdDescriptor().nspace,
                getAuthorizations(),
                "EdDSA",
                getSecurityLevel());
    }

    @Override
    public NamedParameterSpec getParams() {
        return NamedParameterSpec.ED25519;
    }

    @Override
    public EdECPoint getPoint() {
        return mPoint;
    }

    private static int matchesPreamble(byte[] preamble, byte[] encoded) {
        if (encoded.length != (preamble.length + ED25519_KEY_SIZE_BYTES)) {
            return 0;
        }
        if (Arrays.compare(preamble, Arrays.copyOf(encoded, preamble.length)) != 0) {
            return 0;
        }
        return preamble.length;
    }

    private static EdECPoint pointFromKeyByteArray(byte[] coordinates) {
        Objects.requireNonNull(coordinates);

        // Oddity of the key is the most-significant bit of the last byte.
        boolean isOdd = (0x80 & coordinates[coordinates.length - 1]) != 0;
        // Zero out the oddity bit.
        coordinates[coordinates.length - 1] &= (byte) 0x7f;
        // Representation of Y is in little-endian, according to rfc8032 section-3.1.
        reverse(coordinates);
        // The integer representing Y starts from the first bit in the coordinates array.
        BigInteger y = new BigInteger(1, coordinates);
        return new EdECPoint(isOdd, y);
    }

    private static void reverse(byte[] coordinateArray) {
        int start = 0;
        int end = coordinateArray.length - 1;
        while (start < end) {
            byte tmp = coordinateArray[start];
            coordinateArray[start] = coordinateArray[end];
            coordinateArray[end] = tmp;
            start++;
            end--;
        }
    }

    @Override
    public byte[] getEncoded() {
        return mEncodedKey.clone();
    }
}
+3 −3
Original line number Diff line number Diff line
@@ -224,7 +224,6 @@ public class AndroidKeyStoreProvider extends Provider {

        String jcaKeyAlgorithm = publicKey.getAlgorithm();

        KeyStoreSecurityLevel securityLevel = iSecurityLevel;
        if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(jcaKeyAlgorithm)) {
            return new AndroidKeyStoreECPublicKey(descriptor, metadata,
                    iSecurityLevel, (ECPublicKey) publicKey);
@@ -232,8 +231,9 @@ public class AndroidKeyStoreProvider extends Provider {
            return new AndroidKeyStoreRSAPublicKey(descriptor, metadata,
                    iSecurityLevel, (RSAPublicKey) publicKey);
        } else if (ED25519_OID.equalsIgnoreCase(jcaKeyAlgorithm)) {
            //TODO(b/214203951) missing classes in conscrypt
            throw new ProviderException("Curve " + ED25519_OID + " not supported yet");
            final byte[] publicKeyEncoded = publicKey.getEncoded();
            return new AndroidKeyStoreEdECPublicKey(descriptor, metadata, ED25519_OID,
                    iSecurityLevel, publicKeyEncoded);
        } else if (X25519_ALIAS.equalsIgnoreCase(jcaKeyAlgorithm)) {
            //TODO(b/214203951) missing classes in conscrypt
            throw new ProviderException("Curve " + X25519_ALIAS + " not supported yet");
+123 −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 android.security.keystore2;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;

import android.security.KeyStoreSecurityLevel;
import android.system.keystore2.Authorization;
import android.system.keystore2.Domain;
import android.system.keystore2.KeyDescriptor;
import android.system.keystore2.KeyMetadata;

import androidx.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;

import java.math.BigInteger;
import java.util.Base64;

@RunWith(AndroidJUnit4.class)
public class AndroidKeyStoreEdECPublicKeyTest {
    private static KeyDescriptor descriptor() {
        final KeyDescriptor keyDescriptor = new KeyDescriptor();
        keyDescriptor.alias = "key";
        keyDescriptor.blob = null;
        keyDescriptor.domain = Domain.APP;
        keyDescriptor.nspace = -1;
        return keyDescriptor;
    }

    private static KeyMetadata metadata(byte[] cert, byte[] certChain) {
        KeyMetadata metadata = new KeyMetadata();
        metadata.authorizations = new Authorization[0];
        metadata.certificate = cert;
        metadata.certificateChain = certChain;
        metadata.key = descriptor();
        metadata.modificationTimeMs = 0;
        metadata.keySecurityLevel = 1;
        return metadata;
    }

    @Mock
    private KeyStoreSecurityLevel mKeystoreSecurityLevel;

    private static class EdECTestVector {
        public final byte[] encodedKeyBytes;
        public final boolean isOdd;
        public final BigInteger yValue;

        EdECTestVector(String b64KeyBytes, boolean isOdd, String yValue) {
            this.encodedKeyBytes = Base64.getDecoder().decode(b64KeyBytes);
            this.isOdd = isOdd;
            this.yValue = new BigInteger(yValue);
        }
    }

    private static final EdECTestVector[] ED_EC_TEST_VECTORS = new EdECTestVector[]{
            new EdECTestVector("MCowBQYDK2VwAyEADE+wvQqNHxaERPhAZ0rCFlgFbfWLs/YonPXdSTw0VSo=",
                    false,
                    "19147682157189290216699341180089409126316261024914226007941553249095116672780"
                    ),
            new EdECTestVector("MCowBQYDK2VwAyEA/0E1IRNzGj85Ot/TPeXqifkqTkdk4voleH0hIq59D9w=",
                    true,
                    "41640152188550647350742178040529506688513911269563908889464821205156322689535"
                    ),
            new EdECTestVector("MCowBQYDK2VwAyEAunOvGuenetl9GQSXGVo5L3RIr4OOIpFIv/Zre8qTc/8=",
                    true,
                    "57647939198144376128225770417635248407428273266444593100194116168980378907578"
                    ),
            new EdECTestVector("MCowBQYDK2VwAyEA2hHqaZ5IolswN1Yd58Y4hzhmUMCCqc4PW5A/SFLmTX8=",
                    false,
                    "57581368614046789120409806291852629847774713088410311752049592044694364885466"
                    ),
    };

    @Test
    public void testParsingOfValidKeys() {
        for (EdECTestVector testVector : ED_EC_TEST_VECTORS) {
            AndroidKeyStoreEdECPublicKey pkey = new AndroidKeyStoreEdECPublicKey(descriptor(),
                    metadata(null, null), "EdDSA", mKeystoreSecurityLevel,
                    testVector.encodedKeyBytes);

            assertEquals(pkey.getPoint().isXOdd(), testVector.isOdd);
            assertEquals(pkey.getPoint().getY(), testVector.yValue);
        }
    }

    @Test
    public void testFailedParsingOfKeysWithDifferentOid() {
        final byte[] testVectorWithIncorrectOid = Base64.getDecoder().decode(
                "MCowBQYDLGVwAyEADE+wvQqNHxaERPhAZ0rCFlgFbfWLs/YonPXdSTw0VSo=");
        assertThrows("OID should be unrecognized", IllegalArgumentException.class,
                () -> new AndroidKeyStoreEdECPublicKey(descriptor(), metadata(null, null), "EdDSA",
                        mKeystoreSecurityLevel, testVectorWithIncorrectOid));
    }

    @Test
    public void testFailedParsingOfKeysWithWrongSize() {
        final byte[] testVectorWithIncorrectKeySize = Base64.getDecoder().decode(
        "MCwwBQYDK2VwAyMADE+wvQqNHxaERPhAZ0rCFlgFbfWLs/YonPXdSTw0VSrOzg==");
        assertThrows("Key length should be invalid", IllegalArgumentException.class,
                () -> new AndroidKeyStoreEdECPublicKey(descriptor(), metadata(null, null), "EdDSA",
                        mKeystoreSecurityLevel, testVectorWithIncorrectKeySize));
    }
}