aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarin Ivanov <[email protected]>2019-02-13 05:42:29 +0200
committerMarin Ivanov <[email protected]>2019-02-13 05:42:29 +0200
commit2433beed9d9365af0a54e376c6b5ce97963d7bc3 (patch)
tree5237d1f68c2fd2ffa8b2eb5c5b34c91eecfdb9cf
parent518f72942c1cf751010a532c6189cd0eb0a5323b (diff)
parent36ac64bc2b67871d0de16f65a45c7458730e49be (diff)
Merge branch 'enforce-tls'
-rw-r--r--CONTRIBUTORS.md15
-rw-r--r--README.md59
-rw-r--r--messages.go3
-rw-r--r--server.go31
-rw-r--r--server_test.go135
5 files changed, 216 insertions, 27 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
new file mode 100644
index 0000000..0af02ed
--- /dev/null
+++ b/CONTRIBUTORS.md
@@ -0,0 +1,15 @@
+# Client library
+
+- [mmitton](https://github.com/mmitton)
+- [uavila](https://github.com/uavila)
+- [vanackere](https://github.com/vanackere)
+- [juju2013](https://github.com/juju2013)
+- [johnweldon](https://github.com/johnweldon)
+- [marcsauter](https://github.com/marcsauter)
+- [nmcclain](https://github.com/nmcclain)
+
+# Server library
+
+- [nmcclain](https://github.com/nmcclain)
+- [mark-rushakoff](https://github.com/mark-rushakoff)
+- [metala](https://github.com/metala)
diff --git a/README.md b/README.md
index 2418eab..9b38336 100644
--- a/README.md
+++ b/README.md
@@ -4,12 +4,12 @@ This library provides basic LDAP v3 functionality for the GO programming languag
The **client** portion is limited, but sufficient to perform LDAP authentication and directory lookups (binds and searches) against any modern LDAP server (tested with OpenLDAP and AD).
-The **server** portion implements Bind and Search from [RFC4510](http://tools.ietf.org/html/rfc4510), has good testing coverage, and is compatible with any LDAPv3 client. It provides the building blocks for a custom LDAP server, but you must implement the backend datastore of your choice.
-
+The **server** portion implements Bind and Search from [RFC4510](http://tools.ietf.org/html/rfc4510), has good testing coverage, and is compatible with any LDAPv3 client. It provides the building blocks for a custom LDAP server, but you must implement the backend datastore of your choice.
## LDAP client notes:
### A simple LDAP bind operation:
+
```go
l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
// be sure to add error checking!
@@ -23,6 +23,7 @@ if err==nil {
```
### A simple LDAP search operation:
+
```go
search := &SearchRequest{
BaseDN: "dc=example,dc=com",
@@ -33,25 +34,28 @@ searchResults, err := l.Search(search)
```
### Implemented:
-* Connecting, binding to LDAP server
-* Searching for entries with filtering and paging controls
-* Compiling string filters to LDAP filters
-* Modify Requests / Responses
+
+- Connecting, binding to LDAP server
+- Searching for entries with filtering and paging controls
+- Compiling string filters to LDAP filters
+- Modify Requests / Responses
### Not implemented:
-* Add, Delete, Modify DN, Compare operations
-* Most tests / benchmarks
+
+- Add, Delete, Modify DN, Compare operations
+- Most tests / benchmarks
### LDAP client examples:
-* examples/search.go: **Basic client bind and search**
-* examples/searchSSL.go: **Client bind and search over SSL**
-* examples/searchTLS.go: **Client bind and search over TLS**
-* examples/modify.go: **Client modify operation**
-*Client library by: [mmitton](https://github.com/mmitton), with contributions from: [uavila](https://github.com/uavila), [vanackere](https://github.com/vanackere), [juju2013](https://github.com/juju2013), [johnweldon](https://github.com/johnweldon), [marcsauter](https://github.com/marcsauter), and [nmcclain](https://github.com/nmcclain)*
+- examples/search.go: **Basic client bind and search**
+- examples/searchSSL.go: **Client bind and search over SSL**
+- examples/searchTLS.go: **Client bind and search over TLS**
+- examples/modify.go: **Client modify operation**
## LDAP server notes:
-The server library is modeled after net/http - you designate handlers for the LDAP operations you want to support (Bind/Search/etc.), then start the server with ListenAndServe(). You can specify different handlers for different baseDNs - they must implement the interfaces of the operations you want to support:
+
+The server library is modeled after net/http - you designate handlers for the LDAP operations you want to support (Bind/Search/etc.), then start the server with ListenAndServe(). You can specify different handlers for different baseDNs - they must implement the interfaces of the operations you want to support:
+
```go
type Binder interface {
Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error)
@@ -65,6 +69,7 @@ type Closer interface {
```
### A basic bind-only LDAP server
+
```go
func main() {
s := ldap.NewServer()
@@ -84,22 +89,28 @@ func (h ldapHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldap.LDAP
}
```
-* Server.EnforceLDAP: Normally, the LDAP server will return whatever results your handler provides. Set the **Server.EnforceLDAP** flag to **true** and the server will apply the LDAP **search filter**, **attributes limits**, **size/time limits**, **search scope**, and **base DN matching** to your handler's dataset. This makes it a lot simpler to write a custom LDAP server without worrying about LDAP internals.
+- Server.EnforceLDAP: Normally, the LDAP server will return whatever results your handler provides. Set the **Server.EnforceLDAP** flag to **true** and the server will apply the LDAP **search filter**, **attributes limits**, **size/time limits**, **search scope**, and **base DN matching** to your handler's dataset. This makes it a lot simpler to write a custom LDAP server without worrying about LDAP internals.
+- Server.TLSConfig: If you set this variable, you will enable TLS connection upgrades.
+- Server.EnforceTLS: This setting enforces and requires a TLS upgrade before any LDAP operation.
### LDAP server examples:
-* examples/server.go: **Basic LDAP authentication (bind and search only)**
-* examples/proxy.go: **Simple LDAP proxy server.**
-* server_test.go: **The _test.go files have examples of all server functions.**
+
+- examples/server.go: **Basic LDAP authentication (bind and search only)**
+- examples/proxy.go: **Simple LDAP proxy server.**
+- server_test.go: **The \_test.go files have examples of all server functions.**
### Known limitations:
-* Golang's TLS implementation does not support SSLv2. Some old OSs require SSLv2, and are not able to connect to an LDAP server created with this library's ListenAndServeTLS() function. If you *must* support legacy (read: *insecure*) SSLv2 clients, run your LDAP server behind HAProxy.
+- Golang's TLS implementation does not support SSLv2. Some old OSs require SSLv2, and are not able to connect to an LDAP server created with this library's ListenAndServeTLS() function. If you _must_ support legacy (read: _insecure_) SSLv2 clients, run your LDAP server behind HAProxy.
### Not implemented:
+
From the server perspective, all of [RFC4510](http://tools.ietf.org/html/rfc4510) is implemented **except**:
-* 4.5.1.3. SearchRequest.derefAliases
-* 4.5.1.5. SearchRequest.timeLimit
-* 4.5.1.6. SearchRequest.typesOnly
-* 4.14. StartTLS Operation
-*Server library by: [nmcclain](https://github.com/nmcclain)*
+- 4.5.1.3. SearchRequest.derefAliases
+- 4.5.1.5. SearchRequest.timeLimit
+- 4.5.1.6. SearchRequest.typesOnly
+
+## Contributors
+
+See: [CONTRIBUTORS.md](CONTRIBUTORS.md)
diff --git a/messages.go b/messages.go
new file mode 100644
index 0000000..a0d1450
--- /dev/null
+++ b/messages.go
@@ -0,0 +1,3 @@
+package ldap
+
+const errorEnforceTLSRequiresTLSConfig = "EnforceTLS requires TLSConfig set"
diff --git a/server.go b/server.go
index bc7848d..5bad5ff 100644
--- a/server.go
+++ b/server.go
@@ -2,6 +2,7 @@ package ldap
import (
"crypto/tls"
+ "errors"
"io"
"log"
"net"
@@ -60,7 +61,8 @@ type Server struct {
Stats *Stats
// If set, server will accept StartTLS.
- TLSConfig *tls.Config
+ TLSConfig *tls.Config
+ EnforceTLS bool
closing chan struct{}
}
@@ -187,6 +189,10 @@ func (server *Server) ListenAndServe(listenString string) error {
}
func (server *Server) Serve(ln net.Listener) error {
+ if server.TLSConfig == nil && server.EnforceTLS {
+ return errors.New(errorEnforceTLSRequiresTLSConfig)
+ }
+
newConn := make(chan net.Conn)
go func() {
for {
@@ -262,6 +268,19 @@ handler:
}
}
+ // Enforce TLS
+ switch conn.(type) {
+ case *tls.Conn:
+ default:
+ if server.EnforceTLS && req.Tag != ApplicationExtendedRequest {
+ responsePacket := encodeLDAPResponse(messageID, ApplicationExtendedResponse, LDAPResultProtocolError, "Upgrade to TLS is required")
+ if err = sendPacket(conn, responsePacket); err != nil {
+ log.Printf("sendPacket error %s", err.Error())
+ }
+ break handler
+ }
+ }
+
//log.Printf("DEBUG: handling operation: %s [%d]", ApplicationMap[req.Tag], req.Tag)
//ber.PrintPacket(packet) // DEBUG
@@ -318,8 +337,12 @@ handler:
}
var ldapResultCode LDAPResultCode
if tlsConn == nil {
- // Wasn't an upgrade. Pass through.
- ldapResultCode = HandleExtendedRequest(req, boundDN, server.ExtendedFns, conn)
+ // Wasn't an upgrade.
+ if server.EnforceTLS {
+ ldapResultCode = LDAPResultProtocolError
+ } else {
+ ldapResultCode = HandleExtendedRequest(req, boundDN, server.ExtendedFns, conn)
+ }
} else {
ldapResultCode = LDAPResultSuccess
}
@@ -330,6 +353,8 @@ handler:
}
if tlsConn != nil {
conn = tlsConn
+ } else if server.EnforceTLS {
+ break handler
}
case ApplicationAbandonRequest:
HandleAbandonRequest(req, boundDN, server.AbandonFns, conn)
diff --git a/server_test.go b/server_test.go
index 9b9d579..9cde9d7 100644
--- a/server_test.go
+++ b/server_test.go
@@ -230,6 +230,141 @@ which is very heavy-handed for a test like this.
}
}
+func TestEnforcedTLSWithoutTLSConfig(t *testing.T) {
+ s := NewServer()
+ defer s.Close()
+ s.EnforceTLS = true
+ s.Bind = BindAnonOK
+ s.Search = SearchSimple
+
+ ln, _ := mustListen()
+ done := make(chan error)
+ go func() {
+ if err := s.Serve(ln); err != nil {
+ done <- err
+ }
+ }()
+
+ select {
+ case err := <-done:
+ msg := err.Error()
+ if msg != errorEnforceTLSRequiresTLSConfig {
+ t.Errorf("Unexpected server error: %s", msg)
+ }
+ case <-time.After(timeout):
+ t.Error("server did not return an error")
+ }
+}
+func TestEnforcedTLS(t *testing.T) {
+ if runtime.GOOS == "darwin" {
+ defer func() {
+ if t.Failed() {
+ t.Logf(`NOTE: this test won't pass with the built-in Mac ldap utilities.
+Work around this by using brew install openldap, and running the test as PATH=/usr/local/opt/openldap/bin:$PATH go test.
+
+This test uses environment variables that are respected by OpenLDAP, but the Mac utilities don't let you override
+security settings through environment variables; they expect certificates to be added to the system keychain,
+which is very heavy-handed for a test like this.
+`)
+ }
+ }()
+ }
+ cert := newSelfSignedCert()
+ defer cert.cleanup()
+
+ s := NewServer()
+ defer s.Close()
+ s.EnforceTLS = true
+ s.Bind = BindAnonOK
+ s.Search = SearchSimple
+ s.TLSConfig = cert.ServerTLSConfig()
+
+ ln, addr := mustListen()
+ go func() {
+ if err := s.Serve(ln); err != nil {
+ t.Errorf("s.Serve failed: %s", err.Error())
+ }
+ }()
+
+ done := make(chan struct{})
+ go func() {
+ cmd := exec.Command("env",
+ "LDAPTLS_CACERT="+cert.CACertPath,
+ "ldapsearch", "-H", "ldap://"+addr, "-ZZ", "-d", "-1", "-x", "-b", "o=testers,c=test")
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Error(err)
+ }
+
+ if !strings.Contains(string(out), "# numEntries: 3") || !strings.Contains(string(out), "result: 0 Success") {
+ t.Errorf("search did not succeed:\n%s", out)
+ }
+
+ close(done)
+ }()
+
+ select {
+ case <-done:
+ case <-time.After(timeout):
+ t.Error("ldapsearch command timed out")
+ }
+}
+
+func TestEnforcedTLSFail(t *testing.T) {
+ if runtime.GOOS == "darwin" {
+ defer func() {
+ if t.Failed() {
+ t.Logf(`NOTE: this test won't pass with the built-in Mac ldap utilities.
+Work around this by using brew install openldap, and running the test as PATH=/usr/local/opt/openldap/bin:$PATH go test.
+
+This test uses environment variables that are respected by OpenLDAP, but the Mac utilities don't let you override
+security settings through environment variables; they expect certificates to be added to the system keychain,
+which is very heavy-handed for a test like this.
+`)
+ }
+ }()
+ }
+ cert := newSelfSignedCert()
+ defer cert.cleanup()
+
+ s := NewServer()
+ defer s.Close()
+ s.EnforceTLS = true
+ s.Bind = BindAnonOK
+ s.Search = SearchSimple
+ s.TLSConfig = cert.ServerTLSConfig()
+
+ ln, addr := mustListen()
+ go func() {
+ if err := s.Serve(ln); err != nil {
+ t.Errorf("s.Serve failed: %s", err.Error())
+ }
+ }()
+
+ done := make(chan struct{})
+ go func() {
+ cmd := exec.Command("env",
+ "LDAPTLS_CACERT="+cert.CACertPath,
+ "ldapsearch", "-H", "ldap://"+addr, "-d", "-1", "-x", "-b", "o=testers,c=test")
+ out, err := cmd.CombinedOutput()
+ if err == nil {
+ t.Error("search should have failed")
+ }
+
+ if strings.Contains(string(out), "result: 0 Success") {
+ t.Errorf("search did succeed:\n%s", out)
+ }
+
+ close(done)
+ }()
+
+ select {
+ case <-done:
+ case <-time.After(timeout):
+ t.Error("ldapsearch command timed out")
+ }
+}
+
/////////////////////////
func TestBindAnonOK(t *testing.T) {
done := make(chan bool)