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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
|
package ssh
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io"
"net"
"os"
"time"
"github.com/mimecast/dtail/internal/io/dlog"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
// closerFunc adapts a plain func to io.Closer so callers can always
// unconditionally `defer closer.Close()` regardless of the code path taken.
type closerFunc func() error
// Close implements io.Closer.
func (f closerFunc) Close() error {
if f == nil {
return nil
}
return f()
}
// noopCloser is returned when there is no resource to release. Using a single
// shared value keeps allocations out of the fast error paths.
var noopCloser io.Closer = closerFunc(func() error { return nil })
// dialAgent dials the local ssh-agent unix socket. It is a package-level
// variable so unit tests can replace it with a fake for deterministic
// error-path coverage (see ssh_agent_test.go).
var dialAgent = func(addr string) (net.Conn, error) {
// Use context-aware dialing for SSH agent connection (local Unix socket).
// 2-second timeout is reasonable for local socket connections.
dialer := &net.Dialer{Timeout: 2 * time.Second}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
return dialer.DialContext(ctx, "unix", addr)
}
// GeneratePrivateRSAKey is used by the server to generate its key.
func GeneratePrivateRSAKey(size int) (*rsa.PrivateKey, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, size)
if err != nil {
return nil, fmt.Errorf("failed to generate RSA key: %w", err)
}
if err = privateKey.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate generated RSA key: %w", err)
}
return privateKey, nil
}
// EncodePrivateKeyToPEM is a helper function for converting a key to PEM format.
func EncodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte {
derFormat := x509.MarshalPKCS1PrivateKey(privateKey)
block := pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: derFormat,
}
return pem.EncodeToMemory(&block)
}
// Agent used for SSH auth. The returned io.Closer owns the underlying
// ssh-agent connection and MUST be closed by the caller once the returned
// AuthMethod is no longer needed (i.e. after the final SSH handshake that
// uses it has completed).
func Agent() (gossh.AuthMethod, io.Closer, error) {
return AgentWithKeyIndex(-1)
}
// AgentSignersWithKeyIndex returns SSH agent signers together with an
// io.Closer that owns the underlying ssh-agent connection.
//
// The agent signers call back to the agent over the returned connection on
// every Sign() invocation, so the connection must stay open for as long as
// the signers are in use (typically until the SSH handshakes that consume
// them are complete). The caller is responsible for invoking Close() on the
// returned io.Closer to release file descriptors and the agent goroutine.
//
// The returned io.Closer is always non-nil, including on error paths, so
// callers can unconditionally `defer closer.Close()`.
//
// If keyIndex is -1, all keys are used. Otherwise, only the specified key is
// used.
func AgentSignersWithKeyIndex(keyIndex int) ([]gossh.Signer, io.Closer, error) {
sshAgent, err := dialAgent(os.Getenv("SSH_AUTH_SOCK"))
if err != nil {
return nil, noopCloser, fmt.Errorf("failed to connect to SSH agent: %w", err)
}
// Ensure the connection is released on every error path. The success
// path hands ownership to the caller by setting owned to nil right
// before the return.
owned := sshAgent
defer func() {
if owned != nil {
_ = owned.Close()
}
}()
agentClient := agent.NewClient(sshAgent)
keys, err := agentClient.List()
if err != nil {
return nil, noopCloser, fmt.Errorf("failed to list SSH agent keys: %w", err)
}
for i, key := range keys {
dlog.Common.Debug("Public key", i, key)
}
signers, err := agentClient.Signers()
if err != nil {
return nil, noopCloser, fmt.Errorf("failed to load SSH agent signers: %w", err)
}
// If no specific key index requested, use all keys (backwards compatible default)
if keyIndex < 0 {
owned = nil
return signers, sshAgent, nil
}
// Use only the specified key index (0-based)
if keyIndex >= len(signers) {
return nil, noopCloser, fmt.Errorf("key index %d out of range (agent has %d signers)", keyIndex, len(signers))
}
dlog.Common.Debug("Using SSH agent key at index", keyIndex)
owned = nil
return []gossh.Signer{signers[keyIndex]}, sshAgent, nil
}
// AgentWithKeyIndex used for SSH auth with a specific key index from the agent.
// The caller owns the returned io.Closer; see AgentSignersWithKeyIndex for
// lifetime semantics.
// If keyIndex is -1, all keys are used. Otherwise, only the specified key is used.
func AgentWithKeyIndex(keyIndex int) (gossh.AuthMethod, io.Closer, error) {
signers, closer, err := AgentSignersWithKeyIndex(keyIndex)
if err != nil {
return nil, closer, err
}
return gossh.PublicKeys(signers...), closer, nil
}
// PrivateKeySigner returns an SSH signer from the provided private key file.
func PrivateKeySigner(keyFile string) (gossh.Signer, error) {
buffer, err := os.ReadFile(keyFile)
if err != nil {
return nil, err
}
key, err := gossh.ParsePrivateKey(buffer)
if err != nil {
var passphraseMissingErr *gossh.PassphraseMissingError
if !errors.As(err, &passphraseMissingErr) {
return nil, err
}
passphrase := os.Getenv("DTAIL_KEY_PASSPHRASE")
if passphrase == "" {
return nil, err
}
key, err = gossh.ParsePrivateKeyWithPassphrase(buffer, []byte(passphrase))
if err != nil {
return nil, err
}
}
return key, nil
}
// KeyFile returns the key as a SSH auth method.
func KeyFile(keyFile string) (gossh.AuthMethod, error) {
key, err := PrivateKeySigner(keyFile)
if err != nil {
return nil, err
}
// Key phrase support disabled as password will be printed to stdout!
/*
if err == nil {
return gossh.PublicKeys(key), nil
}
keyPhrase := EnterKeyPhrase(keyFile)
key, err = gossh.ParsePrivateKeyWithPassphrase(buffer, keyPhrase)
if err != nil {
return nil, err
}
*/
return gossh.PublicKeys(key), nil
}
// PrivateKey returns the private key as a SSH auth method.
func PrivateKey(keyFile string) (gossh.AuthMethod, error) {
signer, err := KeyFile(keyFile)
if err != nil {
dlog.Common.Debug(keyFile, err)
return nil, err
}
return gossh.AuthMethod(signer), nil
}
|