From 57392c33065fe56e106e87520b049d16aeaa90fd Mon Sep 17 00:00:00 2001 From: Erik Berlin Date: Fri, 10 Apr 2026 02:01:04 -0600 Subject: [PATCH 1/2] Fix X509::Certificate#dup dropping all extensions --- .../java/org/jruby/ext/openssl/X509Cert.java | 45 ++++++ src/test/ruby/x509/test_x509cert.rb | 148 ++++++++++++++++++ 2 files changed, 193 insertions(+) diff --git a/src/main/java/org/jruby/ext/openssl/X509Cert.java b/src/main/java/org/jruby/ext/openssl/X509Cert.java index 87918f7a..a8103faa 100644 --- a/src/main/java/org/jruby/ext/openssl/X509Cert.java +++ b/src/main/java/org/jruby/ext/openssl/X509Cert.java @@ -292,9 +292,53 @@ public IRubyObject initialize_copy(IRubyObject obj) { if ( this == obj ) return this; checkFrozen(); + super.initialize_copy(obj); + + copyState(getRuntime().getCurrentContext(), (X509Cert) obj); return this; } + private void copyState(final ThreadContext context, final X509Cert that) { + final Ruby runtime = context.runtime; + + this.subject = copyName(context, that.subject); + this.issuer = copyName(context, that.issuer); + this.serial = that.serial; + this.not_before = copyTime(runtime, that.not_before); + this.not_after = copyTime(runtime, that.not_after); + this.sig_alg = that.sig_alg == null ? null : that.sig_alg.dup(); + this.version = that.version; + this.cert = copyCertificate(context, that.cert); + this.public_key = that.public_key == null ? null : (PKey) that.public_key.dup(); + + this.extensions.clear(); + for ( X509Extension ext : that.extensions ) { + this.extensions.add( (X509Extension) ext.dup() ); + } + + this.changed = that.changed; + } + + private static IRubyObject copyName(final ThreadContext context, final IRubyObject name) { + if ( name == null || name.isNil() ) return name; + return X509Name.newName(context.runtime, ((X509Name) name).getX500Name()); + } + + private static RubyTime copyTime(final Ruby runtime, final RubyTime time) { + return time == null ? null : RubyTime.newTime(runtime, time.getJavaDate().getTime()); + } + + private static X509Certificate copyCertificate(final ThreadContext context, final X509Certificate cert) { + if ( cert == null ) return null; + try { + final ByteArrayInputStream bis = new ByteArrayInputStream(cert.getEncoded()); + return (X509Certificate) SecurityHelper.getCertificateFactory("X.509").generateCertificate(bis); + } + catch (CertificateException e) { + throw newCertificateError(context.runtime, e); + } + } + @JRubyMethod public IRubyObject to_der() { try { @@ -724,6 +768,7 @@ public RubyArray extensions() { @SuppressWarnings("unchecked") @JRubyMethod(name = "extensions=") public IRubyObject set_extensions(final IRubyObject array) { + changed = true; extensions.clear(); // RubyArray is a List : extensions.addAll( (List) array ); return array; diff --git a/src/test/ruby/x509/test_x509cert.rb b/src/test/ruby/x509/test_x509cert.rb index 01ac8bbf..8ef7ecbe 100644 --- a/src/test/ruby/x509/test_x509cert.rb +++ b/src/test/ruby/x509/test_x509cert.rb @@ -796,4 +796,152 @@ def test_authority_info_access_ocsp_uris # GH-210 assert_equal ['http://cacerts.digicert.com/DigiCertSHA2SecureServerCA.crt'], cert.ca_issuer_uris assert_not_match(/# Date: Mon, 13 Apr 2026 02:29:48 -0700 Subject: [PATCH 2/2] Simplify X509Cert initialize_copy by inlining copyState Also, pass Ruby runtime directly instead of ThreadContext and rename `that` parameter to `other`. --- .../java/org/jruby/ext/openssl/X509Cert.java | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/X509Cert.java b/src/main/java/org/jruby/ext/openssl/X509Cert.java index a8103faa..e1446b82 100644 --- a/src/main/java/org/jruby/ext/openssl/X509Cert.java +++ b/src/main/java/org/jruby/ext/openssl/X509Cert.java @@ -294,48 +294,46 @@ public IRubyObject initialize_copy(IRubyObject obj) { checkFrozen(); super.initialize_copy(obj); - copyState(getRuntime().getCurrentContext(), (X509Cert) obj); - return this; - } - - private void copyState(final ThreadContext context, final X509Cert that) { - final Ruby runtime = context.runtime; - - this.subject = copyName(context, that.subject); - this.issuer = copyName(context, that.issuer); - this.serial = that.serial; - this.not_before = copyTime(runtime, that.not_before); - this.not_after = copyTime(runtime, that.not_after); - this.sig_alg = that.sig_alg == null ? null : that.sig_alg.dup(); - this.version = that.version; - this.cert = copyCertificate(context, that.cert); - this.public_key = that.public_key == null ? null : (PKey) that.public_key.dup(); + final Ruby runtime = getRuntime(); + final X509Cert other = (X509Cert) obj; + + this.subject = copyName(runtime, other.subject); + this.issuer = copyName(runtime, other.issuer); + this.serial = other.serial; + this.not_before = copyTime(runtime, other.not_before); + this.not_after = copyTime(runtime, other.not_after); + this.sig_alg = other.sig_alg == null ? null : other.sig_alg.dup(); + this.version = other.version; + this.cert = copyCertificate(runtime, other.cert); + this.public_key = other.public_key == null ? null : (PKey) other.public_key.dup(); this.extensions.clear(); - for ( X509Extension ext : that.extensions ) { + for ( X509Extension ext : other.extensions ) { this.extensions.add( (X509Extension) ext.dup() ); } - this.changed = that.changed; + this.changed = other.changed; + + return this; } - private static IRubyObject copyName(final ThreadContext context, final IRubyObject name) { + private static IRubyObject copyName(final Ruby runtime, final IRubyObject name) { if ( name == null || name.isNil() ) return name; - return X509Name.newName(context.runtime, ((X509Name) name).getX500Name()); + return X509Name.newName(runtime, ((X509Name) name).getX500Name()); } private static RubyTime copyTime(final Ruby runtime, final RubyTime time) { return time == null ? null : RubyTime.newTime(runtime, time.getJavaDate().getTime()); } - private static X509Certificate copyCertificate(final ThreadContext context, final X509Certificate cert) { + private static X509Certificate copyCertificate(final Ruby runtime, final X509Certificate cert) { if ( cert == null ) return null; try { final ByteArrayInputStream bis = new ByteArrayInputStream(cert.getEncoded()); return (X509Certificate) SecurityHelper.getCertificateFactory("X.509").generateCertificate(bis); } catch (CertificateException e) { - throw newCertificateError(context.runtime, e); + throw newCertificateError(runtime, e); } }