github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/utils/ssh/clientkeys.go (about)

     1  // Copyright 2014 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  	"path/filepath"
    11  	"strings"
    12  	"sync"
    13  
    14  	"code.google.com/p/go.crypto/ssh"
    15  
    16  	"launchpad.net/juju-core/utils"
    17  	"launchpad.net/juju-core/utils/set"
    18  )
    19  
    20  const clientKeyName = "juju_id_rsa"
    21  
    22  // PublicKeySuffix is the file extension for public key files.
    23  const PublicKeySuffix = ".pub"
    24  
    25  var (
    26  	clientKeysMutex sync.Mutex
    27  
    28  	// clientKeys is a cached map of private key filenames
    29  	// to ssh.Signers. The private keys are those loaded
    30  	// from the client key directory, passed to LoadClientKeys.
    31  	clientKeys map[string]ssh.Signer
    32  )
    33  
    34  // LoadClientKeys loads the client SSH keys from the
    35  // specified directory, and caches them as a process-wide
    36  // global. If the directory does not exist, it is created;
    37  // if the directory did not exist, or contains no keys, it
    38  // is populated with a new key pair.
    39  //
    40  // If the directory exists, then all pairs of files where one
    41  // has the same name as the other + ".pub" will be loaded as
    42  // private/public key pairs.
    43  //
    44  // Calls to LoadClientKeys will clear the previously loaded
    45  // keys, and recompute the keys.
    46  func LoadClientKeys(dir string) error {
    47  	clientKeysMutex.Lock()
    48  	defer clientKeysMutex.Unlock()
    49  	dir, err := utils.NormalizePath(dir)
    50  	if err != nil {
    51  		return err
    52  	}
    53  	if _, err := os.Stat(dir); err == nil {
    54  		keys, err := loadClientKeys(dir)
    55  		if err != nil {
    56  			return err
    57  		} else if len(keys) > 0 {
    58  			clientKeys = keys
    59  			return nil
    60  		}
    61  		// Directory exists but contains no keys;
    62  		// fall through and create one.
    63  	}
    64  	if err := os.MkdirAll(dir, 0700); err != nil {
    65  		return err
    66  	}
    67  	keyfile, key, err := generateClientKey(dir)
    68  	if err != nil {
    69  		os.RemoveAll(dir)
    70  		return err
    71  	}
    72  	clientKeys = map[string]ssh.Signer{keyfile: key}
    73  	return nil
    74  }
    75  
    76  // ClearClientKeys clears the client keys cached in memory.
    77  func ClearClientKeys() {
    78  	clientKeysMutex.Lock()
    79  	defer clientKeysMutex.Unlock()
    80  	clientKeys = nil
    81  }
    82  
    83  func generateClientKey(dir string) (keyfile string, key ssh.Signer, err error) {
    84  	private, public, err := GenerateKey("juju-client-key")
    85  	if err != nil {
    86  		return "", nil, err
    87  	}
    88  	clientPrivateKey, err := ssh.ParsePrivateKey([]byte(private))
    89  	if err != nil {
    90  		return "", nil, err
    91  	}
    92  	privkeyFilename := filepath.Join(dir, clientKeyName)
    93  	if err = ioutil.WriteFile(privkeyFilename, []byte(private), 0600); err != nil {
    94  		return "", nil, err
    95  	}
    96  	if err := ioutil.WriteFile(privkeyFilename+PublicKeySuffix, []byte(public), 0600); err != nil {
    97  		os.Remove(privkeyFilename)
    98  		return "", nil, err
    99  	}
   100  	return privkeyFilename, clientPrivateKey, nil
   101  }
   102  
   103  func loadClientKeys(dir string) (map[string]ssh.Signer, error) {
   104  	publicKeyFiles, err := publicKeyFiles(dir)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  	keys := make(map[string]ssh.Signer, len(publicKeyFiles))
   109  	for _, filename := range publicKeyFiles {
   110  		filename = filename[:len(filename)-len(PublicKeySuffix)]
   111  		data, err := ioutil.ReadFile(filename)
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  		keys[filename], err = ssh.ParsePrivateKey(data)
   116  		if err != nil {
   117  			return nil, fmt.Errorf("parsing key file %q: %v", filename, err)
   118  		}
   119  	}
   120  	return keys, nil
   121  }
   122  
   123  // privateKeys returns the private keys loaded by LoadClientKeys.
   124  func privateKeys() (signers []ssh.Signer) {
   125  	clientKeysMutex.Lock()
   126  	defer clientKeysMutex.Unlock()
   127  	for _, key := range clientKeys {
   128  		signers = append(signers, key)
   129  	}
   130  	return signers
   131  }
   132  
   133  // PrivateKeyFiles returns the filenames of private SSH keys loaded by
   134  // LoadClientKeys.
   135  func PrivateKeyFiles() []string {
   136  	clientKeysMutex.Lock()
   137  	defer clientKeysMutex.Unlock()
   138  	keyfiles := make([]string, 0, len(clientKeys))
   139  	for f := range clientKeys {
   140  		keyfiles = append(keyfiles, f)
   141  	}
   142  	return keyfiles
   143  }
   144  
   145  // PublicKeyFiles returns the filenames of public SSH keys loaded by
   146  // LoadClientKeys.
   147  func PublicKeyFiles() []string {
   148  	privkeys := PrivateKeyFiles()
   149  	pubkeys := make([]string, len(privkeys))
   150  	for i, priv := range privkeys {
   151  		pubkeys[i] = priv + PublicKeySuffix
   152  	}
   153  	return pubkeys
   154  }
   155  
   156  // publicKeyFiles returns the filenames of public SSH keys
   157  // in the specified directory (all the files ending with .pub).
   158  func publicKeyFiles(clientKeysDir string) ([]string, error) {
   159  	if clientKeysDir == "" {
   160  		return nil, nil
   161  	}
   162  	var keys []string
   163  	dir, err := os.Open(clientKeysDir)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  	names, err := dir.Readdirnames(-1)
   168  	dir.Close()
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  	candidates := set.NewStrings(names...)
   173  	for _, name := range names {
   174  		if !strings.HasSuffix(name, PublicKeySuffix) {
   175  			continue
   176  		}
   177  		// If the private key filename also exists, add the file.
   178  		priv := name[:len(name)-len(PublicKeySuffix)]
   179  		if candidates.Contains(priv) {
   180  			keys = append(keys, filepath.Join(dir.Name(), name))
   181  		}
   182  	}
   183  	return keys, nil
   184  }