Skip to content

Commit 6a6ce2e

Browse files
committed
[compat] implement PKey::EC public_to_pem and xxx_to_der
1 parent e175e69 commit 6a6ce2e

File tree

3 files changed

+219
-26
lines changed

3 files changed

+219
-26
lines changed

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

Lines changed: 127 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.Locale;
3838
import java.util.Optional;
3939
import javax.crypto.KeyAgreement;
40+
4041
import org.bouncycastle.asn1.ASN1EncodableVector;
4142
import org.bouncycastle.asn1.ASN1Encoding;
4243
import org.bouncycastle.asn1.ASN1InputStream;
@@ -45,19 +46,27 @@
4546
import org.bouncycastle.asn1.ASN1OutputStream;
4647
import org.bouncycastle.asn1.ASN1Primitive;
4748
import org.bouncycastle.asn1.ASN1Sequence;
49+
import org.bouncycastle.asn1.DERNull;
4850
import org.bouncycastle.asn1.DERSequence;
49-
51+
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
52+
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
53+
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
54+
import org.bouncycastle.asn1.x9.X962Parameters;
55+
import org.bouncycastle.asn1.x9.X9ECParameters;
56+
import org.bouncycastle.asn1.x9.X9ECPoint;
57+
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
5058
import org.bouncycastle.crypto.params.ECDomainParameters;
5159
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
5260
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
5361
import org.bouncycastle.crypto.signers.ECDSASigner;
5462
import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util;
5563
import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;
64+
import org.bouncycastle.jcajce.provider.config.ProviderConfiguration;
5665
import org.bouncycastle.jce.ECNamedCurveTable;
5766
import org.bouncycastle.jce.ECPointUtil;
67+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
5868
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
5969
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
60-
6170
import org.bouncycastle.math.ec.ECAlgorithms;
6271
import org.bouncycastle.math.ec.ECCurve;
6372
import org.jruby.Ruby;
@@ -82,13 +91,15 @@
8291
import org.jruby.runtime.component.VariableEntry;
8392

8493
import org.jruby.ext.openssl.impl.CipherSpec;
85-
import static org.jruby.ext.openssl.OpenSSL.debug;
86-
import static org.jruby.ext.openssl.OpenSSL.debugStackTrace;
8794
import org.jruby.ext.openssl.impl.ECPrivateKeyWithName;
88-
import static org.jruby.ext.openssl.impl.PKey.readECPrivateKey;
95+
8996
import org.jruby.ext.openssl.util.ByteArrayOutputStream;
9097
import org.jruby.ext.openssl.x509store.PEMInputOutput;
9198

