/
ssh.go
153 lines (134 loc) · 4.13 KB
/
ssh.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package ssh
import (
"fmt"
"io/ioutil"
"os"
"path"
"reflect"
"strings"
"time"
"github.com/chanzuckerberg/blessclient/pkg/config"
"github.com/chanzuckerberg/blessclient/pkg/errs"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
const (
timeSkew = time.Second * 30
)
// SSH is a namespace
type SSH struct {
keyName string
sshDirectory string
}
// NewSSH returns a new SSH object
func NewSSH(privateKey string) (*SSH, error) {
expandedPrivateKey, err := homedir.Expand(privateKey)
if err != nil {
return nil, errors.Errorf("Could not expand path %s", privateKey)
}
// Basic sanity check key is present
// TODO maybe parse the file to make sure it is actually a private key
_, err = os.Stat(expandedPrivateKey)
if err != nil {
if os.IsNotExist(err) {
return nil, errs.ErrSSHKeyNotFound
}
return nil, errors.Wrapf(err, "Could not stat key at %s", expandedPrivateKey)
}
ssh := &SSH{
keyName: path.Base(expandedPrivateKey),
sshDirectory: path.Dir(expandedPrivateKey),
}
return ssh, nil
}
// ReadPublicKey reads the SSH public key
func (s *SSH) ReadPublicKey() ([]byte, error) {
pubKey := path.Join(s.sshDirectory, fmt.Sprintf("%s.pub", s.keyName))
bytes, err := ioutil.ReadFile(pubKey)
return bytes, errors.Wrap(err, "Could not read public key")
}
// ReadAndParsePublicKey reads and unmarshals a public key
func (s *SSH) ReadAndParsePublicKey() (ssh.PublicKey, error) {
bytes, err := s.ReadPublicKey()
if err != nil {
return nil, err
}
key, err := ssh.ParsePublicKey(bytes)
return key, errors.Wrap(err, "Could not parse public key")
}
// ReadPrivateKey reads the private key
func (s *SSH) ReadPrivateKey() ([]byte, error) {
privKey := path.Join(s.sshDirectory, s.keyName)
bytes, err := ioutil.ReadFile(privKey)
return bytes, errors.Wrapf(err, "Could not read private key %s", privKey)
}
// ReadAndParsePrivateKey reads and unmarshals a private key
func (s *SSH) ReadAndParsePrivateKey() (interface{}, error) {
bytes, err := s.ReadPrivateKey()
if err != nil {
return nil, err
}
key, err := ssh.ParseRawPrivateKey(bytes)
return key, errors.Wrap(err, "Could not parse private key")
}
// ReadCert reads the ssh cert
func (s *SSH) ReadCert() ([]byte, error) {
cert := path.Join(s.sshDirectory, fmt.Sprintf("%s-cert.pub", s.keyName))
bytes, err := ioutil.ReadFile(cert)
if err != nil {
if os.IsNotExist(err) {
return nil, nil // no cert
}
return nil, errors.Wrap(err, "Could not read cert")
}
return bytes, nil
}
// ReadAndParseCert reads a certificate off disk and attempts to unmarshal it
func (s *SSH) ReadAndParseCert() (*ssh.Certificate, error) {
bytes, err := s.ReadCert()
if err != nil {
return nil, err
}
// no cert
if bytes == nil {
return nil, nil
}
k, _, _, _, err := ssh.ParseAuthorizedKey(bytes)
if err != nil {
return nil, errors.Wrap(err, "Could not parse cert")
}
cert, ok := k.(*ssh.Certificate)
if !ok {
return nil, errors.New("Bytes do not correspond to an ssh certificate")
}
return cert, nil
}
// IsCertFresh determines if the cert is still fresh
func (s *SSH) IsCertFresh(c *config.Config) (bool, error) {
cert, err := s.ReadAndParseCert()
if err != nil {
return false, err
}
if cert == nil {
return false, nil
}
now := time.Now()
validBefore := time.Unix(int64(cert.ValidBefore), 0).Add(timeSkew) // uper bound
validAfter := time.Unix(int64(cert.ValidAfter), 0).Add(-1 * timeSkew) // lower bound
isFresh := now.After(validAfter) && now.Before(validBefore)
// TODO: add more validation for certificate critical options
val, ok := cert.CriticalOptions["source-address"]
isFresh = isFresh && ok && val == strings.Join(c.ClientConfig.BastionIPS, ",")
// Compare principals
isFresh = isFresh && reflect.DeepEqual(cert.ValidPrincipals, c.ClientConfig.RemoteUsers)
return isFresh, nil
}
// WriteCert writes a cert to disk
func (s *SSH) WriteCert(b []byte) error {
certPath := path.Join(s.sshDirectory, fmt.Sprintf("%s-cert.pub", s.keyName))
log.Debugf("Writing cert to %s", certPath)
err := ioutil.WriteFile(certPath, b, 0644)
return errors.Wrapf(err, "Could not write cert to %s", certPath)
}