github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/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  	authKeysDir  = "~%s/.ssh"
    33  	authKeysFile = "authorized_keys"
    34  )
    35  
    36  type AuthorisedKey struct {
    37  	Type    string
    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, _, _, err := ssh.ParseAuthorizedKey([]byte(line))
    47  	if err != nil {
    48  		return nil, fmt.Errorf("invalid authorized_key %q", line)
    49  	}
    50  	return &AuthorisedKey{
    51  		Type:    key.Type(),
    52  		Key:     key.Marshal(),
    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  	return writeAuthorisedKeys(user, append(existingNonKeyLines, newKeys...))
   268  }
   269  
   270  // ListKeys returns either the full keys or key comments from the authorized ssh keys file for user.
   271  func ListKeys(user string, mode ListMode) ([]string, error) {
   272  	mutex.Lock()
   273  	defer mutex.Unlock()
   274  	keyData, err := readAuthorisedKeys(user)
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  	var keys []string
   279  	for _, key := range keyData {
   280  		fingerprint, comment, err := KeyFingerprint(key)
   281  		if err != nil {
   282  			// Only log a warning if the unrecognised key line is not a comment.
   283  			if key[0] != '#' {
   284  				logger.Warningf("ignoring invalid ssh key %q: %v", key, err)
   285  			}
   286  			continue
   287  		}
   288  		if mode == FullKeys {
   289  			keys = append(keys, key)
   290  		} else {
   291  			shortKey := fingerprint
   292  			if comment != "" {
   293  				shortKey += fmt.Sprintf(" (%s)", comment)
   294  			}
   295  			keys = append(keys, shortKey)
   296  		}
   297  	}
   298  	return keys, nil
   299  }
   300  
   301  // Any ssh key added to the authorised keys list by Juju will have this prefix.
   302  // This allows Juju to know which keys have been added externally and any such keys
   303  // will always be retained by Juju when updating the authorised keys file.
   304  const JujuCommentPrefix = "Juju:"
   305  
   306  func EnsureJujuComment(key string) string {
   307  	ak, err := ParseAuthorisedKey(key)
   308  	// Just return an invalid key as is.
   309  	if err != nil {
   310  		logger.Warningf("invalid Juju ssh key %s: %v", key, err)
   311  		return key
   312  	}
   313  	if ak.Comment == "" {
   314  		return key + " " + JujuCommentPrefix + "sshkey"
   315  	} else {
   316  		// Add the Juju prefix to the comment if necessary.
   317  		if !strings.HasPrefix(ak.Comment, JujuCommentPrefix) {
   318  			commentIndex := strings.LastIndex(key, ak.Comment)
   319  			return key[:commentIndex] + JujuCommentPrefix + ak.Comment
   320  		}
   321  	}
   322  	return key
   323  }