Skip to content
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
58 changes: 55 additions & 3 deletions src/main/java/org/jruby/ext/openssl/X509Cert.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
}

Expand Down
55 changes: 55 additions & 0 deletions src/test/ruby/x509/test_x509cert.rb
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,61 @@ 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_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')
Expand Down
Loading