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