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  }