From 831aba39c39f5ec113d03832122c72eb2ff8f4ff Mon Sep 17 00:00:00 2001 From: kares Date: Sun, 21 Jun 2026 20:37:01 +0200 Subject: [PATCH 1/2] [fix] make sure Cert#tbs_bytes is up-to-date --- .../java/org/jruby/ext/openssl/X509Cert.java | 58 ++++++++++++++++++- src/test/ruby/x509/test_x509cert.rb | 14 +++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/X509Cert.java b/src/main/java/org/jruby/ext/openssl/X509Cert.java index c998fa67..6c048c76 100644 --- a/src/main/java/org/jruby/ext/openssl/X509Cert.java +++ b/src/main/java/org/jruby/ext/openssl/X509Cert.java @@ -55,13 +55,20 @@ import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.DLSequence; +import org.bouncycastle.asn1.DERTaggedObject; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.asn1.x509.TBSCertificate; +import org.bouncycastle.asn1.x509.Time; +import org.bouncycastle.asn1.x509.Validity; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.operator.ContentSigner; @@ -352,14 +359,57 @@ public IRubyObject tbs_bytes() { if ( cert == null ) { throw newCertificateError(getRuntime(), "no certificate"); } + + final SubjectPublicKeyInfo publicKeyInfo; try { - return StringHelper.newString(getRuntime(), cert.getTBSCertificate()); + publicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Primitive.fromByteArray(getPublicKey().getEncoded())); } - catch (CertificateEncodingException ex) { + catch (Exception e) { + throw newCertificateError(getRuntime(), "invalid public key data", e); + } + + try { + return StringHelper.newString(getRuntime(), buildTBSCertificate(publicKeyInfo)); + } + catch (IOException|CertificateEncodingException ex) { throw newCertificateError(getRuntime(), ex); } } + private byte[] buildTBSCertificate(final SubjectPublicKeyInfo publicKeyInfo) + throws IOException, CertificateEncodingException { + + final TBSCertificate certTBS = + new X509CertificateHolder(cert.getEncoded()).toASN1Structure().getTBSCertificate(); + + final int version = this.version == null ? 0 : RubyNumeric.fix2int(this.version); + final ASN1EncodableVector vec = new ASN1EncodableVector(10); + if ( version != 0 ) vec.add(new DERTaggedObject(true, 0, new ASN1Integer(version))); + vec.add(new ASN1Integer(serial)); + vec.add(certTBS.getSignature()); + vec.add(issuer == null ? certTBS.getIssuer() : ((X509Name) issuer).getX500Name()); + vec.add(new Validity( + new Time(not_before != null ? not_before.getJavaDate() : new Date(0)), + new Time(not_after != null ? not_after.getJavaDate() : new Date(0))) + ); + vec.add(subject == null ? certTBS.getSubject() : ((X509Name) subject).getX500Name()); + vec.add(publicKeyInfo); + + if ( certTBS.getIssuerUniqueId() != null ) { + vec.add(new DERTaggedObject(false, 1, certTBS.getIssuerUniqueId())); + } + if ( certTBS.getSubjectUniqueId() != null ) { + vec.add(new DERTaggedObject(false, 2, certTBS.getSubjectUniqueId())); + } + if ( extensions.size() > 0 ) { + final ASN1EncodableVector extVec = new ASN1EncodableVector(extensions.size()); + for ( X509Extension ext : extensions ) extVec.add(ext.toASN1Sequence()); + vec.add(new DERTaggedObject(true, 3, new DLSequence(extVec))); + } + + return new DLSequence(vec).getEncoded(ASN1Encoding.DER); + } + @Override @JRubyMethod(name = "==") public IRubyObject op_equal(ThreadContext context, IRubyObject obj) { @@ -613,7 +663,9 @@ public IRubyObject set_public_key(IRubyObject public_key) { } private PublicKey getPublicKey() { - if ( public_key == null ) initializePublicKey(); + if (public_key == null) { + return cert.getPublicKey(); + } return public_key.getPublicKey(); } diff --git a/src/test/ruby/x509/test_x509cert.rb b/src/test/ruby/x509/test_x509cert.rb index 8ef7ecbe..c3796aa9 100644 --- a/src/test/ruby/x509/test_x509cert.rb +++ b/src/test/ruby/x509/test_x509cert.rb @@ -494,6 +494,20 @@ def test_tbs_precert_bytes assert_equal 7, seq.value.size end + def test_tbs_bytes_reflects_current_extensions + ca = OpenSSL::X509::Name.parse('/DC=org/DC=ruby-lang/CN=CA') + cert = issue_cert(ca, OpenSSL::PKey::RSA.new(TEST_KEY_RSA1024), 1, + [['basicConstraints', 'CA:TRUE', true]], nil, nil) + + assert_equal 8, OpenSSL::ASN1.decode(cert.tbs_bytes).value.size + + cert.extensions = [] + seq = OpenSSL::ASN1.decode(cert.tbs_bytes) + + assert_equal 7, seq.value.size + assert_equal false, seq.value.any? { |val| val.tag_class == :CONTEXT_SPECIFIC && val.tag == 3 } + end + def test_eq now = Time.now ca = OpenSSL::X509::Name.parse('/DC=org/DC=ruby-lang/CN=CA') From 5c45941cf7b260cb7392adf88b926fdc14b0b190 Mon Sep 17 00:00:00 2001 From: kares Date: Mon, 22 Jun 2026 09:07:41 +0200 Subject: [PATCH 2/2] [test] extra tbs_bytes asserts (from #363) --- src/test/ruby/x509/test_x509cert.rb | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/test/ruby/x509/test_x509cert.rb b/src/test/ruby/x509/test_x509cert.rb index c3796aa9..e6e1b8a8 100644 --- a/src/test/ruby/x509/test_x509cert.rb +++ b/src/test/ruby/x509/test_x509cert.rb @@ -508,6 +508,47 @@ def test_tbs_bytes_reflects_current_extensions assert_equal false, seq.value.any? { |val| val.tag_class == :CONTEXT_SPECIFIC && val.tag == 3 } end + def test_tbs_bytes_reflects_extensions_removal + cert = File.expand_path('digicert.pem', File.dirname(__FILE__)) + cert = OpenSSL::X509::Certificate.new(File.read(cert)) + + assert cert.extensions.size > 1 + original = cert.tbs_bytes + + dup = cert.dup + removed_oid = cert.extensions.first.oid + dup.extensions = dup.extensions.reject { |ext| ext.oid == removed_oid } + + mutated = dup.tbs_bytes + refute_equal original, mutated, 'tbs_bytes must reflect extensions= mutation' + assert mutated.bytesize < original.bytesize, 'removing an extension must shrink tbs_bytes' + + expected = cert.extensions.map(&:oid).reject { |oid| oid == removed_oid } + .map { |oid| OpenSSL::ASN1::ObjectId.new(oid).oid } # OID name -> identifier + assert_equal expected, tbs_extension_oids(mutated) + end + + # Extract the extension OIDs (in order) from a DER-encoded TBSCertificate + def tbs_extension_oids(tbs_der) + tbs = OpenSSL::ASN1.decode(tbs_der) + assert_equal OpenSSL::ASN1::Sequence, tbs.class + + holder = tbs.value.find do |e| + e.respond_to?(:tag) && e.tag == 3 && e.tag_class == :CONTEXT_SPECIFIC + end + + holder ? holder.value[0].value.map { |ext| ext.value[0].oid } : [] + end + + def test_tbs_bytes_unchanged_after_assigning_same_extensions + cert = File.expand_path('digicert.pem', File.dirname(__FILE__)) + cert = OpenSSL::X509::Certificate.new(File.read(cert)) + + dup = cert.dup + dup.extensions = dup.extensions # reassign the same set + assert_equal cert.tbs_bytes, dup.tbs_bytes + end + def test_eq now = Time.now ca = OpenSSL::X509::Name.parse('/DC=org/DC=ruby-lang/CN=CA')