Skip to content

Commit 29abc42

Browse files
committed
[fix] handle PKey::EC.new with encrypted PEM
1 parent f67c0af commit 29abc42

File tree

4 files changed

+71
-9
lines changed

4 files changed

+71
-9
lines changed

src/main/java/org/jruby/ext/openssl/PKeyEC.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -840,16 +840,23 @@ public RubyString to_pem(ThreadContext context, final IRubyObject[] args) {
840840
if ( args.length > 1 ) passwd = password(context, args[1], null);
841841
}
842842

843-
if (privateKey == null) {
844-
return public_to_pem(context);
845-
}
843+
if (privateKey == null) return public_to_pem(context);
846844

847845
try {
848846
final StringWriter writer = new StringWriter();
849-
PEMInputOutput.writeECPrivateKey(writer, (ECPrivateKey) privateKey, spec, passwd);
847+
// Include curve OID and public-key point so the PEM can be decoded
848+
// stand-alone (SEC1 optional fields parameters[0] and publicKey[1]).
849+
final ASN1ObjectIdentifier curveOID = getCurveOID(getCurveName()).orElse(null);
850+
byte[] pubKeyBytes = null;
851+
if (publicKey != null) {
852+
pubKeyBytes = EC5Util.convertPoint(
853+
publicKey.getParams(), publicKey.getW()).getEncoded(false);
854+
}
855+
PEMInputOutput.writeECPrivateKey(writer, (ECPrivateKey) privateKey,
856+
curveOID, pubKeyBytes, spec, passwd);
850857
return RubyString.newString(context.runtime, writer.getBuffer());
851858
} catch (IOException ex) {
852-
throw newECError(context.runtime, ex.getMessage());
859+
throw newECError(context.runtime, ex.getMessage(), ex);
853860
}
854861
}
855862

@@ -860,7 +867,7 @@ public RubyString public_to_pem(ThreadContext context) {
860867
PEMInputOutput.writeECPublicKey(writer, publicKey);
861868
return RubyString.newString(context.runtime, writer.getBuffer());
862869
} catch (IOException ex) {
863-
throw newECError(context.runtime, ex.getMessage());
870+
throw newECError(context.runtime, ex.getMessage(), ex);
864871
}
865872
}
866873

