File: ssh.go

package info (click to toggle)
golang-github-muesli-sasquatch 0.0~git20210519.30aff9d-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 184 kB
  • sloc: makefile: 2
file content (160 lines) | stat: -rw-r--r-- 4,677 bytes parent folder | download
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
package sasquatch

import (
	"crypto/ed25519"
	"crypto/rsa"
	"fmt"
	"net"
	"os"
	"path/filepath"

	"github.com/mitchellh/go-homedir"
	"golang.org/x/crypto/ssh"
	"golang.org/x/crypto/ssh/agent"
)

// EncryptedSSHIdentity is an IdentityMatcher implementation based on a
// passphrase encrypted SSH private key.
//
// It provides public key based matching and deferred decryption so the
// passphrase is only requested if necessary. If the application knows it will
// unconditionally have to decrypt the private key, it would be simpler to use
// ssh.ParseRawPrivateKeyWithPassphrase directly and pass the result to
// NewEd25519Identity or NewRSAIdentity.
type EncryptedSSHIdentity struct {
	pubKey     ssh.PublicKey
	pemBytes   []byte
	passphrase func() ([]byte, error)

	decrypted Identity
}

// NewEncryptedSSHIdentity returns a new EncryptedSSHIdentity.
//
// pubKey must be the public key associated with the encrypted private key, and
// it must have type "ssh-ed25519" or "ssh-rsa". For OpenSSH encrypted files it
// can be extracted from an ssh.PassphraseMissingError, otherwise in can often
// be found in ".pub" files.
//
// pemBytes must be a valid input to ssh.ParseRawPrivateKeyWithPassphrase.
// passphrase is a callback that will be invoked by Unwrap when the passphrase
// is necessary.
func NewEncryptedSSHIdentity(pubKey ssh.PublicKey, pemBytes []byte, passphrase func() ([]byte, error)) (*EncryptedSSHIdentity, error) {
	switch t := pubKey.Type(); t {
	case "ssh-ed25519", "ssh-rsa":
	default:
		return nil, fmt.Errorf("unsupported SSH key type: %v", t)
	}
	return &EncryptedSSHIdentity{
		pubKey:     pubKey,
		pemBytes:   pemBytes,
		passphrase: passphrase,
	}, nil
}

// Type returns the type of the underlying private key, "ssh-ed25519" or "ssh-rsa".
func (i *EncryptedSSHIdentity) Type() string {
	return i.pubKey.Type()
}

// Unwrap implements Identity. If the private key is still encrypted, it
// will request the passphrase. The decrypted private key will be cached after
// the first successful invocation.
func (i *EncryptedSSHIdentity) Unwrap(block *Stanza) (fileKey []byte, err error) {
	if i.decrypted != nil {
		return i.decrypted.Unwrap(block)
	}

	passphrase, err := i.passphrase()
	if err != nil {
		return nil, fmt.Errorf("failed to obtain passphrase: %v", err)
	}
	k, err := ssh.ParseRawPrivateKeyWithPassphrase(i.pemBytes, passphrase)
	if err != nil {
		return nil, fmt.Errorf("failed to decrypt SSH key file: %v", err)
	}

	switch k := k.(type) {
	case *ed25519.PrivateKey:
		i.decrypted, err = NewEd25519Identity(*k)
	case *rsa.PrivateKey:
		i.decrypted, err = NewRSAIdentity(k)
	default:
		return nil, fmt.Errorf("unexpected SSH key type: %T", k)
	}
	if err != nil {
		return nil, fmt.Errorf("invalid SSH key: %v", err)
	}
	if i.decrypted.Type() != i.pubKey.Type() {
		return nil, fmt.Errorf("mismatched SSH key type: got %q, expected %q", i.decrypted.Type(), i.pubKey.Type())
	}

	return i.decrypted.Unwrap(block)
}

// Match implements IdentityMatcher without decrypting the private key, to
// ensure the passphrase is only obtained if necessary.
func (i *EncryptedSSHIdentity) Match(block *Stanza) error {
	if block.Type != i.Type() {
		return ErrIncorrectIdentity
	}
	if len(block.Args) < 1 {
		return fmt.Errorf("invalid %v recipient block", i.Type())
	}

	if block.Args[0] != sshFingerprint(i.pubKey) {
		return ErrIncorrectIdentity
	}
	return nil
}

// FindSSHKeys looks in a user's ~/.ssh dir for possible SSH keys. If no keys
// are found we return an empty slice.
func FindSSHKeys() ([]string, error) {
	path, err := homedir.Expand("~/.ssh")
	if err != nil {
		return nil, err
	}

	m, err := filepath.Glob(filepath.Join(path, "id_*"))
	if err != nil {
		return nil, err
	}

	var found []string
	for _, f := range m {
		switch filepath.Base(f) {
		case "id_dsa":
			fallthrough
		case "id_rsa":
			fallthrough
		case "id_ecdsa":
			fallthrough
		case "id_ed25519":
			found = append(found, f)
		}
	}

	return found, nil
}

// SSHAgentSigners connect to ssh-agent and returns all available signers.
func SSHAgentSigners() ([]ssh.Signer, error) {
	conn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
	if err != nil {
		return nil, fmt.Errorf("Could not connect to the SSH Agent socket. %s", err)
	}

	sshAgent := agent.NewClient(conn)

	signers, err := sshAgent.Signers()
	if err != nil {
		return nil, fmt.Errorf("Could not retrieve signers from the SSH Agent. %v", err)
	}

	if len(signers) == 0 {
		return nil, fmt.Errorf("There are no SSH keys added to the SSH Agent. Check that you have added keys to the SSH Agent and that SSH Agent Forwarding is enabled if you are using this remotely.")
	}

	return signers, nil
}