Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenSSL::PKey::EC (compatibility) improvements #293

Merged
merged 7 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
source 'https://rubygems.org'

# Specify your gem's dependencies in the gemspec
gemspec
gemspec if defined? JRUBY_VERSION

gem "rake", require: false

# for less surprises with newer releases
gem 'jar-dependencies', '>= 0.3.11', require: false
gem 'jar-dependencies', '>= 0.3.11', platform: :jruby, require: false

gem 'mocha', '~> 1.4', '< 2.0'

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/jruby/ext/openssl/PKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public static IRubyObject read(final ThreadContext context, IRubyObject recv, IR
}
if ( "EC".equals(alg) ) {
return new PKeyEC(runtime, _PKey(runtime).getClass("EC"),
(PrivateKey) keyPair.getPrivate(), (PublicKey) keyPair.getPublic()
keyPair.getPrivate(), keyPair.getPublic()
);
}
}
Expand Down
195 changes: 129 additions & 66 deletions src/main/java/org/jruby/ext/openssl/PKeyEC.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.AlgorithmParameters;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
Expand All @@ -20,7 +21,6 @@
import java.security.PrivateKey;
import java.security.PublicKey;

import java.security.SecureRandom;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
Expand All @@ -30,19 +30,27 @@
import java.security.spec.ECPublicKeySpec;
import java.security.spec.EllipticCurve;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Optional;
import javax.crypto.KeyAgreement;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OutputStream;
import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERSequence;

import org.bouncycastle.crypto.params.ECNamedDomainParameters;
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.jce.ECNamedCurveTable;
import org.bouncycastle.jce.ECPointUtil;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
Expand All @@ -69,7 +77,6 @@
import org.jruby.ext.openssl.impl.CipherSpec;
import static org.jruby.ext.openssl.OpenSSL.debug;
import static org.jruby.ext.openssl.OpenSSL.debugStackTrace;
import static org.jruby.ext.openssl.PKey._PKey;
import org.jruby.ext.openssl.impl.ECPrivateKeyWithName;
import static org.jruby.ext.openssl.impl.PKey.readECPrivateKey;
import org.jruby.ext.openssl.util.ByteArrayOutputStream;
Expand Down Expand Up @@ -109,10 +116,14 @@ static RubyClass _EC(final Ruby runtime) {
return _PKey(runtime).getClass("EC");
}

