~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/utils/ssh/authorisedkeys.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

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