github.com/jiasir/deis@v1.12.2/builder/sshd/sshd.go (about) 1 package sshd 2 3 import ( 4 "bufio" 5 "crypto/md5" 6 "crypto/subtle" 7 "encoding/hex" 8 "fmt" 9 "io/ioutil" 10 "os" 11 "os/exec" 12 "strings" 13 14 "golang.org/x/crypto/ssh" 15 16 "github.com/Masterminds/cookoo" 17 "github.com/Masterminds/cookoo/log" 18 ) 19 20 // ParseAuthorizedKeys reads and process an authorized_keys file. 21 // 22 // The file is merely parsed into lines, which are then returned in an array. 23 // 24 // Params: 25 // - path (string): The path to the authorized_keys file. 26 // 27 // Returns: 28 // []string of keys. 29 // 30 func ParseAuthorizedKeys(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 31 path := p.Get("path", "~/.ssh/authorized_keys").(string) 32 33 file, err := os.Open(path) 34 if err != nil { 35 return []string{}, err 36 } 37 defer file.Close() 38 39 reader := bufio.NewScanner(file) 40 buf := []string{} 41 42 for reader.Scan() { 43 data := reader.Text() 44 if len(data) > 0 { 45 log.Infof(c, "Adding key '%s'", data) 46 buf = append(buf, strings.TrimSpace(data)) 47 } 48 } 49 50 return buf, nil 51 52 } 53 54 // ParseHostKeys parses the host key files. 55 // 56 // By default it looks in /etc/ssh for host keys of the patterh ssh_host_{{TYPE}}_key. 57 // 58 // Params: 59 // - keytypes ([]string): Key types to parse. Defaults to []string{rsa, dsa, ecdsa} 60 // - enableV1 (bool): Allow V1 keys. By default this is disabled. 61 // - path (string): Override the lookup pattern. If %s, it will be replaced with the keytype. 62 // 63 // Returns: 64 // []ssh.Signer 65 func ParseHostKeys(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 66 hostKeyTypes := p.Get("keytypes", []string{"rsa", "dsa", "ecdsa"}).([]string) 67 pathTpl := p.Get("path", "/etc/ssh/ssh_host_%s_key").(string) 68 hostKeys := make([]ssh.Signer, 0, len(hostKeyTypes)) 69 for _, t := range hostKeyTypes { 70 path := fmt.Sprintf(pathTpl, t) 71 72 if key, err := ioutil.ReadFile(path); err == nil { 73 if hk, err := ssh.ParsePrivateKey(key); err == nil { 74 log.Infof(c, "Parsed host key %s.", path) 75 hostKeys = append(hostKeys, hk) 76 } else { 77 log.Errf(c, "Failed to parse host key %s (skipping): %s", path, err) 78 } 79 } 80 } 81 if c.Get("enableV1", false).(bool) { 82 path := "/etc/ssh/ssh_host_key" 83 if key, err := ioutil.ReadFile(path); err != nil { 84 log.Errf(c, "Failed to read ssh_host_key") 85 } else if hk, err := ssh.ParsePrivateKey(key); err == nil { 86 log.Infof(c, "Parsed host key %s.", path) 87 hostKeys = append(hostKeys, hk) 88 } else { 89 log.Errf(c, "Failed to parse host key %s: %s", path, err) 90 } 91 } 92 return hostKeys, nil 93 } 94 95 // AuthKey authenticates based on a public key. 96 // 97 // Params: 98 // - metadata (ssh.ConnMetadata) 99 // - key (ssh.PublicKey) 100 // - authorizedKeys ([]string): List of lines from an authorized keys file. 101 // 102 // Returns: 103 // *ssh.Permissions 104 // 105 func AuthKey(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 106 meta := p.Get("metadata", nil).(ssh.ConnMetadata) 107 key := p.Get("key", nil).(ssh.PublicKey) 108 authorized := p.Get("authorizedKeys", []string{}).([]string) 109 110 auth := new(ssh.CertChecker) 111 auth.UserKeyFallback = func(meta ssh.ConnMetadata, pk ssh.PublicKey) (*ssh.Permissions, error) { 112 113 // This gives us a string in the form "ssh-rsa LONG_KEY" 114 suppliedType := key.Type() 115 supplied := key.Marshal() 116 117 for _, allowedKey := range authorized { 118 allowed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(allowedKey)) 119 if err != nil { 120 log.Infof(c, "Could not parse authorized key '%q': %s", allowedKey, err) 121 continue 122 } 123 124 // We use a contstant time compare more as a precaution than anything 125 // else. A timing attack here would be very difficult, but... better 126 // safe than sorry. 127 if allowed.Type() == suppliedType && subtle.ConstantTimeCompare(allowed.Marshal(), supplied) == 1 { 128 log.Infof(c, "Key accepted for user %s.", meta.User()) 129 perm := &ssh.Permissions{ 130 Extensions: map[string]string{ 131 "user": meta.User(), 132 }, 133 } 134 return perm, nil 135 } 136 } 137 138 return nil, fmt.Errorf("No matching keys found.") 139 } 140 141 return auth.Authenticate(meta, key) 142 } 143 144 // compareKeys compares to key files and returns true of they match. 145 func compareKeys(a, b ssh.PublicKey) bool { 146 if a.Type() != b.Type() { 147 return false 148 } 149 // The best way to compare just the key seems to be to marshal both and 150 // then compare the output byte sequence. 151 return subtle.ConstantTimeCompare(a.Marshal(), b.Marshal()) == 1 152 } 153 154 // Start starts an instance of /usr/sbin/sshd. 155 func Start(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 156 dargs := []string{"-e", "-D"} 157 158 sshd := exec.Command("/usr/sbin/sshd", dargs...) 159 sshd.Stdout = os.Stdout 160 sshd.Stderr = os.Stderr 161 162 if err := sshd.Start(); err != nil { 163 return 0, err 164 } 165 166 return sshd.Process.Pid, nil 167 } 168 169 // Configure creates a new SSH configuration object. 170 // 171 // Config sets a PublicKeyCallback handler that forwards public key auth 172 // requests to the route named "pubkeyAuth". 173 // 174 // This assumes certain details about our environment, like the location of the 175 // host keys. It also provides only key-based authentication. 176 // ConfigureServerSshConfig 177 // 178 // Returns: 179 // An *ssh.ServerConfig 180 func Configure(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 181 router := c.Get("cookoo.Router", nil).(*cookoo.Router) 182 183 cfg := &ssh.ServerConfig{ 184 PublicKeyCallback: func(m ssh.ConnMetadata, k ssh.PublicKey) (*ssh.Permissions, error) { 185 c.Put("metadata", m) 186 c.Put("key", k) 187 188 pubkeyAuth := c.Get("route.sshd.pubkeyAuth", "pubkeyAuth").(string) 189 err := router.HandleRequest(pubkeyAuth, c, true) 190 return c.Get("pubkeyAuth", &ssh.Permissions{}).(*ssh.Permissions), err 191 }, 192 } 193 194 return cfg, nil 195 } 196 197 // FingerprintKey fingerprints a key and returns the colon-formatted version 198 // 199 // Params: 200 // - key (ssh.PublicKey): The key to fingerprint. 201 // 202 // Returns: 203 // - A string representation of the key fingerprint. 204 func FingerprintKey(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) { 205 key := p.Get("key", nil).(ssh.PublicKey) 206 return Fingerprint(key), nil 207 } 208 209 // Fingerprint generates a colon-separated fingerprint string from a public key. 210 func Fingerprint(key ssh.PublicKey) string { 211 hash := md5.Sum(key.Marshal()) 212 buf := make([]byte, hex.EncodedLen(len(hash))) 213 hex.Encode(buf, hash[:]) 214 // We need this in colon notation: 215 fp := make([]byte, len(buf)+15) 216 217 i, j := 0, 0 218 for ; i < len(buf); i++ { 219 if i > 0 && i%2 == 0 { 220 fp[j] = ':' 221 j++ 222 } 223 fp[j] = buf[i] 224 j++ 225 } 226 227 return string(fp) 228 }