Skip to content

Commit e4c46a2

Browse files
authored
Merge pull request #279 from tmaher/tls-verify-hostnames
enable TLS hostname validation
2 parents 680d024 + 435332d commit e4c46a2

File tree

13 files changed

+629
-103
lines changed

13 files changed

+629
-103
lines changed

Diff for: README.rdoc

+4-6
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,10 @@ This task will run the test suite and the
5252

5353
rake rubotest
5454

55-
To run the integration tests against an LDAP server:
56-
57-
cd test/support/vm/openldap
58-
vagrant up
59-
cd ../../../..
60-
INTEGRATION=openldap bundle exec rake rubotest
55+
CI takes too long? If your local box supports
56+
{Vagrant}[https://www.vagrantup.com/], you can run most of the tests
57+
in a VM on your local box. For more details and setup instructions, see
58+
{test/support/vm/openldap/README.md}[https://github.com/ruby-ldap/ruby-net-ldap/tree/master/test/support/vm/openldap/README.md]
6159

6260
== Release
6361

Diff for: lib/net/ldap.rb

+48-36
Original file line numberDiff line numberDiff line change
@@ -476,61 +476,73 @@ def self.result2string(code) #:nodoc:
476476
# specify a treebase. If you give a treebase value in any particular
477477
# call to #search, that value will override any treebase value you give
478478
# here.
479+
# * :force_no_page => Set to true to prevent paged results even if your
480+
# server says it supports them. This is a fix for MS Active Directory
481+
# * :instrumentation_service => An object responsible for instrumenting
482+
# operations, compatible with ActiveSupport::Notifications' public API.
479483
# * :encryption => specifies the encryption to be used in communicating
480484
# with the LDAP server. The value must be a Hash containing additional
481485
# parameters, which consists of two keys:
482486
# method: - :simple_tls or :start_tls
483-
# options: - Hash of options for that method
487+
# tls_options: - Hash of options for that method
484488
# The :simple_tls encryption method encrypts <i>all</i> communications
485489
# with the LDAP server. It completely establishes SSL/TLS encryption with
486490
# the LDAP server before any LDAP-protocol data is exchanged. There is no
487491
# plaintext negotiation and no special encryption-request controls are
488492
# sent to the server. <i>The :simple_tls option is the simplest, easiest
489493
# way to encrypt communications between Net::LDAP and LDAP servers.</i>
490-
# It's intended for cases where you have an implicit level of trust in the
491-
# authenticity of the LDAP server. No validation of the LDAP server's SSL
492-
# certificate is performed. This means that :simple_tls will not produce
493-
# errors if the LDAP server's encryption certificate is not signed by a
494-
# well-known Certification Authority. If you get communications or
495-
# protocol errors when using this option, check with your LDAP server
496-
# administrator. Pay particular attention to the TCP port you are
497-
# connecting to. It's impossible for an LDAP server to support plaintext
498-
# LDAP communications and <i>simple TLS</i> connections on the same port.
499-
# The standard TCP port for unencrypted LDAP connections is 389, but the
500-
# standard port for simple-TLS encrypted connections is 636. Be sure you
501-
# are using the correct port.
502-
#
494+
# If you get communications or protocol errors when using this option,
495+
# check with your LDAP server administrator. Pay particular attention
496+
# to the TCP port you are connecting to. It's impossible for an LDAP
497+
# server to support plaintext LDAP communications and <i>simple TLS</i>
498+
# connections on the same port. The standard TCP port for unencrypted
499+
# LDAP connections is 389, but the standard port for simple-TLS
500+
# encrypted connections is 636. Be sure you are using the correct port.
503501
# The :start_tls like the :simple_tls encryption method also encrypts all
504502
# communcations with the LDAP server. With the exception that it operates
505503
# over the standard TCP port.
506504
#
507-
# In order to verify certificates and enable other TLS options, the
508-
# :tls_options hash can be passed alongside :simple_tls or :start_tls.
509-
# This hash contains any options that can be passed to
510-
# OpenSSL::SSL::SSLContext#set_params(). The most common options passed
511-
# should be OpenSSL::SSL::SSLContext::DEFAULT_PARAMS, or the :ca_file option,
512-
# which contains a path to a Certificate Authority file (PEM-encoded).
513-
#
514-
# Example for a default setup without custom settings:
515-
# {
516-
# :method => :simple_tls,
517-
# :tls_options => OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
518-
# }
505+
# To validate the LDAP server's certificate (a security must if you're
506+
# talking over the public internet), you need to set :tls_options
507+
# something like this...
519508
#
520-
# Example for specifying a CA-File and only allowing TLSv1.1 connections:
521-
#
522-
# {
523-
# :method => :start_tls,
524-
# :tls_options => { :ca_file => "/etc/cafile.pem", :ssl_version => "TLSv1_1" }
509+
# Net::LDAP.new(
510+
# # ... set host, bind dn, etc ...
511+
# encryption: {
512+
# method: :simple_tls,
513+
# tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS,
525514
# }
526-
# * :force_no_page => Set to true to prevent paged results even if your
527-
# server says it supports them. This is a fix for MS Active Directory
528-
# * :instrumentation_service => An object responsible for instrumenting
529-
# operations, compatible with ActiveSupport::Notifications' public API.
515+
# )
516+
#
517+
# The above will use the operating system-provided store of CA
518+
# certificates to validate your LDAP server's cert.
519+
# If cert validation fails, it'll happen during the #bind
520+
# whenever you first try to open a connection to the server.
521+
# Those methods will throw Net::LDAP::ConnectionError with
522+
# a message about certificate verify failing. If your
523+
# LDAP server's certificate is signed by DigiCert, Comodo, etc.,
524+
# you're probably good. If you've got a self-signed cert but it's
525+
# been added to the host's OS-maintained CA store (e.g. on Debian
526+
# add foobar.crt to /usr/local/share/ca-certificates/ and run
527+
# `update-ca-certificates`), then the cert should pass validation.
528+
# To ignore the OS's CA store, put your CA in a PEM-encoded file and...
529+
#
530+
# encryption: {
531+
# method: :simple_tls,
532+
# tls_options: { ca_file: '/path/to/my-little-ca.pem',
533+
# ssl_version: 'TLSv1_1' },
534+
# }
535+
#
536+
# As you might guess, the above example also fails the connection
537+
# if the client can't negotiate TLS v1.1.
538+
# tls_options is ultimately passed to OpenSSL::SSL::SSLContext#set_params
539+
# For more details, see
540+
# http://ruby-doc.org/stdlib-2.0.0/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html
530541
#
531542
# Instantiating a Net::LDAP object does <i>not</i> result in network
532543
# traffic to the LDAP server. It simply stores the connection and binding
533-
# parameters in the object.
544+
# parameters in the object. That's why Net::LDAP.new doesn't throw
545+
# cert validation errors itself; #bind does instead.
534546
def initialize(args = {})
535547
@host = args[:host] || DefaultHost
536548
@port = args[:port] || DefaultPort

Diff for: lib/net/ldap/connection.rb

+14-6
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ def open_connection(server)
5252
hosts.each do |host, port|
5353
begin
5454
prepare_socket(server.merge(socket: @socket_class.new(host, port, socket_opts)), timeout)
55+
if encryption
56+
if encryption[:tls_options] &&
57+
encryption[:tls_options][:verify_mode] &&
58+
encryption[:tls_options][:verify_mode] == OpenSSL::SSL::VERIFY_NONE
59+
warn "not verifying SSL hostname of LDAPS server '#{host}:#{port}'"
60+
else
61+
@conn.post_connection_check(host)
62+
end
63+
end
5564
return
5665
rescue Net::LDAP::Error, SocketError, SystemCallError,
5766
OpenSSL::SSL::SSLError => e
@@ -392,12 +401,11 @@ def search(args = nil)
392401
# should collect this into a private helper to clarify the structure
393402
query_limit = 0
394403
if size > 0
395-
if paged
396-
query_limit = (((size - n_results) < 126) ? (size -
397-
n_results) : 0)
398-
else
399-
query_limit = size
400-
end
404+
query_limit = if paged
405+
(((size - n_results) < 126) ? (size - n_results) : 0)
406+
else
407+
size
408+
end
401409
end
402410

403411
request = [

Diff for: script/generate-fixture-ca

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/bin/bash
2+
3+
BASE_PATH=$( cd "`dirname $0`/../test/fixtures/ca" && pwd )
4+
cd "${BASE_PATH}" || exit 4
5+
6+
USAGE=$( cat << EOS
7+
Usage:
8+
$0 --regenerate
9+
10+
Generates a new self-signed CA, for integration testing. This should only need
11+
to be run if you are writing new TLS/SSL tests, and need to generate
12+
additional fixtuer CAs.
13+
14+
This script uses the GnuTLS certtool CLI. If you are on macOS,
15+
'brew install gnutls', and it will be installed as 'gnutls-certtool'.
16+
Apple unfortunately ships with an incompatible /usr/bin/certtool that does
17+
different things.
18+
EOS
19+
)
20+
21+
if [ "x$1" != 'x--regenerate' ]; then
22+
echo "${USAGE}"
23+
exit 1
24+
fi
25+
26+
TOOL=`type -p certtool`
27+
if [ "$(uname)" = "Darwin" ]; then
28+
TOOL=`type -p gnutls-certtool`
29+
if [ ! -x "${TOOL}" ]; then
30+
echo "Sorry, Darwin requires gnutls-certtool; try `brew install gnutls`"
31+
exit 2
32+
fi
33+
fi
34+
35+
if [ ! -x "${TOOL}" ]; then
36+
echo "Sorry, no certtool found!"
37+
exit 3
38+
fi
39+
export TOOL
40+
41+
42+
${TOOL} --generate-privkey > ./cakey.pem
43+
${TOOL} --generate-self-signed \
44+
--load-privkey ./cakey.pem \
45+
--template ./ca.info \
46+
--outfile ./cacert.pem
47+
48+
echo "cert and private key generated! Don't forget to check them in"

Diff for: script/install-openldap

+38-19
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
set -e
33
set -x
44

5-
BASE_PATH="$( cd `dirname $0`/../test/fixtures/openldap && pwd )"
6-
SEED_PATH="$( cd `dirname $0`/../test/fixtures && pwd )"
5+
BASE_PATH=$( cd "`dirname $0`/../test/fixtures/openldap" && pwd )
6+
SEED_PATH=$( cd "`dirname $0`/../test/fixtures" && pwd )
77

88
dpkg -s slapd time ldap-utils gnutls-bin ssl-cert > /dev/null ||\
99
DEBIAN_FRONTEND=noninteractive apt-get update -y --force-yes && \
@@ -48,47 +48,58 @@ chown -R openldap.openldap /var/lib/ldap
4848
rm -rf $TMPDIR
4949

5050
# SSL
51+
export CA_CERT="/usr/local/share/ca-certificates/rubyldap-ca.crt"
52+
export CA_KEY="/etc/ssl/private/rubyldap-ca.key"
5153

52-
sh -c "certtool --generate-privkey > /etc/ssl/private/cakey.pem"
54+
# The self-signed fixture CA cert & key are generated by
55+
# `script/generate-fiuxture-ca` and checked into version control.
56+
# You shouldn't need to muck with these unless you're writing more
57+
# TLS/SSL integration tests, and need special magic values in the cert.
5358

54-
sh -c "cat > /etc/ssl/ca.info <<EOF
55-
cn = rubyldap
56-
ca
57-
cert_signing_key
58-
EOF"
59+
cp "${SEED_PATH}/ca/cacert.pem" "${CA_CERT}"
60+
cp "${SEED_PATH}/ca/cakey.pem" "${CA_KEY}"
5961

60-
# Create the self-signed CA certificate:
61-
certtool --generate-self-signed \
62-
--load-privkey /etc/ssl/private/cakey.pem \
63-
--template /etc/ssl/ca.info \
64-
--outfile /etc/ssl/certs/cacert.pem
62+
# actually add the fixture CA to the system store
63+
update-ca-certificates
6564

6665
# Make a private key for the server:
6766
certtool --generate-privkey \
68-
--bits 1024 \
69-
--outfile /etc/ssl/private/ldap01_slapd_key.pem
67+
--bits 1024 \
68+
--outfile /etc/ssl/private/ldap01_slapd_key.pem
7069

7170
sh -c "cat > /etc/ssl/ldap01.info <<EOF
7271
organization = Example Company
7372
cn = ldap01.example.com
73+
dns_name = ldap01.example.com
74+
dns_name = ldap02.example.com
75+
dns_name = localhost
7476
tls_www_server
7577
encryption_key
7678
signing_key
7779
expiration_days = 3650
7880
EOF"
7981

82+
# The integration server may be accessed by IP address, in which case
83+
# we want some of the IPs included in the cert. We skip loopback (127.0.0.1)
84+
# because that's the IP we use in the integration test for cert name mismatches.
85+
ADDRS=$(ifconfig -a | grep 'inet addr:' | cut -f 2 -d : | cut -f 1 -d ' ')
86+
for ip in $ADDRS; do
87+
if [ "x$ip" = 'x127.0.0.1' ]; then continue; fi
88+
echo "ip_address = $ip" >> /etc/ssl/ldap01.info
89+
done
90+
8091
# Create the server certificate
8192
certtool --generate-certificate \
8293
--load-privkey /etc/ssl/private/ldap01_slapd_key.pem \
83-
--load-ca-certificate /etc/ssl/certs/cacert.pem \
84-
--load-ca-privkey /etc/ssl/private/cakey.pem \
94+
--load-ca-certificate "${CA_CERT}" \
95+
--load-ca-privkey "${CA_KEY}" \
8596
--template /etc/ssl/ldap01.info \
8697
--outfile /etc/ssl/certs/ldap01_slapd_cert.pem
8798

8899
ldapmodify -Y EXTERNAL -H ldapi:/// <<EOF | true
89100
dn: cn=config
90101
add: olcTLSCACertificateFile
91-
olcTLSCACertificateFile: /etc/ssl/certs/cacert.pem
102+
olcTLSCACertificateFile: ${CA_CERT}
92103
-
93104
add: olcTLSCertificateFile
94105
olcTLSCertificateFile: /etc/ssl/certs/ldap01_slapd_cert.pem
@@ -110,6 +121,14 @@ chmod g+r /etc/ssl/private/ldap01_slapd_key.pem
110121
chmod o-r /etc/ssl/private/ldap01_slapd_key.pem
111122

112123
# Drop packets on a secondary port used to specific timeout tests
113-
iptables -A OUTPUT -p tcp -j DROP --dport 8389
124+
iptables -A INPUT -p tcp -j DROP --dport 8389
125+
126+
# Forward a port for Vagrant
127+
iptables -t nat -A PREROUTING -p tcp --dport 9389 -j REDIRECT --to-port 389
128+
129+
# fix up /etc/hosts for cert validation
130+
grep ldap01 /etc/hosts || echo "127.0.0.1 ldap01.example.com" >> /etc/hosts
131+
grep ldap02 /etc/hosts || echo "127.0.0.1 ldap02.example.com" >> /etc/hosts
132+
grep bogus /etc/hosts || echo "127.0.0.1 bogus.example.com" >> /etc/hosts
114133

115134
service slapd restart

Diff for: test/fixtures/ca/ca.info

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
cn = rubyldap
2+
ca
3+
cert_signing_key
4+
expiration_days = 7200

Diff for: test/fixtures/ca/cacert.pem

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIID7zCCAlegAwIBAgIMV7zWei6SNfABx6jMMA0GCSqGSIb3DQEBCwUAMBMxETAP
3+
BgNVBAMTCHJ1YnlsZGFwMB4XDTE2MDgyMzIzMDQyNloXDTM2MDUxMDIzMDQyNlow
4+
EzERMA8GA1UEAxMIcnVieWxkYXAwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGK
5+
AoIBgQDGe9wziGHZJhIf+IEKSk1tpT9Mu7YgsUwjrlutvkoO1Q6K+amTAVDXizPf
6+
1DVSDpZP5+CfBOznhgLMsPvrQ02w4qx5/6X9L+zJcMk8jTNYSKj5uIKpK52E7Uok
7+
aygMXeaqroPONGkoJIZiVGgdbWfTvcffTm8FOhztXUbMrMXJNinFsocGHEoMNN8b
8+
vqgAyG4+DFHoK4L0c6eQjE4nZBChieZdShUhaBpV7r2qSNbPw67cvAKuEzml58mV
9+
1ZF1F73Ua8gPWXHEfUe2GEfG0NnRq6sGbsDYe/DIKxC7AZ89udZF3WZXNrPhvXKj
10+
ZT7njwcMQemns4dNPQ0k2V4vAQ8pD8r8Qvb65FiSopUhVaGQswAnIMS1DnFq88AQ
11+
KJTKIXbBuMwuaNNSs6R/qTS2RDk1w+CGpRXAg7+1SX5NKdrEsu1IaABA/tQ/zKKk
12+
OLLJaD0giX1weBVmNeFcKxIoT34VS59eEt5APmPcguJnx+aBrA9TLzSO788apBN0
13+
4lGAmR0CAwEAAaNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwQA
14+
MB0GA1UdDgQWBBRTvXSkge03oqLu7UUjFI+oLYwnujANBgkqhkiG9w0BAQsFAAOC
15+
AYEATSZQWH+uSN5GvOUvJ8LHWkeVovn0UhboK0K7GzmMeGz+dp/Xrj6eQ4ONK0zI
16+
RCJyoo/nCR7CfQ5ujVXr03XD2SUgyD565ulXuhw336DasL5//fucmQYDeqhwbKML
17+
FTzsF9H9dO4J5TjxJs7e5dRJ0wrP/XEY+WFhXXdSHTl8vGCI6QqWc7TvDpmbS4iX
18+
uTzjJswu9Murt9JUJNMN2DlDi/vBBeruaj4c2cMMnKMvkfj14kd8wMocmzj+gVQl
19+
r+fRQbKAJNec65lA4/Zeb6sD9SAi0ZIVgxA4a7g8/sdNWHIAxPicpJkIJf30TsyY
20+
F+8+Hd5mBtCbvFfAVkT6bHBP1OiAgNke+Rh/j/sQbyWbKCKw0+jpFJgO9KUNGfC0
21+
O/CqX+J4G7HqL8VJqrLnBvOdhfetAvNQtf1gcw5ZwpeEFM+Kvx/lsILaIYdAUSjX
22+
ePOc5gI2Bi9WXq+T9AuhSf+TWUR874m/rdTWe5fM8mXCNl7C4I5zCqLltEDkSoMP
23+
jDj/
24+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)