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  }