diff options
Diffstat (limited to 'ldap.go')
-rw-r--r-- | ldap.go | 227 |
1 files changed, 227 insertions, 0 deletions
@@ -0,0 +1,227 @@ +package main + +import ( + "crypto/sha256" + "crypto/tls" + "encoding/base64" + "fmt" + "gopkg.in/ldap.v2" + "log" + "strings" +) + +// Search ldap for keys... +type LdapInfo struct { + Server string + Port int + User string + Password string + SSLSkipVerify bool + UserDNFmt string +} + +func (i *LdapInfo) LdapConnect() (*ldap.Conn, error) { + var tlsConf *tls.Config + if i.SSLSkipVerify { + tlsConf = &tls.Config{InsecureSkipVerify: true} + } else { + tlsConf = &tls.Config{ServerName: i.Server} + } + l, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", i.Server, i.Port), tlsConf) + if err != nil { + return nil, fmt.Errorf("LDAP Unable to connect to %s on port %d. Got error: %s", i.Server, i.Port, err) + } + + return l, nil +} + +func (i *LdapInfo) LdapConnectBind() (*ldap.Conn, error) { + // check if we have LDAP credentials + if i.User == "" || i.Password == "" { + return nil, fmt.Errorf("LDAP Bind user and/or password missing") + } + + l, err := i.LdapConnect() + if err != nil { + return nil, err + } + + err = l.Bind(i.User, i.Password) + if err != nil { + l.Close() + return nil, err + } + return l, nil +} + +func (i *LdapInfo) UserDN(username string) string { + if i.UserDNFmt != "" { + return fmt.Sprintf(i.UserDNFmt, username) + } else { + return fmt.Sprintf("uid=%s,ou=People,dc=nordu,dc=net", username) + } +} + +type SSHPubKey struct { + Format string + Key string + Comment string + Fingerprint string +} + +func NewSSHPubKey(ssh_key string) SSHPubKey { + key_parts := strings.SplitN(ssh_key, " ", 3) + comment := "" + if len(key_parts) > 2 { + comment = key_parts[2] + } + return SSHPubKey{key_parts[0], key_parts[1], comment, calculateFingerprint(key_parts[1])} +} + +func (k SSHPubKey) String() string { + return fmt.Sprintf("%s %s %s", k.Format, k.Key, k.Comment) +} + +func (k SSHPubKey) KeyEnd() string { + i := len(k.Key) - 8 + if i < 0 { + i = 0 + } + return k.Key[i:] +} + +// Get SSH keys +// Preferably keyformat, fingerprint, comment, but full key is how it is now +func (i *LdapInfo) GetSSHKeys(username string) ([]SSHPubKey, error) { + l, err := i.LdapConnect() + if err != nil { + return nil, err + } + defer l.Close() + + searchRequest := ldap.NewSearchRequest( + i.UserDN(username), + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + "(objectClass=person)", + []string{"sshPublicKey"}, + nil, + ) + + sr, err := l.Search(searchRequest) + if err != nil { + return nil, fmt.Errorf("LDAP Search for user '%s' failed: %s", username, err) + } + + if len(sr.Entries) < 1 { + return nil, fmt.Errorf("LDAP User %s does not exist", username) + } else if len(sr.Entries) > 1 { + return nil, fmt.Errorf("LDAP User %s returned more than one enty (Results: %d)", username, len(sr.Entries)) + } + + pubKeys := make([]SSHPubKey, len(sr.Entries[0].GetAttributeValues("sshPublicKey"))) + for i, key := range sr.Entries[0].GetAttributeValues("sshPublicKey") { + pubKeys[i] = NewSSHPubKey(key) + } + return pubKeys, nil + +} + +// Add ssh key +func (i *LdapInfo) AddSSHKey(username string, ssh_keys []string) error { + l, err := i.LdapConnectBind() + if err != nil { + return err + } + defer l.Close() + + dn := i.UserDN(username) + // Add objectClass ldapPublicKey if missing + mod := ldap.NewModifyRequest(dn) + mod.Add("objectClass", []string{"ldapPublicKey"}) + err = l.Modify(mod) + if err != nil { + // check err if type or value exist + } + + // Add keys + // One mod req per key, to handle errors for existing keys + for _, key := range ssh_keys { + if err = validateSSHkey(key); err != nil { + log.Println(err) + } else { + mod = ldap.NewModifyRequest(dn) + mod.Add("sshPublicKey", []string{key}) + err = l.Modify(mod) + if err != nil { + // Ignore Attribute or value exists errors + if !strings.Contains(err.Error(), "Code 20") { + return err + } + } + } + } + + return nil +} + +// Delete ssh key +// Use fingerprint, or full key to delete +func (i *LdapInfo) DeleteSSHKey(username, ssh_key string) error { + l, err := i.LdapConnectBind() + if err != nil { + return err + } + defer l.Close() + + del := ldap.NewModifyRequest(i.UserDN(username)) + del.Delete("sshPublicKey", []string{ssh_key}) + err = l.Modify(del) + if err != nil { + // Ignore error about No such attribute + if !strings.Contains(err.Error(), "Code 16") { + return err + } + } + return nil +} + +// Sanity checks on a ssh key. +// Checks if key has 2-3 parts (key_format, key, comment) +// Checks if key_format is: ssh-rsa or ssh-ed25519 +// If ssh-rsa check that key is at least 2048 bit +// Check that key can be base64 decoded +func validateSSHkey(ssh_key string) error { + key_parts := strings.SplitN(ssh_key, " ", 3) + if len(key_parts) < 2 || len(key_parts) > 3 { + return fmt.Errorf("SSH key is invalid. Expected 2-3 parts, got %d. Key was: '%s'", len(key_parts), ssh_key) + } + // Check base64 + decoded_key, err := base64.StdEncoding.DecodeString(key_parts[1]) + if err != nil { + return fmt.Errorf("SSH key is not properly base64 encoded. Key was: %s", ssh_key) + } + // Check keyformat: ssh-rsa, ssh-ed25519 + switch key_parts[0] { + case "ssh-rsa": + padding := strings.Count(key_parts[1], "=") + key_length := (len(decoded_key) - padding) * 8 + if key_length < 2048 { + return fmt.Errorf("SSH rsa key should at least be a 2048 bit key. Was: %d", key_length) + } + case "ssh-ed25519": + // nothing to check + default: + return fmt.Errorf("SSH key is not an acceptable format (ssh-rsa or ssh-ed25519). Key was: %s", ssh_key) + } + // Key looks ok + return nil +} + +func calculateFingerprint(ssh_key string) string { + key, _ := base64.StdEncoding.DecodeString(ssh_key) + fingerprint := sha256.Sum256(key) + return fmt.Sprintf("SHA256:%s", base64.StdEncoding.EncodeToString(fingerprint[:])) + //return fmt.Sprintf("SHA256:%x", fingerprint) +} + +//// set_nordunet_ldap_pw_sasl used on sso pw set if change pw fail? |