Skip to content

Commit d5408f0

Browse files
committed
[fix] properly encode nameConstraints extension
create_extension("nameConstraints", "permitted;DNS:.example.com") was falling through to the generic OCTET STRING path, producing raw ASCII instead of the NameConstraints ASN.1 structure. Tests updated to use create_extension directly instead of manual ASN.1 construction workaround.
1 parent 853f08e commit d5408f0

File tree

2 files changed

+47
-33
lines changed

2 files changed

+47
-33
lines changed

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
import org.bouncycastle.asn1.x509.DistributionPointName;
4343
import org.bouncycastle.asn1.x509.GeneralName;
4444
import org.bouncycastle.asn1.x509.GeneralNames;
45+
import org.bouncycastle.asn1.x509.GeneralSubtree;
46+
import org.bouncycastle.asn1.x509.NameConstraints;
4547

4648
import org.jruby.Ruby;
4749
import org.jruby.RubyArray;
@@ -214,6 +216,9 @@ else if (id.equals("1.3.6.1.5.5.7.1.1")) { // authorityInfoAccess
214216
else if (isNetscapeIA5StringExtension(id)) {
215217
value = new DEROctetString(new DERIA5String(valuex).getEncoded(ASN1Encoding.DER));
216218
}
219+
else if (id.equals("2.5.29.30")) { // nameConstraints
220+
value = parseNameConstraints(valuex);
221+
}
217222
else {
218223
value = new DEROctetString(new DEROctetString(ByteList.plain(valuex)).getEncoded(ASN1Encoding.DER));
219224
}
@@ -719,6 +724,42 @@ private ASN1Sequence parseAuthorityInfoAccess(final String valuex) throws IOExce
719724
return new DERSequence(vector);
720725
}
721726

