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