2016-04-19 07:10:48 +00:00
|
|
|
/*
|
|
|
|
Copyright 2016 Gravitational, Inc.
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2016-01-12 16:21:09 +00:00
|
|
|
package client
|
2016-01-09 22:33:30 +00:00
|
|
|
|
|
|
|
import (
|
2016-05-17 23:40:13 +00:00
|
|
|
"bufio"
|
2016-04-19 07:10:48 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2016-01-09 22:33:30 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
2016-03-08 17:26:27 +00:00
|
|
|
"os/user"
|
2016-01-09 22:33:30 +00:00
|
|
|
"path/filepath"
|
2016-05-17 23:40:13 +00:00
|
|
|
"strings"
|
2016-01-09 22:33:30 +00:00
|
|
|
"time"
|
|
|
|
|
2016-04-19 21:56:01 +00:00
|
|
|
"github.com/gravitational/teleport/lib/sshutils"
|
2016-04-22 23:18:39 +00:00
|
|
|
"github.com/gravitational/teleport/lib/utils"
|
2016-04-19 21:56:01 +00:00
|
|
|
|
2016-02-03 01:53:21 +00:00
|
|
|
log "github.com/Sirupsen/logrus"
|
2016-01-20 15:52:25 +00:00
|
|
|
"github.com/gravitational/trace"
|
2016-04-19 00:31:02 +00:00
|
|
|
|
2016-01-20 15:52:25 +00:00
|
|
|
"golang.org/x/crypto/ssh"
|
2016-01-09 22:33:30 +00:00
|
|
|
)
|
|
|
|
|
2016-04-19 00:31:02 +00:00
|
|
|
const (
|
2016-04-19 17:46:28 +00:00
|
|
|
defaultKeyDir = ".tsh"
|
2016-05-07 04:57:39 +00:00
|
|
|
fileExtCert = ".cert"
|
|
|
|
fileExtKey = ".key"
|
|
|
|
fileExtPub = ".pub"
|
|
|
|
sessionKeyDir = "keys"
|
2016-04-19 17:46:28 +00:00
|
|
|
fileNameKnownHosts = "known_hosts"
|
2016-04-19 00:31:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// FSLocalKeyStore implements LocalKeyStore interface using the filesystem
|
2016-04-19 07:10:48 +00:00
|
|
|
// Here's the file layout for the FS store:
|
|
|
|
// ~/.tsh/
|
2016-04-19 17:46:28 +00:00
|
|
|
// ├── known_hosts --> trusted certificate authorities (their keys) in a format similar to known_hosts
|
2016-04-19 10:00:07 +00:00
|
|
|
// └── sessions --> server-signed session keys
|
2016-04-19 07:10:48 +00:00
|
|
|
// └── host-a
|
|
|
|
// | ├── cert
|
|
|
|
// | ├── key
|
|
|
|
// | └── pub
|
|
|
|
// └── host-b
|
|
|
|
// ├── cert
|
|
|
|
// ├── key
|
|
|
|
// └── pub
|
2016-04-19 00:31:02 +00:00
|
|
|
type FSLocalKeyStore struct {
|
|
|
|
LocalKeyStore
|
|
|
|
|
|
|
|
// KeyDir is the directory where all keys are stored
|
2016-04-05 23:58:44 +00:00
|
|
|
KeyDir string
|
|
|
|
}
|
|
|
|
|
2016-04-19 00:31:02 +00:00
|
|
|
// NewFSLocalKeyStore creates a new filesystem-based local keystore object
|
|
|
|
// and initializes it.
|
2016-03-14 23:51:42 +00:00
|
|
|
//
|
2016-04-19 00:31:02 +00:00
|
|
|
// if dirPath is empty, sets it to ~/.tsh
|
|
|
|
func NewFSLocalKeyStore(dirPath string) (s *FSLocalKeyStore, err error) {
|
|
|
|
log.Infof("using FSLocalKeyStore")
|
|
|
|
dirPath, err = initKeysDir(dirPath)
|
2016-02-12 15:25:54 +00:00
|
|
|
if err != nil {
|
2016-04-19 00:31:02 +00:00
|
|
|
return nil, trace.Wrap(err)
|
2016-02-12 15:25:54 +00:00
|
|
|
}
|
2016-04-19 00:31:02 +00:00
|
|
|
return &FSLocalKeyStore{
|
|
|
|
KeyDir: dirPath,
|
|
|
|
}, nil
|
2016-02-12 15:25:54 +00:00
|
|
|
}
|
|
|
|
|
2016-04-19 07:10:48 +00:00
|
|
|
// GetKeys returns all user session keys stored in the store
|
2016-05-07 04:57:39 +00:00
|
|
|
func (fs *FSLocalKeyStore) GetKeys(username string) (keys []Key, err error) {
|
2016-04-19 00:31:02 +00:00
|
|
|
dirPath := filepath.Join(fs.KeyDir, sessionKeyDir)
|
2016-04-22 23:18:39 +00:00
|
|
|
if !utils.IsDir(dirPath) {
|
2016-04-19 00:31:02 +00:00
|
|
|
return make([]Key, 0), nil
|
2016-02-12 15:25:54 +00:00
|
|
|
}
|
2016-04-19 00:31:02 +00:00
|
|
|
dirEntries, err := ioutil.ReadDir(dirPath)
|
2016-02-12 15:25:54 +00:00
|
|
|
if err != nil {
|
2016-04-19 00:31:02 +00:00
|
|
|
return nil, trace.Wrap(err)
|
2016-02-12 15:25:54 +00:00
|
|
|
}
|
2016-04-19 00:31:02 +00:00
|
|
|
for _, fi := range dirEntries {
|
|
|
|
if !fi.IsDir() {
|
|
|
|
continue
|
2016-02-18 19:10:34 +00:00
|
|
|
}
|
2016-05-07 04:57:39 +00:00
|
|
|
k, err := fs.GetKey(fi.Name(), username)
|
2016-04-19 00:31:02 +00:00
|
|
|
if err != nil {
|
|
|
|
// if a key is reported as 'not found' it's probably because it expired
|
|
|
|
if !trace.IsNotFound(err) {
|
|
|
|
return nil, trace.Wrap(err)
|
2016-02-18 19:10:34 +00:00
|
|
|
}
|
2016-04-19 17:37:46 +00:00
|
|
|
continue
|
2016-02-18 19:10:34 +00:00
|
|
|
}
|
2016-04-19 00:31:02 +00:00
|
|
|
keys = append(keys, *k)
|
2016-02-12 15:25:54 +00:00
|
|
|
}
|
2016-04-19 00:31:02 +00:00
|
|
|
return keys, nil
|
2016-02-12 15:25:54 +00:00
|
|
|
}
|
|
|
|
|
2016-04-19 07:10:48 +00:00
|
|
|
// AddKey adds a new key to the session store. If a key for the host is already
|
|
|
|
// stored, overwrites it.
|
2016-05-07 04:57:39 +00:00
|
|
|
func (fs *FSLocalKeyStore) AddKey(host, username string, key *Key) error {
|
2016-04-19 00:31:02 +00:00
|
|
|
dirPath, err := fs.dirFor(host)
|
2016-01-09 22:33:30 +00:00
|
|
|
if err != nil {
|
2016-04-19 00:31:02 +00:00
|
|
|
return trace.Wrap(err)
|
2016-01-09 22:33:30 +00:00
|
|
|
}
|
2016-04-19 00:31:02 +00:00
|
|
|
writeBytes := func(fname string, data []byte) error {
|
|
|
|
fp := filepath.Join(dirPath, fname)
|
|
|
|
err := ioutil.WriteFile(fp, data, 0640)
|
2016-01-09 22:33:30 +00:00
|
|
|
if err != nil {
|
2016-04-19 00:31:02 +00:00
|
|
|
log.Error(err)
|
2016-01-09 22:33:30 +00:00
|
|
|
}
|
2016-04-19 00:31:02 +00:00
|
|
|
return err
|
2016-03-10 03:39:15 +00:00
|
|
|
}
|
2016-05-07 04:57:39 +00:00
|
|
|
if err = writeBytes(username+fileExtCert, key.Cert); err != nil {
|
2016-04-19 00:31:02 +00:00
|
|
|
return trace.Wrap(err)
|
2016-04-05 23:58:44 +00:00
|
|
|
}
|
2016-05-07 04:57:39 +00:00
|
|
|
if err = writeBytes(username+fileExtPub, key.Pub); err != nil {
|
2016-04-19 00:31:02 +00:00
|
|
|
return trace.Wrap(err)
|
2016-04-05 23:58:44 +00:00
|
|
|
}
|
2016-05-07 04:57:39 +00:00
|
|
|
if err = writeBytes(username+fileExtKey, key.Priv); err != nil {
|
2016-04-19 00:31:02 +00:00
|
|
|
return trace.Wrap(err)
|
2016-03-10 03:39:15 +00:00
|
|
|
}
|
2016-04-19 00:31:02 +00:00
|
|
|
return nil
|
2016-01-09 22:33:30 +00:00
|
|
|
}
|
|
|
|
|
2016-06-02 00:02:39 +00:00
|
|
|
// DeleteKey deletes a key from the local store
|
|
|
|
func (fs *FSLocalKeyStore) DeleteKey(host string, username string) error {
|
|
|
|
dirPath, err := fs.dirFor(host)
|
|
|
|
if err != nil {
|
|
|
|
return trace.Wrap(err)
|
|
|
|
}
|
|
|
|
files := []string{
|
|
|
|
filepath.Join(dirPath, username+fileExtCert),
|
|
|
|
filepath.Join(dirPath, username+fileExtPub),
|
|
|
|
filepath.Join(dirPath, username+fileExtKey),
|
|
|
|
}
|
|
|
|
for _, fn := range files {
|
|
|
|
if err = os.Remove(fn); err != nil {
|
|
|
|
return trace.Wrap(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-04-19 07:10:48 +00:00
|
|
|
// GetKey returns a key for a given host. If the key is not found,
|
|
|
|
// returns trace.NotFound error.
|
2016-05-07 04:57:39 +00:00
|
|
|
func (fs *FSLocalKeyStore) GetKey(host, username string) (*Key, error) {
|
2016-04-19 00:31:02 +00:00
|
|
|
dirPath, err := fs.dirFor(host)
|
|
|
|
if err != nil {
|
|
|
|
return nil, trace.Wrap(err)
|
|
|
|
}
|
2016-05-07 04:57:39 +00:00
|
|
|
certFile := filepath.Join(dirPath, username+fileExtCert)
|
2016-04-19 23:05:28 +00:00
|
|
|
cert, err := ioutil.ReadFile(certFile)
|
2016-04-05 23:58:44 +00:00
|
|
|
if err != nil {
|
2016-04-19 00:31:02 +00:00
|
|
|
log.Error(err)
|
|
|
|
return nil, trace.Wrap(err)
|
2016-04-05 23:58:44 +00:00
|
|
|
}
|
2016-05-07 04:57:39 +00:00
|
|
|
pub, err := ioutil.ReadFile(filepath.Join(dirPath, username+fileExtPub))
|
2016-01-09 22:33:30 +00:00
|
|
|
if err != nil {
|
2016-04-19 00:31:02 +00:00
|
|
|
log.Error(err)
|
|
|
|
return nil, trace.Wrap(err)
|
2016-01-09 22:33:30 +00:00
|
|
|
}
|
2016-05-07 04:57:39 +00:00
|
|
|
priv, err := ioutil.ReadFile(filepath.Join(dirPath, username+fileExtKey))
|
2016-01-09 22:33:30 +00:00
|
|
|
if err != nil {
|
2016-04-19 00:31:02 +00:00
|
|
|
log.Error(err)
|
|
|
|
return nil, trace.Wrap(err)
|
2016-01-09 22:33:30 +00:00
|
|
|
}
|
2016-04-19 23:05:28 +00:00
|
|
|
|
|
|
|
key := &Key{Pub: pub, Priv: priv, Cert: cert}
|
|
|
|
|
|
|
|
// expired certificate? this key won't be accepted anymore, lets delete it:
|
|
|
|
certExpiration, err := key.CertValidBefore()
|
2016-01-09 22:33:30 +00:00
|
|
|
if err != nil {
|
2016-04-19 00:31:02 +00:00
|
|
|
return nil, trace.Wrap(err)
|
2016-01-09 22:33:30 +00:00
|
|
|
}
|
2016-04-19 23:05:28 +00:00
|
|
|
log.Infof("returning cert %v valid until %v", certFile, certExpiration)
|
|
|
|
if certExpiration.Before(time.Now().UTC()) {
|
2016-04-22 02:41:10 +00:00
|
|
|
log.Infof("TTL expired (%v) for session key %v", certExpiration, dirPath)
|
2016-04-19 23:05:28 +00:00
|
|
|
os.RemoveAll(dirPath)
|
|
|
|
return nil, trace.NotFound("session keys for %s are not found", host)
|
|
|
|
}
|
|
|
|
return key, nil
|
2016-04-19 00:31:02 +00:00
|
|
|
}
|
2016-01-09 22:33:30 +00:00
|
|
|
|
2016-04-19 10:00:07 +00:00
|
|
|
// AddKnownHost adds a new entry to 'known_CAs' file
|
|
|
|
func (fs *FSLocalKeyStore) AddKnownCA(domainName string, hostKeys []ssh.PublicKey) error {
|
2016-05-17 23:40:13 +00:00
|
|
|
fp, err := os.OpenFile(filepath.Join(fs.KeyDir, fileNameKnownHosts), os.O_CREATE|os.O_RDWR, 0640)
|
2016-04-19 07:10:48 +00:00
|
|
|
if err != nil {
|
|
|
|
return trace.Wrap(err)
|
|
|
|
}
|
2016-05-17 23:40:13 +00:00
|
|
|
defer fp.Sync()
|
2016-04-19 07:10:48 +00:00
|
|
|
defer fp.Close()
|
2016-05-17 23:40:13 +00:00
|
|
|
// read all existing entries into a map (this removes any pre-existing dupes)
|
|
|
|
entries := make(map[string]int)
|
|
|
|
output := make([]string, 0)
|
|
|
|
scanner := bufio.NewScanner(fp)
|
|
|
|
for scanner.Scan() {
|
|
|
|
line := scanner.Text()
|
|
|
|
if _, exists := entries[line]; !exists {
|
|
|
|
output = append(output, line)
|
|
|
|
entries[line] = 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// add every host key to the list of entries
|
2016-04-19 07:10:48 +00:00
|
|
|
for i := range hostKeys {
|
2016-04-19 21:56:01 +00:00
|
|
|
log.Infof("adding known CA %v %v", domainName, sshutils.Fingerprint(hostKeys[i]))
|
2016-05-17 23:40:13 +00:00
|
|
|
bytes := ssh.MarshalAuthorizedKey(hostKeys[i])
|
|
|
|
line := strings.TrimSpace(fmt.Sprintf("%s %s", domainName, bytes))
|
|
|
|
if _, exists := entries[line]; !exists {
|
|
|
|
output = append(output, line)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// re-create the file:
|
|
|
|
_, err = fp.Seek(0, 0)
|
|
|
|
if err != nil {
|
|
|
|
return trace.Wrap(err)
|
|
|
|
}
|
|
|
|
if err = fp.Truncate(0); err != nil {
|
|
|
|
return trace.Wrap(err)
|
|
|
|
}
|
|
|
|
for _, line := range output {
|
|
|
|
fmt.Fprintf(fp, "%s\n", line)
|
2016-04-19 07:10:48 +00:00
|
|
|
}
|
2016-04-19 00:31:02 +00:00
|
|
|
return nil
|
|
|
|
}
|
2016-01-09 22:33:30 +00:00
|
|
|
|
2016-04-19 10:00:07 +00:00
|
|
|
// GetKnownHost returns public keys of all trusted CAs
|
|
|
|
func (fs *FSLocalKeyStore) GetKnownCAs() ([]ssh.PublicKey, error) {
|
2016-04-19 17:46:28 +00:00
|
|
|
bytes, err := ioutil.ReadFile(filepath.Join(fs.KeyDir, fileNameKnownHosts))
|
2016-04-19 07:10:48 +00:00
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
return nil, trace.Wrap(err)
|
|
|
|
}
|
|
|
|
var (
|
|
|
|
pubKey ssh.PublicKey
|
|
|
|
retval []ssh.PublicKey = make([]ssh.PublicKey, 0)
|
|
|
|
)
|
|
|
|
for err == nil {
|
|
|
|
_, _, pubKey, _, bytes, err = ssh.ParseKnownHosts(bytes)
|
|
|
|
if err == nil {
|
|
|
|
retval = append(retval, pubKey)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err != io.EOF {
|
|
|
|
return nil, trace.Wrap(err)
|
|
|
|
}
|
|
|
|
return retval, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// dirFor is a helper function. It returns a directory where session keys
|
|
|
|
// for a given host are stored
|
2016-04-19 00:31:02 +00:00
|
|
|
func (fs *FSLocalKeyStore) dirFor(hostname string) (string, error) {
|
|
|
|
dirPath := filepath.Join(fs.KeyDir, sessionKeyDir, hostname)
|
2016-04-22 23:18:39 +00:00
|
|
|
if err := os.MkdirAll(dirPath, 0777); err != nil {
|
|
|
|
log.Error(err)
|
|
|
|
return "", trace.Wrap(err)
|
2016-04-19 00:31:02 +00:00
|
|
|
}
|
|
|
|
return dirPath, nil
|
2016-01-09 22:33:30 +00:00
|
|
|
}
|
|
|
|
|
2016-04-19 07:10:48 +00:00
|
|
|
// initKeysDir initializes the keystore root directory. Usually it is ~/.tsh
|
2016-04-19 00:31:02 +00:00
|
|
|
func initKeysDir(dirPath string) (string, error) {
|
|
|
|
var err error
|
|
|
|
// not specified? use `~/.tsh`
|
|
|
|
if dirPath == "" {
|
|
|
|
u, err := user.Current()
|
|
|
|
if err != nil {
|
|
|
|
dirPath = os.TempDir()
|
|
|
|
} else {
|
|
|
|
dirPath = u.HomeDir
|
|
|
|
}
|
|
|
|
dirPath = filepath.Join(dirPath, defaultKeyDir)
|
2016-01-09 22:33:30 +00:00
|
|
|
}
|
2016-04-19 00:31:02 +00:00
|
|
|
// create if doesn't exist:
|
|
|
|
_, err = os.Stat(dirPath)
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
err = os.MkdirAll(dirPath, os.ModeDir|0777)
|
2016-01-09 22:33:30 +00:00
|
|
|
if err != nil {
|
2016-04-19 00:31:02 +00:00
|
|
|
return "", trace.Wrap(err)
|
2016-01-09 22:33:30 +00:00
|
|
|
}
|
2016-04-19 00:31:02 +00:00
|
|
|
} else {
|
|
|
|
return "", trace.Wrap(err)
|
2016-01-09 22:33:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-19 00:31:02 +00:00
|
|
|
return dirPath, nil
|
|
|
|
}
|