99+
import static org.jruby.ext.openssl.OpenSSL.debug;
100+
import static org.jruby.ext.openssl.OpenSSL.debugStackTrace;
101+
import static org.jruby.ext.openssl.impl.PKey.readECPrivateKey;
102+
92103
/**
93104
* OpenSSL::PKey::EC implementation.
94105
*
@@ -626,27 +637,111 @@ public RubyBoolean private_p() {
626637
return privateKey != null ? getRuntime().getTrue() : getRuntime().getFalse();
627638
}
628639

640+
@JRubyMethod
641+
public RubyString public_to_der(ThreadContext context) {
642+
return public_to_der(context.runtime);
643+
}
644+
645+
private RubyString public_to_der(final Ruby runtime) {
646+
final byte[] bytes;
647+
try {
648+
bytes = publicKey.getEncoded();
649+
} catch (Exception e) {
650+
throw newECError(runtime, e.getMessage(), e);
651+
}
652+
return StringHelper.newString(runtime, bytes);
653+
}
654+
629655
@Override
630656
@JRubyMethod(name = "to_der")
631657
public RubyString to_der() {
632-
final byte[] bytes;
658+
final Ruby runtime = getRuntime();
659+
if (publicKey != null && privateKey == null) {
660+
return public_to_der(runtime);
661+
}
662+
if (privateKey == null) {
663+
throw new IllegalStateException("private key as well as public key are null");
664+
}
665+
633666
try {
634-
bytes = toDER();
667+
byte[] encoded = toPrivateKeyStructure((ECPrivateKey) privateKey, publicKey, false).getEncoded(ASN1Encoding.DER);
668+
return StringHelper.newString(runtime, encoded);
669+
} catch (Exception e) {
670+
throw newECError(runtime, e.getMessage(), e);
671+
}
672+
}
673+
674+
@JRubyMethod
675+
public RubyString private_to_der(ThreadContext context) {
676+
return private_to_der(context.runtime);
677+
}
678+
679+
private RubyString private_to_der(final Ruby runtime) {
680+
final byte[] encoded;
681+
if (privateKey instanceof ECPrivateKey) {
682+
try {
683+
encoded = toPrivateKeyInfo((ECPrivateKey) privateKey, publicKey).getEncoded(ASN1Encoding.DER);
684+
} catch (IOException e) {
685+
throw newECError(runtime, e.getMessage(), e);
686+
}
687+
} else {
688+
try {
689+
encoded = privateKey.getEncoded();
690+
} catch (Exception e) {
691+
throw newECError(runtime, e.getMessage(), e);
692+
}
635693
}
636-
catch (IOException e) {
637-
throw newECError(getRuntime(), e.getMessage());
694+
return StringHelper.newString(runtime, encoded);
695+
}
696+
697+
private static org.bouncycastle.asn1.sec.ECPrivateKey toPrivateKeyStructure(final ECPrivateKey privateKey,
698+
final ECPublicKey publicKey,
699+
final boolean compressed) throws IOException {
700+
final ProviderConfiguration configuration = BouncyCastleProvider.CONFIGURATION;
701+
final ECParameterSpec ecSpec = privateKey.getParams();
702+
final X962Parameters params = getDomainParametersFromName(ecSpec, compressed);
703+
704+
int orderBitLength = ECUtil.getOrderBitLength(configuration, ecSpec == null ? null : ecSpec.getOrder(), privateKey.getS());
705+
706+
if (publicKey == null) {
707+
return new org.bouncycastle.asn1.sec.ECPrivateKey(orderBitLength, privateKey.getS(), params);
638708
}
639-
return StringHelper.newString(getRuntime(), bytes);
709+
710+
SubjectPublicKeyInfo info = SubjectPublicKeyInfo.getInstance(ASN1Primitive.fromByteArray(publicKey.getEncoded()));
711+
return new org.bouncycastle.asn1.sec.ECPrivateKey(orderBitLength, privateKey.getS(), info.getPublicKeyData(), params);
712+
}
713+
714+
private static PrivateKeyInfo toPrivateKeyInfo(final ECPrivateKey privateKey,
715+
final ECPublicKey publicKey) throws IOException {
716+
final ECParameterSpec ecSpec = privateKey.getParams();
717+
final X962Parameters params = getDomainParametersFromName(ecSpec, false);
718+
719+
org.bouncycastle.asn1.sec.ECPrivateKey keyStructure = toPrivateKeyStructure(privateKey, publicKey, false);
720+
return new PrivateKeyInfo(new AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, params), keyStructure);
640721
}
641722

642-
private byte[] toDER() throws IOException {
643-
if ( publicKey != null && privateKey == null ) {
644-
return publicKey.getEncoded();
723+
private static X962Parameters getDomainParametersFromName(ECParameterSpec ecSpec, boolean compressed) {
724+
if (ecSpec instanceof ECNamedCurveSpec) {
725+
ASN1ObjectIdentifier curveOid = ECUtil.getNamedCurveOid(((ECNamedCurveSpec)ecSpec).getName());
726+
if (curveOid == null)
727+
{
728+
curveOid = new ASN1ObjectIdentifier(((ECNamedCurveSpec)ecSpec).getName());
729+
}
730+
return new X962Parameters(curveOid);
645731
}
646-
if ( privateKey == null ) {
647-
throw new IllegalStateException("private key as well as public key are null");
732+
if (ecSpec == null) {
733+
return new X962Parameters(DERNull.INSTANCE);
648734
}
649-
return privateKey.getEncoded();
735+
ECCurve curve = EC5Util.convertCurve(ecSpec.getCurve());
736+
737+
X9ECParameters ecParameters = new X9ECParameters(
738+
curve,
739+
new X9ECPoint(EC5Util.convertPoint(curve, ecSpec.getGenerator()), compressed),
740+
ecSpec.getOrder(),
741+
BigInteger.valueOf(ecSpec.getCofactor()),
742+
ecSpec.getCurve().getSeed());
743+
744+
return new X962Parameters(ecParameters);
650745
}
651746

652747
@Override
@@ -660,17 +755,26 @@ public RubyString to_pem(ThreadContext context, final IRubyObject[] args) {
660755
if ( args.length > 1 ) passwd = password(context, args[1], null);
661756
}
662757

758+
if (privateKey == null) {
759+
return public_to_pem(context);
760+
}
761+
663762
try {
664763
final StringWriter writer = new StringWriter();
665-
if ( privateKey != null ) {
666-
PEMInputOutput.writeECPrivateKey(writer, (ECPrivateKey) privateKey, spec, passwd);
667-
}
668-
else {
669-
PEMInputOutput.writeECPublicKey(writer, publicKey);
670-
}
764+
PEMInputOutput.writeECPrivateKey(writer, (ECPrivateKey) privateKey, spec, passwd);
671765
return RubyString.newString(context.runtime, writer.getBuffer());
766+
} catch (IOException ex) {
767+
throw newECError(context.runtime, ex.getMessage());
672768
}
673-
catch (IOException ex) {
769+
}
770+
771+
@JRubyMethod
772+
public RubyString public_to_pem(ThreadContext context) {
773+
try {
774+
final StringWriter writer = new StringWriter();
775+
PEMInputOutput.writeECPublicKey(writer, publicKey);
776+
return RubyString.newString(context.runtime, writer.getBuffer());
777+
} catch (IOException ex) {
674778
throw newECError(context.runtime, ex.getMessage());
675779
}
676780
}

src/test/ruby/ec/test_ec.rb

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,80 @@
33

44
class TestEC < TestCase
55

6+
def test_PUBKEY
7+
p256 = Fixtures.pkey("p256")
8+
p256pub = OpenSSL::PKey::EC.new(p256.public_to_der)
9+
10+
public_to_der = "0Y0\x13\x06\a*\x86H\xCE=\x02\x01\x06\b*\x86H\xCE=\x03\x01\a\x03B\x00\x04\x16\td\xD9\xCF\xA8UB\nC\xAE\x1Edo[\x84\xB3OX\x1E\xE5I\x9F\xC0\xAC\xAE5xl\xB9\xC0\f\xD4\xFFA\xB9\xD5{m\t\xE0T\x97\xE3\x1A\x85\x9Bg\xF5\xF3\xB5$\xA7E\xE2\xA2fK\x7F]^zD6"
11+
assert_equal public_to_der, p256.public_to_der
12+
13+
# MRI:
14+
uncompressed_public_key = "\x04\x16\td\xD9\xCF\xA8UB\nC\xAE\x1Edo[\x84\xB3OX\x1E\xE5I\x9F\xC0\xAC\xAE5xl\xB9\xC0\f\xD4\xFFA\xB9\xD5{m\t\xE0T\x97\xE3\x1A\x85\x9Bg\xF5\xF3\xB5$\xA7E\xE2\xA2fK\x7F]^zD6"
15+
assert_equal uncompressed_public_key, p256.public_key.to_octet_string(:uncompressed)
16+
17+
asn1 = OpenSSL::ASN1::Sequence([
18+
OpenSSL::ASN1::Sequence([
19+
OpenSSL::ASN1::ObjectId("id-ecPublicKey"),
20+
OpenSSL::ASN1::ObjectId("prime256v1")
21+
]),
22+
OpenSSL::ASN1::BitString(
23+
p256.public_key.to_octet_string(:uncompressed)
24+
)
25+
])
26+
assert_equal public_to_der, asn1.to_der
27+
28+
to_der = "0w\x02\x01\x01\x04 \x80\xF8\xF4P\xEAq\xFDN\xD5\xE3\xBC\xB1\xA4\xE0\e\xBD\x14mt0\xF4Z\xB0\xB1\xE9b\x8A\xDD\x9AZ\x11\xF5\xA0\n\x06\b*\x86H\xCE=\x03\x01\a\xA1D\x03B\x00\x04\x16\td\xD9\xCF\xA8UB\nC\xAE\x1Edo[\x84\xB3OX\x1E\xE5I\x9F\xC0\xAC\xAE5xl\xB9\xC0\f\xD4\xFFA\xB9\xD5{m\t\xE0T\x97\xE3\x1A\x85\x9Bg\xF5\xF3\xB5$\xA7E\xE2\xA2fK\x7F]^zD6"
29+
#pp OpenSSL::ASN1.decode(to_der)
30+
# #<OpenSSL::ASN1::Sequence:0x000072229cabc698
31+
# @indefinite_length=false,
32+
# @tag=16,
33+
# @tag_class=:UNIVERSAL,
34+
# @tagging=nil,
35+
# @value=
36+
# [#<OpenSSL::ASN1::Integer:0x000072229cabc8c8 @indefinite_length=false, @tag=2, @tag_class=:UNIVERSAL, @tagging=nil, @value=#<OpenSSL::BN 1>>,
37+
# #<OpenSSL::ASN1::OctetString:0x000072229cabc828 @indefinite_length=false, @tag=4, @tag_class=:UNIVERSAL, @tagging=nil, @value="\x80\xF8\xF4P\xEAq\xFDN\xD5\xE3\xBC\xB1\xA4\xE0\e\xBD\x14mt0\xF4Z\xB0\xB1\xE9b\x8A\xDD\x9AZ\x11\xF5">,
38+
# #<OpenSSL::ASN1::ASN1Data:0x000072229cabc760
39+
# @indefinite_length=false,
40+
# @tag=0,
41+
# @tag_class=:CONTEXT_SPECIFIC,
42+
# @value=[#<OpenSSL::ASN1::ObjectId:0x000072229cabc7b0 @indefinite_length=false, @tag=6, @tag_class=:UNIVERSAL, @tagging=nil, @value="prime256v1">]>,
43+
# #<OpenSSL::ASN1::ASN1Data:0x000072229cabc6c0
44+
# @indefinite_length=false,
45+
# @tag=1,
46+
# @tag_class=:CONTEXT_SPECIFIC,
47+
# @value=
48+
# [#<OpenSSL::ASN1::BitString:0x000072229cabc6e8
49+
# @indefinite_length=false,
50+
# @tag=3,
51+
# @tag_class=:UNIVERSAL,
52+
# @tagging=nil,
53+
# @unused_bits=0,
54+
# @value="\x04\x16\td\xD9\xCF\xA8UB\n" + "C\xAE\x1Edo[\x84\xB3OX\x1E\xE5I\x9F\xC0\xAC\xAE5xl\xB9\xC0\f\xD4\xFFA\xB9\xD5{m\t\xE0T\x97\xE3\x1A\x85\x9Bg\xF5\xF3\xB5$\xA7E\xE2\xA2fK\x7F]^zD6">]>]>
55+
56+
assert_equal to_der, p256.to_der
57+
58+
key = OpenSSL::PKey::EC.new(asn1.to_der)
59+
assert_not_predicate key, :private?
60+
assert_same_ec p256pub, key
61+
62+
pem = <<~EOF
63+
-----BEGIN PUBLIC KEY-----
64+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFglk2c+oVUIKQ64eZG9bhLNPWB7l
65+
SZ/ArK41eGy5wAzU/0G51XttCeBUl+MahZtn9fO1JKdF4qJmS39dXnpENg==
66+
-----END PUBLIC KEY-----
67+
EOF
68+
key = OpenSSL::PKey::EC.new(pem)
69+
assert_same_ec p256pub, key
70+
71+
assert_equal asn1.to_der, key.to_der
72+
assert_equal pem, key.export
73+
74+
assert_equal asn1.to_der, p256.public_to_der
75+
assert_equal asn1.to_der, key.public_to_der
76+
assert_equal pem, p256.public_to_pem
77+
assert_equal pem, key.public_to_pem
78+
end
79+
680
def test_oid
781
key = OpenSSL::PKey::EC.new
882
assert_equal 'id-ecPublicKey', key.oid
@@ -285,7 +359,7 @@ def test_check_key
285359
end
286360

287361
def test_sign_verify
288-
p256 = Fixtures.pkey("p256.pem")
362+
p256 = Fixtures.pkey("p256")
289363
data = "Sign me!"
290364
signature = p256.sign("SHA1", data)
291365
assert_equal true, p256.verify("SHA1", signature, data)
@@ -356,7 +430,7 @@ def test_dsa_sign_verify_all
356430
end
357431

358432
def test_sign_verify_raw
359-
key = Fixtures.pkey("p256.pem")
433+
key = Fixtures.pkey("p256")
360434
data1 = "foo"
361435
data2 = "bar"
362436

@@ -501,4 +575,19 @@ def decode_octets(base64_encoded_coordinate); require 'base64'
501575
# end
502576
# end
503577

578+
private
579+
580+
def B(ary)
581+
[Array(ary).join].pack("H*")
582+
end
583+
584+
def assert_same_ec(expected, key)
585+
check_component(expected, key, [:group, :public_key, :private_key])
586+
end
587+
588+
def check_component(base, test, keys)
589+
keys.each { |comp|
590+
assert_equal base.send(comp), test.send(comp)
591+
}
592+
end
504593
end

src/test/ruby/fixtures/pkey/p256.pem renamed to src/test/ruby/fixtures/pkey/p256

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
MHcCAQEEIID49FDqcf1O1eO8saTgG70UbXQw9Fqwseliit2aWhH1oAoGCCqGSM49
33
AwEHoUQDQgAEFglk2c+oVUIKQ64eZG9bhLNPWB7lSZ/ArK41eGy5wAzU/0G51Xtt
44
CeBUl+MahZtn9fO1JKdF4qJmS39dXnpENg==
5-
-----END EC PRIVATE KEY-----
5+
-----END EC PRIVATE KEY-----

0 commit comments

Comments
 (0)