diff --git a/CHANGELOG.md b/CHANGELOG.md index 2970b0d8a..3ccfc619b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Maintenance * Add support for standard test vectors via `testVectorZip` system property. * No longer require use of BouncyCastle with RSA `JceMasterKey`s +* No longer use BouncyCastle for Elliptic Curve key generation and point compression/decompression ## 1.6.0 -- 2019-05-31 diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/TrailingSignatureAlgorithm.java b/src/main/java/com/amazonaws/encryptionsdk/internal/TrailingSignatureAlgorithm.java index 36049c4bd..11a418380 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/TrailingSignatureAlgorithm.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/TrailingSignatureAlgorithm.java @@ -1,22 +1,31 @@ package com.amazonaws.encryptionsdk.internal; +import java.math.BigInteger; +import java.security.AlgorithmParameters; import java.security.GeneralSecurityException; +import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; import java.security.PublicKey; - -import org.bouncycastle.crypto.params.ECDomainParameters; -import org.bouncycastle.crypto.params.ECPublicKeyParameters; -import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; -import org.bouncycastle.jce.ECNamedCurveTable; -import org.bouncycastle.jce.interfaces.ECPublicKey; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; -import org.bouncycastle.math.ec.ECPoint; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.InvalidParameterSpecException; +import java.util.Arrays; import com.amazonaws.encryptionsdk.CryptoAlgorithm; -import static com.amazonaws.encryptionsdk.internal.BouncyCastleConfiguration.INTERNAL_BOUNCY_CASTLE_PROVIDER; +import static com.amazonaws.encryptionsdk.internal.Utils.bigIntegerToByteArray; +import static com.amazonaws.encryptionsdk.internal.Utils.encodeBase64String; +import static java.math.BigInteger.ONE; +import static java.math.BigInteger.ZERO; +import static org.apache.commons.lang3.Validate.isInstanceOf; +import static org.apache.commons.lang3.Validate.notNull; /** * Provides a consistent interface across various trailing signature algorithms. @@ -36,15 +45,36 @@ private TrailingSignatureAlgorithm() { public abstract String serializePublicKey(PublicKey key); public abstract KeyPair generateKey() throws GeneralSecurityException; + /* Standards for Efficient Cryptography over a prime field */ + private static final String SEC_PRIME_FIELD_PREFIX = "secp"; + private static final class ECDSASignatureAlgorithm extends TrailingSignatureAlgorithm { - private final ECNamedCurveParameterSpec ecSpec; + private final ECGenParameterSpec ecSpec; + private final ECParameterSpec ecParameterSpec; private final String messageDigestAlgorithm; private final String hashAndSignAlgorithm; + private static final String ELLIPTIC_CURVE_ALGORITHM = "EC"; + /* Constants used by SEC-1 v2 point compression and decompression algorithms */ + private static final BigInteger TWO = BigInteger.valueOf(2); + private static final BigInteger THREE = BigInteger.valueOf(3); + private static final BigInteger FOUR = BigInteger.valueOf(4); + + private ECDSASignatureAlgorithm(ECGenParameterSpec ecSpec, String messageDigestAlgorithm) { + if (!ecSpec.getName().startsWith(SEC_PRIME_FIELD_PREFIX)) { + throw new IllegalStateException("Non-prime curves are not supported at this time"); + } - private ECDSASignatureAlgorithm(ECNamedCurveParameterSpec ecSpec, String messageDigestAlgorithm) { this.ecSpec = ecSpec; this.messageDigestAlgorithm = messageDigestAlgorithm; this.hashAndSignAlgorithm = messageDigestAlgorithm + "withECDSA"; + + try { + final AlgorithmParameters parameters = AlgorithmParameters.getInstance(ELLIPTIC_CURVE_ALGORITHM); + parameters.init(ecSpec); + this.ecParameterSpec = parameters.getParameterSpec(ECParameterSpec.class); + } catch (NoSuchAlgorithmException | InvalidParameterSpecException e) { + throw new IllegalStateException("Invalid algorithm", e); + } } @Override @@ -62,31 +92,97 @@ public String getRawSignatureAlgorithm() { return "NONEwithECDSA"; } - @Override public String getHashAndSignAlgorithm() { + @Override + public String getHashAndSignAlgorithm() { return hashAndSignAlgorithm; } + /** + * Decodes a compressed elliptic curve point as described in SEC-1 v2 section 2.3.4 + * + * @param keyString The serialized and compressed public key + * @return The PublicKey + * @see http://www.secg.org/sec1-v2.pdf + */ @Override public PublicKey deserializePublicKey(String keyString) { - final ECPoint q = ecSpec.getCurve().decodePoint(Utils.decodeBase64String(keyString)); - - ECPublicKeyParameters keyParams = new ECPublicKeyParameters( - q, - new ECDomainParameters(ecSpec.getCurve(), ecSpec.getG(), ecSpec.getN(), ecSpec.getH()) - ); - - return new BCECPublicKey("EC", keyParams, ecSpec, BouncyCastleProvider.CONFIGURATION); + notNull(keyString, "keyString is required"); + + final byte[] decodedKey = Utils.decodeBase64String(keyString); + final BigInteger x = new BigInteger(1, Arrays.copyOfRange(decodedKey, 1, decodedKey.length)); + + final byte compressedY = decodedKey[0]; + final BigInteger yOrder; + + if (compressedY == TWO.byteValue()) { + yOrder = ZERO; + } else if (compressedY == THREE.byteValue()) { + yOrder = ONE; + } else { + throw new IllegalArgumentException("Compressed y value was invalid"); + } + + final BigInteger p = ((ECFieldFp) ecParameterSpec.getCurve().getField()).getP(); + final BigInteger a = ecParameterSpec.getCurve().getA(); + final BigInteger b = ecParameterSpec.getCurve().getB(); + + //alpha must be equal to y^2, this is validated below + final BigInteger alpha = x.modPow(THREE, p) + .add(a.multiply(x).mod(p)) + .add(b) + .mod(p); + + final BigInteger beta; + if (p.mod(FOUR).equals(THREE)) { + beta = alpha.modPow(p.add(ONE).divide(FOUR), p); + } else { + throw new IllegalArgumentException("Curve not supported at this time"); + } + + final BigInteger y = beta.mod(TWO).equals(yOrder) ? beta : p.subtract(beta); + + //Validate that Y is a root of Y^2 to prevent invalid point attacks + if (!alpha.equals(y.modPow(TWO, p))) { + throw new IllegalArgumentException("Y was invalid"); + } + + try { + return KeyFactory.getInstance(ELLIPTIC_CURVE_ALGORITHM).generatePublic( + new ECPublicKeySpec(new ECPoint(x, y), ecParameterSpec)); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new IllegalStateException("Invalid algorithm", e); + } } + /** + * Encodes a compressed elliptic curve point as described in SEC-1 v2 section 2.3.3 + * + * @param key The Elliptic Curve public key to compress and serialize + * @return The serialized and compressed public key + * @see http://www.secg.org/sec1-v2.pdf + */ @Override public String serializePublicKey(PublicKey key) { - return Utils.encodeBase64String(((ECPublicKey)key).getQ().getEncoded(true)); + notNull(key, "key is required"); + isInstanceOf(ECPublicKey.class, key, "key must be an instance of ECPublicKey"); + + final BigInteger x = ((ECPublicKey) key).getW().getAffineX(); + final BigInteger y = ((ECPublicKey) key).getW().getAffineY(); + final BigInteger compressedY = y.mod(TWO).equals(ZERO) ? TWO : THREE; + + final byte[] xBytes = bigIntegerToByteArray(x, + ecParameterSpec.getCurve().getField().getFieldSize() / Byte.SIZE); + + final byte[] compressedKey = new byte[xBytes.length + 1]; + System.arraycopy(xBytes, 0, compressedKey, 1, xBytes.length); + compressedKey[0] = compressedY.byteValue(); + + return encodeBase64String(compressedKey); } @Override public KeyPair generateKey() throws GeneralSecurityException { - // We use BouncyCastle for this so that we can easily serialize the compressed point. - KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", INTERNAL_BOUNCY_CASTLE_PROVIDER); + KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ELLIPTIC_CURVE_ALGORITHM); keyGen.initialize(ecSpec, Utils.getSecureRandom()); return keyGen.generateKeyPair(); @@ -94,9 +190,9 @@ public KeyPair generateKey() throws GeneralSecurityException { } private static final ECDSASignatureAlgorithm SHA256_ECDSA_P256 - = new ECDSASignatureAlgorithm(ECNamedCurveTable.getParameterSpec("secp256r1"), "SHA256"); + = new ECDSASignatureAlgorithm(new ECGenParameterSpec(SEC_PRIME_FIELD_PREFIX + "256r1"), "SHA256"); private static final ECDSASignatureAlgorithm SHA384_ECDSA_P384 - = new ECDSASignatureAlgorithm(ECNamedCurveTable.getParameterSpec("secp384r1"), "SHA384"); + = new ECDSASignatureAlgorithm(new ECGenParameterSpec(SEC_PRIME_FIELD_PREFIX + "384r1"), "SHA384"); public static TrailingSignatureAlgorithm forCryptoAlgorithm(CryptoAlgorithm algorithm) { switch (algorithm) { diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java b/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java index 29c2636e1..b64c53143 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java @@ -14,6 +14,7 @@ package com.amazonaws.encryptionsdk.internal; import java.io.Serializable; +import java.math.BigInteger; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -279,4 +280,34 @@ public static byte[] decodeBase64String(final String encoded) { public static String encodeBase64String(final byte[] data) { return Base64.toBase64String(data); } + + /** + * Removes the leading zero sign byte from the byte array representation of a BigInteger (if present) + * and left pads with zeroes to produce a byte array of the given length. + * @param bigInteger The BigInteger to convert to a byte array + * @param length The length of the byte array, must be at least + * as long as the BigInteger byte array without the sign byte + * @return The byte array + */ + public static byte[] bigIntegerToByteArray(final BigInteger bigInteger, final int length) { + byte[] rawBytes = bigInteger.toByteArray(); + // If rawBytes is already the correct length, return it. + if (rawBytes.length == length) { + return rawBytes; + } + + // If we're exactly one byte too large, but we have a leading zero byte, remove it and return. + if(rawBytes.length == length + 1 && rawBytes[0] == 0) { + return Arrays.copyOfRange(rawBytes, 1, rawBytes.length); + } + + if (rawBytes.length > length) { + throw new IllegalArgumentException("Length must be at least as long as the BigInteger byte array " + + "without the sign byte"); + } + + final byte[] paddedResult = new byte[length]; + System.arraycopy(rawBytes, 0, paddedResult, length - rawBytes.length, rawBytes.length); + return paddedResult; + } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestUtils.java b/src/test/java/com/amazonaws/encryptionsdk/TestUtils.java index b86df7f36..6763798f9 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/TestUtils.java +++ b/src/test/java/com/amazonaws/encryptionsdk/TestUtils.java @@ -174,4 +174,41 @@ public static int[] getFrameSizesToTest(final CryptoAlgorithm cryptoAlg) { }; return frameSizeToTest; } + + /** + * Converts an array of unsigned bytes (represented as int values between 0 and 255 inclusive) + * to an array of Java primitive type byte, which are by definition signed. + * + * @param unsignedBytes An array on unsigned bytes + * @return An array of signed bytes + */ + public static byte[] unsignedBytesToSignedBytes(final int[] unsignedBytes) { + byte[] signedBytes = new byte[unsignedBytes.length]; + + for (int i = 0; i < unsignedBytes.length; i++) { + if (unsignedBytes[i] > 255) { + throw new IllegalArgumentException("Encountered unsigned byte value > 255"); + } + signedBytes[i] = (byte) (unsignedBytes[i] & 0xff); + } + + return signedBytes; + } + + /** + * Converts an array of Java primitive type bytes (which are by definition signed) to + * an array of unsigned bytes (represented as int values between 0 and 255 inclusive). + * + * @param signedBytes An array of signed bytes + * @return An array of unsigned bytes + */ + public static int[] signedBytesToUnsignedBytes(final byte[] signedBytes) { + int[] unsignedBytes = new int[signedBytes.length]; + + for (int i = 0; i < signedBytes.length; i++) { + unsignedBytes[i] = ((int) signedBytes[i]) & 0xff; + } + + return unsignedBytes; + } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/UtilsTest.java b/src/test/java/com/amazonaws/encryptionsdk/UtilsTest.java index 27129ff54..50987611f 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/UtilsTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/UtilsTest.java @@ -5,6 +5,7 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; +import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -96,5 +97,29 @@ public void base64something() { assertEquals(encoded, Utils.encodeBase64String(data)); assertArrayEquals(data, Utils.decodeBase64String(encoded)); } + + @Test + public void testBigIntegerToByteArray() { + byte[] bytes = new byte[] {23, 47, 126, -42, 34}; + + assertArrayEquals(new byte[]{0, 0, 0, 23, 47, 126, -42, 34}, + Utils.bigIntegerToByteArray(new BigInteger(bytes), 8)); + assertArrayEquals(new byte[]{23, 47, 126, -42, 34}, + Utils.bigIntegerToByteArray(new BigInteger(bytes), 5)); + + bytes = new byte[] {0, -47, 126, -42, 34}; + + assertArrayEquals(new byte[]{-47, 126, -42, 34}, + Utils.bigIntegerToByteArray(new BigInteger(bytes), 4)); + } + + @Test(expected = IllegalArgumentException.class) + public void testBigIntegerToByteArray_InvalidLength() { + byte[] bytes = new byte[] {0, -47, 126, -42, 34}; + + assertArrayEquals(bytes, + Utils.bigIntegerToByteArray(new BigInteger(bytes), 3)); + } + } diff --git a/src/test/java/com/amazonaws/encryptionsdk/internal/TrailingSignatureAlgorithmTest.java b/src/test/java/com/amazonaws/encryptionsdk/internal/TrailingSignatureAlgorithmTest.java new file mode 100644 index 000000000..3a4eda85a --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/internal/TrailingSignatureAlgorithmTest.java @@ -0,0 +1,170 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except + * in compliance with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.amazonaws.encryptionsdk.internal; + +import com.amazonaws.encryptionsdk.CryptoAlgorithm; +import com.amazonaws.encryptionsdk.TestUtils; +import org.junit.Test; + +import java.math.BigInteger; +import java.security.AlgorithmParameters; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class TrailingSignatureAlgorithmTest { + + private static final int[] secp256r1PublicFixture_X = new int[] { + 163, 132, 202, 41, 50, 135, 193, 159, 67, 19, 186, + 212, 0, 129, 16, 182, 186, 176, 124, 94, 242, 139, + 48, 143, 158, 96, 51, 133, 188, 144, 137, 148}; + + private static final int[] secp256r1PublicFixture_Y = new int[] { + 71, 234, 253, 112, 131, 106, 243, 169, 143, 58, 39, + 222, 47, 211, 230, 90, 139, 163, 54, 249, 187, 115, + 209, 203, 239, 98, 26, 47, 101, 213, 140, 212}; + + private static final int[] secp2561CompressedFixture = new int[] { + 2, + 163, 132, 202, 41, 50, 135, 193, 159, 67, 19, 186, + 212, 0, 129, 16, 182, 186, 176, 124, 94, 242, 139, + 48, 143, 158, 96, 51, 133, 188, 144, 137, 148}; + + private static final int[] secp384r1PublicFixture_X = new int[] { + 207, 62, 215, 143, 116, 128, 174, 103, 1, 81, 127, + 212, 163, 19, 165, 220, 74, 144, 26, 59, 87, 0, + 214, 47, 66, 73, 152, 227, 196, 81, 14, 28, 58, + 221, 178, 63, 150, 119, 62, 195, 99, 63, 60, 42, + 223, 207, 28, 65}; + + private static final int[] secp384r1PublicFixture_Y = new int[] { + 180, 143, 190, 5, 150, 247, 225, 240, 153, 150, 119, + 109, 210, 243, 151, 206, 217, 120, 2, 171, 75, + 180, 31, 4, 91, 78, 206, 217, 241, 119, 55, 230, + 216, 23, 237, 101, 21, 89, 132, 84, 100, 3, 255, + 90, 197, 237, 139, 209}; + + private static final int[] secp384r1CompressedFixture = new int[] { + 3, + 207, 62, 215, 143, 116, 128, 174, 103, 1, 81, 127, + 212, 163, 19, 165, 220, 74, 144, 26, 59, 87, 0, + 214, 47, 66, 73, 152, 227, 196, 81, 14, 28, 58, + 221, 178, 63, 150, 119, 62, 195, 99, 63, 60, 42, + 223, 207, 28, 65 + }; + + + @Test + public void serializationEquality() throws Exception { + CryptoAlgorithm algorithm = CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256; + + PublicKey publicKey = TrailingSignatureAlgorithm.forCryptoAlgorithm(algorithm).generateKey().getPublic(); + + String serializedPublicKey = TrailingSignatureAlgorithm.forCryptoAlgorithm(algorithm).serializePublicKey(publicKey); + PublicKey deserializedPublicKey = TrailingSignatureAlgorithm.forCryptoAlgorithm(algorithm).deserializePublicKey(serializedPublicKey); + + assertEquals(publicKey, deserializedPublicKey); + + algorithm = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384; + + publicKey = TrailingSignatureAlgorithm.forCryptoAlgorithm(algorithm).generateKey().getPublic(); + + serializedPublicKey = TrailingSignatureAlgorithm.forCryptoAlgorithm(algorithm).serializePublicKey(publicKey); + deserializedPublicKey = TrailingSignatureAlgorithm.forCryptoAlgorithm(algorithm).deserializePublicKey(serializedPublicKey); + + assertEquals(publicKey, deserializedPublicKey); + } + + @Test + public void deserializeSecp384() { + testDeserialization(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + secp384r1CompressedFixture, secp384r1PublicFixture_X, secp384r1PublicFixture_Y); + } + + @Test + public void serializeSecp384() throws Exception { + testSerialization(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + "secp384r1", secp384r1PublicFixture_X, secp384r1PublicFixture_Y, secp384r1CompressedFixture); + } + + @Test + public void deserializeSecp256() { + testDeserialization(CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256, + secp2561CompressedFixture, secp256r1PublicFixture_X, secp256r1PublicFixture_Y); + } + + @Test + public void serializeSecp256() throws Exception { + testSerialization(CryptoAlgorithm.ALG_AES_128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256, + "secp256r1", secp256r1PublicFixture_X, secp256r1PublicFixture_Y, secp2561CompressedFixture); + } + + @Test(expected = IllegalArgumentException.class) + public void testBadPoint() { + byte[] bytes = TestUtils.unsignedBytesToSignedBytes(secp384r1CompressedFixture); + bytes[20]++; + + String publicKey = Utils.encodeBase64String(bytes); + + TrailingSignatureAlgorithm + .forCryptoAlgorithm(CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384) + .deserializePublicKey(publicKey); + } + + private void testSerialization(CryptoAlgorithm algorithm, String curveName, int[] x, int[] y, int[] expected) throws Exception { + byte[] xBytes = TestUtils.unsignedBytesToSignedBytes(x); + byte[] yBytes = TestUtils.unsignedBytesToSignedBytes(y); + + final AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC"); + parameters.init(new ECGenParameterSpec(curveName)); + ECParameterSpec ecParameterSpec = parameters.getParameterSpec(ECParameterSpec.class); + + PublicKey publicKey = KeyFactory.getInstance("EC").generatePublic( + new ECPublicKeySpec(new ECPoint(new BigInteger(1, xBytes), new BigInteger(1, yBytes)), ecParameterSpec)); + + int[] result = TestUtils.signedBytesToUnsignedBytes(Utils.decodeBase64String(TrailingSignatureAlgorithm + .forCryptoAlgorithm(algorithm) + .serializePublicKey(publicKey))); + + assertArrayEquals(expected, result); + } + + private void testDeserialization(CryptoAlgorithm algorithm, int[] compressedKey, int[] expectedX, int[] expectedY) { + byte[] bytes = TestUtils.unsignedBytesToSignedBytes(compressedKey); + + String publicKey = Utils.encodeBase64String(bytes); + + PublicKey publicKeyDeserialized = TrailingSignatureAlgorithm + .forCryptoAlgorithm(algorithm) + .deserializePublicKey(publicKey); + + ECPublicKey desKey = (ECPublicKey) publicKeyDeserialized; + + BigInteger x = desKey.getW().getAffineX(); + BigInteger y = desKey.getW().getAffineY(); + + BigInteger expectedXBigInteger = new BigInteger(1, TestUtils.unsignedBytesToSignedBytes(expectedX)); + BigInteger expectedYBigInteger = new BigInteger(1, TestUtils.unsignedBytesToSignedBytes(expectedY)); + + assertEquals(expectedXBigInteger, x); + assertEquals(expectedYBigInteger, y); + } +} \ No newline at end of file