727+
/**
728+
* Parses nameConstraints from the OpenSSL config format:
729+
* "permitted;DNS:.example.com,excluded;IP:10.0.0.0/255.0.0.0"
730+
*
731+
* C OpenSSL's v2i_NAME_CONSTRAINTS strips the "permitted"/"excluded" prefix
732+
* and passes the remainder to v2i_GENERAL_NAME_ex.
733+
*/
734+
private DEROctetString parseNameConstraints(final String valuex) throws IOException {
735+
final java.util.List<GeneralSubtree> permitted = new java.util.ArrayList<>();
736+
final java.util.List<GeneralSubtree> excluded = new java.util.ArrayList<>();
737+
738+
final String[] entries = valuex.split(",");
739+
for (String entry : entries) {
740+
entry = entry.trim();
741+
final String nameValue;
742+
final java.util.List<GeneralSubtree> target;
743+
if (entry.startsWith("permitted;")) {
744+
nameValue = entry.substring("permitted;".length()).trim();
745+
target = permitted;
746+
} else if (entry.startsWith("excluded;")) {
747+
nameValue = entry.substring("excluded;".length()).trim();
748+
target = excluded;
749+
} else {
750+
throw new IOException("Invalid nameConstraints syntax: " + entry);
751+
}
752+
final GeneralName name = parseGeneralName(nameValue);
753+
target.add(new GeneralSubtree(name));
754+
}
755+
756+
final NameConstraints nc = new NameConstraints(
757+
permitted.isEmpty() ? null : permitted.toArray(new GeneralSubtree[0]),
758+
excluded.isEmpty() ? null : excluded.toArray(new GeneralSubtree[0]));
759+
760+
return new DEROctetString(nc.getEncoded(ASN1Encoding.DER));
761+
}
762+
722763
private GeneralNames parseGeneralNames(final ThreadContext context, final String valuex) throws IOException {
723764
final String[] vals = splitGeneralNameParts(valuex);
724765
final GeneralName[] names = new GeneralName[vals.length];

src/test/ruby/x509/test_x509store.rb

Lines changed: 6 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -592,30 +592,8 @@ def test_v_flag_partial_chain
592592
assert_equal OpenSSL::X509::V_OK, store2.error
593593
end
594594

595-
# Helper: build a proper ASN.1 nameConstraints extension since
596-
# JRuby's create_extension doesn't encode nameConstraints correctly yet.
597595
private
598596

599-
def build_name_constraints_ext(permitted_dns: nil, excluded_dns: nil)
600-
subtrees = []
601-
if permitted_dns
602-
dns_names = Array(permitted_dns).map do |name|
603-
dns = OpenSSL::ASN1::IA5String.new(name, 2, :IMPLICIT, :CONTEXT_SPECIFIC)
604-
OpenSSL::ASN1::Sequence.new([dns])
605-
end
606-
subtrees << OpenSSL::ASN1::Sequence.new(dns_names, 0, :IMPLICIT, :CONTEXT_SPECIFIC)
607-
end
608-
if excluded_dns
609-
dns_names = Array(excluded_dns).map do |name|
610-
dns = OpenSSL::ASN1::IA5String.new(name, 2, :IMPLICIT, :CONTEXT_SPECIFIC)
611-
OpenSSL::ASN1::Sequence.new([dns])
612-
end
613-
subtrees << OpenSSL::ASN1::Sequence.new(dns_names, 1, :IMPLICIT, :CONTEXT_SPECIFIC)
614-
end
615-
nc = OpenSSL::ASN1::Sequence.new(subtrees)
616-
OpenSSL::X509::Extension.new("nameConstraints", nc.to_der, true)
617-
end
618-
619597
def build_cert_with_san(name, serial, san_dns, issuer_cert, issuer_key)
620598
key = OpenSSL::PKey::RSA.new(2048)
621599
cert = OpenSSL::X509::Certificate.new
@@ -638,10 +616,9 @@ def test_name_constraints_permitted_dns
638616
now = Time.now
639617
ca_key = OpenSSL::PKey::RSA.new(2048)
640618
ca_cert = issue_cert(OpenSSL::X509::Name.parse("/CN=CA"), ca_key, 1,
641-
[["basicConstraints","CA:TRUE",true],["keyUsage","cRLSign,keyCertSign",true]],
619+
[["basicConstraints","CA:TRUE",true],["keyUsage","cRLSign,keyCertSign",true],
620+
["nameConstraints","permitted;DNS:.example.com",true]],
642621
nil, nil, not_before: now, not_after: now + 3600)
643-
ca_cert.add_extension(build_name_constraints_ext(permitted_dns: [".example.com"]))
644-
ca_cert.sign(ca_key, "SHA256") # re-sign after adding extension
645622

646623
good = build_cert_with_san("good", 10, "good.example.com", ca_cert, ca_key)
647624
bad = build_cert_with_san("bad", 11, "evil.attacker.com", ca_cert, ca_key)
@@ -658,10 +635,9 @@ def test_name_constraints_excluded_dns
658635
now = Time.now
659636
ca_key = OpenSSL::PKey::RSA.new(2048)
660637
ca_cert = issue_cert(OpenSSL::X509::Name.parse("/CN=CA"), ca_key, 1,
661-
[["basicConstraints","CA:TRUE",true],["keyUsage","cRLSign,keyCertSign",true]],
638+
[["basicConstraints","CA:TRUE",true],["keyUsage","cRLSign,keyCertSign",true],
639+
["nameConstraints","excluded;DNS:.evil.com",true]],
662640
nil, nil, not_before: now, not_after: now + 3600)
663-
ca_cert.add_extension(build_name_constraints_ext(excluded_dns: [".evil.com"]))
664-
ca_cert.sign(ca_key, "SHA256")
665641

666642
good = build_cert_with_san("good", 10, "good.example.com", ca_cert, ca_key)
667643
bad = build_cert_with_san("bad", 11, "bad.evil.com", ca_cert, ca_key)
@@ -690,12 +666,9 @@ def test_name_constraints_permitted_and_excluded_combined
690666
now = Time.now
691667
ca_key = OpenSSL::PKey::RSA.new(2048)
692668
ca_cert = issue_cert(OpenSSL::X509::Name.parse("/CN=CA"), ca_key, 1,
693-
[["basicConstraints","CA:TRUE",true],["keyUsage","cRLSign,keyCertSign",true]],
669+
[["basicConstraints","CA:TRUE",true],["keyUsage","cRLSign,keyCertSign",true],
670+
["nameConstraints","permitted;DNS:.example.com,excluded;DNS:.bad.example.com",true]],
694671
nil, nil, not_before: now, not_after: now + 3600)
695-
# Permit .example.com but exclude .bad.example.com
696-
ca_cert.add_extension(build_name_constraints_ext(
697-
permitted_dns: [".example.com"], excluded_dns: [".bad.example.com"]))
698-
ca_cert.sign(ca_key, "SHA256")
699672

700673
good = build_cert_with_san("good", 10, "good.example.com", ca_cert, ca_key)
701674
bad = build_cert_with_san("bad", 11, "test.bad.example.com", ca_cert, ca_key)

0 commit comments

Comments
 (0)