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