Skip to content

Commit 89f3c70

Browse files
Update parsing/checking private key encoding and equals() (#1091)
The method that parses the private key encoding is updated to check the encoding's validity. An equals() method is added that calls the super.equals() and, if that fails, directly compares the keys and params. Several tests are, also, added to test key import, direct and interop, through ECPrivateKeySpec. Signed-off-by: Kostas Tsiounis <kostas.tsiounis@ibm.com>
1 parent 1d00d44 commit 89f3c70

File tree

4 files changed

+193
-50
lines changed

4 files changed

+193
-50
lines changed

src/main/java/com/ibm/crypto/plus/provider/ECKeyFactory.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@ protected PrivateKey engineGeneratePrivate(KeySpec keySpec) throws InvalidKeySpe
8282
}
8383

8484
} catch (InvalidKeyException e) {
85-
throw new InvalidKeySpecException("Inappropriate key specification: " + e.getMessage());
85+
throw new InvalidKeySpecException("Inappropriate key specification: ", e);
8686
} catch (InvalidParameterSpecException e) {
8787
throw new InvalidKeySpecException(
88-
"Inappropriate Parameter specification: " + e.getMessage());
88+
"Inappropriate Parameter specification: ", e);
8989
}
9090
}
9191

src/main/java/com/ibm/crypto/plus/provider/ECPrivateKey.java

Lines changed: 92 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,10 @@ final class ECPrivateKey extends PKCS8Key implements java.security.interfaces.EC
103103
this.provider = provider;
104104

