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

This commit is contained in:
Eran Messeri
2022-05-09 12:25:56 +00:00
committed by Android (Google) Code Review
4 changed files with 317 additions and 3 deletions

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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");

View File

@@ -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));
}
}