Skip to content

Commit 57392c3

Browse files
committed
Fix X509::Certificate#dup dropping all extensions
1 parent e7eda5b commit 57392c3

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,53 @@ public IRubyObject initialize_copy(IRubyObject obj) {
292292
if ( this == obj ) return this;
293293

294294
checkFrozen();
295+
super.initialize_copy(obj);
296+
297+
copyState(getRuntime().getCurrentContext(), (X509Cert) obj);
295298
return this;
296299
}
297300

301+
private void copyState(final ThreadContext context, final X509Cert that) {
302+
final Ruby runtime = context.runtime;
303+
304+
this.subject = copyName(context, that.subject);
305+
this.issuer = copyName(context, that.issuer);
306+
this.serial = that.serial;
307+
this.not_before = copyTime(runtime, that.not_before);
308+
this.not_after = copyTime(runtime, that.not_after);
309+
this.sig_alg = that.sig_alg == null ? null : that.sig_alg.dup();
310+
this.version = that.version;
311+
this.cert = copyCertificate(context, that.cert);
312+
this.public_key = that.public_key == null ? null : (PKey) that.public_key.dup();
313+
314+
this.extensions.clear();
315+
for ( X509Extension ext : that.extensions ) {
316+
this.extensions.add( (X509Extension) ext.dup() );
317+
}
318+
319+
this.changed = that.changed;
320+
}
321+
322+
private static IRubyObject copyName(final ThreadContext context, final IRubyObject name) {
323+
if ( name == null || name.isNil() ) return name;
324+
return X509Name.newName(context.runtime, ((X509Name) name).getX500Name());
325+
}
326+
327+
private static RubyTime copyTime(final Ruby runtime, final RubyTime time) {
328+
return time == null ? null : RubyTime.newTime(runtime, time.getJavaDate().getTime());
329+
}
330+
331+
private static X509Certificate copyCertificate(final ThreadContext context, final X509Certificate cert) {
332+
if ( cert == null ) return null;
333+
try {
334+
final ByteArrayInputStream bis = new ByteArrayInputStream(cert.getEncoded());
335+
return (X509Certificate) SecurityHelper.getCertificateFactory("X.509").generateCertificate(bis);
336+
}
337+
catch (CertificateException e) {
338+
throw newCertificateError(context.runtime, e);
339+
}
340+
}
341+
298342
@JRubyMethod
299343
public IRubyObject to_der() {
300344
try {
@@ -724,6 +768,7 @@ public RubyArray extensions() {
724768
@SuppressWarnings("unchecked")
725769
@JRubyMethod(name = "extensions=")
726770
public IRubyObject set_extensions(final IRubyObject array) {
771+
changed = true;
727772
extensions.clear(); // RubyArray is a List :
728773
extensions.addAll( (List<X509Extension>) array );
729774
return array;

src/test/ruby/x509/test_x509cert.rb

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,4 +796,152 @@ def test_authority_info_access_ocsp_uris # GH-210
796796
assert_equal ['http://cacerts.digicert.com/DigiCertSHA2SecureServerCA.crt'], cert.ca_issuer_uris
797797
assert_not_match(/#<OpenSSL::ASN1::/, aia_ext.value)
798798
end
799+
800+
def test_dup_preserves_extensions
801+
rsa2048 = OpenSSL::PKey::RSA.new TEST_KEY_RSA2048
802+
ca = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=CA")
803+
804+
ca_exts = [
805+
[ "basicConstraints", "CA:TRUE", true ],
806+
[ "keyUsage", "keyCertSign, cRLSign", true ],
807+
[ "subjectKeyIdentifier", "hash", false ],
808+
]
809+
810+
now = Time.now
811+
cert = issue_cert(ca, rsa2048, 1, ca_exts, nil, nil,
812+
not_before: now, not_after: now + 3600)
813+
814+
duped = cert.dup
815+
816+
assert_equal cert.extensions.size, duped.extensions.size,
817+
"dup should preserve extensions"
818+
assert_equal cert.extensions.map(&:oid).sort, duped.extensions.map(&:oid).sort
819+
end
820+
821+
def test_dup_preserves_subject_and_issuer
822+
rsa2048 = OpenSSL::PKey::RSA.new TEST_KEY_RSA2048
823+
ca = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=CA")
824+
825+
now = Time.now
826+
cert = issue_cert(ca, rsa2048, 42, [], nil, nil,
827+
not_before: now, not_after: now + 3600)
828+
829+
duped = cert.dup
830+
831+
assert_equal cert.subject.to_s, duped.subject.to_s
832+
assert_equal cert.issuer.to_s, duped.issuer.to_s
833+
assert_equal cert.serial, duped.serial
834+
assert_equal cert.version, duped.version
835+
assert_equal cert.not_before.to_i, duped.not_before.to_i
836+
assert_equal cert.not_after.to_i, duped.not_after.to_i
837+
end
838+
839+
def test_dup_produces_independent_copy
840+
rsa2048 = OpenSSL::PKey::RSA.new TEST_KEY_RSA2048
841+
ca = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=CA")
842+
843+
ca_exts = [
844+
[ "basicConstraints", "CA:TRUE", true ],
845+
[ "subjectKeyIdentifier", "hash", false ],
846+
]
847+
848+
now = Time.now
849+
cert = issue_cert(ca, rsa2048, 1, ca_exts, nil, nil,
850+
not_before: now, not_after: now + 3600)
851+
852+
duped = cert.dup
853+
854+
# Modifying the dup's extensions should not affect the original
855+
duped.extensions = []
856+
assert_equal 2, cert.extensions.size,
857+
"modifying duped cert's extensions should not affect original"
858+
end
859+
860+
def test_dup_to_der_matches_original
861+
rsa2048 = OpenSSL::PKey::RSA.new TEST_KEY_RSA2048
862+
ca = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=CA")
863+
864+
ca_exts = [
865+
[ "basicConstraints", "CA:TRUE", true ],
866+
[ "keyUsage", "keyCertSign, cRLSign", true ],
867+
[ "subjectKeyIdentifier", "hash", false ],
868+
]
869+
870+
now = Time.now
871+
cert = issue_cert(ca, rsa2048, 1, ca_exts, nil, nil,
872+
not_before: now, not_after: now + 3600)
873+
874+
duped = cert.dup
875+
876+
assert_equal cert.to_der, duped.to_der
877+
end
878+
879+
def test_dup_preserves_live_subject_after_signed_cert_name_is_mutated
880+
rsa2048 = OpenSSL::PKey::RSA.new TEST_KEY_RSA2048
881+
ca = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=CA")
882+
883+
now = Time.now
884+
cert = issue_cert(ca, rsa2048, 1, [], nil, nil,
885+
not_before: now, not_after: now + 3600)
886+
887+
cert.subject.add_entry("O", "mutated")
888+
duped = cert.dup
889+
890+
assert_equal cert.subject.to_a, duped.subject.to_a
891+
end
892+
893+
def test_dup_preserves_live_extensions_after_signed_cert_is_modified
894+
rsa2048 = OpenSSL::PKey::RSA.new TEST_KEY_RSA2048
895+
ca = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=CA")
896+
897+
ca_exts = [
898+
[ "basicConstraints", "CA:TRUE", true ],
899+
[ "subjectKeyIdentifier", "hash", false ],
900+
]
901+
902+
now = Time.now
903+
cert = issue_cert(ca, rsa2048, 1, ca_exts, nil, nil,
904+
not_before: now, not_after: now + 3600)
905+
906+
cert.extensions = []
907+
duped = cert.dup
908+
909+
assert_equal [], duped.extensions.map(&:oid)
910+
end
911+
912+
def test_dup_unsigned_cert_preserves_fields
913+
rsa2048 = OpenSSL::PKey::RSA.new TEST_KEY_RSA2048
914+
915+
cert = OpenSSL::X509::Certificate.new
916+
cert.version = 2
917+
cert.serial = 99
918+
cert.subject = OpenSSL::X509::Name.parse("/CN=unsigned")
919+
cert.issuer = OpenSSL::X509::Name.parse("/CN=issuer")
920+
cert.not_before = Time.now
921+
cert.not_after = Time.now + 3600
922+
cert.public_key = rsa2048.public_key
923+
924+
duped = cert.dup
925+
926+
assert_equal cert.subject.to_s, duped.subject.to_s
927+
assert_equal cert.issuer.to_s, duped.issuer.to_s
928+
assert_equal cert.serial, duped.serial
929+
assert_equal cert.version, duped.version
930+
assert_equal cert.public_key.to_der, duped.public_key.to_der
931+
end
932+
933+
def test_dup_unsigned_cert_deep_copies_names
934+
cert = OpenSSL::X509::Certificate.new
935+
cert.subject = OpenSSL::X509::Name.parse("/CN=unsigned")
936+
cert.issuer = OpenSSL::X509::Name.parse("/CN=issuer")
937+
938+
duped = cert.dup
939+
duped.subject.add_entry("O", "mutated")
940+
duped.issuer.add_entry("O", "mutated")
941+
942+
assert_equal "/CN=unsigned", cert.subject.to_s
943+
assert_equal "/CN=issuer", cert.issuer.to_s
944+
assert_equal "/CN=unsigned/O=mutated", duped.subject.to_s
945+
assert_equal "/CN=issuer/O=mutated", duped.issuer.to_s
946+
end
799947
end

0 commit comments

Comments
 (0)