Skip to content

Commit a1e72af

Browse files
authored
Merge pull request cli#4392 from cli/jg/validate-host-key
codespace: validate host public keys
2 parents c3ce95e + 2ce14f6 commit a1e72af

File tree

7 files changed

+88
-56
lines changed

7 files changed

+88
-56
lines changed

internal/api/api.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,11 @@ const (
172172
)
173173

174174
type CodespaceEnvironmentConnection struct {
175-
SessionID string `json:"sessionId"`
176-
SessionToken string `json:"sessionToken"`
177-
RelayEndpoint string `json:"relayEndpoint"`
178-
RelaySAS string `json:"relaySas"`
175+
SessionID string `json:"sessionId"`
176+
SessionToken string `json:"sessionToken"`
177+
RelayEndpoint string `json:"relayEndpoint"`
178+
RelaySAS string `json:"relaySas"`
179+
HostPublicKeys []string `json:"hostPublicKeys"`
179180
}
180181

181182
func (a *API) ListCodespaces(ctx context.Context, user string) ([]*Codespace, error) {

internal/codespaces/codespaces.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,10 @@ func ConnectToLiveshare(ctx context.Context, log logger, apiClient apiClient, us
6868
log.Println("Connecting to your codespace...")
6969

7070
return liveshare.Connect(ctx, liveshare.Options{
71-
SessionID: codespace.Environment.Connection.SessionID,
72-
SessionToken: codespace.Environment.Connection.SessionToken,
73-
RelaySAS: codespace.Environment.Connection.RelaySAS,
74-
RelayEndpoint: codespace.Environment.Connection.RelayEndpoint,
71+
SessionID: codespace.Environment.Connection.SessionID,
72+
SessionToken: codespace.Environment.Connection.SessionToken,
73+
RelaySAS: codespace.Environment.Connection.RelaySAS,
74+
RelayEndpoint: codespace.Environment.Connection.RelayEndpoint,
75+
HostPublicKeys: codespace.Environment.Connection.HostPublicKeys,
7576
})
7677
}

internal/liveshare/client.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ import (
2424

2525
// An Options specifies Live Share connection parameters.
2626
type Options struct {
27-
SessionID string
28-
SessionToken string // token for SSH session
29-
RelaySAS string
30-
RelayEndpoint string
31-
TLSConfig *tls.Config // (optional)
27+
SessionID string
28+
SessionToken string // token for SSH session
29+
RelaySAS string
30+
RelayEndpoint string
31+
HostPublicKeys []string
32+
TLSConfig *tls.Config // (optional)
3233
}
3334

3435
// uri returns a websocket URL for the specified options.
@@ -71,7 +72,7 @@ func Connect(ctx context.Context, opts Options) (*Session, error) {
7172
if opts.SessionToken == "" {
7273
return nil, errors.New("SessionToken is required")
7374
}
74-
ssh := newSSHSession(opts.SessionToken, sock)
75+
ssh := newSSHSession(opts.SessionToken, opts.HostPublicKeys, sock)
7576
if err := ssh.connect(ctx); err != nil {
7677
return nil, fmt.Errorf("error connecting to ssh session: %w", err)
7778
}

internal/liveshare/client_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import (
1515

1616
func TestConnect(t *testing.T) {
1717
opts := Options{
18-
SessionID: "session-id",
19-
SessionToken: "session-token",
20-
RelaySAS: "relay-sas",
18+
SessionID: "session-id",
19+
SessionToken: "session-token",
20+
RelaySAS: "relay-sas",
21+
HostPublicKeys: []string{livesharetest.SSHPublicKey},
2122
}
2223
joinWorkspace := func(req *jsonrpc2.Request) (interface{}, error) {
2324
var joinWorkspaceReq joinWorkspaceArgs

internal/liveshare/session_test.go

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ func makeMockSession(opts ...livesharetest.ServerOption) (*livesharetest.Server,
2929
}
3030

3131
session, err := Connect(context.Background(), Options{
32-
SessionID: "session-id",
33-
SessionToken: sessionToken,
34-
RelayEndpoint: "sb" + strings.TrimPrefix(testServer.URL(), "https"),
35-
RelaySAS: "relay-sas",
36-
TLSConfig: &tls.Config{InsecureSkipVerify: true},
32+
SessionID: "session-id",
33+
SessionToken: sessionToken,
34+
RelayEndpoint: "sb" + strings.TrimPrefix(testServer.URL(), "https"),
35+
RelaySAS: "relay-sas",
36+
HostPublicKeys: []string{livesharetest.SSHPublicKey},
37+
TLSConfig: &tls.Config{InsecureSkipVerify: true},
3738
})
3839
if err != nil {
3940
return nil, nil, fmt.Errorf("error connecting to Live Share: %w", err)
@@ -194,3 +195,29 @@ func TestServerUpdateSharedVisibility(t *testing.T) {
194195
}
195196
}
196197
}
198+
199+
func TestInvalidHostKey(t *testing.T) {
200+
joinWorkspace := func(req *jsonrpc2.Request) (interface{}, error) {
201+
return joinWorkspaceResult{1}, nil
202+
}
203+
const sessionToken = "session-token"
204+
opts := []livesharetest.ServerOption{
205+
livesharetest.WithPassword(sessionToken),
206+
livesharetest.WithService("workspace.joinWorkspace", joinWorkspace),
207+
}
208+
testServer, err := livesharetest.NewServer(opts...)
209+
if err != nil {
210+
t.Errorf("error creating server: %w", err)
211+
}
212+
_, err = Connect(context.Background(), Options{
213+
SessionID: "session-id",
214+
SessionToken: sessionToken,
215+
RelayEndpoint: "sb" + strings.TrimPrefix(testServer.URL(), "https"),
216+
RelaySAS: "relay-sas",
217+
HostPublicKeys: []string{},
218+
TLSConfig: &tls.Config{InsecureSkipVerify: true},
219+
})
220+
if err == nil {
221+
t.Error("expected invalid host key error, got: nil")
222+
}
223+
}

internal/liveshare/ssh.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package liveshare
22

33
import (
44
"context"
5+
"encoding/base64"
6+
"errors"
57
"fmt"
68
"io"
79
"net"
@@ -12,15 +14,16 @@ import (
1214

1315
type sshSession struct {
1416
*ssh.Session
15-
token string
16-
socket net.Conn
17-
conn ssh.Conn
18-
reader io.Reader
19-
writer io.Writer
17+
token string
18+
hostPublicKeys []string
19+
socket net.Conn
20+
conn ssh.Conn
21+
reader io.Reader
22+
writer io.Writer
2023
}
2124

22-
func newSSHSession(token string, socket net.Conn) *sshSession {
23-
return &sshSession{token: token, socket: socket}
25+
func newSSHSession(token string, hostPublicKeys []string, socket net.Conn) *sshSession {
26+
return &sshSession{token: token, hostPublicKeys: hostPublicKeys, socket: socket}
2427
}
2528

2629
func (s *sshSession) connect(ctx context.Context) error {
@@ -30,8 +33,16 @@ func (s *sshSession) connect(ctx context.Context) error {
3033
ssh.Password(s.token),
3134
},
3235
HostKeyAlgorithms: []string{"rsa-sha2-512", "rsa-sha2-256"},
33-
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
34-
Timeout: 10 * time.Second,
36+
HostKeyCallback: func(hostname string, addr net.Addr, key ssh.PublicKey) error {
37+
encodedKey := base64.StdEncoding.EncodeToString(key.Marshal())
38+
for _, hpk := range s.hostPublicKeys {
39+
if encodedKey == hpk {
40+
return nil // we found a match for expected public key, safely return
41+
}
42+
}
43+
return errors.New("invalid host public key")
44+
},
45+
Timeout: 10 * time.Second,
3546
}
3647

3748
sshClientConn, chans, reqs, err := ssh.NewClientConn(s.socket, "", &clientConfig)

internal/liveshare/test/server.go

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,23 @@ import (
1616
)
1717

1818
const sshPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
19-
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
4432
-----END RSA PRIVATE KEY-----`
4533

34+
const SSHPublicKey = `AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6VU6XsMaTot9ogsGcJ+juvJOmDvvCZmgJRTRwKkW0u2BLz4yVrCzQcxaY4kaIuR80Y+1f0BLnZgh4pTREDR0T+p8hUsDSHim1ttKI8rK0hRtJ2qhYlR4qt7P51rPA4KFA9z9gDjTwQLbDq21QMC4+n4d8CL3xRVGtlUAMM3Kl3w==`
35+
4636
// Server represents a LiveShare relay host server.
4737
type Server struct {
4838
password string

0 commit comments

Comments
 (0)