105105
try {
106-
// Set parameters.
107-
AlgorithmParameters algParams = this.algid.getParameters();
108-
if (algParams == null) {
109-
throw new IOException(
110-
"EC domain parameters must be encoded in the algorithm identifier");
111-
}
112-
this.params = algParams.getParameterSpec(ECParameterSpec.class);
113-
114106
// Get from the encoding:
115107
// * the private key as a BigInteger (this.s)
116108
// * the public key, if available (this.pubKeyEncoded)
109+
// and set parameters.
117110
parsePrivateKeyEncoding();
118111

119112
// Create appropriate encoding and create ecKey.
@@ -140,20 +133,13 @@ final class ECPrivateKey extends PKCS8Key implements java.security.interfaces.EC
140133
algidOut.putDerValue(new DerValue(ecKey.getParameters()));
141134
this.algid = AlgorithmId
142135
.parse(new DerValue(DerValue.tag_Sequence, algidOut.toByteArray()));
143-
144-
AlgorithmParameters algParams = this.algid.getParameters();
145-
if (algParams == null) {
146-
throw new IOException(
147-
"EC domain parameters must be encoded in the algorithm identifier");
148-
}
149-
this.params = algParams.getParameterSpec(ECParameterSpec.class);
150-
151136
// Get private key encoding from ECKey.
152137
this.privKeyMaterial = ecKey.getPrivateKeyBytes();
153138

154139
// Get from the encoding:
155140
// * the private key as a BigInteger (this.s)
156141
// * the public key, if available (this.pubKeyEncoded)
142+
// and set parameters.
157143
parsePrivateKeyEncoding();
158144
} catch (Exception exception) {
159145
throw new InvalidKeyException("Failed to create EC private key", exception);
@@ -181,33 +167,13 @@ private byte[] createEncodedPrivateKeyWithParams() throws IOException {
181167
DerValue[] inputDerValue = privKeyBytesEncodedStream.getSequence(4);
182168
DerOutputStream outEncodedStream = new DerOutputStream();
183169

184-
if (inputDerValue.length < 2) {
185-
throw new IOException("Incorrect EC private key encoding");
186-
}
187170
BigInteger tempVersion1 = inputDerValue[0].getBigInteger();
188-
if (tempVersion1.compareTo(BigInteger.ONE) != 0) {
189-
throw new IOException("Decoding EC private key failed. The version must be 1");
190-
}
191171
outEncodedStream.putInteger(tempVersion1);
192172

193173
byte[] privateKeyBytes = inputDerValue[1].getOctetString();
194174
outEncodedStream.putOctetString(privateKeyBytes);
195175

196176
byte[] encodedParams = this.getAlgorithmId().getEncodedParams();
197-
if (inputDerValue.length > 2) {
198-
if (inputDerValue[2].isContextSpecific(TAG_PARAMETERS_ATTRS)) {
199-
DerInputStream paramDerInputStream = inputDerValue[2].getData();
200-
byte[] privateKeyParams = paramDerInputStream.toByteArray();
201-
// Check against the existing parameters created by PKCS8Key.
202-
if (!Arrays.equals(privateKeyParams, encodedParams)) {
203-
throw new IOException("Decoding EC private key failed. The params are not the same as PKCS8Key's");
204-
}
205-
} else if (!inputDerValue[2].isContextSpecific(TAG_PUBLIC_KEY_ATTRS)) {
206-
// Unknown third+ element: we can throw, or ignore.
207-
// Keeping old behavior would be to throw; but RFC allows only [0]/[1] here.
208-
throw new IOException("Decoding EC private key failed. Unexpected tagged field in ECPrivateKey");
209-
}
210-
}
211177
// The native library needs the ASN.1 DER decoding of the private key to contain the parameters (i.e., the OID).
212178
outEncodedStream.write(
213179
DerValue.createTag(DerValue.TAG_CONTEXT, true, TAG_PARAMETERS_ATTRS),
@@ -219,25 +185,69 @@ private byte[] createEncodedPrivateKeyWithParams() throws IOException {
219185
}
220186

221187
/**
222-
* Parse the private key encoding to:
188+
* Check that the encoding is correct and at the same time
189+
* parse the private key encoding to:
223190
* - get the key and set it as a BigInteger (i.e., this.s)
191+
* - validate the parameters, if available
224192
* - get the public key, if available, and save its X.509 encoding
225193
*
226-
* @throws IOException
194+
* @throws InvalidKeyException
227195
*/
228-
private void parsePrivateKeyEncoding() throws IOException {
229-
DerInputStream privKeyBytesEncodedStream = new DerInputStream(this.privKeyMaterial);
230-
DerValue[] inputDerValue = privKeyBytesEncodedStream.getSequence(4);
196+
private void parsePrivateKeyEncoding() throws InvalidKeyException {
197+
// Parse private key material from PKCS8Key.decode()
198+
try {
199+
DerInputStream in = new DerInputStream(this.privKeyMaterial);
200+
DerValue derValue = in.getDerValue();
201+
if (derValue.tag != DerValue.tag_Sequence) {
202+
throw new IOException("Not a SEQUENCE");
203+
}
204+
DerInputStream data = derValue.data;
205+
int version = data.getInteger();
206+
if (version != 1) {
207+
throw new IOException("Version must be 1");
208+
}
209+
byte[] privData = data.getOctetString();
210+
this.s = new BigInteger(1, privData);
231211

232-
byte[] privateKeyBytes = inputDerValue[1].getOctetString();
233-
this.s = new BigInteger(1, privateKeyBytes);
212+
// Validate parameters stored from PKCS8Key.decode()
213+
AlgorithmParameters algParams = this.algid.getParameters();
214+
if (algParams == null) {
215+
throw new InvalidKeyException("EC domain parameters must be "
216+
+ "encoded in the algorithm identifier");
217+
}
218+
this.params = algParams.getParameterSpec(ECParameterSpec.class);
219+
220+
if (data.available() == 0) {
221+
return;
222+
}
223+
224+
DerValue value = data.getDerValue();
225+
if (value.isContextSpecific(TAG_PARAMETERS_ATTRS)) {
226+
byte[] privateKeyParams = value.getDataBytes();
227+
byte[] encodedParams = this.getAlgorithmId().getEncodedParams();
228+
// Check against the existing parameters created by PKCS8Key.
229+
if (!Arrays.equals(privateKeyParams, encodedParams)) {
230+
throw new InvalidKeyException("Decoding EC private key failed. The params are not the same as PKCS8Key's");
231+
}
232+
if (data.available() == 0) {
233+
return;
234+
}
235+
value = data.getDerValue();
236+
}
234237

235-
for (int i = 2; i < inputDerValue.length; i++) {
236-
DerValue v = inputDerValue[i];
237-
if (v.isContextSpecific(TAG_PUBLIC_KEY_ATTRS)) {
238-
DerValue bits = v.withTag(DerValue.tag_BitString);
238+
if (value.isContextSpecific(TAG_PUBLIC_KEY_ATTRS)) {
239+
DerValue bits = value.withTag(DerValue.tag_BitString);
239240
this.pubKeyEncoded = new X509Key(this.algid, bits.data.getUnalignedBitString()).getEncoded();
241+
} else {
242+
throw new InvalidKeyException("Unexpected value: " + value);
243+
}
244+
245+
if (data.available() != 0) {
246+
throw new InvalidKeyException("Encoding has more than 4 values.");
240247
}
248+
249+
} catch (IOException | InvalidParameterSpecException e) {
250+
throw new InvalidKeyException("Invalid EC private key", e);
241251
}
242252
}
243253

@@ -324,4 +334,39 @@ private void checkDestroyed() {
324334
throw new IllegalStateException("This key is no longer valid");
325335
}
326336
}
337+
338+
/**
339+
* Compares two private keys.
340+
*
341+
* The PKCS8Key.equals() method that compares encodings is used first.
342+
*
343+
* If that fails, we compare the private part of the key and the params to validate equivalence,
344+
* since the keys might be equal but have different encodings if one or more of the optional
345+
* parts are missing.
346+
*
347+
* @param object the object with which to compare
348+
* @return {@code true} if this key is equal to the object argument; {@code false} otherwise.
349+
*/
350+
@Override
351+
public boolean equals(Object object) {
352+
boolean sameEncoding = super.equals(object);
353+
if (!sameEncoding) {
354+
if (!(object instanceof java.security.interfaces.ECPrivateKey ecObj)) {
355+
return false;
356+
}
357+
358+
// 1. Compare the secret scalar (S)
359+
if (!this.getS().equals(ecObj.getS())) {
360+
return false;
361+
}
362+
363+
// 2. Compare the Curve Parameters
364+
ECParameterSpec s1 = this.getParams();
365+
ECParameterSpec s2 = ecObj.getParams();
366+
367+
return ECUtils.equals(s1, s2);
368+
}
369+
370+
return true;
371+
}
327372
}

src/test/java/ibm/jceplus/junit/base/BaseTestECKeyImport.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121
import java.security.spec.ECGenParameterSpec;
2222
import java.security.spec.ECParameterSpec;
2323
import java.security.spec.ECPoint;
24+
import java.security.spec.ECPrivateKeySpec;
2425
import java.security.spec.EllipticCurve;
2526
import java.security.spec.EncodedKeySpec;
27+
import java.security.spec.InvalidKeySpecException;
28+
import java.security.spec.KeySpec;
2629
import java.security.spec.PKCS8EncodedKeySpec;
2730
import java.security.spec.X509EncodedKeySpec;
2831
import java.util.Arrays;
@@ -34,6 +37,7 @@
3437
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
3538
import static org.junit.jupiter.api.Assertions.assertNotNull;
3639
import static org.junit.jupiter.api.Assertions.assertTrue;
40+
import static org.junit.jupiter.api.Assertions.fail;
3741

3842
public class BaseTestECKeyImport extends BaseTestJunit5 {
3943

@@ -92,6 +96,19 @@ public class BaseTestECKeyImport extends BaseTestJunit5 {
9296
"Jlk6HKBiJlBvMWVSxEWYNipV2SOgCgYIKoZIzj0DAQehRANCAARFcF00hBK8Es2M" +
9397
"H29DmA3fsYf4qWFSloVWoFct4CxffJ7hG0O4TXkMaPrAjgXc42SPdKRb7FcO0Lhz" +
9498
"EVpYquVY";
99+
100+
private static final String private_secp256r1_parameters_twice_publickey =
101+
"MIGhAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBIGGMIGDAgEBBCDXlTQ8RGqy6mfo" +
102+
"fLkmWTocoGImUG8xZVLERZg2KlXZI6AKBggqhkjOPQMBB6AKBggqhkjOPQMBB6FE" +
103+
"A0IABEVwXTSEErwSzYwfb0OYDd+xh/ipYVKWhVagVy3gLF98nuEbQ7hNeQxo+sCO" +
104+
"BdzjZI90pFvsVw7QuHMRWliq5Vg=";
105+
106+
private static final String private_secp256r1_publickey_parameters =
107+
"MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg15U0PERqsupn6Hy5" +
108+
"Jlk6HKBiJlBvMWVSxEWYNipV2SOhRANCAARFcF00hBK8Es2MH29DmA3fsYf4qWFS" +
109+
"loVWoFct4CxffJ7hG0O4TXkMaPrAjgXc42SPdKRb7FcO0LhzEVpYquVYoAoGCCqG" +
110+
"SM49AwEH";
111+
95112
private static final String public_secp256r1_parameters_publickey =
96113
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERXBdNIQSvBLNjB9vQ5gN37GH+Klh" +
97114
"UpaFVqBXLeAsX3ye4RtDuE15DGj6wI4F3ONkj3SkW+xXDtC4cxFaWKrlWA==";
@@ -152,12 +169,17 @@ public void testCreateKeyPairECGenParamImport() throws Exception {
152169
EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privKeyBytes);
153170
PrivateKey privateKey2 = keyFactory.generatePrivate(privateKeySpec);
154171

172+
KeySpec privateKeySpec2 = keyFactory.getKeySpec(privateKey, ECPrivateKeySpec.class);
173+
PrivateKey privateKey3 = keyFactory.generatePrivate(privateKeySpec2);
174+
155175
EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(pubKeyBytes);
156176
PublicKey publicKey2 = keyFactory.generatePublic(publicKeySpec);
157177

158178
// The original and new keys are the same
159179
boolean same = privateKey.equals(privateKey2);
160180
assertTrue(same);
181+
same = privateKey.equals(privateKey3);
182+
assertTrue(same);
161183
same = publicKey.equals(publicKey2);
162184
assertTrue(same);
163185
}
@@ -214,14 +236,20 @@ public void testCreateKeyPairECParamImport() throws Exception {
214236
EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privKeyBytes);
215237
PrivateKey privateKey2 = keyFactory.generatePrivate(privateKeySpec);
216238

239+
KeySpec privateKeySpec2 = keyFactory.getKeySpec(privateKey, ECPrivateKeySpec.class);
240+
PrivateKey privateKey3 = keyFactory.generatePrivate(privateKeySpec2);
241+
217242
EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes);
218243
PublicKey publicKey2 = keyFactory.generatePublic(publicKeySpec);
219244

220245
// The original and new keys are the same
221246
boolean same = privateKey.equals(privateKey2);
222247
assertTrue(same);
248+
same = privateKey.equals(privateKey3);
249+
assertTrue(same);
223250
same = publicKey.equals(publicKey2);
224251
assertTrue(same);
252+
225253
byte[] publicKey2Bytes = publicKey2.getEncoded();
226254
byte[] privateKey2Bytes = privateKey2.getEncoded();
227255

@@ -383,6 +411,28 @@ public void testECPrivateKeyWithVariousOptionalFields() throws Exception {
383411
System.out.println("ALL tests completed.");
384412
}
385413

414+
@Test
415+
public void testInvalidEncodings() throws Exception {
416+
KeyFactory kf = KeyFactory.getInstance("EC", getProviderName());
417+
try {
418+
// Test encoding with two parameters.
419+
kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getMimeDecoder()
420+
.decode(private_secp256r1_parameters_twice_publickey)));
421+
fail("Expected InvalidKeySpecException not thrown.");
422+
} catch (InvalidKeySpecException ikse) {
423+
assertTrue("Inappropriate key specification: ".equals(ikse.getMessage()));
424+
}
425+
426+
try {
427+
// Test encoding where the public key is before the parameters.
428+
kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getMimeDecoder()
429+
.decode(private_secp256r1_publickey_parameters)));
430+
fail("Expected InvalidKeySpecException not thrown.");
431+
} catch (InvalidKeySpecException ikse) {
432+
assertTrue("Inappropriate key specification: ".equals(ikse.getMessage()));
433+
}
434+
}
435+
386436
private static void testSignAndVerify(
387437
String label,
388438
KeyFactory kf,

src/test/java/ibm/jceplus/junit/base/BaseTestECKeyImportInterop.java

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright IBM Corp. 2023, 2024
2+
* Copyright IBM Corp. 2023, 2026
33
*
44
* This code is free software; you can redistribute it and/or modify it
55
* under the terms provided by IBM in the LICENSE file that accompanied
@@ -19,8 +19,10 @@
1919
import java.security.spec.ECGenParameterSpec;
2020
import java.security.spec.ECParameterSpec;
2121
import java.security.spec.ECPoint;
22+
import java.security.spec.ECPrivateKeySpec;
2223
import java.security.spec.EllipticCurve;
2324
import java.security.spec.EncodedKeySpec;
25+
import java.security.spec.KeySpec;
2426
import java.security.spec.PKCS8EncodedKeySpec;
2527
import java.security.spec.X509EncodedKeySpec;
2628
import java.util.Arrays;
@@ -114,6 +116,52 @@ public void doCreateKeyPairECGenParamImport(String generateProviderName,
114116
assertTrue(Arrays.equals(privateKey2.getEncoded(), privKeyBytes));
115117
}
116118

119+
@Test
120+
public void testCreateKeyPairECImportCompareKeys() throws Exception {
121+
doCreateKeyPairECImportCompareKeys(getProviderName(), getInteropProviderName());
122+
doCreateKeyPairECImportCompareKeys(getInteropProviderName(), getProviderName());
123+
}
124+
125+
private void doCreateKeyPairECImportCompareKeys(String createProviderName,
126+
String importProviderName) throws Exception {
127+
128+
//final String methodName = "testCreateKeyPairECImportCompareKeys";
129+
130+
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("EC", createProviderName);
131+
132+
keyPairGen.initialize(256);
133+
KeyPair keyPair = keyPairGen.generateKeyPair();
134+
PrivateKey privateKey = keyPair.getPrivate();
135+
PublicKey publicKey = keyPair.getPublic();
136+
137+
byte[] publicKeyBytes = publicKey.getEncoded();
138+
byte[] privKeyBytes = privateKey.getEncoded();
139+
140+
KeyFactory keyFactory = KeyFactory.getInstance("EC", importProviderName);
141+
EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privKeyBytes);
142+
PrivateKey privateKey2 = keyFactory.generatePrivate(privateKeySpec);
143+
144+
KeySpec privateKeySpec2 = keyFactory.getKeySpec(privateKey, ECPrivateKeySpec.class);
145+
PrivateKey privateKey3 = keyFactory.generatePrivate(privateKeySpec2);
146+
147+
EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes);
148+
PublicKey publicKey2 = keyFactory.generatePublic(publicKeySpec);
149+
150+
// The original and new keys are the same
151+
boolean same = privateKey.equals(privateKey2);
152+
assertTrue(same);
153+
same = privateKey.equals(privateKey3);
154+
assertTrue(same);
155+
same = publicKey.equals(publicKey2);
156+
assertTrue(same);
157+
158+
byte[] publicKey2Bytes = publicKey2.getEncoded();
159+
byte[] privateKey2Bytes = privateKey2.getEncoded();
160+
161+
assertArrayEquals(publicKeyBytes, publicKey2Bytes);
162+
assertArrayEquals(privKeyBytes, privateKey2Bytes);
163+
}
164+
117165
@Test
118166
public void testCreateKeyPairECParamCustomCurveImport() throws Exception {
119167
doCreateKeyPairECParamCustomCurveImport(getProviderName(), getInteropProviderName());

0 commit comments

Comments
 (0)