Skip to content

Commit a958df6

Browse files
committed
Match OpenSSL::X509::Name.hash implementation with Ruby
1 parent 73516e1 commit a958df6

File tree

4 files changed

+172
-11
lines changed

4 files changed

+172
-11
lines changed

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

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.bouncycastle.asn1.ASN1Primitive;
5050
import org.bouncycastle.asn1.ASN1String;
5151
import org.bouncycastle.asn1.BERTags;
52+
import org.bouncycastle.asn1.DERUTF8String;
5253
import org.bouncycastle.asn1.DLSequence;
5354
import org.bouncycastle.asn1.DLSet;
5455
import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
@@ -176,6 +177,7 @@ public X509Name(Ruby runtime, RubyClass type) {
176177
private final List<RubyInteger> types;
177178

178179
private transient X500Name name;
180+
private transient X500Name canonicalName;
179181

180182
private void fromASN1Sequence(final byte[] encoded) {
181183
try {
@@ -242,6 +244,7 @@ private void addValue(final ASN1Encodable value) {
242244
@SuppressWarnings("unchecked")
243245
private void addType(final Ruby runtime, final ASN1Encodable value) {
244246
this.name = null; // NOTE: each fromX factory calls this ...
247+
this.canonicalName = null;
245248
final Integer type = ASN1.typeId(value);
246249
if ( type == null ) {
247250
warn(runtime.getCurrentContext(), this + " addType() could not resolve type for: " +
@@ -256,6 +259,7 @@ private void addType(final Ruby runtime, final ASN1Encodable value) {
256259
private void addEntry(ASN1ObjectIdentifier oid, RubyString value, RubyInteger type)
257260
throws IOException {
258261
this.name = null;
262+
this.canonicalName = null;
259263
this.oids.add(oid);
260264
final ASN1Encodable convertedValue = getNameEntryConverted().
261265
getConvertedValue(oid, value.toString()
@@ -515,6 +519,50 @@ final X500Name getX500Name() {
515519
return name = builder.build();
516520
}
517521

522+
final X500Name getCanonicalX500Name() {
523+
if ( canonicalName != null ) return canonicalName;
524+
525+
final X500NameBuilder builder = new X500NameBuilder( BCStyle.INSTANCE );
526+
for ( int i = 0; i < oids.size(); i++ ) {
527+
ASN1Encodable value = values.get(i);
528+
value = canonicalize(value);
529+
builder.addRDN( oids.get(i), value );
530+
}
531+
return canonicalName = builder.build();
532+
}
533+
534+
private ASN1Encodable canonicalize(ASN1Encodable value) {
535+
if (value instanceof ASN1String) {
536+
ASN1String string = (ASN1String) value;
537+
return new DERUTF8String(canonicalize(string.getString()));
538+
}
539+
return value;
540+
}
541+
542+
private String canonicalize(String string) {
543+
//asn1_string_canon (trim, to lower case, collapse multiple spaces)
544+
string = string.trim();
545+
if (string.length() == 0) {
546+
return string;
547+
}
548+
549+
StringBuilder out = new StringBuilder();
550+
int i = 0;
551+
while (i < string.length()) {
552+
char c = string.charAt(i);
553+
if (Character.isWhitespace(c)){
554+
out.append(' ');
555+
while (i < string.length() && Character.isWhitespace(string.charAt(i))) {
556+
i++;
557+
}
558+
} else {
559+
out.append(Character.toLowerCase(c));
560+
i++;
561+
}
562+
}
563+
return out.toString();
564+
}
565+
518566
@JRubyMethod(name = { "cmp", "<=>" })
519567
public RubyFixnum cmp(IRubyObject other) {
520568
if ( equals(other) ) {
@@ -523,8 +571,8 @@ public RubyFixnum cmp(IRubyObject other) {
523571
// TODO: do we really need cmp - if so what order huh?
524572
if ( other instanceof X509Name ) {
525573
final X509Name that = (X509Name) other;
526-
final X500Name thisName = this.getX500Name();
527-
final X500Name thatName = that.getX500Name();
574+
final X500Name thisName = this.getCanonicalX500Name();
575+
final X500Name thatName = that.getCanonicalX500Name();
528576
int cmp = thisName.toString().compareTo( thatName.toString() );
529577
return RubyFixnum.newFixnum( getRuntime(), cmp );
530578
}
@@ -536,8 +584,8 @@ public boolean equals(Object other) {
536584
if ( this == other ) return true;
537585
if ( other instanceof X509Name ) {
538586
final X509Name that = (X509Name) other;
539-
final X500Name thisName = this.getX500Name();
540-
final X500Name thatName = that.getX500Name();
587+
final X500Name thisName = this.getCanonicalX500Name();
588+
final X500Name thatName = that.getCanonicalX500Name();
541589
return thisName.equals(thatName);
542590
}
543591
return false;
@@ -546,15 +594,14 @@ public boolean equals(Object other) {
546594
@Override
547595
public int hashCode() {
548596
try {
549-
return Name.hash( getX500Name() );
597+
return (int) Name.hash( getCanonicalX500Name() );
550598
}
551599
catch (IOException e) {
552600
debugStackTrace(getRuntime(), e); return 0;
553601
}
554602
catch (RuntimeException e) {
555603
debugStackTrace(getRuntime(), e); return 0;
556604
}
557-
// return 41 * this.oids.hashCode();
558605
}
559606

560607
@JRubyMethod(name = "eql?")
@@ -571,7 +618,32 @@ public IRubyObject eql_p(final IRubyObject obj) {
571618
@Override
572619
@JRubyMethod
573620
public RubyFixnum hash() {
574-
return getRuntime().newFixnum( hashCode() );
621+
long hash;
622+
try {
623+
hash = Name.hash( getCanonicalX500Name() );
624+
}
625+
catch (IOException e) {
626+
debugStackTrace(getRuntime(), e); hash = 0;
627+
}
628+
catch (RuntimeException e) {
629+
debugStackTrace(getRuntime(), e); hash = 0;
630+
}
631+
return getRuntime().newFixnum(hash);
632+
}
633+
634+
@JRubyMethod
635+
public RubyFixnum hash_old() {
636+
int hash;
637+
try {
638+
hash = Name.hashOld( getX500Name() );
639+
}
640+
catch (IOException e) {
641+
debugStackTrace(getRuntime(), e); hash = 0;
642+
}
643+
catch (RuntimeException e) {
644+
debugStackTrace(getRuntime(), e); hash = 0;
645+
}
646+
return getRuntime().newFixnum( hash );
575647
}
576648

577649
@JRubyMethod

src/main/java/org/jruby/ext/openssl/x509store/Name.java

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import javax.security.auth.x500.X500Principal;
3636

3737
import org.bouncycastle.asn1.ASN1Encoding;
38+
import org.bouncycastle.asn1.x500.RDN;
3839
import org.bouncycastle.asn1.x500.X500Name;
3940
import org.bouncycastle.jce.X509Principal;
4041
import org.bouncycastle.jce.provider.X509CertificateObject;
@@ -58,7 +59,7 @@ public Name(final X500Name name) {
5859
this.name = name;
5960
}
6061

61-
public static int hash(final X500Name name) throws IOException {
62+
public static int hashOld(final X500Name name) throws IOException {
6263
try {
6364
final byte[] bytes = name.getEncoded();
6465
MessageDigest md5 = SecurityHelper.getMessageDigest("MD5");
@@ -75,9 +76,43 @@ public static int hash(final X500Name name) throws IOException {
7576
}
7677
}
7778

78-
private transient int hash = 0;
79+
public static long hash(final X500Name canonicalName) throws IOException {
80+
try {
81+
final byte[] bytes = canonicalName.getEncoded();
82+
MessageDigest sha = SecurityHelper.getMessageDigest("SHA1");
83+
int n = getLeadingTLLength(bytes);
84+
sha.update(bytes, n, bytes.length - n); //canonical form does not include leading SEQUENCE Tag-Length
85+
final byte[] digest = sha.digest();
86+
long result = 0;
87+
result |= digest[3] & 0xff; result <<= 8;
88+
result |= digest[2] & 0xff; result <<= 8;
89+
result |= digest[1] & 0xff; result <<= 8;
90+
result |= digest[0] & 0xff;
91+
return result & 0xffffffff;
92+
}
93+
catch (NoSuchAlgorithmException e) {
94+
throw new RuntimeException(e);
95+
}
96+
}
97+
98+
private static int getLeadingTLLength(byte[] bytes) throws IOException {
99+
if (bytes.length <= 1) {
100+
return bytes.length; //should not happen tough
101+
}
102+
byte length = bytes[1];
103+
104+
if ((length & 0x80) == 0x80) {
105+
// long form: Two to 127 octets. Bit 8 of first octet has value "1" and
106+
// bits 7-1 give the number of additional length octets.
107+
int size = length & 0x7f;
108+
return 1 + 1 + size;
109+
}
110+
return 2; //short form: 1 byte tag, 1 byte length
111+
}
112+
113+
private transient long hash = 0;
79114

80-
public final int hash() {
115+
public final long hash() {
81116
try {
82117
return hash == 0 ? hash = hash(name) : hash;
83118
}
@@ -93,7 +128,7 @@ public final int hash() {
93128
* c: X509_NAME_hash
94129
*/
95130
@Override
96-
public int hashCode() { return hash(); }
131+
public int hashCode() { return (int)hash(); }
97132

98133
@Override
99134
public boolean equals(final Object that) {

src/test/ruby/x509/test_x509cert.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,4 +472,30 @@ def test_cert_loading_regression
472472
-----END RSA PRIVATE KEY-----
473473
_end_of_pem_
474474

475+
def test_cert_subject_hash
476+
cert = OpenSSL::X509::Certificate.new <<-EOF
477+
-----BEGIN CERTIFICATE-----
478+
MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG
479+
A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv
480+
b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw
481+
MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i
482+
YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT
483+
aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ
484+
jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp
485+
xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp
486+
1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG
487+
snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ
488+
U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8
489+
9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E
490+
BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B
491+
AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz
492+
yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE
493+
38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP
494+
AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad
495+
DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME
496+
HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==
497+
-----END CERTIFICATE-----
498+
EOF
499+
assert_equal '5ad8a5d6', cert.subject.hash.to_s(16)
500+
end
475501
end

src/test/ruby/x509/test_x509name.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,32 @@ def test_new_from_der
5858
assert_equal [["DC", "org", 22], ["DC", "ruby-lang", 22], ["CN", "TestCA", 12]], name.to_a
5959
end
6060

61+
def test_hash_empty
62+
name = OpenSSL::X509::Name.new
63+
assert_equal 4003674586, name.hash
64+
end
65+
66+
def test_hash
67+
name = OpenSSL::X509::Name.new [['CN', 'nobody'], ['DC', 'example']]
68+
assert_equal 3974220101, name.hash
69+
end
70+
71+
def test_hash_multiple_spaces_mixed_case
72+
name = OpenSSL::X509::Name.new [['CN', 'foo bar'], ['DC', 'BAZ']]
73+
name2 = OpenSSL::X509::Name.new [['CN', 'foo bar'], ['DC', 'baz']]
74+
assert_equal 1941551332, name.hash
75+
assert_equal 1941551332, name2.hash
76+
end
77+
78+
def test_hash_long_name
79+
puts 'test_hash_long_name'
80+
name = OpenSSL::X509::Name.new [['CN', 'a' * 255], ['DC', 'example']]
81+
assert_equal 214469118, name.hash
82+
end
83+
84+
def test_hash_old
85+
name = OpenSSL::X509::Name.new [['CN', 'nobody'], ['DC', 'example']]
86+
assert_equal 1460400684, name.hash_old
87+
end
88+
6189
end

0 commit comments

Comments
 (0)