github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/utils/ssh/authorisedkeys.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package ssh 5 6 import ( 7 "fmt" 8 "io/ioutil" 9 "os" 10 "os/user" 11 "path/filepath" 12 "runtime" 13 "strconv" 14 "strings" 15 "sync" 16 17 "code.google.com/p/go.crypto/ssh" 18 "github.com/juju/loggo" 19 "github.com/juju/utils" 20 ) 21 22 var logger = loggo.GetLogger("juju.utils.ssh") 23 24 type ListMode bool 25 26 var ( 27 FullKeys ListMode = true 28 Fingerprints ListMode = false 29 ) 30 31 const ( 32 authKeysFile = "authorized_keys" 33 ) 34 35 type AuthorisedKey struct { 36 Type string 37 Key []byte 38 Comment string 39 } 40 41 func authKeysDir(username string) (string, error) { 42 homeDir, err := utils.UserHomeDir(username) 43 if err != nil { 44 return "", err 45 } 46 homeDir, err = utils.NormalizePath(homeDir) 47 if err != nil { 48 return "", err 49 } 50 return filepath.Join(homeDir, ".ssh"), nil 51 } 52 53 // ParseAuthorisedKey parses a non-comment line from an 54 // authorized_keys file and returns the constituent parts. 55 // Based on description in "man sshd". 56 func ParseAuthorisedKey(line string) (*AuthorisedKey, error) { 57 key, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(line)) 58 if err != nil { 59 return nil, fmt.Errorf("invalid authorized_key %q", line) 60 } 61 return &AuthorisedKey{ 62 Type: key.Type(), 63 Key: key.Marshal(), 64 Comment: comment, 65 }, nil 66 } 67 68 // SplitAuthorisedKeys extracts a key slice from the specified key data, 69 // by splitting the key data into lines and ignoring comments and blank lines. 70 func SplitAuthorisedKeys(keyData string) []string { 71 var keys []string 72 for _, key := range strings.Split(string(keyData), "\n") { 73 key = strings.Trim(key, " \r") 74 if len(key) == 0 { 75 continue 76 } 77 if key[0] == '#' { 78 continue 79 } 80 keys = append(keys, key) 81 } 82 return keys 83 } 84 85 func readAuthorisedKeys(username string) ([]string, error) { 86 keyDir, err := authKeysDir(username) 87 if err != nil { 88 return nil, err 89 } 90 sshKeyFile := filepath.Join(keyDir, authKeysFile) 91 logger.Debugf("reading authorised keys file %s", sshKeyFile) 92 keyData, err := ioutil.ReadFile(sshKeyFile) 93 if os.IsNotExist(err) { 94 return []string{}, nil 95 } 96 if err != nil { 97 return nil, fmt.Errorf("reading ssh authorised keys file: %v", err) 98 } 99 var keys []string 100 for _, key := range strings.Split(string(keyData), "\n") { 101 if len(strings.Trim(key, " \r")) == 0 { 102 continue 103 } 104 keys = append(keys, key) 105 } 106 return keys, nil 107 } 108 109 func writeAuthorisedKeys(username string, keys []string) error { 110 keyDir, err := authKeysDir(username) 111 if err != nil { 112 return err 113 } 114 err = os.MkdirAll(keyDir, os.FileMode(0755)) 115 if err != nil { 116 return fmt.Errorf("cannot create ssh key directory: %v", err) 117 } 118 keyData := strings.Join(keys, "\n") + "\n" 119 120 // Get perms to use on auth keys file 121 sshKeyFile := filepath.Join(keyDir, authKeysFile) 122 perms := os.FileMode(0644) 123 info, err := os.Stat(sshKeyFile) 124 if err == nil { 125 perms = info.Mode().Perm() 126 } 127 128 logger.Debugf("writing authorised keys file %s", sshKeyFile) 129 err = utils.AtomicWriteFile(sshKeyFile, []byte(keyData), perms) 130 if err != nil { 131 return err 132 } 133 134 // TODO (wallyworld) - what to do on windows (if anything) 135 // TODO(dimitern) - no need to use user.Current() if username 136 // is "" - it will use the current user anyway. 137 if runtime.GOOS != "windows" { 138 // Ensure the resulting authorised keys file has its ownership 139 // set to the specified username. 140 var u *user.User 141 if username == "" { 142 u, err = user.Current() 143 } else { 144 u, err = user.Lookup(username) 145 } 146 if err != nil { 147 return err 148 } 149 // chown requires ints but user.User has strings for windows. 150 uid, err := strconv.Atoi(u.Uid) 151 if err != nil { 152 return err 153 } 154 gid, err := strconv.Atoi(u.Gid) 155 if err != nil { 156 return err 157 } 158 err = os.Chown(sshKeyFile, uid, gid) 159 if err != nil { 160 return err 161 } 162 } 163 return nil 164 } 165 166 // We need a mutex because updates to the authorised keys file are done by 167 // reading the contents, updating, and writing back out. So only one caller 168 // at a time can use either Add, Delete, List. 169 var mutex sync.Mutex 170 171 // AddKeys adds the specified ssh keys to the authorized_keys file for user. 172 // Returns an error if there is an issue with *any* of the supplied keys. 173 func AddKeys(user string, newKeys ...string) error { 174 mutex.Lock() 175 defer mutex.Unlock() 176 existingKeys, err := readAuthorisedKeys(user) 177 if err != nil { 178 return err 179 } 180 for _, newKey := range newKeys { 181 fingerprint, comment, err := KeyFingerprint(newKey) 182 if err != nil { 183 return err 184 } 185 if comment == "" { 186 return fmt.Errorf("cannot add ssh key without comment") 187 } 188 for _, key := range existingKeys { 189 existingFingerprint, existingComment, err := KeyFingerprint(key) 190 if err != nil { 191 // Only log a warning if the unrecognised key line is not a comment. 192 if key[0] != '#' { 193 logger.Warningf("invalid existing ssh key %q: %v", key, err) 194 } 195 continue 196 } 197 if existingFingerprint == fingerprint { 198 return fmt.Errorf("cannot add duplicate ssh key: %v", fingerprint) 199 } 200 if existingComment == comment { 201 return fmt.Errorf("cannot add ssh key with duplicate comment: %v", comment) 202 } 203 } 204 } 205 sshKeys := append(existingKeys, newKeys...) 206 return writeAuthorisedKeys(user, sshKeys) 207 } 208 209 // DeleteKeys removes the specified ssh keys from the authorized ssh keys file for user. 210 // keyIds may be either key comments or fingerprints. 211 // Returns an error if there is an issue with *any* of the keys to delete. 212 func DeleteKeys(user string, keyIds ...string) error { 213 mutex.Lock() 214 defer mutex.Unlock() 215 existingKeyData, err := readAuthorisedKeys(user) 216 if err != nil { 217 return err 218 } 219 // Build up a map of keys indexed by fingerprint, and fingerprints indexed by comment 220 // so we can easily get the key represented by each keyId, which may be either a fingerprint 221 // or comment. 222 var keysToWrite []string 223 var sshKeys = make(map[string]string) 224 var keyComments = make(map[string]string) 225 for _, key := range existingKeyData { 226 fingerprint, comment, err := KeyFingerprint(key) 227 if err != nil { 228 logger.Debugf("keeping unrecognised existing ssh key %q: %v", key, err) 229 keysToWrite = append(keysToWrite, key) 230 continue 231 } 232 sshKeys[fingerprint] = key 233 if comment != "" { 234 keyComments[comment] = fingerprint 235 } 236 } 237 for _, keyId := range keyIds { 238 // assume keyId may be a fingerprint 239 fingerprint := keyId 240 _, ok := sshKeys[keyId] 241 if !ok { 242 // keyId is a comment 243 fingerprint, ok = keyComments[keyId] 244 } 245 if !ok { 246 return fmt.Errorf("cannot delete non existent key: %v", keyId) 247 } 248 delete(sshKeys, fingerprint) 249 } 250 for _, key := range sshKeys { 251 keysToWrite = append(keysToWrite, key) 252 } 253 if len(keysToWrite) == 0 { 254 return fmt.Errorf("cannot delete all keys") 255 } 256 return writeAuthorisedKeys(user, keysToWrite) 257 } 258 259 // ReplaceKeys writes the specified ssh keys to the authorized_keys file for user, 260 // replacing any that are already there. 261 // Returns an error if there is an issue with *any* of the supplied keys. 262 func ReplaceKeys(user string, newKeys ...string) error { 263 mutex.Lock() 264 defer mutex.Unlock() 265 266 existingKeyData, err := readAuthorisedKeys(user) 267 if err != nil { 268 return err 269 } 270 var existingNonKeyLines []string 271 for _, line := range existingKeyData { 272 _, _, err := KeyFingerprint(line) 273 if err != nil { 274 existingNonKeyLines = append(existingNonKeyLines, line) 275 } 276 } 277 return writeAuthorisedKeys(user, append(existingNonKeyLines, newKeys...)) 278 } 279 280 // ListKeys returns either the full keys or key comments from the authorized ssh keys file for user. 281 func ListKeys(user string, mode ListMode) ([]string, error) { 282 mutex.Lock() 283 defer mutex.Unlock() 284 keyData, err := readAuthorisedKeys(user) 285 if err != nil { 286 return nil, err 287 } 288 var keys []string 289 for _, key := range keyData { 290 fingerprint, comment, err := KeyFingerprint(key) 291 if err != nil { 292 // Only log a warning if the unrecognised key line is not a comment. 293 if key[0] != '#' { 294 logger.Warningf("ignoring invalid ssh key %q: %v", key, err) 295 } 296 continue 297 } 298 if mode == FullKeys { 299 keys = append(keys, key) 300 } else { 301 shortKey := fingerprint 302 if comment != "" { 303 shortKey += fmt.Sprintf(" (%s)", comment) 304 } 305 keys = append(keys, shortKey) 306 } 307 } 308 return keys, nil 309 } 310 311 // Any ssh key added to the authorised keys list by Juju will have this prefix. 312 // This allows Juju to know which keys have been added externally and any such keys 313 // will always be retained by Juju when updating the authorised keys file. 314 const JujuCommentPrefix = "Juju:" 315 316 func EnsureJujuComment(key string) string { 317 ak, err := ParseAuthorisedKey(key) 318 // Just return an invalid key as is. 319 if err != nil { 320 logger.Warningf("invalid Juju ssh key %s: %v", key, err) 321 return key 322 } 323 if ak.Comment == "" { 324 return key + " " + JujuCommentPrefix + "sshkey" 325 } else { 326 // Add the Juju prefix to the comment if necessary. 327 if !strings.HasPrefix(ak.Comment, JujuCommentPrefix) { 328 commentIndex := strings.LastIndex(key, ak.Comment) 329 return key[:commentIndex] + JujuCommentPrefix + ak.Comment 330 } 331 } 332 return key 333 }