Skip to content

Commit 6cd9fda

Browse files
Remove use BouncyCastle for EC key generation and point (de)compression
*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 6cd9fda

File tree

6 files changed

+356
-29
lines changed

6 files changed

+356
-29
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

+108-29
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
package com.amazonaws.encryptionsdk.internal;
22

3-
import java.security.GeneralSecurityException;
4-
import java.security.KeyPair;
5-
import java.security.KeyPairGenerator;
6-
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;
3+
import java.math.BigInteger;
4+
import java.security.*;
5+
import java.security.interfaces.ECPublicKey;
6+
import java.security.spec.*;
7+
import java.util.Arrays;
168

179
import com.amazonaws.encryptionsdk.CryptoAlgorithm;
1810

19-
import static com.amazonaws.encryptionsdk.internal.BouncyCastleConfiguration.INTERNAL_BOUNCY_CASTLE_PROVIDER;
11+
import static com.amazonaws.encryptionsdk.internal.Utils.bigIntegerToByteArray;
12+
import static com.amazonaws.encryptionsdk.internal.Utils.encodeBase64String;
13+
import static org.apache.commons.lang3.Validate.*;
14+
import static java.math.BigInteger.ONE;
15+
import static java.math.BigInteger.ZERO;
2016

2117
/**
2218
* Provides a consistent interface across various trailing signature algorithms.
@@ -36,15 +32,36 @@ private TrailingSignatureAlgorithm() {
3632
public abstract String serializePublicKey(PublicKey key);
3733
public abstract KeyPair generateKey() throws GeneralSecurityException;
3834

35+
/** Standards for Efficient Cryptography over a prime field **/
36+
private static final String SEC_PRIME_FIELD_PREFIX = "secp";
37+
3938
private static final class ECDSASignatureAlgorithm extends TrailingSignatureAlgorithm {
40-
private final ECNamedCurveParameterSpec ecSpec;
39+
private final ECGenParameterSpec ecSpec;
40+
private final ECParameterSpec ecParameterSpec;
4141
private final String messageDigestAlgorithm;
4242
private final String hashAndSignAlgorithm;
43+
private static final String ELLIPTIC_CURVE_ALGORITHM = "EC";
44+
/* Constants used by SEC-1 v2 point compression and decompression algorithms */
45+
private static final BigInteger TWO = BigInteger.valueOf(2);
46+
private static final BigInteger THREE = BigInteger.valueOf(3);
47+
private static final BigInteger FOUR = BigInteger.valueOf(4);
48+
49+
private ECDSASignatureAlgorithm(ECGenParameterSpec ecSpec, String messageDigestAlgorithm) {
50+
if(!ecSpec.getName().startsWith(SEC_PRIME_FIELD_PREFIX)) {
51+
throw new IllegalStateException("Non-prime curves are not supported at this time");
52+
}
4353

44-
private ECDSASignatureAlgorithm(ECNamedCurveParameterSpec ecSpec, String messageDigestAlgorithm) {
4554
this.ecSpec = ecSpec;
4655
this.messageDigestAlgorithm = messageDigestAlgorithm;
4756
this.hashAndSignAlgorithm = messageDigestAlgorithm + "withECDSA";
57+
58+
try {
59+
final AlgorithmParameters parameters = AlgorithmParameters.getInstance(ELLIPTIC_CURVE_ALGORITHM);
60+
parameters.init(ecSpec);
61+
this.ecParameterSpec = parameters.getParameterSpec(ECParameterSpec.class);
62+
} catch (NoSuchAlgorithmException | InvalidParameterSpecException e) {
63+
throw new IllegalStateException("Invalid algorithm", e);
64+
}
4865
}
4966

5067
@Override
@@ -66,37 +83,99 @@ public String getRawSignatureAlgorithm() {
6683
return hashAndSignAlgorithm;
6784
}
6885

86+
/**
87+
* Decodes a compressed elliptic curve point as described in SEC-1 v2 section 2.3.4
88+
* @see <a href="http://www.secg.org/sec1-v2.pdf">http://www.secg.org/sec1-v2.pdf</a>
89+
* @param keyString The serialized and compressed public key
90+
* @return The PublicKey
91+
*/
6992
@Override
7093
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);
94+
notNull(keyString, "keyString is required");
95+
96+
final byte[] decodedKey = Utils.decodeBase64String(keyString);
97+
final BigInteger x = new BigInteger(1, Arrays.copyOfRange(decodedKey, 1, decodedKey.length));
98+
99+
final byte compressedY = decodedKey[0];
100+
final BigInteger yOrder;
101+
102+
if(compressedY == TWO.byteValue()) {
103+
yOrder = ZERO;
104+
} else if (compressedY == THREE.byteValue()) {
105+
yOrder = ONE;
106+
} else {
107+
throw new IllegalArgumentException("Compressed y value was invalid");
108+
}
109+
110+
final BigInteger p = ((ECFieldFp)ecParameterSpec.getCurve().getField()).getP();
111+
final BigInteger a = ecParameterSpec.getCurve().getA();
112+
final BigInteger b = ecParameterSpec.getCurve().getB();
113+
114+
final BigInteger alpha = x.modPow(THREE, p)
115+
.add(a.multiply(x).mod(p))
116+
.add(b)
117+
.mod(p);
118+
119+
final BigInteger beta;
120+
if(p.mod(FOUR).equals(THREE)) {
121+
beta = alpha.modPow(p.add(ONE).divide(FOUR), p);
122+
} else {
123+
throw new IllegalArgumentException("Curve not supported at this time");
124+
}
125+
126+
final BigInteger y = beta.mod(TWO).equals(yOrder) ? beta : p.subtract(beta);
127+
128+
//Validate that Y is a root of Y^2 to prevent invalid point attacks
129+
if(!alpha.equals(y.modPow(TWO, p))) {
130+
throw new IllegalArgumentException("Y was invalid");
131+
}
132+
133+
try {
134+
return KeyFactory.getInstance(ELLIPTIC_CURVE_ALGORITHM).generatePublic(
135+
new ECPublicKeySpec(new ECPoint(x, y), ecParameterSpec));
136+
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
137+
throw new IllegalStateException("Invalid algorithm", e);
138+
}
79139
}
80140

