Skip to content

Commit 862e04a

Browse files
committed
internal/gomote, internal/coordinator/remote: add the sign SSH key endpoint implementation
This change adds the implementation for the sign SSH key endpoint. The endpoint accepts a public SSH key and signs it with the gomote server's certificate authority. The certificate added to the public key will be used to validate if the certificate authority was used to sign the certificate. It will also be used to determine if the requestor has rights to initiate and SSH session with the gomote instance being requested. This is part of a shift to OpenSSH certificate authentication in the gomote SSH server. For golang/go#47521 Updates golang/go#48742 Change-Id: I427b34c7f006ae20f5643322dc0754bf7a82e5f1 Reviewed-on: https://go-review.googlesource.com/c/build/+/391516 Trust: Carlos Amedee <[email protected]> Run-TryBot: Carlos Amedee <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Heschi Kreinick <[email protected]>
1 parent 289767d commit 862e04a

File tree

8 files changed

+505
-173
lines changed

8 files changed

+505
-173
lines changed

cmd/coordinator/coordinator.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ func main() {
341341
if err := loadStatic(); err != nil {
342342
log.Printf("Failed to load static resources: %v", err)
343343
}
344+
sshCA := mustRetrieveSSHCertificateAuthority()
344345

345346
var opts []grpc.ServerOption
346347
if *buildEnvName == "" && *mode != "dev" && metadata.OnGCE() {
@@ -362,7 +363,7 @@ func main() {
362363
dashV1 := legacydash.Handler(gce.GoDSClient(), maintnerClient, string(masterKey()), grpcServer)
363364
dashV2 := &builddash.Handler{Datastore: gce.GoDSClient(), Maintner: maintnerClient}
364365
gs := &gRPCServer{dashboardURL: "https://build.golang.org"}
365-
gomoteServer := gomote.New(remote.NewSessionPool(context.Background()), sched)
366+
gomoteServer := gomote.New(remote.NewSessionPool(context.Background()), sched, sshCA)
366367
protos.RegisterCoordinatorServer(grpcServer, gs)
367368
gomoteprotos.RegisterGomoteServiceServer(grpcServer, gomoteServer)
368369
mux.HandleFunc("/", grpcHandlerFunc(grpcServer, handleStatus)) // Serve a status page at farmer.golang.org.
@@ -2214,3 +2215,11 @@ func mustCreateEC2BuildletPool(sc *secret.Client) *pool.EC2Buildlet {
22142215
}
22152216
return ec2Pool
22162217
}
2218+
2219+
func mustRetrieveSSHCertificateAuthority() (privateKey []byte) {
2220+
privateKey, _, err := remote.SSHKeyPair()
2221+
if err != nil {
2222+
log.Fatalf("unable to create SSH CA cert: %s", err)
2223+
}
2224+
return
2225+
}

internal/coordinator/remote/ssh.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package remote
6+
7+
import (
8+
"context"
9+
"crypto/ecdsa"
10+
"crypto/elliptic"
11+
"crypto/rand"
12+
"crypto/x509"
13+
"encoding/pem"
14+
"fmt"
15+
"time"
16+
17+
"golang.org/x/crypto/ssh"
18+
)
19+
20+
// SignPublicSSHKey signs a public SSH key using the certificate authority. These keys are intended for use with the specified gomote and owner.
21+
// The public SSH are intended to be used in OpenSSH certificate authentication with the gomote SSH server.
22+
func SignPublicSSHKey(ctx context.Context, caPriKey ssh.Signer, rawPubKey []byte, sessionID, ownerID string, d time.Duration) ([]byte, error) {
23+
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(rawPubKey)
24+
if err != nil {
25+
return nil, fmt.Errorf("unable to parse public key=%w", err)
26+
}
27+
cert := &ssh.Certificate{
28+
Key: pubKey,
29+
Serial: 1,
30+
CertType: ssh.UserCert,
31+
KeyId: "go_build",
32+
ValidPrincipals: []string{fmt.Sprintf("%[email protected]", sessionID), ownerID},
33+
ValidAfter: uint64(time.Now().Unix()),
34+
ValidBefore: uint64(time.Now().Add(d).Unix()),
35+
Permissions: ssh.Permissions{
36+
Extensions: map[string]string{
37+
"permit-X11-forwarding": "",
38+
"permit-agent-forwarding": "",
39+
"permit-port-forwarding": "",
40+
"permit-pty": "",
41+
"permit-user-rc": "",
42+
},
43+
},
44+
}
45+
if err := cert.SignCert(rand.Reader, caPriKey); err != nil {
46+
return nil, fmt.Errorf("cerificate.SignCert() = %w", err)
47+
}
48+
mCert := ssh.MarshalAuthorizedKey(cert)
49+
return mCert, nil
50+
}
51+
52+
// SSHKeyPair generates a set of ecdsa256 SSH Keys. The public key is serialized for inclusion in
53+
// an OpenSSH authorized_keys file. The private key is PEM encoded.
54+
func SSHKeyPair() (privateKey []byte, publicKey []byte, err error) {
55+
private, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
56+
if err != nil {
57+
return nil, nil, err
58+
}
59+
public, err := ssh.NewPublicKey(&private.PublicKey)
60+
if err != nil {
61+
return nil, nil, err
62+
}
63+
publicKey = ssh.MarshalAuthorizedKey(public)
64+
priKeyByt, err := x509.MarshalECPrivateKey(private)
65+
if err != nil {
66+
return nil, nil, fmt.Errorf("unable to marshal private key=%w", err)
67+
}
68+
privateKey = pem.EncodeToMemory(&pem.Block{
69+
Type: "EC PRIVATE KEY",
70+
Bytes: priKeyByt,
71+
})
72+
return
73+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package remote
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"testing"
11+
"time"
12+
13+
"github.com/google/go-cmp/cmp"
14+
"golang.org/x/crypto/ssh"
15+
)
16+
17+
func TestSignPublicSSHKey(t *testing.T) {
18+
signer, err := ssh.ParsePrivateKey([]byte(devCertCAPrivate))
19+
if err != nil {
20+
t.Fatalf("ssh.ParsePrivateKey() = %s", err)
21+
}
22+
ownerID := "accounts.google.com:userIDvalue"
23+
sessionID := "user-maria-linux-amd64-12"
24+
gotPubKey, err := SignPublicSSHKey(context.Background(), signer, []byte(devCertClientPublic), sessionID, ownerID, time.Minute)
25+
if err != nil {
26+
t.Fatalf("SignPublicSSHKey(...) = _, %s; want no error", err)
27+
}
28+
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(gotPubKey)
29+
if err != nil {
30+
t.Fatalf("ssh.ParseAuthorizedKey(...) = %s; want no error", err)
31+
}
32+
certChecker := &ssh.CertChecker{}
33+
wantPrinciple := fmt.Sprintf("%[email protected]", sessionID)
34+
pubKeyCert := pubKey.(*ssh.Certificate)
35+
if err := certChecker.CheckCert(wantPrinciple, pubKeyCert); err != nil {
36+
t.Fatalf("certChecker.CheckCert(%s, %+v) = %s", wantPrinciple, pubKeyCert, err)
37+
}
38+
if diff := cmp.Diff(pubKeyCert.SignatureKey.Marshal(), signer.PublicKey().Marshal()); diff != "" {
39+
t.Fatalf("Public Keys mismatch (-want +got):\n%s", diff)
40+
}
41+
}
42+
43+
const (
44+
// devCertCAPrivate is a private SSH CA certificate to be used for development.
45+
devCertCAPrivate = `-----BEGIN OPENSSH PRIVATE KEY-----
46+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
47+
QyNTUxOQAAACCVd2FJ3Db/oV53iRDt1RLscTn41hYXbunuCWIlXze2WAAAAJhjy3ePY8t3
48+
jwAAAAtzc2gtZWQyNTUxOQAAACCVd2FJ3Db/oV53iRDt1RLscTn41hYXbunuCWIlXze2WA
49+
AAAEALuUJMb/rEaFNa+vn5RejeoBiiViyda7djgEvMnQ8fRJV3YUncNv+hXneJEO3VEuxx
50+
OfjWFhdu6e4JYiVfN7ZYAAAAE3Rlc3R1c2VyQGdvbGFuZy5vcmcBAg==
51+
-----END OPENSSH PRIVATE KEY-----`
52+
53+
// devCertCAPublic is a public SSH CA certificate to be used for development.
54+
devCertCAPublic = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJV3YUncNv+hXneJEO3VEuxxOfjWFhdu6e4JYiVfN7ZY [email protected]`
55+
56+
// devCertClientPrivate is a private SSH certificate to be used for development.
57+
devCertClientPrivate = `-----BEGIN OPENSSH PRIVATE KEY-----
58+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
59+
QyNTUxOQAAACBxCM6ADdHnjTIHG/IpMa3z32CLwtu3BDUR3k2NNbI3owAAAKDFZ7xtxWe8
60+
bQAAAAtzc2gtZWQyNTUxOQAAACBxCM6ADdHnjTIHG/IpMa3z32CLwtu3BDUR3k2NNbI3ow
61+
AAAECidrOyYbTlYxyBSPP7W/UHk3Si2dgWSfkT+eEIETcvqHEIzoAN0eeNMgcb8ikxrfPf
62+
YIvC27cENRHeTY01sjejAAAAFnRlc3RfY2xpZW50QGdvbGFuZy5vcmcBAgMEBQYH
63+
-----END OPENSSH PRIVATE KEY-----`
64+
65+
// devCertClientPublic is a public SSH certificate to be used for development.
66+
devCertClientPublic = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHEIzoAN0eeNMgcb8ikxrfPfYIvC27cENRHeTY01sjej [email protected]`
67+
68+
// devCertAlternateClientPrivate is a private SSH certificate to be used for development.
69+
devCertAlternateClientPrivate = `-----BEGIN OPENSSH PRIVATE KEY-----
70+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
71+
QyNTUxOQAAACDOj8K2lbCSv+LojNcrUf0XH1vqknuEZBkAceiBHuNuEQAAAKDYNRtZ2DUb
72+
WQAAAAtzc2gtZWQyNTUxOQAAACDOj8K2lbCSv+LojNcrUf0XH1vqknuEZBkAceiBHuNuEQ
73+
AAAEDS4G3tQt5S4v7CD+DVyT/mwOKgIScIgFOpFt/EsCXL9M6PwraVsJK/4uiM1ytR/Rcf
74+
W+qSe4RkGQBx6IEe424RAAAAF3Rlc3RfZGlzY2FyZEBnb2xhbmcub3JnAQIDBAUG
75+
-----END OPENSSH PRIVATE KEY-----`
76+
77+
// devCertAlternateClientPublic is a public SSH to be used for development.
78+
devCertAlternateClientPublic = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM6PwraVsJK/4uiM1ytR/RcfW+qSe4RkGQBx6IEe424R [email protected]`
79+
)

internal/gomote/gomote.go

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"golang.org/x/build/internal/coordinator/schedule"
2424
"golang.org/x/build/internal/gomote/protos"
2525
"golang.org/x/build/types"
26+
"golang.org/x/crypto/ssh"
2627
"google.golang.org/grpc/codes"
2728
"google.golang.org/grpc/status"
2829
)
@@ -38,15 +39,21 @@ type Server struct {
3839
// embed the unimplemented server.
3940
protos.UnimplementedGomoteServiceServer
4041

41-
buildlets *remote.SessionPool
42-
scheduler scheduler
42+
buildlets *remote.SessionPool
43+
scheduler scheduler
44+
sshCertificateAuthority ssh.Signer
4345
}
4446

45-
// New creates a gomote server.
46-
func New(rsp *remote.SessionPool, sched *schedule.Scheduler) *Server {
47+
// New creates a gomote server. If the rawCAPriKey is invalid, the program will exit.
48+
func New(rsp *remote.SessionPool, sched *schedule.Scheduler, rawCAPriKey []byte) *Server {
49+
signer, err := ssh.ParsePrivateKey(rawCAPriKey)
50+
if err != nil {
51+
log.Fatalf("unable to parse raw certificate authority private key into signer=%s", err)
52+
}
4753
return &Server{
48-
buildlets: rsp,
49-
scheduler: sched,
54+
buildlets: rsp,
55+
scheduler: sched,
56+
sshCertificateAuthority: signer,
5057
}
5158
}
5259

@@ -161,6 +168,7 @@ func (s *Server) InstanceAlive(ctx context.Context, req *protos.InstanceAliveReq
161168
return &protos.InstanceAliveResponse{}, nil
162169
}
163170

171+
// ListDirectory lists the contents of the directory on a gomote instance.
164172
func (s *Server) ListDirectory(ctx context.Context, req *protos.ListDirectoryRequest) (*protos.ListDirectoryResponse, error) {
165173
creds, err := access.IAPFromContext(ctx)
166174
if err != nil {
@@ -306,6 +314,28 @@ func (s *Server) RemoveFiles(ctx context.Context, req *protos.RemoveFilesRequest
306314
return &protos.RemoveFilesResponse{}, nil
307315
}
308316

317+
// SignSSHKey signs the public SSH key with a certificate. The signed public SSH key is intended for use with the gomote service SSH
318+
// server. It will be signed by the certificate authority of the server and will restrict access to the gomote instance that it was
319+
// signed for.
320+
func (s *Server) SignSSHKey(ctx context.Context, req *protos.SignSSHKeyRequest) (*protos.SignSSHKeyResponse, error) {
321+
creds, err := access.IAPFromContext(ctx)
322+
if err != nil {
323+
return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
324+
}
325+
session, err := s.session(req.GetGomoteId(), creds.ID)
326+
if err != nil {
327+
// the helper function returns meaningful GRPC error.
328+
return nil, err
329+
}
330+
signedPublicKey, err := remote.SignPublicSSHKey(ctx, s.sshCertificateAuthority, req.GetPublicSshKey(), session.ID, session.OwnerID, 5*time.Minute)
331+
if err != nil {
332+
return nil, status.Errorf(codes.InvalidArgument, "unable to sign ssh key")
333+
}
334+
return &protos.SignSSHKeyResponse{
335+
SignedPublicSshKey: signedPublicKey,
336+
}, nil
337+
}
338+
309339
// WriteTGZFromURL will instruct the gomote instance to download the tar.gz from the provided URL. The tar.gz file will be unpacked in the work directory
310340
// relative to the directory provided.
311341
func (s *Server) WriteTGZFromURL(ctx context.Context, req *protos.WriteTGZFromURLRequest) (*protos.WriteTGZFromURLResponse, error) {

0 commit comments

Comments
 (0)