From 6a6ce2e810321f8010bb3123ac4b354b4faf9cf9 Mon Sep 17 00:00:00 2001 From: kares Date: Thu, 9 Jan 2025 19:02:03 +0100 Subject: [PATCH] [compat] implement PKey::EC public_to_pem and xxx_to_der --- .../java/org/jruby/ext/openssl/PKeyEC.java | 150 +++++++++++++++--- src/test/ruby/ec/test_ec.rb | 93 ++++++++++- .../ruby/fixtures/pkey/{p256.pem => p256} | 2 +- 3 files changed, 219 insertions(+), 26 deletions(-) rename src/test/ruby/fixtures/pkey/{p256.pem => p256} (87%) diff --git a/src/main/java/org/jruby/ext/openssl/PKeyEC.java b/src/main/java/org/jruby/ext/openssl/PKeyEC.java index e64aeef8..8335a77a 100644 --- a/src/main/java/org/jruby/ext/openssl/PKeyEC.java +++ b/src/main/java/org/jruby/ext/openssl/PKeyEC.java @@ -37,6 +37,7 @@ import java.util.Locale; import java.util.Optional; import javax.crypto.KeyAgreement; + import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.ASN1Encoding; import org.bouncycastle.asn1.ASN1InputStream; @@ -45,19 +46,27 @@ import org.bouncycastle.asn1.ASN1OutputStream; import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERNull; import org.bouncycastle.asn1.DERSequence; - +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.asn1.x9.X962Parameters; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.asn1.x9.X9ECPoint; +import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; import org.bouncycastle.crypto.params.ECDomainParameters; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.signers.ECDSASigner; import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util; import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; +import org.bouncycastle.jcajce.provider.config.ProviderConfiguration; import org.bouncycastle.jce.ECNamedCurveTable; import org.bouncycastle.jce.ECPointUtil; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; import org.bouncycastle.jce.spec.ECNamedCurveSpec; - import org.bouncycastle.math.ec.ECAlgorithms; import org.bouncycastle.math.ec.ECCurve; import org.jruby.Ruby; @@ -82,13 +91,15 @@ import org.jruby.runtime.component.VariableEntry; import org.jruby.ext.openssl.impl.CipherSpec; -import static org.jruby.ext.openssl.OpenSSL.debug; -import static org.jruby.ext.openssl.OpenSSL.debugStackTrace; import org.jruby.ext.openssl.impl.ECPrivateKeyWithName; -import static org.jruby.ext.openssl.impl.PKey.readECPrivateKey; + import org.jruby.ext.openssl.util.ByteArrayOutputStream; import org.jruby.ext.openssl.x509store.PEMInputOutput; +import static org.jruby.ext.openssl.OpenSSL.debug; +import static org.jruby.ext.openssl.OpenSSL.debugStackTrace; +import static org.jruby.ext.openssl.impl.PKey.readECPrivateKey; + /** * OpenSSL::PKey::EC implementation. * @@ -626,27 +637,111 @@ public RubyBoolean private_p() { return privateKey != null ? getRuntime().getTrue() : getRuntime().getFalse(); } + @JRubyMethod + public RubyString public_to_der(ThreadContext context) { + return public_to_der(context.runtime); + } + + private RubyString public_to_der(final Ruby runtime) { + final byte[] bytes; + try { + bytes = publicKey.getEncoded(); + } catch (Exception e) { + throw newECError(runtime, e.getMessage(), e); + } + return StringHelper.newString(runtime, bytes); + } + @Override @JRubyMethod(name = "to_der") public RubyString to_der() { - final byte[] bytes; + final Ruby runtime = getRuntime(); + if (publicKey != null && privateKey == null) { + return public_to_der(runtime); + } + if (privateKey == null) { + throw new IllegalStateException("private key as well as public key are null"); + } + try { - bytes = toDER(); + byte[] encoded = toPrivateKeyStructure((ECPrivateKey) privateKey, publicKey, false).getEncoded(ASN1Encoding.DER); + return StringHelper.newString(runtime, encoded); + } catch (Exception e) { + throw newECError(runtime, e.getMessage(), e); + } + } + + @JRubyMethod + public RubyString private_to_der(ThreadContext context) { + return private_to_der(context.runtime); + } + + private RubyString private_to_der(final Ruby runtime) { + final byte[] encoded; + if (privateKey instanceof ECPrivateKey) { + try { + encoded = toPrivateKeyInfo((ECPrivateKey) privateKey, publicKey).getEncoded(ASN1Encoding.DER); + } catch (IOException e) { + throw newECError(runtime, e.getMessage(), e); + } + } else { + try { + encoded = privateKey.getEncoded(); + } catch (Exception e) { + throw newECError(runtime, e.getMessage(), e); + } } - catch (IOException e) { - throw newECError(getRuntime(), e.getMessage()); + return StringHelper.newString(runtime, encoded); + } + + private static org.bouncycastle.asn1.sec.ECPrivateKey toPrivateKeyStructure(final ECPrivateKey privateKey, + final ECPublicKey publicKey, + final boolean compressed) throws IOException { + final ProviderConfiguration configuration = BouncyCastleProvider.CONFIGURATION; + final ECParameterSpec ecSpec = privateKey.getParams(); + final X962Parameters params = getDomainParametersFromName(ecSpec, compressed); + + int orderBitLength = ECUtil.getOrderBitLength(configuration, ecSpec == null ? null : ecSpec.getOrder(), privateKey.getS()); + + if (publicKey == null) { + return new org.bouncycastle.asn1.sec.ECPrivateKey(orderBitLength, privateKey.getS(), params); } - return StringHelper.newString(getRuntime(), bytes); + + SubjectPublicKeyInfo info = SubjectPublicKeyInfo.getInstance(ASN1Primitive.fromByteArray(publicKey.getEncoded())); + return new org.bouncycastle.asn1.sec.ECPrivateKey(orderBitLength, privateKey.getS(), info.getPublicKeyData(), params); + } + + private static PrivateKeyInfo toPrivateKeyInfo(final ECPrivateKey privateKey, + final ECPublicKey publicKey) throws IOException { + final ECParameterSpec ecSpec = privateKey.getParams(); + final X962Parameters params = getDomainParametersFromName(ecSpec, false); + + org.bouncycastle.asn1.sec.ECPrivateKey keyStructure = toPrivateKeyStructure(privateKey, publicKey, false); + return new PrivateKeyInfo(new AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, params), keyStructure); } - private byte[] toDER() throws IOException { - if ( publicKey != null && privateKey == null ) { - return publicKey.getEncoded(); + private static X962Parameters getDomainParametersFromName(ECParameterSpec ecSpec, boolean compressed) { + if (ecSpec instanceof ECNamedCurveSpec) { + ASN1ObjectIdentifier curveOid = ECUtil.getNamedCurveOid(((ECNamedCurveSpec)ecSpec).getName()); + if (curveOid == null) + { + curveOid = new ASN1ObjectIdentifier(((ECNamedCurveSpec)ecSpec).getName()); + } + return new X962Parameters(curveOid); } - if ( privateKey == null ) { - throw new IllegalStateException("private key as well as public key are null"); + if (ecSpec == null) { + return new X962Parameters(DERNull.INSTANCE); } - return privateKey.getEncoded(); + ECCurve curve = EC5Util.convertCurve(ecSpec.getCurve()); + + X9ECParameters ecParameters = new X9ECParameters( + curve, + new X9ECPoint(EC5Util.convertPoint(curve, ecSpec.getGenerator()), compressed), + ecSpec.getOrder(), + BigInteger.valueOf(ecSpec.getCofactor()), + ecSpec.getCurve().getSeed()); + + return new X962Parameters(ecParameters); } @Override @@ -660,17 +755,26 @@ public RubyString to_pem(ThreadContext context, final IRubyObject[] args) { if ( args.length > 1 ) passwd = password(context, args[1], null); } + if (privateKey == null) { + return public_to_pem(context); + } + try { final StringWriter writer = new StringWriter(); - if ( privateKey != null ) { - PEMInputOutput.writeECPrivateKey(writer, (ECPrivateKey) privateKey, spec, passwd); - } - else { - PEMInputOutput.writeECPublicKey(writer, publicKey); - } + PEMInputOutput.writeECPrivateKey(writer, (ECPrivateKey) privateKey, spec, passwd); return RubyString.newString(context.runtime, writer.getBuffer()); + } catch (IOException ex) { + throw newECError(context.runtime, ex.getMessage()); } - catch (IOException ex) { + } + + @JRubyMethod + public RubyString public_to_pem(ThreadContext context) { + try { + final StringWriter writer = new StringWriter(); + PEMInputOutput.writeECPublicKey(writer, publicKey); + return RubyString.newString(context.runtime, writer.getBuffer()); + } catch (IOException ex) { throw newECError(context.runtime, ex.getMessage()); } } diff --git a/src/test/ruby/ec/test_ec.rb b/src/test/ruby/ec/test_ec.rb index cd10ee75..a5e24061 100644 --- a/src/test/ruby/ec/test_ec.rb +++ b/src/test/ruby/ec/test_ec.rb @@ -3,6 +3,80 @@ class TestEC < TestCase + def test_PUBKEY + p256 = Fixtures.pkey("p256") + p256pub = OpenSSL::PKey::EC.new(p256.public_to_der) + + 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" + assert_equal public_to_der, p256.public_to_der + + # MRI: + 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" + assert_equal uncompressed_public_key, p256.public_key.to_octet_string(:uncompressed) + + asn1 = OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::ObjectId("id-ecPublicKey"), + OpenSSL::ASN1::ObjectId("prime256v1") + ]), + OpenSSL::ASN1::BitString( + p256.public_key.to_octet_string(:uncompressed) + ) + ]) + assert_equal public_to_der, asn1.to_der + + 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" + #pp OpenSSL::ASN1.decode(to_der) + # #>, + # #, + # #]>, + # #]>]> + + assert_equal to_der, p256.to_der + + key = OpenSSL::PKey::EC.new(asn1.to_der) + assert_not_predicate key, :private? + assert_same_ec p256pub, key + + pem = <<~EOF + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFglk2c+oVUIKQ64eZG9bhLNPWB7l + SZ/ArK41eGy5wAzU/0G51XttCeBUl+MahZtn9fO1JKdF4qJmS39dXnpENg== + -----END PUBLIC KEY----- + EOF + key = OpenSSL::PKey::EC.new(pem) + assert_same_ec p256pub, key + + assert_equal asn1.to_der, key.to_der + assert_equal pem, key.export + + assert_equal asn1.to_der, p256.public_to_der + assert_equal asn1.to_der, key.public_to_der + assert_equal pem, p256.public_to_pem + assert_equal pem, key.public_to_pem + end + def test_oid key = OpenSSL::PKey::EC.new assert_equal 'id-ecPublicKey', key.oid @@ -285,7 +359,7 @@ def test_check_key end def test_sign_verify - p256 = Fixtures.pkey("p256.pem") + p256 = Fixtures.pkey("p256") data = "Sign me!" signature = p256.sign("SHA1", data) assert_equal true, p256.verify("SHA1", signature, data) @@ -356,7 +430,7 @@ def test_dsa_sign_verify_all end def test_sign_verify_raw - key = Fixtures.pkey("p256.pem") + key = Fixtures.pkey("p256") data1 = "foo" data2 = "bar" @@ -501,4 +575,19 @@ def decode_octets(base64_encoded_coordinate); require 'base64' # end # end + private + + def B(ary) + [Array(ary).join].pack("H*") + end + + def assert_same_ec(expected, key) + check_component(expected, key, [:group, :public_key, :private_key]) + end + + def check_component(base, test, keys) + keys.each { |comp| + assert_equal base.send(comp), test.send(comp) + } + end end diff --git a/src/test/ruby/fixtures/pkey/p256.pem b/src/test/ruby/fixtures/pkey/p256 similarity index 87% rename from src/test/ruby/fixtures/pkey/p256.pem rename to src/test/ruby/fixtures/pkey/p256 index 4cdb0a30..97c97d9f 100644 --- a/src/test/ruby/fixtures/pkey/p256.pem +++ b/src/test/ruby/fixtures/pkey/p256 @@ -2,4 +2,4 @@ MHcCAQEEIID49FDqcf1O1eO8saTgG70UbXQw9Fqwseliit2aWhH1oAoGCCqGSM49 AwEHoUQDQgAEFglk2c+oVUIKQ64eZG9bhLNPWB7lSZ/ArK41eGy5wAzU/0G51Xtt CeBUl+MahZtn9fO1JKdF4qJmS39dXnpENg== ------END EC PRIVATE KEY----- \ No newline at end of file +-----END EC PRIVATE KEY-----