141+
/**
142+
* Encodes a compressed elliptic curve point as described in SEC-1 v2 section 2.3.3
143+
* @see <a href="http://www.secg.org/sec1-v2.pdf">http://www.secg.org/sec1-v2.pdf</a>
144+
* @param key The Elliptic Curve public key to compress and serialize
145+
* @return The serialized and compressed public key
146+
*/
81147
@Override
82148
public String serializePublicKey(PublicKey key) {
83-
return Utils.encodeBase64String(((ECPublicKey)key).getQ().getEncoded(true));
149+
notNull(key, "key is required");
150+
isInstanceOf(ECPublicKey.class, key, "key must be an instance of ECPublicKey");
151+
152+
final BigInteger x = ((ECPublicKey)key).getW().getAffineX();
153+
final BigInteger y = ((ECPublicKey)key).getW().getAffineY();
154+
final BigInteger compressedY = y.mod(TWO).equals(ZERO) ? TWO : THREE;
155+
156+
final byte[] xBytes = bigIntegerToByteArray(x,
157+
ecParameterSpec.getCurve().getField().getFieldSize() / Byte.SIZE);
158+
159+
final byte[] compressedKey = new byte[xBytes.length + 1];
160+
System.arraycopy(xBytes, 0, compressedKey, 1, xBytes.length);
161+
compressedKey[0] = compressedY.byteValue();
162+
163+
return encodeBase64String(compressedKey);
84164
}
85165

86166
@Override
87167
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);
168+
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ELLIPTIC_CURVE_ALGORITHM);
90169
keyGen.initialize(ecSpec, Utils.getSecureRandom());
91170

92171
return keyGen.generateKeyPair();
93172
}
94173
}
95174

96175
private static final ECDSASignatureAlgorithm SHA256_ECDSA_P256
97-
= new ECDSASignatureAlgorithm(ECNamedCurveTable.getParameterSpec("secp256r1"), "SHA256");
176+
= new ECDSASignatureAlgorithm(new ECGenParameterSpec(SEC_PRIME_FIELD_PREFIX + "256r1"), "SHA256");
98177
private static final ECDSASignatureAlgorithm SHA384_ECDSA_P384
99-
= new ECDSASignatureAlgorithm(ECNamedCurveTable.getParameterSpec("secp384r1"), "SHA384");
178+
= new ECDSASignatureAlgorithm(new ECGenParameterSpec(SEC_PRIME_FIELD_PREFIX + "384r1"), "SHA384");
100179

101180
public static TrailingSignatureAlgorithm forCryptoAlgorithm(CryptoAlgorithm algorithm) {
102181
switch (algorithm) {

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

+27
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,30 @@ 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
289+
* @return The byte array
290+
*/
291+
public static byte[] bigIntegerToByteArray(final BigInteger bigInteger, final int length) {
292+
final byte[] result = new byte[length];
293+
byte[] rawBytes = bigInteger.toByteArray();
294+
295+
//Remove sign byte if one is present
296+
if(rawBytes[0] == 0) {
297+
rawBytes = Arrays.copyOfRange(rawBytes, 1, rawBytes.length);
298+
}
299+
300+
if(length < rawBytes.length) {
301+
throw new IllegalArgumentException("Length must be as least as long as the BigInteger byte array " +
302+
"without the sign byte");
303+
}
304+
305+
System.arraycopy(rawBytes, 0, result, length - rawBytes.length, rawBytes.length);
306+
307+
return result;
308+
}
282309
}

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

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

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

+15
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,19 @@ 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+
108+
bytes = new byte[] {0, -47, 126, -42, 34};
109+
110+
assertArrayEquals(new byte[]{-47, 126, -42, 34},
111+
Utils.bigIntegerToByteArray(new BigInteger(bytes), 4));
112+
113+
}
99114
}
100115

0 commit comments

Comments
 (0)