diff --git a/src/main/java/org/jruby/ext/openssl/X509Name.java b/src/main/java/org/jruby/ext/openssl/X509Name.java index 9c1b1563..ded0a6c5 100644 --- a/src/main/java/org/jruby/ext/openssl/X509Name.java +++ b/src/main/java/org/jruby/ext/openssl/X509Name.java @@ -49,6 +49,7 @@ import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.ASN1String; import org.bouncycastle.asn1.BERTags; +import org.bouncycastle.asn1.DERUTF8String; import org.bouncycastle.asn1.DLSequence; import org.bouncycastle.asn1.DLSet; import org.bouncycastle.asn1.x500.AttributeTypeAndValue; @@ -176,6 +177,7 @@ public X509Name(Ruby runtime, RubyClass type) { private final List types; private transient X500Name name; + private transient X500Name canonicalName; private void fromASN1Sequence(final byte[] encoded) { try { @@ -242,6 +244,7 @@ private void addValue(final ASN1Encodable value) { @SuppressWarnings("unchecked") private void addType(final Ruby runtime, final ASN1Encodable value) { this.name = null; // NOTE: each fromX factory calls this ... + this.canonicalName = null; final Integer type = ASN1.typeId(value); if ( type == null ) { warn(runtime.getCurrentContext(), this + " addType() could not resolve type for: " + @@ -256,6 +259,7 @@ private void addType(final Ruby runtime, final ASN1Encodable value) { private void addEntry(ASN1ObjectIdentifier oid, RubyString value, RubyInteger type) throws IOException { this.name = null; + this.canonicalName = null; this.oids.add(oid); final ASN1Encodable convertedValue = getNameEntryConverted(). getConvertedValue(oid, value.toString() @@ -515,6 +519,50 @@ final X500Name getX500Name() { return name = builder.build(); } + final X500Name getCanonicalX500Name() { + if ( canonicalName != null ) return canonicalName; + + final X500NameBuilder builder = new X500NameBuilder( BCStyle.INSTANCE ); + for ( int i = 0; i < oids.size(); i++ ) { + ASN1Encodable value = values.get(i); + value = canonicalize(value); + builder.addRDN( oids.get(i), value ); + } + return canonicalName = builder.build(); + } + + private ASN1Encodable canonicalize(ASN1Encodable value) { + if (value instanceof ASN1String) { + ASN1String string = (ASN1String) value; + return new DERUTF8String(canonicalize(string.getString())); + } + return value; + } + + private String canonicalize(String string) { + //asn1_string_canon (trim, to lower case, collapse multiple spaces) + string = string.trim(); + if (string.length() == 0) { + return string; + } + + StringBuilder out = new StringBuilder(); + int i = 0; + while (i < string.length()) { + char c = string.charAt(i); + if (Character.isWhitespace(c)){ + out.append(' '); + while (i < string.length() && Character.isWhitespace(string.charAt(i))) { + i++; + } + } else { + out.append(Character.toLowerCase(c)); + i++; + } + } + return out.toString(); + } + @JRubyMethod(name = { "cmp", "<=>" }) public RubyFixnum cmp(IRubyObject other) { if ( equals(other) ) { @@ -523,8 +571,8 @@ public RubyFixnum cmp(IRubyObject other) { // TODO: do we really need cmp - if so what order huh? if ( other instanceof X509Name ) { final X509Name that = (X509Name) other; - final X500Name thisName = this.getX500Name(); - final X500Name thatName = that.getX500Name(); + final X500Name thisName = this.getCanonicalX500Name(); + final X500Name thatName = that.getCanonicalX500Name(); int cmp = thisName.toString().compareTo( thatName.toString() ); return RubyFixnum.newFixnum( getRuntime(), cmp ); } @@ -536,8 +584,8 @@ public boolean equals(Object other) { if ( this == other ) return true; if ( other instanceof X509Name ) { final X509Name that = (X509Name) other; - final X500Name thisName = this.getX500Name(); - final X500Name thatName = that.getX500Name(); + final X500Name thisName = this.getCanonicalX500Name(); + final X500Name thatName = that.getCanonicalX500Name(); return thisName.equals(thatName); } return false; @@ -546,7 +594,7 @@ public boolean equals(Object other) { @Override public int hashCode() { try { - return Name.hash( getX500Name() ); + return (int) Name.hash( getCanonicalX500Name() ); } catch (IOException e) { debugStackTrace(getRuntime(), e); return 0; @@ -554,7 +602,6 @@ public int hashCode() { catch (RuntimeException e) { debugStackTrace(getRuntime(), e); return 0; } - // return 41 * this.oids.hashCode(); } @JRubyMethod(name = "eql?") @@ -571,7 +618,32 @@ public IRubyObject eql_p(final IRubyObject obj) { @Override @JRubyMethod public RubyFixnum hash() { - return getRuntime().newFixnum( hashCode() ); + long hash; + try { + hash = Name.hash( getCanonicalX500Name() ); + } + catch (IOException e) { + debugStackTrace(getRuntime(), e); hash = 0; + } + catch (RuntimeException e) { + debugStackTrace(getRuntime(), e); hash = 0; + } + return getRuntime().newFixnum(hash); + } + + @JRubyMethod + public RubyFixnum hash_old() { + int hash; + try { + hash = Name.hashOld( getX500Name() ); + } + catch (IOException e) { + debugStackTrace(getRuntime(), e); hash = 0; + } + catch (RuntimeException e) { + debugStackTrace(getRuntime(), e); hash = 0; + } + return getRuntime().newFixnum( hash ); } @JRubyMethod diff --git a/src/main/java/org/jruby/ext/openssl/x509store/Name.java b/src/main/java/org/jruby/ext/openssl/x509store/Name.java index 9cb4894a..04cf4644 100644 --- a/src/main/java/org/jruby/ext/openssl/x509store/Name.java +++ b/src/main/java/org/jruby/ext/openssl/x509store/Name.java @@ -35,6 +35,7 @@ import javax.security.auth.x500.X500Principal; import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.x500.RDN; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.jce.X509Principal; import org.bouncycastle.jce.provider.X509CertificateObject; @@ -58,7 +59,7 @@ public Name(final X500Name name) { this.name = name; } - public static int hash(final X500Name name) throws IOException { + public static int hashOld(final X500Name name) throws IOException { try { final byte[] bytes = name.getEncoded(); MessageDigest md5 = SecurityHelper.getMessageDigest("MD5"); @@ -75,9 +76,43 @@ public static int hash(final X500Name name) throws IOException { } } - private transient int hash = 0; + public static long hash(final X500Name canonicalName) throws IOException { + try { + final byte[] bytes = canonicalName.getEncoded(); + MessageDigest sha = SecurityHelper.getMessageDigest("SHA1"); + int n = getLeadingTLLength(bytes); + sha.update(bytes, n, bytes.length - n); //canonical form does not include leading SEQUENCE Tag-Length + final byte[] digest = sha.digest(); + long result = 0; + result |= digest[3] & 0xff; result <<= 8; + result |= digest[2] & 0xff; result <<= 8; + result |= digest[1] & 0xff; result <<= 8; + result |= digest[0] & 0xff; + return result & 0xffffffff; + } + catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + private static int getLeadingTLLength(byte[] bytes) throws IOException { + if (bytes.length <= 1) { + return bytes.length; //should not happen tough + } + byte length = bytes[1]; + + if ((length & 0x80) == 0x80) { + // long form: Two to 127 octets. Bit 8 of first octet has value "1" and + // bits 7-1 give the number of additional length octets. + int size = length & 0x7f; + return 1 + 1 + size; + } + return 2; //short form: 1 byte tag, 1 byte length + } + + private transient long hash = 0; - public final int hash() { + public final long hash() { try { return hash == 0 ? hash = hash(name) : hash; } @@ -93,7 +128,7 @@ public final int hash() { * c: X509_NAME_hash */ @Override - public int hashCode() { return hash(); } + public int hashCode() { return (int)hash(); } @Override public boolean equals(final Object that) { diff --git a/src/test/ruby/x509/test_x509cert.rb b/src/test/ruby/x509/test_x509cert.rb index 0ccad178..8a907d84 100644 --- a/src/test/ruby/x509/test_x509cert.rb +++ b/src/test/ruby/x509/test_x509cert.rb @@ -472,4 +472,30 @@ def test_cert_loading_regression -----END RSA PRIVATE KEY----- _end_of_pem_ + def test_cert_subject_hash + cert = OpenSSL::X509::Certificate.new <<-EOF +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- +EOF + assert_equal '5ad8a5d6', cert.subject.hash.to_s(16) + end end diff --git a/src/test/ruby/x509/test_x509name.rb b/src/test/ruby/x509/test_x509name.rb index f0190cd5..c312f739 100644 --- a/src/test/ruby/x509/test_x509name.rb +++ b/src/test/ruby/x509/test_x509name.rb @@ -58,4 +58,32 @@ def test_new_from_der assert_equal [["DC", "org", 22], ["DC", "ruby-lang", 22], ["CN", "TestCA", 12]], name.to_a end + def test_hash_empty + name = OpenSSL::X509::Name.new + assert_equal 4003674586, name.hash + end + + def test_hash + name = OpenSSL::X509::Name.new [['CN', 'nobody'], ['DC', 'example']] + assert_equal 3974220101, name.hash + end + + def test_hash_multiple_spaces_mixed_case + name = OpenSSL::X509::Name.new [['CN', 'foo bar'], ['DC', 'BAZ']] + name2 = OpenSSL::X509::Name.new [['CN', 'foo bar'], ['DC', 'baz']] + assert_equal 1941551332, name.hash + assert_equal 1941551332, name2.hash + end + + def test_hash_long_name + puts 'test_hash_long_name' + name = OpenSSL::X509::Name.new [['CN', 'a' * 255], ['DC', 'example']] + assert_equal 214469118, name.hash + end + + def test_hash_old + name = OpenSSL::X509::Name.new [['CN', 'nobody'], ['DC', 'example']] + assert_equal 1460400684, name.hash_old + end + end \ No newline at end of file