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  }