diff options
author | Eran Messeri <eranm@google.com> | 2022-05-10 11:38:58 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2022-05-10 11:38:58 +0000 |
commit | fa8aac4629b21feafe195a88006fe17cbcc7bb08 (patch) | |
tree | 47ed50c8af47d6f72be13075eb862ef9549b3922 | |
parent | aca9b26c9377824325c9b865546c32ad2600e5d1 (diff) | |
parent | 51d7088287a4f7a3f20308b86e1efcb59c8dc4da (diff) | |
download | base-fa8aac4629b21feafe195a88006fe17cbcc7bb08.tar.gz |
Merge "Keystore: Support Ed25519 keys"
4 files changed, 317 insertions, 3 deletions
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreEdECPrivateKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreEdECPrivateKey.java new file mode 100644 index 000000000000..4855ad0f7293 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreEdECPrivateKey.java @@ -0,0 +1,46 @@ +/* + * 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; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreEdECPublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreEdECPublicKey.java new file mode 100644 index 000000000000..642e08813291 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreEdECPublicKey.java @@ -0,0 +1,145 @@ +/* + * 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(); + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java index d31499e8b36d..0355628b8135 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java @@ -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"); diff --git a/keystore/tests/src/android/security/keystore2/AndroidKeyStoreEdECPublicKeyTest.java b/keystore/tests/src/android/security/keystore2/AndroidKeyStoreEdECPublicKeyTest.java new file mode 100644 index 000000000000..5bd5797859c9 --- /dev/null +++ b/keystore/tests/src/android/security/keystore2/AndroidKeyStoreEdECPublicKeyTest.java @@ -0,0 +1,123 @@ +/* + * 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)); + } +} + |