src/main/java/org/jruby/ext/openssl/impl/PKey.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,13 +299,17 @@ public static KeyPair readECPrivateKey(final KeyFactory keyFactory, final Privat
299299
if (algId == null) { // mockPrivateKeyInfo
300300
algId = new AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, key.getParameters());
301301
}
302-
final SubjectPublicKeyInfo pubInfo = new SubjectPublicKeyInfo(algId, key.getPublicKey().getBytes());
303302
final PrivateKeyInfo privInfo = new PrivateKeyInfo(algId, key);
304-
305303
ECPrivateKey privateKey = (ECPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privInfo.getEncoded()));
306304
if (algId.getParameters() instanceof ASN1ObjectIdentifier) {
307305
privateKey = ECPrivateKeyWithName.wrap(privateKey, (ASN1ObjectIdentifier) algId.getParameters());
308306
}
307+
308+
// The publicKey field in ECPrivateKey DER is optional (RFC 5915).
309+
// Keys written by older JRuby-OpenSSL may omit it; handle gracefully.
310+
final org.bouncycastle.asn1.ASN1BitString pubKeyBits = key.getPublicKey();
311+
if (pubKeyBits == null) return new KeyPair(null, privateKey);
312+
final SubjectPublicKeyInfo pubInfo = new SubjectPublicKeyInfo(algId, pubKeyBits.getBytes());
309313
return new KeyPair(keyFactory.generatePublic(new X509EncodedKeySpec(pubInfo.getEncoded())), privateKey);
310314
}
311315
catch (ClassCastException ex) {

src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
import org.bouncycastle.asn1.ASN1Integer;
9494
import org.bouncycastle.asn1.DEROctetString;
9595
import org.bouncycastle.asn1.DERUTF8String;
96+
import org.bouncycastle.asn1.DERBitString;
9697
import org.bouncycastle.asn1.DLSequence;
9798
import org.bouncycastle.asn1.DERTaggedObject;
9899
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
@@ -1089,11 +1090,32 @@ public static void writeRSAPrivateKey(Writer _out, RSAPrivateCrtKey obj, CipherS
10891090
}
10901091

10911092
public static void writeECPrivateKey(Writer _out, ECPrivateKey obj, CipherSpec cipher, char[] passwd) throws IOException {
1093+
writeECPrivateKey(_out, obj, null, null, cipher, passwd);
1094+
}
1095+
1096+
/**
1097+
* Writes an EC private key in SEC1 / "EC PRIVATE KEY" PEM format.
1098+
* When {@code curveOID} and {@code pubKeyBytes} are provided they are
1099+
* embedded as the optional {@code parameters[0]} and {@code publicKey[1]}
1100+
* fields so that the PEM can be decoded stand-alone (without external
1101+
* knowledge of the curve).
1102+
*/
1103+
public static void writeECPrivateKey(Writer _out, ECPrivateKey obj,
1104+
ASN1ObjectIdentifier curveOID, byte[] pubKeyBytes,
1105+
CipherSpec cipher, char[] passwd) throws IOException {
10921106
assert (obj != null);
10931107
final String PEM_STRING_EC = "EC PRIVATE KEY";
10941108
BufferedWriter out = makeBuffered(_out);
10951109
final int bitLength = obj.getParams().getOrder().bitLength();
1096-
org.bouncycastle.asn1.sec.ECPrivateKey keyStruct = new org.bouncycastle.asn1.sec.ECPrivateKey(bitLength, obj.getS());
1110+
final org.bouncycastle.asn1.sec.ECPrivateKey keyStruct;
1111+
if (curveOID != null && pubKeyBytes != null) {
1112+
keyStruct = new org.bouncycastle.asn1.sec.ECPrivateKey(
1113+
bitLength, obj.getS(),
1114+
new DERBitString(pubKeyBytes),
1115+
curveOID);
1116+
} else {
1117+
keyStruct = new org.bouncycastle.asn1.sec.ECPrivateKey(bitLength, obj.getS());
1118+
}
10971119
if (cipher != null && passwd != null) {
10981120
writePemEncrypted(out, PEM_STRING_EC, keyStruct.getEncoded(), cipher, passwd);
10991121
} else {

src/test/ruby/ec/test_ec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,35 @@ def test_sign_verify_raw
573573
assert_sign_verify_false_or_error { key.verify_raw(nil, malformed_sig, data1) }
574574
end
575575

576+
def test_ECPrivateKey_encrypted
577+
p256 = Fixtures.pkey("p256")
578+
# key = abcdef (hardcoded encrypted PEM from MRI test suite)
579+
pem = <<~EOF
580+
-----BEGIN EC PRIVATE KEY-----
581+
Proc-Type: 4,ENCRYPTED
582+
DEK-Info: AES-128-CBC,85743EB6FAC9EA76BF99D9328AFD1A66
583+
584+
nhsP1NHxb53aeZdzUe9umKKyr+OIwQq67eP0ONM6E1vFTIcjkDcFLR6PhPFufF4m
585+
y7E2HF+9uT1KPQhlE+D63i1m1Mvez6PWfNM34iOQp2vEhaoHHKlR3c43lLyzaZDI
586+
0/dGSU5SzFG+iT9iFXCwCvv+bxyegkBOyALFje1NAsM=
587+
-----END EC PRIVATE KEY-----
588+
EOF
589+
key = OpenSSL::PKey::EC.new(pem, "abcdef")
590+
assert_same_ec p256, key
591+
key = OpenSSL::PKey::EC.new(pem) { "abcdef" }
592+
assert_same_ec p256, key
593+
594+
# Round-trip: to_pem with encryption and read back
595+
cipher = OpenSSL::Cipher.new("aes-128-cbc")
596+
exported = p256.to_pem(cipher, "abcdef\0\1")
597+
assert_same_ec p256, OpenSSL::PKey::EC.new(exported, "abcdef\0\1")
598+
# MRI raises OpenSSL::PKey::PKeyError;
599+
# TODO JRuby raises more specific ECError
600+
assert_raise(OpenSSL::PKey::ECError) {
601+
OpenSSL::PKey::EC.new(exported, "abcdef")
602+
}
603+
end
604+
576605
def test_new_from_der
577606
priv_key_hex = '05768F097A19FFE5022D4A862CDBAE22019695D1C2F88FD41607417AD45E2F55'
578607
pub_key_hex = '04B827833DC1BC38CE0BBE36E0357B1D08AB0BFA05DBD211F0FC677FF9913FAF0EB3A3CC562EEAE8D841B112DBFDAD494E10CFBD4964DC2D175D06F17ACC5771CF'

0 commit comments

Comments
 (0)