public static RaiseException newECError(Ruby runtime, String message) {
private static RaiseException newECError(Ruby runtime, String message) {
return Utils.newError(runtime, _PKey(runtime).getClass("ECError"), message);
}

private static RaiseException newECError(Ruby runtime, String message, Exception cause) {
return Utils.newError(runtime, _PKey(runtime).getClass("ECError"), message, cause);
}

@JRubyMethod(meta = true)
public static RubyArray builtin_curves(ThreadContext context, IRubyObject self) {
final Ruby runtime = context.runtime;
Expand Down Expand Up @@ -157,24 +168,21 @@ public static RubyArray builtin_curves(ThreadContext context, IRubyObject self)
return curves;
}

private static ASN1ObjectIdentifier getCurveOID(final String curveName) {
private static Optional<ASN1ObjectIdentifier> getCurveOID(final String curveName) {
ASN1ObjectIdentifier id;
id = org.bouncycastle.asn1.sec.SECNamedCurves.getOID(curveName);
if ( id != null ) return id;
if ( id != null ) return Optional.of(id);
id = org.bouncycastle.asn1.x9.X962NamedCurves.getOID(curveName);
if ( id != null ) return id;
if ( id != null ) return Optional.of(id);
id = org.bouncycastle.asn1.nist.NISTNamedCurves.getOID(curveName);
if ( id != null ) return id;
if ( id != null ) return Optional.of(id);
id = org.bouncycastle.asn1.teletrust.TeleTrusTNamedCurves.getOID(curveName);
if ( id != null ) return id;
throw new IllegalStateException("could not identify curve name: " + curveName);
if ( id != null ) return Optional.of(id);
return Optional.empty();
}

private static boolean isCurveName(final String curveName) {
try {
return getCurveOID(curveName) != null;
}
catch (IllegalStateException ex) { return false; }
return getCurveOID(curveName).isPresent();
}

private static String getCurveName(final ASN1ObjectIdentifier oid) {
Expand All @@ -200,8 +208,12 @@ public PKeyEC(Ruby runtime, RubyClass type) {

PKeyEC(Ruby runtime, RubyClass type, PrivateKey privKey, PublicKey pubKey) {
super(runtime, type);
this.privateKey = privKey;
this.publicKey = (ECPublicKey) pubKey;
if (privKey instanceof ECPrivateKey) {
setPrivateKey((ECPrivateKey) privKey);
} else {
this.privateKey = privKey;
}
}

private transient Group group;
Expand All @@ -213,9 +225,10 @@ public PKeyEC(Ruby runtime, RubyClass type) {

private String getCurveName() { return curveName; }

// private ECNamedCurveParameterSpec getParameterSpec() {
// return ECNamedCurveTable.getParameterSpec( getCurveName() );
// }
private ECNamedCurveParameterSpec getParameterSpec() {
assert curveName != null;
return ECNamedCurveTable.getParameterSpec(getCurveName());
}

@Override
public PublicKey getPublicKey() { return publicKey; }
Expand All @@ -242,8 +255,7 @@ public IRubyObject initialize(final ThreadContext context, final IRubyObject[] a
IRubyObject arg = args[0];

if ( arg instanceof Group ) {
this.group = (Group) arg;
this.curveName = this.group.getCurveName();
setGroup((Group) arg);
return this;
}

Expand Down Expand Up @@ -342,28 +354,34 @@ public IRubyObject initialize(final ThreadContext context, final IRubyObject[] a
throw newECError(runtime, "Neither PUB key nor PRIV key: (invalid key type " + privKey.getClass().getName() + ")");
}
this.publicKey = (ECPublicKey) pubKey;
this.privateKey = (ECPrivateKey) privKey;
unwrapPrivateKeyWithName();
setPrivateKey((ECPrivateKey) privKey);
}
else if ( key instanceof ECPrivateKey ) {
this.privateKey = (ECPrivateKey) key;
unwrapPrivateKeyWithName();
setPrivateKey((ECPrivateKey) key);
}
else if ( key instanceof ECPublicKey ) {
this.publicKey = (ECPublicKey) key; this.privateKey = null;
this.publicKey = (ECPublicKey) key;
this.privateKey = null;
}
else {
throw newECError(runtime, "Neither PUB key nor PRIV key: " + key.getClass().getName());
}

if ( publicKey != null ) {
publicKey.getParams().getCurve();
if ( curveName == null && publicKey != null ) {
final String oid = getCurveNameObjectIdFromKey(context, publicKey);
if (isCurveName(oid)) {
this.curveName = getCurveName(new ASN1ObjectIdentifier(oid));
}
}
// TODO set curveName ?!?!?!?!?!?!?!

return this;
}

void setPrivateKey(final ECPrivateKey key) {
this.privateKey = key;
unwrapPrivateKeyWithName();
}

private void unwrapPrivateKeyWithName() {
final ECPrivateKey privKey = (ECPrivateKey) this.privateKey;
if ( privKey instanceof ECPrivateKeyWithName ) {
Expand All @@ -372,6 +390,25 @@ private void unwrapPrivateKeyWithName() {
}
}

private static String getCurveNameObjectIdFromKey(final ThreadContext context, final ECPublicKey key) {
try {
AlgorithmParameters algParams = AlgorithmParameters.getInstance("EC");
algParams.init(key.getParams());
return algParams.getParameterSpec(ECGenParameterSpec.class).getName();
}
catch (NoSuchAlgorithmException|InvalidParameterSpecException ex) {
throw newECError(context.runtime, ex.getMessage());
}
catch (Exception ex) {
throw (RaiseException) newECError(context.runtime, ex.toString()).initCause(ex);
}
}

private void setGroup(final Group group) {
this.group = group;
this.curveName = this.group.getCurveName();
}

//private static ECNamedCurveParameterSpec readECParameters(final byte[] input) throws IOException {
// ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(input);
// return ECNamedCurveTable.getParameterSpec(oid.getId());
Expand All @@ -384,11 +421,10 @@ public IRubyObject check_key(final ThreadContext context) {

@JRubyMethod(name = "generate_key")
public PKeyEC generate_key(final ThreadContext context) {
// final ECDomainParameters params = getDomainParameters();
try {
ECGenParameterSpec genSpec = new ECGenParameterSpec(getCurveName());
KeyPairGenerator gen = SecurityHelper.getKeyPairGenerator("EC"); // "BC"
gen.initialize(genSpec, new SecureRandom());
gen.initialize(genSpec, OpenSSL.getSecureRandom(context));
KeyPair pair = gen.generateKeyPair();
this.publicKey = (ECPublicKey) pair.getPublic();
this.privateKey = pair.getPrivate();
Expand All @@ -399,56 +435,84 @@ public PKeyEC generate_key(final ThreadContext context) {
return this;
}

@JRubyMethod(meta = true)
public static IRubyObject generate(final ThreadContext context, final IRubyObject self, final IRubyObject group) {
PKeyEC randomKey = new PKeyEC(context.runtime, (RubyClass) self);

if (group instanceof Group) {
randomKey.setGroup((Group) group);
} else {
randomKey.curveName = group.convertToString().toString();
}

return randomKey.generate_key(context);
}

@JRubyMethod(name = "dsa_sign_asn1")
public IRubyObject dsa_sign_asn1(final ThreadContext context, final IRubyObject data) {
if (privateKey == null) {
throw newECError(context.runtime, "Private EC key needed!");
}
try {
ECNamedCurveParameterSpec params = ECNamedCurveTable.getParameterSpec(getCurveName());
ASN1ObjectIdentifier oid = getCurveOID(getCurveName());
ECNamedDomainParameters domainParams = new ECNamedDomainParameters(oid,
params.getCurve(), params.getG(), params.getN(), params.getH(), params.getSeed()
);
final ECNamedCurveParameterSpec params = getParameterSpec();

final ECDSASigner signer = new ECDSASigner();
final ECPrivateKey privKey = (ECPrivateKey) this.privateKey;
signer.init(true, new ECPrivateKeyParameters(privKey.getS(), domainParams));

final byte[] message = data.convertToString().getBytes();
BigInteger[] signature = signer.generateSignature(message); // [r, s]
signer.init(true, new ECPrivateKeyParameters(
((ECPrivateKey) this.privateKey).getS(),
new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH())
));

// final byte[] r = signature[0].toByteArray();
// final byte[] s = signature[1].toByteArray();
// // ASN.1 encode as: 0x30 len 0x02 rlen (r) 0x02 slen (s)
// final int len = 1 + (1 + r.length) + 1 + (1 + s.length);
//
// final byte[] encoded = new byte[1 + 1 + len]; int i;
// encoded[0] = 0x30;
// encoded[1] = (byte) len;
// encoded[2] = 0x20;
// encoded[3] = (byte) r.length;
// System.arraycopy(r, 0, encoded, i = 4, r.length); i += r.length;
// encoded[i++] = 0x20;
// encoded[i++] = (byte) s.length;
// System.arraycopy(s, 0, encoded, i, s.length);
BigInteger[] signature = signer.generateSignature(data.convertToString().getBytes()); // [r, s]

ByteArrayOutputStream bytes = new ByteArrayOutputStream();
ASN1OutputStream asn1 = ASN1OutputStream.create(bytes);
ASN1OutputStream asn1 = ASN1OutputStream.create(bytes, ASN1Encoding.DER);

ASN1EncodableVector v = new ASN1EncodableVector();
ASN1EncodableVector v = new ASN1EncodableVector(2);
v.add(new ASN1Integer(signature[0])); // r
v.add(new ASN1Integer(signature[1])); // s

asn1.writeObject(new DLSequence(v));
asn1.writeObject(new DERSequence(v));
asn1.close();

return StringHelper.newString(context.runtime, bytes.buffer(), bytes.size());
}
catch (IOException ex) {
throw newECError(context.runtime, ex.toString());
throw newECError(context.runtime, ex.getMessage());
}
catch (RuntimeException ex) {
throw newECError(context.runtime, ex.toString());
catch (Exception ex) {
throw newECError(context.runtime, ex.toString(), ex);
}
}

@JRubyMethod(name = "dsa_verify_asn1")
public IRubyObject dsa_verify_asn1(final ThreadContext context, final IRubyObject data, final IRubyObject sign) {
final Ruby runtime = context.runtime;
try {
final ECNamedCurveParameterSpec params = getParameterSpec();

final ECDSASigner signer = new ECDSASigner();
signer.init(false, new ECPublicKeyParameters(
EC5Util.convertPoint(publicKey.getParams(), publicKey.getW()),
new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH())
));

ASN1Primitive vec = new ASN1InputStream(sign.convertToString().getBytes()).readObject();

if (!(vec instanceof ASN1Sequence)) {
throw newECError(runtime, "invalid signature (not a sequence)");
}

ASN1Sequence seq = (ASN1Sequence) vec;
ASN1Integer r = ASN1Integer.getInstance(seq.getObjectAt(0));
ASN1Integer s = ASN1Integer.getInstance(seq.getObjectAt(1));

boolean verify = signer.verifySignature(data.convertToString().getBytes(), r.getPositiveValue(), s.getPositiveValue());
return runtime.newBoolean(verify);
}
catch (IOException|IllegalArgumentException|IllegalStateException ex) {
throw newECError(runtime, "invalid signature: " + ex.getMessage(), ex);
}
}

@JRubyMethod(name = "dh_compute_key")
public IRubyObject dh_compute_key(final ThreadContext context, final IRubyObject point) {
Expand All @@ -470,11 +534,8 @@ public IRubyObject dh_compute_key(final ThreadContext context, final IRubyObject
final byte[] secret = agreement.generateSecret();
return StringHelper.newString(context.runtime, secret);
}
catch (NoSuchAlgorithmException ex) {
throw newECError(context.runtime, ex.toString());
}
catch (InvalidKeyException ex) {
throw newECError(context.runtime, ex.toString());
throw newECError(context.runtime, "invalid key: " + ex.getMessage());
}
catch (GeneralSecurityException ex) {
throw newECError(context.runtime, ex.toString());
Expand Down Expand Up @@ -755,7 +816,9 @@ public RubyString to_pem(final ThreadContext context, final IRubyObject[] args)

try {
final StringWriter writer = new StringWriter();
PEMInputOutput.writeECParameters(writer, getCurveOID(getCurveName()), spec, passwd);
final ASN1ObjectIdentifier oid = getCurveOID(getCurveName())
.orElseThrow(() -> newECError(context.runtime, "invalid curve name: " + getCurveName()));
PEMInputOutput.writeECParameters(writer, oid, spec, passwd);
return RubyString.newString(context.runtime, writer.getBuffer());
}
catch (IOException ex) {
Expand Down
Loading
Loading