Skip to content

Commit

Permalink
[compat] implement PKey::EC public_to_pem and xxx_to_der
Browse files Browse the repository at this point in the history
  • Loading branch information
kares committed Jan 9, 2025
1 parent e175e69 commit 6a6ce2e
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 26 deletions.
150 changes: 127 additions & 23 deletions src/main/java/org/jruby/ext/openssl/PKeyEC.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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.
*
Expand Down Expand Up @@ -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
Expand All @@ -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());
}
}
Expand Down
93 changes: 91 additions & 2 deletions src/test/ruby/ec/test_ec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
# #<OpenSSL::ASN1::Sequence:0x000072229cabc698
# @indefinite_length=false,
# @tag=16,
# @tag_class=:UNIVERSAL,
# @tagging=nil,
# @value=
# [#<OpenSSL::ASN1::Integer:0x000072229cabc8c8 @indefinite_length=false, @tag=2, @tag_class=:UNIVERSAL, @tagging=nil, @value=#<OpenSSL::BN 1>>,
# #<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">,
# #<OpenSSL::ASN1::ASN1Data:0x000072229cabc760
# @indefinite_length=false,
# @tag=0,
# @tag_class=:CONTEXT_SPECIFIC,
# @value=[#<OpenSSL::ASN1::ObjectId:0x000072229cabc7b0 @indefinite_length=false, @tag=6, @tag_class=:UNIVERSAL, @tagging=nil, @value="prime256v1">]>,
# #<OpenSSL::ASN1::ASN1Data:0x000072229cabc6c0
# @indefinite_length=false,
# @tag=1,
# @tag_class=:CONTEXT_SPECIFIC,
# @value=
# [#<OpenSSL::ASN1::BitString:0x000072229cabc6e8
# @indefinite_length=false,
# @tag=3,
# @tag_class=:UNIVERSAL,
# @tagging=nil,
# @unused_bits=0,
# @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">]>]>

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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
MHcCAQEEIID49FDqcf1O1eO8saTgG70UbXQw9Fqwseliit2aWhH1oAoGCCqGSM49
AwEHoUQDQgAEFglk2c+oVUIKQ64eZG9bhLNPWB7lSZ/ArK41eGy5wAzU/0G51Xtt
CeBUl+MahZtn9fO1JKdF4qJmS39dXnpENg==
-----END EC PRIVATE KEY-----
-----END EC PRIVATE KEY-----

0 comments on commit 6a6ce2e

Please sign in to comment.