Skip to content

Commit 91dcdc5

Browse files
committed
[fix] format otherName and nameConstraints exts (#324)
Extension#value dumped BC's toString() for an otherName entry and flagged as ';'-separated, ssl.rb's split(/,\s+/) then merged the DNS entry that followed - verify_certificate_identity then rejected a valid certificate nameConstraints had no formatter at all and fell back to an ASN.1 dump both now match what C OpenSSL prints
1 parent 88e08ef commit 91dcdc5

2 files changed

Lines changed: 139 additions & 13 deletions

File tree

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

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
6262
import org.bouncycastle.asn1.x509.GeneralName;
6363
import org.bouncycastle.asn1.x509.GeneralNames;
64+
import org.bouncycastle.asn1.x509.GeneralSubtree;
65+
import org.bouncycastle.asn1.x509.NameConstraints;
6466
import org.bouncycastle.util.encoders.Hex;
6567

6668
import org.jruby.Ruby;
@@ -315,9 +317,6 @@ public IRubyObject set_oid(final ThreadContext context, IRubyObject arg) {
315317
private static final byte[] Object_Signing_CA = {'O', 'b', 'j', 'e', 'c', 't', ' ', 'S', 'i', 'g', 'n', 'i', 'n', 'g', ' ', 'C', 'A'};
316318
private static final byte[] Unused = {'U', 'n', 'u', 's', 'e', 'd'};
317319
private static final byte[] Unspecified = {'U', 'n', 's', 'p', 'e', 'c', 'i', 'f', 'i', 'e', 'd'};
318-
//private static final byte[] Key_Compromise = { 'K','e','y',' ','C','o','m','p','r','o','m','i','s','e' };
319-
//private static final byte[] CA_Compromise = { 'C','A',' ','C','o','m','p','r','o','m','i','s','e' };
320-
//private static final byte[] Affiliation_Changed = { 'A','f','f','i','l','i','a','t','i','o','n',' ','C','h','a','n','g','e','d' };
321320
private static final byte[] keyid_ = {'k', 'e', 'y', 'i', 'd', ':'};
322321

323322
@JRubyMethod
@@ -329,6 +328,18 @@ public RubyString value(final ThreadContext context) {
329328
final Ruby runtime = context.runtime;
330329
final String oid = getRealObjectID().getId();
331330
try {
331+
if ( oid.equals("2.5.29.30") ) { // nameConstraints
332+
ASN1Encodable value = getRealValue();
333+
if ( value instanceof ASN1OctetString ) {
334+
value = ASN1.readObject( ((ASN1OctetString) value).getOctets() );
335+
}
336+
final NameConstraints nameConstraints = NameConstraints.getInstance(value);
337+
final ByteList val = new ByteList(64);
338+
appendGeneralSubtrees(val, "Permitted", nameConstraints.getPermittedSubtrees());
339+
appendGeneralSubtrees(val, "Excluded", nameConstraints.getExcludedSubtrees());
340+
return runtime.newString( val );
341+
}
342+
332343
if ( oid.equals("2.5.29.19") ) { // basicConstraints
333344
ASN1Sequence seq2 = (ASN1Sequence) ASN1.readObject( getRealValueEncoded() );
334345
final ByteList val = new ByteList(32);
@@ -709,6 +720,50 @@ private static String accessDescriptionMethodName(final Ruby runtime, final Acce
709720
return method.getId();
710721
}
711722

723+
static final String MS_UPN_OID = "1.3.6.1.4.1.311.20.2.3"; // otherName type used by AD
724+
725+
// C: "Permitted:\n DNS:example.com\n DNS:other.com" (and/or an "Excluded:" block)
726+
private static void appendGeneralSubtrees(final ByteList out, final String label,
727+
final GeneralSubtree[] subtrees) {
728+
if ( subtrees == null || subtrees.length == 0 ) return;
729+
730+
if ( out.length() > 0 ) out.append('\n');
731+
out.append( ByteList.plain(label) );
732+
out.append(':');
733+
for ( int i = 0; i < subtrees.length; i++ ) {
734+
out.append('\n').append(' ').append(' ');
735+
final GeneralName base = subtrees[i].getBase();
736+
// a constrained iPAddress carries address *and* mask (8 or 32 octets)
737+
if ( base.getTagNo() == GeneralName.iPAddress ) {
738+
final byte[] ip = ((ASN1OctetString) base.getName()).getOctets();
739+
if ( ip.length == 8 || ip.length == 32 ) {
740+
out.append('I').append('P').append(':');
741+
final int half = ip.length / 2;
742+
appendIPAddress(out, ip, 0, half);
743+
out.append('/');
744+
appendIPAddress(out, ip, half, half);
745+
continue;
746+
}
747+
}
748+
formatGeneralName(base, out, false);
749+
}
750+
}
751+
752+
private static void appendIPAddress(final ByteList out, final byte[] ip, final int off, final int len) {
753+
if ( len == 4 ) {
754+
for ( int i = 0; i < len; i++ ) {
755+
out.append( ConvertBytes.intToCharBytes( ((int) ip[off + i]) & 0xff ) );
756+
if ( i != len - 1 ) out.append('.');
757+
}
758+
}
759+
else {
760+
for ( int i = 0; i < len; i += 2 ) {
761+
out.append( ConvertBytes.intToHexBytes( ((ip[off+i] & 0xff) << 8 | (ip[off+i+1] & 0xff)) ) );
762+
if ( i != len - 2 ) out.append(':');
763+
}
764+
}
765+
}
766+
712767
@SuppressWarnings("unchecked")
713768
private static boolean formatGeneralName(final GeneralName name, final ByteList out, final boolean slashed) {
714769
final ASN1Encodable obj = name.getName();
@@ -762,11 +817,24 @@ private static boolean formatGeneralName(final GeneralName name, final ByteList
762817
}
763818
break;
764819
case GeneralName.otherName:
765-
out.append('o').append('t').append('h').append('e').append('r').append('N').append('a').append('m').append('e').
766-
append(':');
767-
out.append( ByteList.plain( obj.toString() ) );
768-
return true;
769-
//tagged = true;
820+
// OtherName ::= SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY }
821+
// C: "othername: UPN:<value>" for the MS UPN type, "othername: <oid>:<value>" otherwise
822+
out.append( ByteList.plain("othername: ") );
823+
final ASN1Sequence otherName = ASN1Sequence.getInstance(obj);
824+
final String typeId = ASN1ObjectIdentifier.getInstance(otherName.getObjectAt(0)).getId();
825+
out.append( ByteList.plain( MS_UPN_OID.equals(typeId) ? "UPN" : typeId ) );
826+
out.append(':');
827+
ASN1Encodable otherValue = otherName.getObjectAt(1);
828+
if ( otherValue instanceof ASN1TaggedObject ) { // [0] EXPLICIT
829+
otherValue = ASN1Shim.getTaggedObject((ASN1TaggedObject) otherValue);
830+
}
831+
if ( otherValue instanceof ASN1String ) {
832+
out.append( ByteList.plain( ((ASN1String) otherValue).getString() ) );
833+
}
834+
else {
835+
out.append( ByteList.plain( otherValue.toString() ) );
836+
}
837+
break; // NOTE: not a ';' separated entry (OpenSSL separates every name with ", ")
770838
case GeneralName.registeredID:
771839
out.append('R').append('I').append('D').
772840
append(':');

test/x509/test_x509ext.rb

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@
33

44
class TestX509Extension < TestCase
55

6-
if defined? JRUBY_VERSION
7-
def setup; require 'jopenssl/load' end
8-
else
9-
def setup; require 'openssl' end
10-
end
6+
def setup; require 'openssl' end
117

128
def test_new
139
assert_raise(ArgumentError) { OpenSSL::X509::Extension.new }
@@ -246,6 +242,68 @@ def test_subject_alt_name_sequence
246242
}
247243
end
248244

245+
# -- SAN with otherName + nameConstraints value formatting -------------------
246+
# built straight from DER so both engines decode byte-identical input
247+
248+
MS_UPN_OID = '1.3.6.1.4.1.311.20.2.3' # otherName type used by Active Directory
249+
250+
# [0] IMPLICIT OtherName ::= SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY }
251+
def other_name(oid, value)
252+
seq = OpenSSL::ASN1::Sequence.new([
253+
OpenSSL::ASN1::ObjectId.new(oid),
254+
OpenSSL::ASN1::ASN1Data.new([ OpenSSL::ASN1::UTF8String.new(value) ], 0, :CONTEXT_SPECIFIC)
255+
])
256+
OpenSSL::ASN1::ASN1Data.new(seq.value, 0, :CONTEXT_SPECIFIC)
257+
end
258+
259+
def dns_name(name); OpenSSL::ASN1::ASN1Data.new(name, 2, :CONTEXT_SPECIFIC) end
260+
261+
def san_extension(*entries)
262+
OpenSSL::X509::Extension.new('subjectAltName', OpenSSL::ASN1::Sequence.new(entries).to_der, false)
263+
end
264+
265+
def test_subject_alt_name_value_with_other_name_upn
266+
ext = san_of(cert_with_san(other_name(MS_UPN_OID, 'HOST1$@example.com'),
267+
dns_name('host1.example.com'), dns_name('example.com')))
268+
assert_equal 'othername: UPN:HOST1$@example.com, DNS:host1.example.com, DNS:example.com', ext.value
269+
end
270+
271+
def test_subject_alt_name_value_with_generic_other_name
272+
ext = san_of(cert_with_san(other_name('1.2.3.4', 'xx'), dns_name('a.example.com')))
273+
assert_equal 'othername: 1.2.3.4:xx, DNS:a.example.com', ext.value
274+
end
275+
276+
def san_of(cert); cert.extensions.find { |e| e.oid == 'subjectAltName' } end
277+
278+
# GH-324: the otherName dump used to leak ", " (and a ';' separator) into the value,
279+
# which made ssl.rb's split(/,\s+/) swallow the DNS entry that followed
280+
def test_verify_certificate_identity_with_other_name
281+
cert = cert_with_san(other_name(MS_UPN_OID, 'HOST1$@example.com'),
282+
dns_name('host1.example.com'), dns_name('example.com'))
283+
assert_equal true, OpenSSL::SSL.verify_certificate_identity(cert, 'host1.example.com')
284+
assert_equal true, OpenSSL::SSL.verify_certificate_identity(cert, 'example.com')
285+
assert_equal false, OpenSSL::SSL.verify_certificate_identity(cert, 'other.example.com')
286+
end
287+
288+
def test_name_constraints_value
289+
ext = OpenSSL::X509::ExtensionFactory.new.
290+
create_extension('nameConstraints', 'permitted;DNS:example.com,permitted;DNS:bla.example.net', true)
291+
assert_equal "Permitted:\n DNS:example.com\n DNS:bla.example.net", ext.value
292+
end
293+
294+
# signed + re-parsed (as received over the wire);
295+
# subject CN deliberately does not match so the identity check can only pass through the SAN
296+
def cert_with_san(*entries)
297+
key = OpenSSL::PKey::RSA.new(2048)
298+
cert = OpenSSL::X509::Certificate.new
299+
cert.subject = cert.issuer = OpenSSL::X509::Name.parse('/CN=not.the.name')
300+
cert.not_before = Time.now - 60; cert.not_after = Time.now + 3600
301+
cert.public_key = key.public_key; cert.serial = 1; cert.version = 2
302+
cert.add_extension(san_extension(*entries))
303+
cert.sign(key, OpenSSL::Digest.new('SHA256'))
304+
OpenSSL::X509::Certificate.new(cert.to_der)
305+
end
306+
249307
def test_authority_key_identifier
250308
cn = [ %w[CN localhost] ]
251309
# key = OpenSSL::PKey::RSA.new TEST_KEY_RSA2048

0 commit comments

Comments
 (0)