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

Commit 143fa393 authored by Eran Messeri's avatar Eran Messeri
Browse files

Keystore: Support Ed25519 keys

Implement support for Ed25519 signing keys in Android Keystore.
Because Conscrypt does not yet handle those keys, the Keystore classes
implement EdECPublicKey directly and parse the keys.

Specifically, AndroidKeyStoreEdECPublicKey can take an encoded X.509 key
specification, validate the encoding is of an Ed25519 key, then parse
the oddity and Y point on the curve.
RFC8032 describes EdDSA signature scheme, particularly Ed25519.
RFC8410, Section 3, defines the OID for Ed25519 keys (1.3.101.112).
RFC8410, Section 4, describes the encoding of the public key.

Bug: 195309719
Bug: 194359292
Bug: 214203951
Test: atest android.security.keystore2.AndroidKeyStoreEdECPublicKeyTest
Change-Id: I07b793cbd5029630768368ad4a863bbc1c828ced
parent 12931e77
Loading
Loading
Loading
Loading
+46 −0
Original line number Original line 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 Original line 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 Original line Diff line number Diff line
@@ -224,7 +224,6 @@ public class AndroidKeyStoreProvider extends Provider {


        String jcaKeyAlgorithm = publicKey.getAlgorithm();
        String jcaKeyAlgorithm = publicKey.getAlgorithm();


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