mirror of
https://github.com/gravitational/teleport
synced 2024-10-22 10:13:21 +00:00
486 lines
14 KiB
Go
486 lines
14 KiB
Go
/*
|
||
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.
|
||
*/
|
||
|
||
package client
|
||
|
||
import (
|
||
"bufio"
|
||
"crypto/x509"
|
||
"encoding/pem"
|
||
"fmt"
|
||
"io"
|
||
"io/ioutil"
|
||
"os"
|
||
"os/user"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"golang.org/x/crypto/ssh"
|
||
|
||
"github.com/gravitational/teleport"
|
||
"github.com/gravitational/teleport/lib/auth"
|
||
"github.com/gravitational/teleport/lib/sshutils"
|
||
"github.com/gravitational/teleport/lib/utils"
|
||
|
||
"github.com/gravitational/trace"
|
||
"github.com/sirupsen/logrus"
|
||
)
|
||
|
||
const (
|
||
defaultKeyDir = ProfileDir
|
||
fileExtTLSCert = "-x509.pem"
|
||
fileExtCert = "-cert.pub"
|
||
fileExtPub = ".pub"
|
||
sessionKeyDir = "keys"
|
||
fileNameKnownHosts = "known_hosts"
|
||
fileNameTLSCerts = "certs.pem"
|
||
|
||
// profileDirPerms is the default permissions applied to the profile
|
||
// directory (usually ~/.tsh)
|
||
profileDirPerms os.FileMode = 0700
|
||
|
||
// keyFilePerms is the default permissions applied to key files (.cert, .key, pub)
|
||
// under ~/.tsh
|
||
keyFilePerms os.FileMode = 0600
|
||
)
|
||
|
||
// LocalKeyStore interface allows for different storage backends for tsh to
|
||
// load/save its keys.
|
||
//
|
||
// The _only_ filesystem-based implementation of LocalKeyStore is declared
|
||
// below (FSLocalKeyStore)
|
||
type LocalKeyStore interface {
|
||
// AddKey adds the given session key for the proxy and username to the
|
||
// storage backend.
|
||
AddKey(proxy string, username string, key *Key) error
|
||
|
||
// GetKey returns the session key for the given username and proxy.
|
||
GetKey(proxy string, username string) (*Key, error)
|
||
|
||
// DeleteKey removes a specific session key from a proxy.
|
||
DeleteKey(proxyHost string, username string) error
|
||
|
||
// DeleteKeys removes all session keys from disk.
|
||
DeleteKeys() error
|
||
|
||
// AddKnownHostKeys adds the public key to the list of known hosts for
|
||
// a hostname.
|
||
AddKnownHostKeys(hostname string, keys []ssh.PublicKey) error
|
||
|
||
// GetKnownHostKeys returns all public keys for a hostname.
|
||
GetKnownHostKeys(hostname string) ([]ssh.PublicKey, error)
|
||
|
||
// SaveCerts saves trusted TLS certificates of certificate authorities.
|
||
SaveCerts(proxy string, cas []auth.TrustedCerts) error
|
||
|
||
// GetCerts gets trusted TLS certificates of certificate authorities.
|
||
GetCerts(proxy string) (*x509.CertPool, error)
|
||
|
||
// GetCertsPEM gets trusted TLS certificates of certificate authorities.
|
||
// Each returned byte slice contains an individual PEM block.
|
||
GetCertsPEM(proxy string) ([][]byte, error)
|
||
}
|
||
|
||
// FSLocalKeyStore implements LocalKeyStore interface using the filesystem.
|
||
// Here's the file layout for the FS store:
|
||
//
|
||
// ~/.tsh/
|
||
// ├── known_hosts --> trusted certificate authorities (their keys) in a format similar to known_hosts
|
||
// └── keys
|
||
// ├── one.example.com
|
||
// │ ├── certs.pem
|
||
// │ ├── foo --> RSA Private Key
|
||
// │ ├── foo-cert.pub --> SSH certificate for proxies and nodes
|
||
// │ ├── foo.pub --> Public Key
|
||
// │ └── foo-x509.pem --> TLS client certificate for Auth Server
|
||
// └── two.example.com
|
||
// ├── certs.pem
|
||
// ├── bar
|
||
// ├── bar-cert.pub
|
||
// ├── bar.pub
|
||
// └── bar-x509.pem
|
||
type FSLocalKeyStore struct {
|
||
// log holds the structured logger.
|
||
log *logrus.Entry
|
||
|
||
// KeyDir is the directory where all keys are stored.
|
||
KeyDir string
|
||
}
|
||
|
||
// NewFSLocalKeyStore creates a new filesystem-based local keystore object
|
||
// and initializes it.
|
||
//
|
||
// If dirPath is empty, sets it to ~/.tsh.
|
||
func NewFSLocalKeyStore(dirPath string) (s *FSLocalKeyStore, err error) {
|
||
dirPath, err = initKeysDir(dirPath)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
return &FSLocalKeyStore{
|
||
log: logrus.WithFields(logrus.Fields{
|
||
trace.Component: teleport.ComponentKeyStore,
|
||
}),
|
||
KeyDir: dirPath,
|
||
}, nil
|
||
}
|
||
|
||
// AddKey adds a new key to the session store. If a key for the host is already
|
||
// stored, overwrites it.
|
||
func (fs *FSLocalKeyStore) AddKey(host, username string, key *Key) error {
|
||
dirPath, err := fs.dirFor(host, true)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
writeBytes := func(fname string, data []byte) error {
|
||
fp := filepath.Join(dirPath, fname)
|
||
err := ioutil.WriteFile(fp, data, keyFilePerms)
|
||
if err != nil {
|
||
fs.log.Error(err)
|
||
}
|
||
return err
|
||
}
|
||
if err = writeBytes(username+fileExtCert, key.Cert); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
if err = writeBytes(username+fileExtTLSCert, key.TLSCert); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
if err = writeBytes(username+fileExtPub, key.Pub); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
if err = writeBytes(username, key.Priv); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// DeleteKey deletes a key from the local store
|
||
func (fs *FSLocalKeyStore) DeleteKey(host string, username string) error {
|
||
dirPath, err := fs.dirFor(host, false)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
files := []string{
|
||
filepath.Join(dirPath, username+fileExtCert),
|
||
filepath.Join(dirPath, username+fileExtTLSCert),
|
||
filepath.Join(dirPath, username+fileExtPub),
|
||
filepath.Join(dirPath, username),
|
||
}
|
||
for _, fn := range files {
|
||
if err = os.Remove(fn); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// DeleteKeys removes all session keys from disk.
|
||
func (fs *FSLocalKeyStore) DeleteKeys() error {
|
||
dirPath := filepath.Join(fs.KeyDir, sessionKeyDir)
|
||
|
||
err := os.RemoveAll(dirPath)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetKey returns a key for a given host. If the key is not found,
|
||
// returns trace.NotFound error.
|
||
func (fs *FSLocalKeyStore) GetKey(proxyHost string, username string) (*Key, error) {
|
||
dirPath, err := fs.dirFor(proxyHost, false)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
_, err = ioutil.ReadDir(dirPath)
|
||
if err != nil {
|
||
return nil, trace.NotFound("no session keys for %v in %v", username, proxyHost)
|
||
}
|
||
|
||
certFile := filepath.Join(dirPath, username+fileExtCert)
|
||
cert, err := ioutil.ReadFile(certFile)
|
||
if err != nil {
|
||
fs.log.Error(err)
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
tlsCertFile := filepath.Join(dirPath, username+fileExtTLSCert)
|
||
tlsCert, err := ioutil.ReadFile(tlsCertFile)
|
||
if err != nil {
|
||
fs.log.Error(err)
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
pub, err := ioutil.ReadFile(filepath.Join(dirPath, username+fileExtPub))
|
||
if err != nil {
|
||
fs.log.Error(err)
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
priv, err := ioutil.ReadFile(filepath.Join(dirPath, username))
|
||
if err != nil {
|
||
fs.log.Error(err)
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
tlsCA, err := fs.GetCertsPEM(proxyHost)
|
||
if err != nil {
|
||
fs.log.Error(err)
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
key := &Key{
|
||
Pub: pub,
|
||
Priv: priv,
|
||
Cert: cert,
|
||
ProxyHost: proxyHost,
|
||
TLSCert: tlsCert,
|
||
TrustedCA: []auth.TrustedCerts{{
|
||
TLSCertificates: tlsCA,
|
||
}},
|
||
}
|
||
|
||
// Validate the key loaded from disk.
|
||
err = key.CheckCert()
|
||
if err != nil {
|
||
// KeyStore should return expired certificates as well
|
||
if !utils.IsCertExpiredError(err) {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
}
|
||
sshCertExpiration, err := key.CertValidBefore()
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
tlsCertExpiration, err := key.TLSCertValidBefore()
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
// Note, we may be returning expired certificates here, that is okay. If the
|
||
// certificates is expired, it's the responsibility of the TeleportClient to
|
||
// perform cleanup of the certificates and the profile.
|
||
fs.log.Debugf("Returning SSH certificate %q valid until %q, TLS certificate %q valid until %q.",
|
||
certFile, sshCertExpiration, tlsCertFile, tlsCertExpiration)
|
||
|
||
return key, nil
|
||
}
|
||
|
||
// SaveCerts saves trusted TLS certificates of certificate authorities
|
||
func (fs *FSLocalKeyStore) SaveCerts(proxy string, cas []auth.TrustedCerts) error {
|
||
dir, err := fs.dirFor(proxy, true)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
fp, err := os.OpenFile(filepath.Join(dir, fileNameTLSCerts), os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0640)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
defer fp.Close()
|
||
for _, ca := range cas {
|
||
for _, cert := range ca.TLSCertificates {
|
||
_, err := fp.Write(cert)
|
||
if err != nil {
|
||
return trace.ConvertSystemError(err)
|
||
}
|
||
_, err = fp.WriteString("\n")
|
||
if err != nil {
|
||
return trace.ConvertSystemError(err)
|
||
}
|
||
}
|
||
}
|
||
return fp.Sync()
|
||
}
|
||
|
||
// GetCertsPEM returns trusted TLS certificates of certificate authorities PEM
|
||
// blocks.
|
||
func (fs *FSLocalKeyStore) GetCertsPEM(proxy string) ([][]byte, error) {
|
||
dir, err := fs.dirFor(proxy, false)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
data, err := ioutil.ReadFile(filepath.Join(dir, fileNameTLSCerts))
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
var blocks [][]byte
|
||
for len(data) > 0 {
|
||
block, rest := pem.Decode(data)
|
||
if block == nil {
|
||
break
|
||
}
|
||
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
|
||
fs.log.Debugf("Skipping PEM block type=%v headers=%v.", block.Type, block.Headers)
|
||
continue
|
||
}
|
||
// rest contains the remainder of data after reading a block.
|
||
// Therefore, the block length is len(data) - len(rest).
|
||
// Use that length to slice the block from the start of data.
|
||
blocks = append(blocks, data[:len(data)-len(rest)])
|
||
data = rest
|
||
}
|
||
return blocks, nil
|
||
}
|
||
|
||
// GetCerts returns trusted TLS certificates of certificate authorities as
|
||
// x509.CertPool.
|
||
func (fs *FSLocalKeyStore) GetCerts(proxy string) (*x509.CertPool, error) {
|
||
blocks, err := fs.GetCertsPEM(proxy)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
pool := x509.NewCertPool()
|
||
for _, bytes := range blocks {
|
||
block, _ := pem.Decode(bytes)
|
||
if block == nil {
|
||
continue
|
||
}
|
||
|
||
cert, err := x509.ParseCertificate(block.Bytes)
|
||
if err != nil {
|
||
return nil, trace.BadParameter("failed to parse certificate: %v", err)
|
||
}
|
||
fs.log.Debugf("Adding trusted cluster certificate authority %q to trusted pool.", cert.Issuer)
|
||
pool.AddCert(cert)
|
||
}
|
||
return pool, nil
|
||
}
|
||
|
||
// AddKnownHostKeys adds a new entry to 'known_hosts' file
|
||
func (fs *FSLocalKeyStore) AddKnownHostKeys(hostname string, hostKeys []ssh.PublicKey) error {
|
||
fp, err := os.OpenFile(filepath.Join(fs.KeyDir, fileNameKnownHosts), os.O_CREATE|os.O_RDWR, 0640)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
defer fp.Close()
|
||
// 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
|
||
for i := range hostKeys {
|
||
fs.log.Debugf("Adding known host %s with key: %v", hostname, sshutils.Fingerprint(hostKeys[i]))
|
||
bytes := ssh.MarshalAuthorizedKey(hostKeys[i])
|
||
line := strings.TrimSpace(fmt.Sprintf("%s %s", hostname, 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)
|
||
}
|
||
return fp.Sync()
|
||
}
|
||
|
||
// GetKnownHostKeys returns all known public keys from 'known_hosts'
|
||
func (fs *FSLocalKeyStore) GetKnownHostKeys(hostname string) ([]ssh.PublicKey, error) {
|
||
bytes, err := ioutil.ReadFile(filepath.Join(fs.KeyDir, fileNameKnownHosts))
|
||
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)
|
||
hosts []string
|
||
hostMatch bool
|
||
)
|
||
for err == nil {
|
||
_, hosts, pubKey, _, bytes, err = ssh.ParseKnownHosts(bytes)
|
||
if err == nil {
|
||
hostMatch = (hostname == "")
|
||
if !hostMatch {
|
||
for i := range hosts {
|
||
if hosts[i] == hostname {
|
||
hostMatch = true
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if hostMatch {
|
||
retval = append(retval, pubKey)
|
||
}
|
||
}
|
||
}
|
||
if err != io.EOF {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
return retval, nil
|
||
}
|
||
|
||
// dirFor returns the path to the session keys for a given host. The value
|
||
// for fs.KeyDir is typically "~/.tsh", sessionKeyDir is typically "keys",
|
||
// and proxyHost typically has values like "proxy.example.com".
|
||
//
|
||
// If the create flag is true, the directory will be created if it does
|
||
// not exist.
|
||
func (fs *FSLocalKeyStore) dirFor(proxyHost string, create bool) (string, error) {
|
||
dirPath := filepath.Join(fs.KeyDir, sessionKeyDir, proxyHost)
|
||
|
||
if create {
|
||
if err := os.MkdirAll(dirPath, profileDirPerms); err != nil {
|
||
fs.log.Error(err)
|
||
return "", trace.ConvertSystemError(err)
|
||
}
|
||
}
|
||
|
||
return dirPath, nil
|
||
}
|
||
|
||
// initKeysDir initializes the keystore root directory. Usually it is ~/.tsh
|
||
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)
|
||
}
|
||
// create if doesn't exist:
|
||
_, err = os.Stat(dirPath)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
err = os.MkdirAll(dirPath, os.ModeDir|profileDirPerms)
|
||
if err != nil {
|
||
return "", trace.ConvertSystemError(err)
|
||
}
|
||
} else {
|
||
return "", trace.Wrap(err)
|
||
}
|
||
}
|
||
|
||
return dirPath, nil
|
||
}
|