From bade33cf1529893a92a283f6dc86e73f8766049e Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Sun, 19 Jan 2020 12:49:49 +0100 Subject: [PATCH] Begin Go reimplementation of Bottin --- .gitignore | 1 + main.go | 212 +++++++++++++++++++++++++++++++++++++++++++++++++++++ ssha.go | 53 ++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 .gitignore create mode 100644 main.go create mode 100644 ssha.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5e5c05 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +gobottin diff --git a/main.go b/main.go new file mode 100644 index 0000000..baf0e09 --- /dev/null +++ b/main.go @@ -0,0 +1,212 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + "syscall" + "strings" + "encoding/json" + "encoding/base64" + "math/rand" + + consul "github.com/hashicorp/consul/api" + ldap "github.com/vjeantet/ldapserver" + message "github.com/vjeantet/goldap/message" +) + +func dnToConsul(dn string) string { + rdns := strings.Split(dn, ",") + + // Reverse rdns + for i, j := 0, len(rdns)-1; i < j; i, j = i+1, j-1 { + rdns[i], rdns[j] = rdns[j], rdns[i] + } + + return strings.Join(rdns, "/") +} + +type DNComponent struct { + Type string + Value string +} + +func parseDN(dn string) ([]DNComponent, error) { + rdns := strings.Split(dn, ",") + + ret := []DNComponent{} + + for _, rdn := range rdns { + splits := strings.Split(rdn, "=") + if len(splits) != 2 { + return nil, fmt.Errorf("Wrong DN component: %s (expected type=value)", rdn) + } + ret = append(ret, DNComponent{ + Type: splits[0], + Value: splits[1], + }) + } + return ret, nil +} + +type Config struct { + Suffix string +} + +type Server struct { + config Config + kv *consul.KV +} + +type Attributes map[string]interface{} + +func main() { + //ldap logger + ldap.Logger = log.New(os.Stdout, "[server] ", log.LstdFlags) + + // Connect to Consul + client, err := consul.NewClient(consul.DefaultConfig()) + if err != nil { + panic(err) + } + kv := client.KV() + + // TODO read config from somewhere + config := Config { + Suffix: "dc=gobottin,dc=eu", + } + + gobottin := Server{config: config, kv: kv} + err = gobottin.init() + if err != nil { + panic(err) + } + + //Create a new LDAP Server + ldapserver := ldap.NewServer() + + routes := ldap.NewRouteMux() + routes.Bind(gobottin.handleBind) + ldapserver.Handle(routes) + + // listen on 10389 + go ldapserver.ListenAndServe("127.0.0.1:10389") + + // When CTRL+C, SIGINT and SIGTERM signal occurs + // Then stop server gracefully + ch := make(chan os.Signal) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + <-ch + close(ch) + + ldapserver.Stop() +} + +func (server *Server) init() error { + pair, _, err := server.kv.Get(dnToConsul(server.config.Suffix) + "/attribute=objectClass", nil) + if err != nil { + return err + } + + if pair != nil { + return nil + } + + base_attributes := Attributes{ + "objectClass": []string{"top", "dcObject", "organization"}, + "structuralObjectClass": "Organization", + } + suffix_dn, err := parseDN(server.config.Suffix) + if err != nil { + return err + } + base_attributes[suffix_dn[0].Type] = suffix_dn[0].Value + + err = server.addElements(server.config.Suffix, base_attributes) + if err != nil { + return err + } + + admin_pass := make([]byte, 8) + rand.Read(admin_pass) + admin_pass_str := base64.RawURLEncoding.EncodeToString(admin_pass) + admin_pass_hash := SSHAEncode([]byte(admin_pass_str)) + + admin_dn := "cn=admin," + server.config.Suffix + admin_attributes := Attributes{ + "objectClass": []string{"simpleSecurityObject", "organizationalRole"}, + "description": "LDAP administrator", + "cn": "admin", + "userpassword": admin_pass_hash, + "structuralObjectClass": "organizationalRole", + "permissions": []string{"read", "write"}, + } + + err = server.addElements(admin_dn, admin_attributes) + if err != nil { + return err + } + + log.Printf( + "It seems to be a new installation, we created a default user for you:\n\n dn: %s\n password: %s\n\nWe didn't use true random, you should replace it as soon as possible.", + admin_dn, + admin_pass_str, + ) + + return nil +} + +func (server *Server) addElements(dn string, attrs Attributes) error { + prefix := dnToConsul(dn) + for k, v := range attrs { + json, err := json.Marshal(v) + if err != nil { + return err + } + pair := &consul.KVPair{Key: prefix + "/attribute=" + k, Value: json} + _, err = server.kv.Put(pair, nil) + if err != nil { + return err + } + } + return nil +} + +func (server *Server) handleBind(w ldap.ResponseWriter, m *ldap.Message) { + r := m.GetBindRequest() + + result_code, err := server.handleBindInternal(w, r) + + res := ldap.NewBindResponse(result_code) + if err != nil { + res.SetDiagnosticMessage(err.Error()) + log.Printf("Failed bind for %s: %s", string(r.Name()), err.Error()) + } + w.Write(res) +} + +func (server *Server) handleBindInternal(w ldap.ResponseWriter, r message.BindRequest) (int, error) { + + pair, _, err := server.kv.Get(dnToConsul(string(r.Name())) + "/attribute=userpassword", nil) + if err != nil { + return ldap.LDAPResultOperationsError, err + } + + if pair == nil { + return ldap.LDAPResultNoSuchObject, nil + } + + hash := "" + err = json.Unmarshal(pair.Value, &hash) + if err != nil { + return ldap.LDAPResultOperationsError, err + } + + valid := SSHAMatches(hash, []byte(r.AuthenticationSimple())) + if valid { + return ldap.LDAPResultSuccess, nil + } else { + return ldap.LDAPResultInvalidCredentials, nil + } +} diff --git a/ssha.go b/ssha.go new file mode 100644 index 0000000..203b994 --- /dev/null +++ b/ssha.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "bytes" + "math/rand" + "encoding/base64" + "crypto/sha1" +) + +// Encode encodes the []byte of raw password +func SSHAEncode(rawPassPhrase []byte) string { + hash := makeSSHAHash(rawPassPhrase, makeSalt()) + b64 := base64.StdEncoding.EncodeToString(hash) + return fmt.Sprintf("{ssha}%s", b64) +} + +// Matches matches the encoded password and the raw password +func SSHAMatches(encodedPassPhrase string, rawPassPhrase []byte) bool { + if encodedPassPhrase[:6] != "{ssha}" { + return false + } + + bhash, err := base64.StdEncoding.DecodeString(encodedPassPhrase[6:]) + if err != nil { + return false + } + salt := bhash[20:] + + newssha := makeSSHAHash(rawPassPhrase, salt) + + if bytes.Compare(newssha, bhash) != 0 { + return false + } + return true +} + +// makeSalt make a 32 byte array containing random bytes. +func makeSalt() []byte { + sbytes := make([]byte, 32) + rand.Read(sbytes) + return sbytes +} + +// makeSSHAHash make hasing using SHA-1 with salt. This is not the final output though. You need to append {SSHA} string with base64 of this hash. +func makeSSHAHash(passphrase, salt []byte) []byte { + sha := sha1.New() + sha.Write(passphrase) + sha.Write(salt) + + h := sha.Sum(nil) + return append(h, salt...) +}