Skip to content

Commit 4ebbbcc

Browse files
Remove use of BouncyCastle for EC key generation and point (de)compression (#129)
*Issue #, if available:* #41 *Description of changes:* Removes explicit use of BouncyCastle from the `ECDSASignatureAlgorithm` implementation of `TrailingSignatureAlgorithm`. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. # Check any applicable: - [ ] Were any files moved? Moving files changes their URL, which breaks all hyperlinks to the files.
1 parent e836a51 commit 4ebbbcc

File tree

6 files changed

+386
-26
lines changed

6 files changed

+386
-26
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
### Maintenance
55
* Add support for standard test vectors via `testVectorZip` system property.
66
* No longer require use of BouncyCastle with RSA `JceMasterKey`s
7+
* No longer use BouncyCastle for Elliptic Curve key generation and point compression/decompression
78

89
## 1.6.0 -- 2019-05-31
910

src/main/java/com/amazonaws/encryptionsdk/internal/TrailingSignatureAlgorithm.java

+122-26
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
11
package com.amazonaws.encryptionsdk.internal;
22

3+
import java.math.BigInteger;
4+
import java.security.AlgorithmParameters;
35
import java.security.GeneralSecurityException;
6+
import java.security.KeyFactory;
47
import java.security.KeyPair;
58
import java.security.KeyPairGenerator;
9+
import java.security.NoSuchAlgorithmException;
610
import java.security.PublicKey;
7-
8-
import org.bouncycastle.crypto.params.ECDomainParameters;
9-
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
10-
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
11-
import org.bouncycastle.jce.ECNamedCurveTable;
12-
import org.bouncycastle.jce.interfaces.ECPublicKey;
13-
import org.bouncycastle.jce.provider.BouncyCastleProvider;
14-
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
15-
import org.bouncycastle.math.ec.ECPoint;
11+
import java.security.interfaces.ECPublicKey;
12+
import java.security.spec.ECFieldFp;
13+
import java.security.spec.ECGenParameterSpec;
14+
import java.security.spec.ECParameterSpec;
15+
import java.security.spec.ECPoint;
16+
import java.security.spec.ECPublicKeySpec;
17+
import java.security.spec.InvalidKeySpecException;
18+
import java.security.spec.InvalidParameterSpecException;
19+
import java.util.Arrays;
1620

1721
import com.amazonaws.encryptionsdk.CryptoAlgorithm;
1822

19-
import static com.amazonaws.encryptionsdk.internal.BouncyCastleConfiguration.INTERNAL_BOUNCY_CASTLE_PROVIDER;
23+
import static com.amazonaws.encryptionsdk.internal.Utils.bigIntegerToByteArray;
24+
import static com.amazonaws.encryptionsdk.internal.Utils.encodeBase64String;
25+
import static java.math.BigInteger.ONE;
26+
import static java.math.BigInteger.ZERO;
27+
import static org.apache.commons.lang3.Validate.isInstanceOf;
28+
import static org.apache.commons.lang3.Validate.notNull;
2029

2130
/**
2231
* Provides a consistent interface across various trailing signature algorithms.
@@ -36,15 +45,36 @@ private TrailingSignatureAlgorithm() {
3645
public abstract String serializePublicKey(PublicKey key);
3746
public abstract KeyPair generateKey() throws GeneralSecurityException;
3847

48+
/* Standards for Efficient Cryptography over a prime field */
49+
private static final String SEC_PRIME_FIELD_PREFIX = "secp";
50+
3951
private static final class ECDSASignatureAlgorithm extends TrailingSignatureAlgorithm {
40-
private final ECNamedCurveParameterSpec ecSpec;
52+
private final ECGenParameterSpec ecSpec;
53+
private final ECParameterSpec ecParameterSpec;
4154
private final String messageDigestAlgorithm;
4255
private final String hashAndSignAlgorithm;
56+
private static final String ELLIPTIC_CURVE_ALGORITHM = "EC";
57+
/* Constants used by SEC-1 v2 point compression and decompression algorithms */
58+
private static final BigInteger TWO = BigInteger.valueOf(2);
59+
private static final BigInteger THREE = BigInteger.valueOf(3);
60+
private static final BigInteger FOUR = BigInteger.valueOf(4);
61+
62+
private ECDSASignatureAlgorithm(ECGenParameterSpec ecSpec, String messageDigestAlgorithm) {
63+
if (!ecSpec.getName().startsWith(SEC_PRIME_FIELD_PREFIX)) {
64+
throw new IllegalStateException("Non-prime curves are not supported at this time");
65+
}
4366

44-
private ECDSASignatureAlgorithm(ECNamedCurveParameterSpec ecSpec, String messageDigestAlgorithm) {
4567
this.ecSpec = ecSpec;
4668
this.messageDigestAlgorithm = messageDigestAlgorithm;
4769
this.hashAndSignAlgorithm = messageDigestAlgorithm + "withECDSA";
70+
71+
try {
72+
final AlgorithmParameters parameters = AlgorithmParameters.getInstance(ELLIPTIC_CURVE_ALGORITHM);
73+
parameters.init(ecSpec);
74+
this.ecParameterSpec = parameters.getParameterSpec(ECParameterSpec.class);
75+
} catch (NoSuchAlgorithmException | InvalidParameterSpecException e) {
76+
throw new IllegalStateException("Invalid algorithm", e);
77+
}
4878
}
4979

5080
@Override
@@ -62,41 +92,107 @@ public String getRawSignatureAlgorithm() {
6292
return "NONEwithECDSA";
6393
}
6494

65-
@Override public String getHashAndSignAlgorithm() {
95+
@Override
96+
public String getHashAndSignAlgorithm() {
6697
return hashAndSignAlgorithm;
6798
}
6899

100+
/**
101+
* Decodes a compressed elliptic curve point as described in SEC-1 v2 section 2.3.4
102+
*
103+
* @param keyString The serialized and compressed public key
104+
* @return The PublicKey
105+
* @see <a href="http://www.secg.org/sec1-v2.pdf">http://www.secg.org/sec1-v2.pdf</a>
106+
*/
69107
@Override
70108
public PublicKey deserializePublicKey(String keyString) {
71-
final ECPoint q = ecSpec.getCurve().decodePoint(Utils.decodeBase64String(keyString));
72-
73-
ECPublicKeyParameters keyParams = new ECPublicKeyParameters(
74-
q,
75-
new ECDomainParameters(ecSpec.getCurve(), ecSpec.getG(), ecSpec.getN(), ecSpec.getH())
76-
);
77-
78-
return new BCECPublicKey("EC", keyParams, ecSpec, BouncyCastleProvider.CONFIGURATION);
109+
notNull(keyString, "keyString is required");
110+
111+
final byte[] decodedKey = Utils.decodeBase64String(keyString);
112+
final BigInteger x = new BigInteger(1, Arrays.copyOfRange(decodedKey, 1, decodedKey.length));
113+
114+
final byte compressedY = decodedKey[0];
115+
final BigInteger yOrder;
116+
117+
if (compressedY == TWO.byteValue()) {
118+
yOrder = ZERO;
119+
} else if (compressedY == THREE.byteValue()) {
120+
yOrder = ONE;
121+
} else {
122+
throw new IllegalArgumentException("Compressed y value was invalid");
123+
}
124+
125+
final BigInteger p = ((ECFieldFp) ecParameterSpec.getCurve().getField()).getP();
126+
final BigInteger a = ecParameterSpec.getCurve().getA();
127+
final BigInteger b = ecParameterSpec.getCurve().getB();
128+
129+
//alpha must be equal to y^2, this is validated below
130+
final BigInteger alpha = x.modPow(THREE, p)
131+
.add(a.multiply(x).mod(p))
132+
.add(b)
133+
.mod(p);
134+
135+
final BigInteger beta;
136+
if (p.mod(FOUR).equals(THREE)) {
137+
beta = alpha.modPow(p.add(ONE).divide(FOUR), p);
138+
} else {
139+
throw new IllegalArgumentException("Curve not supported at this time");
140+
}
141+
142+
final BigInteger y = beta.mod(TWO).equals(yOrder) ? beta : p.subtract(beta);
143+
144+
//Validate that Y is a root of Y^2 to prevent invalid point attacks
145+
if (!alpha.equals(y.modPow(TWO, p))) {
146+
throw new IllegalArgumentException("Y was invalid");
147+
}
148+
149+
try {
150+
return KeyFactory.getInstance(ELLIPTIC_CURVE_ALGORITHM).generatePublic(
151+
new ECPublicKeySpec(new ECPoint(x, y), ecParameterSpec));
152+
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
153+
throw new IllegalStateException("Invalid algorithm", e);
154+
}
79155
}
80156

157+
/**
158+
* Encodes a compressed elliptic curve point as described in SEC-1 v2 section 2.3.3
159+
*
160+
* @param key The Elliptic Curve public key to compress and serialize
161+
* @return The serialized and compressed public key
162+
* @see <a href="http://www.secg.org/sec1-v2.pdf">http://www.secg.org/sec1-v2.pdf</a>
163+
*/
81164
@Override
82165
public String serializePublicKey(PublicKey key) {
83-
return Utils.encodeBase64String(((ECPublicKey)key).getQ().getEncoded(true));
166+
notNull(key, "key is required");
167+
isInstanceOf(ECPublicKey.class, key, "key must be an instance of ECPublicKey");
168+
169+
final BigInteger x = ((ECPublicKey) key).getW().getAffineX();
170+
final BigInteger y = ((ECPublicKey) key).getW().getAffineY();
171+
final BigInteger compressedY = y.mod(TWO).equals(ZERO) ? TWO : THREE;
172+
173+
final byte[] xBytes = bigIntegerToByteArray(x,
174+
ecParameterSpec.getCurve().getField().getFieldSize() / Byte.SIZE);
175+
176+
final byte[] compressedKey = new byte[xBytes.length + 1];
177+
System.arraycopy(xBytes, 0, compressedKey, 1, xBytes.length);
178+
compressedKey[0] = compressedY.byteValue();
179+
180+
return encodeBase64String(compressedKey);
84181
}
85182

86183
@Override
87184
public KeyPair generateKey() throws GeneralSecurityException {
88-
// We use BouncyCastle for this so that we can easily serialize the compressed point.
89-
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", INTERNAL_BOUNCY_CASTLE_PROVIDER);
185+
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ELLIPTIC_CURVE_ALGORITHM);
90186
keyGen.initialize(ecSpec, Utils.getSecureRandom());
91187

92188
return keyGen.generateKeyPair();
93189
}
94190
}
95191

96192
private static final ECDSASignatureAlgorithm SHA256_ECDSA_P256
97-
= new ECDSASignatureAlgorithm(ECNamedCurveTable.getParameterSpec("secp256r1"), "SHA256");
193+
= new ECDSASignatureAlgorithm(new ECGenParameterSpec(SEC_PRIME_FIELD_PREFIX + "256r1"), "SHA256");
98194
private static final ECDSASignatureAlgorithm SHA384_ECDSA_P384
99-
= new ECDSASignatureAlgorithm(ECNamedCurveTable.getParameterSpec("secp384r1"), "SHA384");
195+
= new ECDSASignatureAlgorithm(new ECGenParameterSpec(SEC_PRIME_FIELD_PREFIX + "384r1"), "SHA384");
100196

101197
public static TrailingSignatureAlgorithm forCryptoAlgorithm(CryptoAlgorithm algorithm) {
102198
switch (algorithm) {

src/main/java/com/amazonaws/encryptionsdk/internal/Utils.java

+31
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package com.amazonaws.encryptionsdk.internal;
1515

1616
import java.io.Serializable;
17+
import java.math.BigInteger;
1718
import java.nio.Buffer;
1819
import java.nio.ByteBuffer;
1920
import java.nio.charset.StandardCharsets;
@@ -279,4 +280,34 @@ public static byte[] decodeBase64String(final String encoded) {
279280
public static String encodeBase64String(final byte[] data) {
280281
return Base64.toBase64String(data);
281282
}
283+
284+
/**
285+
* Removes the leading zero sign byte from the byte array representation of a BigInteger (if present)
286+
* and left pads with zeroes to produce a byte array of the given length.
287+
* @param bigInteger The BigInteger to convert to a byte array
288+
* @param length The length of the byte array, must be at least
289+
* as long as the BigInteger byte array without the sign byte
290+
* @return The byte array
291+
*/
292+
public static byte[] bigIntegerToByteArray(final BigInteger bigInteger, final int length) {
293+
byte[] rawBytes = bigInteger.toByteArray();
294+
// If rawBytes is already the correct length, return it.
295+
if (rawBytes.length == length) {
296+
return rawBytes;
297+
}
298+
299+
// If we're exactly one byte too large, but we have a leading zero byte, remove it and return.
300+
if(rawBytes.length == length + 1 && rawBytes[0] == 0) {
301+
return Arrays.copyOfRange(rawBytes, 1, rawBytes.length);
302+
}
303+
304+
if (rawBytes.length > length) {
305+
throw new IllegalArgumentException("Length must be at least as long as the BigInteger byte array " +
306+
"without the sign byte");
307+
}
308+
309+
final byte[] paddedResult = new byte[length];
310+
System.arraycopy(rawBytes, 0, paddedResult, length - rawBytes.length, rawBytes.length);
311+
return paddedResult;
312+
}
282313
}

src/test/java/com/amazonaws/encryptionsdk/TestUtils.java

+37
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,41 @@ public static int[] getFrameSizesToTest(final CryptoAlgorithm cryptoAlg) {
174174
};
175175
return frameSizeToTest;
176176
}
177+
178+
/**
179+
* Converts an array of unsigned bytes (represented as int values between 0 and 255 inclusive)
180+
* to an array of Java primitive type byte, which are by definition signed.
181+
*
182+
* @param unsignedBytes An array on unsigned bytes
183+
* @return An array of signed bytes
184+
*/
185+
public static byte[] unsignedBytesToSignedBytes(final int[] unsignedBytes) {
186+
byte[] signedBytes = new byte[unsignedBytes.length];
187+
188+
for (int i = 0; i < unsignedBytes.length; i++) {
189+
if (unsignedBytes[i] > 255) {
190+
throw new IllegalArgumentException("Encountered unsigned byte value > 255");
191+
}
192+
signedBytes[i] = (byte) (unsignedBytes[i] & 0xff);
193+
}
194+
195+
return signedBytes;
196+
}
197+
198+
/**
199+
* Converts an array of Java primitive type bytes (which are by definition signed) to
200+
* an array of unsigned bytes (represented as int values between 0 and 255 inclusive).
201+
*
202+
* @param signedBytes An array of signed bytes
203+
* @return An array of unsigned bytes
204+
*/
205+
public static int[] signedBytesToUnsignedBytes(final byte[] signedBytes) {
206+
int[] unsignedBytes = new int[signedBytes.length];
207+
208+
for (int i = 0; i < signedBytes.length; i++) {
209+
unsignedBytes[i] = ((int) signedBytes[i]) & 0xff;
210+
}
211+
212+
return unsignedBytes;
213+
}
177214
}

src/test/java/com/amazonaws/encryptionsdk/UtilsTest.java

+25
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static org.junit.Assert.assertNotEquals;
66
import static org.junit.Assert.assertTrue;
77

8+
import java.math.BigInteger;
89
import java.nio.charset.StandardCharsets;
910
import java.util.Arrays;
1011

@@ -96,5 +97,29 @@ public void base64something() {
9697
assertEquals(encoded, Utils.encodeBase64String(data));
9798
assertArrayEquals(data, Utils.decodeBase64String(encoded));
9899
}
100+
101+
@Test
102+
public void testBigIntegerToByteArray() {
103+
byte[] bytes = new byte[] {23, 47, 126, -42, 34};
104+
105+
assertArrayEquals(new byte[]{0, 0, 0, 23, 47, 126, -42, 34},
106+
Utils.bigIntegerToByteArray(new BigInteger(bytes), 8));
107+
assertArrayEquals(new byte[]{23, 47, 126, -42, 34},
108+
Utils.bigIntegerToByteArray(new BigInteger(bytes), 5));
109+
110+
bytes = new byte[] {0, -47, 126, -42, 34};
111+
112+
assertArrayEquals(new byte[]{-47, 126, -42, 34},
113+
Utils.bigIntegerToByteArray(new BigInteger(bytes), 4));
114+
}
115+
116+
@Test(expected = IllegalArgumentException.class)
117+
public void testBigIntegerToByteArray_InvalidLength() {
118+
byte[] bytes = new byte[] {0, -47, 126, -42, 34};
119+
120+
assertArrayEquals(bytes,
121+
Utils.bigIntegerToByteArray(new BigInteger(bytes), 3));
122+
}
123+
99124
}
100125

0 commit comments

Comments
 (0)