Skip to content

Commit 27de4c6

Browse files
committed
[fix] parse certificate crlDistributionPoints (#205)
1 parent e239384 commit 27de4c6

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@
5353
import org.bouncycastle.asn1.x500.RDN;
5454
import org.bouncycastle.asn1.x500.X500Name;
5555
import org.bouncycastle.asn1.x500.style.BCStyle;
56+
import org.bouncycastle.asn1.x509.CRLDistPoint;
57+
import org.bouncycastle.asn1.x509.DistributionPoint;
58+
import org.bouncycastle.asn1.x509.DistributionPointName;
5659
import org.bouncycastle.asn1.x509.Extension;
5760
import org.bouncycastle.asn1.x509.GeneralName;
5861
import org.bouncycastle.asn1.x509.GeneralNames;
@@ -564,6 +567,52 @@ else if ( entry.respondsTo("value") ) {
564567
return runtime.newString( val );
565568
}
566569

570+
if ( oid.equals("2.5.29.31") ) { // crlDistributionPoints
571+
try {
572+
ASN1Encodable value = getRealValue();
573+
final ByteList val = new ByteList(64);
574+
575+
if ( value instanceof ASN1OctetString ) {
576+
value = ASN1.readObject( ((ASN1OctetString) value).getOctets() );
577+
}
578+
579+
final CRLDistPoint distPoint = CRLDistPoint.getInstance(value);
580+
final DistributionPoint[] points = distPoint.getDistributionPoints();
581+
582+
for ( int i = 0; i < points.length; i++ ) {
583+
if ( i > 0 ) val.append('\n').append('\n');
584+
585+
final DistributionPoint point = points[i];
586+
final DistributionPointName dpName = point.getDistributionPoint();
587+
588+
if ( dpName != null && dpName.getType() == DistributionPointName.FULL_NAME ) {
589+
val.append(ByteList.plain("Full Name:"));
590+
val.append('\n');
591+
592+
final GeneralNames generalNames = GeneralNames.getInstance(dpName.getName());
593+
final GeneralName[] names = generalNames.getNames();
594+
for ( int j = 0; j < names.length; j++ ) {
595+
val.append(ByteList.plain(" "));
596+
formatGeneralName(names[j], val, false);
597+
if ( j < names.length - 1 ) val.append('\n');
598+
}
599+
}
600+
else if ( dpName != null && dpName.getType() == DistributionPointName.NAME_RELATIVE_TO_CRL_ISSUER ) {
601+
val.append(ByteList.plain("Relative Name:"));
602+
val.append('\n');
603+
val.append(ByteList.plain(" "));
604+
val.append(ByteList.plain(dpName.getName().toString()));
605+
}
606+
}
607+
608+
return runtime.newString( val );
609+
}
610+
catch (IllegalArgumentException e) {
611+
debugStackTrace(runtime, e);
612+
return rawValueAsString(context);
613+
}
614+
}
615+
567616
return rawValueAsString(context);
568617
}
569618
catch (IOException e) {

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

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535

3636
import org.bouncycastle.asn1.*;
3737
import org.bouncycastle.asn1.x500.X500Name;
38+
import org.bouncycastle.asn1.x500.X500NameBuilder;
39+
import org.bouncycastle.asn1.x500.style.BCStyle;
40+
import org.bouncycastle.asn1.x509.AccessDescription;
41+
import org.bouncycastle.asn1.x509.DistributionPoint;
42+
import org.bouncycastle.asn1.x509.DistributionPointName;
3843
import org.bouncycastle.asn1.x509.GeneralName;
3944
import org.bouncycastle.asn1.x509.GeneralNames;
4045

@@ -50,6 +55,7 @@
5055
import org.jruby.exceptions.RaiseException;
5156
import org.jruby.runtime.Arity;
5257
import org.jruby.runtime.Block;
58+
import org.jruby.runtime.Helpers;
5359
import org.jruby.runtime.ThreadContext;
5460
import org.jruby.runtime.Visibility;
5561
import org.jruby.runtime.builtin.IRubyObject;
@@ -199,6 +205,12 @@ else if (id.equals("2.16.840.1.113730.1.1")) { // nsCertType
199205
else if (id.equals("2.5.29.37")) { // extendedKeyUsage
200206
value = parseExtendedKeyUsage(valuex);
201207
}
208+
else if (id.equals("2.5.29.31")) { // crlDistributionPoints
209+
value = parseCRLDistributionPoints(context, valuex);
210+
}
211+
else if (id.equals("1.3.6.1.5.5.7.1.1")) { // authorityInfoAccess
212+
value = parseAuthorityInfoAccess(valuex);
213+
}
202214
else {
203215
value = new DEROctetString(new DEROctetString(ByteList.plain(valuex)).getEncoded(ASN1Encoding.DER));
204216
}
@@ -610,4 +622,154 @@ private static DLSequence parseExtendedKeyUsage(final String valuex) {
610622
return new DLSequence(vector);
611623
}
612624

625+
private ASN1Sequence parseCRLDistributionPoints(final ThreadContext context, final String valuex)
626+
throws IOException {
627+
final ASN1EncodableVector points = new ASN1EncodableVector();
628+
629+
final String trimmed = valuex.trim();
630+
if ( trimmed.startsWith("@") ) {
631+
addDistributionPointsFromConfigSection(context, trimmed.substring(1).trim(), true, points);
632+
}
633+
else if ( trimmed.indexOf(':') == -1 ) {
634+
final RubyHash section = getConfigSection(context, trimmed);
635+
if ( section != null ) {
636+
addDistributionPointsFromConfigSection(context, trimmed, false, points);
637+
}
638+
else {
639+
points.add(new DistributionPoint(new DistributionPointName(parseGeneralNames(context, trimmed)), null, null));
640+
}
641+
}
642+
else {
643+
points.add(new DistributionPoint(new DistributionPointName(parseGeneralNames(context, trimmed)), null, null));
644+
}
645+
646+
return new DERSequence(points);
647+
}
648+
649+
private void addDistributionPointsFromConfigSection(final ThreadContext context,
650+
final String sectionName, final boolean oneNamePerEntry, final ASN1EncodableVector points) throws IOException {
651+
final RubyHash section = getConfigSection(context, sectionName);
652+
if ( section == null ) throw new IOException("Malformed CRLDistributionPoints section: " + sectionName + " in @config");
653+
654+
if ( ! oneNamePerEntry ) {
655+
final IRubyObject fullName = section.fastARef(StringHelper.newString(context.runtime, "fullname"));
656+
if ( fullName != null && !fullName.isNil() ) {
657+
addDistributionPointToVector(context, points, fullName.toString());
658+
return;
659+
}
660+
}
661+
662+
section.visitAll(new RubyHash.Visitor() {
663+
public void visit(final IRubyObject key, final IRubyObject value) {
664+
final String keyName = stripNumericSuffix(key.toString());
665+
try {
666+
addDistributionPointToVector(context, points, keyName + ':' + value);
667+
} catch (IOException e) {
668+
Helpers.throwException(e);
669+
}
670+
}
671+
});
672+
}
673+
674+
private void addDistributionPointToVector(final ThreadContext context, ASN1EncodableVector points, final String part)
675+
throws IOException {
676+
final GeneralNames partNames = new GeneralNames(parseGeneralName(context, part));
677+
points.add(new DistributionPoint(new DistributionPointName(partNames), null, null));
678+
}
679+
680+
private ASN1Sequence parseAuthorityInfoAccess(final String valuex) throws IOException {
681+
final ASN1EncodableVector vector = new ASN1EncodableVector();
682+
final String[] values = splitGeneralNameParts(valuex);
683+
for ( int i = 0; i < values.length; i++ ) {
684+
final String value = values[i];
685+
final int index = value.indexOf(';');
686+
if ( index <= 0 || index >= value.length() - 1 ) {
687+
throw new IOException("Malformed AuthorityInfoAccess: " + valuex);
688+
}
689+
690+
final String accessMethod = value.substring(0, index).trim();
691+
final ASN1ObjectIdentifier method = ASN1Registry.sym2oid(accessMethod);
692+
if ( method == null ) throw new IOException("Unknown AuthorityInfoAccess method: " + accessMethod);
693+
694+
final String accessLocation = value.substring(index + 1).trim();
695+
vector.add(new AccessDescription(method, parseGeneralName(accessLocation)));
696+
}
697+
return new DERSequence(vector);
698+
}
699+
700+
private GeneralNames parseGeneralNames(final ThreadContext context, final String valuex) throws IOException {
701+
final String[] vals = splitGeneralNameParts(valuex);
702+
final GeneralName[] names = new GeneralName[vals.length];
703+
for ( int i = 0; i < vals.length; i++ ) {
704+
names[i] = parseGeneralName(context, vals[i]);
705+
}
706+
return new GeneralNames(names);
707+
}
708+
709+
private GeneralName parseGeneralName(final ThreadContext context, final String valuex) throws IOException {
710+
if ( valuex.startsWith("dir") ) {
711+
final String dir = valuex.substring(dirName_.length()).trim();
712+
final RubyHash section = getConfigSection(context, dir);
713+
if ( section != null ) {
714+
return new GeneralName(GeneralName.directoryName, buildX500NameFromConfigSection(context, section));
715+
}
716+
}
717+
return parseGeneralName(valuex);
718+
}
719+
720+
private X500Name buildX500NameFromConfigSection(final ThreadContext context, final RubyHash section) {
721+
final X500NameBuilder builder = new X500NameBuilder(BCStyle.INSTANCE);
722+
section.visitAll(new RubyHash.Visitor() {
723+
public void visit(final IRubyObject key, final IRubyObject value) {
724+
final String keyName = stripNumericSuffix(key.toString());
725+
try {
726+
builder.addRDN(ASN1.getObjectID(context.runtime, keyName), value.toString());
727+
}
728+
catch (IllegalArgumentException e) {
729+
throw newExtensionError(context.runtime, "invalid X509 name field: " + keyName);
730+
}
731+
}
732+
});
733+
return builder.build();
734+
}
735+
736+
private IRubyObject getConfigValue(final ThreadContext context, final String key) {
737+
final IRubyObject config = config();
738+
if (config == null || config.isNil()) { // TODO: support fallback to DEFAULT_CONFIG
739+
return null;
740+
}
741+
return config.callMethod(context, "[]", StringHelper.newString(context.runtime, key));
742+
}
743+
744+
private RubyHash getConfigSection(final ThreadContext context, final String sectionName) {
745+
final IRubyObject section = getConfigValue(context, sectionName);
746+
if (section instanceof RubyHash) {
747+
final RubyHash hash = (RubyHash) section;
748+
return hash.isEmpty() ? null : hash;
749+
}
750+
return null;
751+
}
752+
753+
private static String[] splitGeneralNameParts(final String valuex) {
754+
// allow up to three levels of escaping of ','
755+
final String[] vals = valuex.split("(?<!(^|[^\\\\])((\\\\\\\\)?\\\\\\\\)?\\\\),");
756+
for ( int i = 0; i < vals.length; i++ ) {
757+
vals[i] = vals[i].replaceAll("\\\\([,\\\\])", "$1").trim();
758+
}
759+
return vals;
760+
}
761+
762+
private static String stripNumericSuffix(final String key) {
763+
final int index = key.lastIndexOf('.');
764+
if ( index > 0 ) {
765+
boolean numeric = true;
766+
for ( int i = index + 1; i < key.length(); i++ ) {
767+
if ( ! Character.isDigit(key.charAt(i)) ) {
768+
numeric = false; break;
769+
}
770+
}
771+
if ( numeric ) return key.substring(0, index);
772+
}
773+
return key;
774+
}
613775
}

src/test/ruby/x509/test_x509cert.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,4 +618,50 @@ def test_dsa_public_key
618618
cert = OpenSSL::X509::Certificate.new(cert_string)
619619
assert_same OpenSSL::PKey::DSA, cert.public_key.class
620620
end
621+
622+
def test_crl_distribution_points_to_text
623+
key = OpenSSL::PKey::RSA.new(2048)
624+
subject = "/C=FR/ST=IDF/L=PARIS/O=Company/CN=myhost.example"
625+
626+
cert = OpenSSL::X509::Certificate.new
627+
cert.subject = cert.issuer = OpenSSL::X509::Name.parse(subject)
628+
cert.not_before = Time.now
629+
cert.not_after = Time.now + 365*24*60*60
630+
cert.public_key = key.public_key
631+
cert.serial = 0x0
632+
cert.version = 2
633+
634+
ef = OpenSSL::X509::ExtensionFactory.new
635+
ef.subject_certificate = ef.issuer_certificate = cert
636+
637+
cert.add_extension ef.create_extension('basicConstraints', 'CA:FALSE', true)
638+
cert.add_extension ef.create_extension('keyUsage', 'keyEncipherment,dataEncipherment,digitalSignature')
639+
cert.add_extension ef.create_extension('subjectKeyIdentifier', 'hash')
640+
cert.add_extension ef.create_extension('authorityKeyIdentifier', 'keyid:always,issuer:always')
641+
cert.add_extension ef.create_extension('crlDistributionPoints', "URI:http://example.com")
642+
643+
cert.sign key, OpenSSL::Digest::SHA256.new
644+
645+
# Test that the extension value is properly formatted
646+
crl_ext = cert.extensions.find { |e| e.oid == 'crlDistributionPoints' }
647+
assert_not_nil crl_ext, "crlDistributionPoints extension not found"
648+
assert_equal "Full Name:\n URI:http://example.com", crl_ext.value
649+
650+
# Test that to_text output matches C-Ruby format
651+
text = cert.to_text
652+
assert text.include?('X509v3 CRL Distribution Points:'), "Missing 'X509v3 CRL Distribution Points:' in to_text output"
653+
654+
# Extract the CRL Distribution Points section
655+
lines = text.split("\n")
656+
crl_idx = lines.index { |l| l.include?("X509v3 CRL Distribution Points") }
657+
assert_not_nil crl_idx, "Could not find CRL Distribution Points line"
658+
659+
# Check the formatting matches C-Ruby:
660+
# Line 0: " X509v3 CRL Distribution Points: "
661+
# Line 1: " Full Name:"
662+
# Line 2: " URI:http://example.com"
663+
assert_match(/X509v3 CRL Distribution Points:\s*$/, lines[crl_idx])
664+
assert_match(/^\s+Full Name:\s*$/, lines[crl_idx + 1])
665+
assert_match(/^\s+URI:http:\/\/example\.com/, lines[crl_idx + 2])
666+
end
621667
end

0 commit comments

Comments
 (0)