blob: f404d07fb0c18333ba8bf799ff913c169be2d04c [file] [log] [blame]
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.16 && (linux || darwin)
package remote
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"golang.org/x/build/buildlet"
"golang.org/x/crypto/ssh"
"golang.org/x/net/nettest"
)
func TestSignPublicSSHKey(t *testing.T) {
signer, err := ssh.ParsePrivateKey([]byte(devCertCAPrivate))
if err != nil {
t.Fatalf("ssh.ParsePrivateKey() = %s", err)
}
ownerID := "accounts.google.com:userIDvalue"
sessionID := "user-maria-linux-amd64-12"
gotPubKey, err := SignPublicSSHKey(context.Background(), signer, []byte(devCertClientPublic), sessionID, ownerID, time.Minute)
if err != nil {
t.Fatalf("SignPublicSSHKey(...) = _, %s; want no error", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(gotPubKey)
if err != nil {
t.Fatalf("ssh.ParseAuthorizedKey(...) = %s; want no error", err)
}
certChecker := &ssh.CertChecker{}
wantPrinciple := fmt.Sprintf("%s@farmer.golang.org", sessionID)
pubKeyCert := pubKey.(*ssh.Certificate)
if err := certChecker.CheckCert(wantPrinciple, pubKeyCert); err != nil {
t.Fatalf("certChecker.CheckCert(%s, %+v) = %s", wantPrinciple, pubKeyCert, err)
}
if diff := cmp.Diff(pubKeyCert.SignatureKey.Marshal(), signer.PublicKey().Marshal()); diff != "" {
t.Fatalf("Public Keys mismatch (-want +got):\n%s", diff)
}
}
func TestHandleCertificateAuthFunc(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
addr, sp, s := setupSSHServer(t, ctx)
defer s.Close()
ownerID := "accounts.google.com:userIDvalue"
sessionID := sp.AddSession(ownerID, "maria", "linux-amd64", "xyz", &buildlet.FakeClient{})
certSigner := parsePrivateKey(t, []byte(devCertCAPrivate))
clientPubKey, err := SignPublicSSHKey(ctx, certSigner, []byte(devCertClientPublic), sessionID, ownerID, time.Minute)
if err != nil {
t.Fatalf("SignPublicSSHKey(...) = _, %s; want no error", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(clientPubKey)
if err != nil {
t.Fatalf("ParsePublicKey(...) = _, %s; want no error", err)
}
cert := pubKey.(*ssh.Certificate)
clientCertSigner := parsePrivateKey(t, []byte(devCertClientPrivate))
clientSigner, err := ssh.NewCertSigner(cert, clientCertSigner)
if err != nil {
t.Fatalf("NewCertSigner(...) = _, %s; want no error", err)
}
clientConfig := &ssh.ClientConfig{
User: sessionID,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(clientSigner),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
client, err := ssh.Dial("tcp", addr, clientConfig)
if err != nil {
t.Fatalf("Dial(...) = _, %s; want no error", err)
}
client.Close()
}
func TestHandleCertificateAuthFuncErrors(t *testing.T) {
t.Run("no certificate", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
addr, sp, s := setupSSHServer(t, ctx)
defer s.Close()
ownerID := "accounts.google.com:userIDvalue"
sessionID := sp.AddSession(ownerID, "maria", "linux-amd64", "xyz", &buildlet.FakeClient{})
clientSigner := parsePrivateKey(t, []byte(devCertClientPrivate))
clientConfig := &ssh.ClientConfig{
User: sessionID,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(clientSigner),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
_, err := ssh.Dial("tcp", addr, clientConfig)
if err == nil {
t.Fatal("Dial(...) = client, nil; want error")
}
})
t.Run("wrong certificate signer", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
addr, sp, s := setupSSHServer(t, ctx)
defer s.Close()
ownerID := "accounts.google.com:userIDvalue"
sessionID := sp.AddSession(ownerID, "maria", "linux-amd64", "xyz", &buildlet.FakeClient{})
certSigner := parsePrivateKey(t, []byte(devCertAlternateClientPrivate))
clientPubKey, err := SignPublicSSHKey(ctx, certSigner, []byte(devCertClientPublic), sessionID, ownerID, time.Minute)
if err != nil {
t.Fatalf("SignPublicSSHKey(...) = _, %s; want no error", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(clientPubKey)
if err != nil {
t.Fatalf("ParsePublicKey(...) = _, %s; want no error", err)
}
cert := pubKey.(*ssh.Certificate)
clientCertSigner := parsePrivateKey(t, []byte(devCertClientPrivate))
clientSigner, err := ssh.NewCertSigner(cert, clientCertSigner)
if err != nil {
t.Fatalf("NewCertSigner(...) = _, %s; want no error", err)
}
clientConfig := &ssh.ClientConfig{
User: sessionID,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(clientSigner),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
_, err = ssh.Dial("tcp", addr, clientConfig)
if err == nil {
t.Fatalf("Dial(...) = _, %s; want no error", err)
}
})
t.Run("wrong user", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
addr, sp, s := setupSSHServer(t, ctx)
defer s.Close()
ownerID := "accounts.google.com:userIDvalue"
sessionID := sp.AddSession(ownerID, "maria", "linux-amd64", "xyz", &buildlet.FakeClient{})
certSigner := parsePrivateKey(t, []byte(devCertCAPrivate))
clientPubKey, err := SignPublicSSHKey(ctx, certSigner, []byte(devCertClientPublic), sessionID, ownerID, time.Minute)
if err != nil {
t.Fatalf("SignPublicSSHKey(...) = _, %s; want no error", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(clientPubKey)
if err != nil {
t.Fatalf("ParsePublicKey(...) = _, %s; want no error", err)
}
cert := pubKey.(*ssh.Certificate)
clientCertSigner := parsePrivateKey(t, []byte(devCertClientPrivate))
clientSigner, err := ssh.NewCertSigner(cert, clientCertSigner)
if err != nil {
t.Fatalf("NewCertSigner(...) = _, %s; want no error", err)
}
clientConfig := &ssh.ClientConfig{
User: sessionID + "_i_do_not_exist",
Auth: []ssh.AuthMethod{
ssh.PublicKeys(clientSigner),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
_, err = ssh.Dial("tcp", addr, clientConfig)
if err == nil {
t.Fatal("Dial(...) = _, nil; want error")
}
})
t.Run("wrong principle", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
addr, sp, s := setupSSHServer(t, ctx)
defer s.Close()
ownerID := "accounts.google.com:userIDvalue"
sessionID := sp.AddSession(ownerID, "maria", "linux-amd64", "xyz", &buildlet.FakeClient{})
certSigner := parsePrivateKey(t, []byte(devCertCAPrivate))
clientPubKey, err := SignPublicSSHKey(ctx, certSigner, []byte(devCertClientPublic), sessionID+"WRONG", ownerID, time.Minute)
if err != nil {
t.Fatalf("SignPublicSSHKey(...) = _, %s; want no error", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(clientPubKey)
if err != nil {
t.Fatalf("ParsePublicKey(...) = _, %s; want no error", err)
}
cert := pubKey.(*ssh.Certificate)
clientCertSigner := parsePrivateKey(t, []byte(devCertClientPrivate))
clientSigner, err := ssh.NewCertSigner(cert, clientCertSigner)
if err != nil {
t.Fatalf("NewCertSigner(...) = _, %s; want no error", err)
}
clientConfig := &ssh.ClientConfig{
User: sessionID,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(clientSigner),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
_, err = ssh.Dial("tcp", addr, clientConfig)
if err == nil {
t.Fatal("Dial(...) = _, nil; want error")
}
})
t.Run("wrong owner", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
addr, sp, s := setupSSHServer(t, ctx)
defer s.Close()
ownerID := "accounts.google.com:userIDvalue"
sessionID := sp.AddSession(ownerID, "maria", "linux-amd64", "xyz", &buildlet.FakeClient{})
certSigner := parsePrivateKey(t, []byte(devCertCAPrivate))
clientPubKey, err := SignPublicSSHKey(ctx, certSigner, []byte(devCertClientPublic), sessionID, ownerID+"WRONG", time.Minute)
if err != nil {
t.Fatalf("SignPublicSSHKey(...) = _, %s; want no error", err)
}
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(clientPubKey)
if err != nil {
t.Fatalf("ParsePublicKey(...) = _, %s; want no error", err)
}
cert := pubKey.(*ssh.Certificate)
clientCertSigner := parsePrivateKey(t, []byte(devCertClientPrivate))
clientSigner, err := ssh.NewCertSigner(cert, clientCertSigner)
if err != nil {
t.Fatalf("NewCertSigner(...) = _, %s; want no error", err)
}
clientConfig := &ssh.ClientConfig{
User: sessionID,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(clientSigner),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
_, err = ssh.Dial("tcp", addr, clientConfig)
if err == nil {
t.Fatal("Dial(...) = _, nil; want error")
}
})
}
func setupSSHServer(t *testing.T, ctx context.Context) (addr string, sp *SessionPool, s *SSHServer) {
sp = NewSessionPool(ctx)
l, err := nettest.NewLocalListener("tcp")
if err != nil {
t.Fatalf("nettest.NewLocalListener(tcp) = _, %s; want no error", err)
}
addr = l.Addr().String()
s, err = NewSSHServer(addr, []byte(devCertAlternateClientPrivate), []byte(devCertCAPublic), []byte(devCertCAPrivate), sp)
if err != nil {
t.Fatalf("NewSSHServer(...) = %s; want no error", err)
}
go s.serve(l)
if err != nil {
t.Fatalf("server.serve(l) = %s; want no error", err)
}
return
}
func parsePrivateKey(t *testing.T, pemEncoded []byte) ssh.Signer {
cert, err := ssh.ParsePrivateKey(pemEncoded)
if err != nil {
t.Fatalf("ssh.ParsePrivateKey() = _, %s; want no error", err)
}
return cert
}
const (
// devCertCAPrivate is a private SSH CA certificate to be used for development.
devCertCAPrivate = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCVd2FJ3Db/oV53iRDt1RLscTn41hYXbunuCWIlXze2WAAAAJhjy3ePY8t3
jwAAAAtzc2gtZWQyNTUxOQAAACCVd2FJ3Db/oV53iRDt1RLscTn41hYXbunuCWIlXze2WA
AAAEALuUJMb/rEaFNa+vn5RejeoBiiViyda7djgEvMnQ8fRJV3YUncNv+hXneJEO3VEuxx
OfjWFhdu6e4JYiVfN7ZYAAAAE3Rlc3R1c2VyQGdvbGFuZy5vcmcBAg==
-----END OPENSSH PRIVATE KEY-----`
// devCertCAPublic is a public SSH CA certificate to be used for development.
devCertCAPublic = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJV3YUncNv+hXneJEO3VEuxxOfjWFhdu6e4JYiVfN7ZY testuser@golang.org`
// devCertClientPrivate is a private SSH certificate to be used for development.
devCertClientPrivate = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBxCM6ADdHnjTIHG/IpMa3z32CLwtu3BDUR3k2NNbI3owAAAKDFZ7xtxWe8
bQAAAAtzc2gtZWQyNTUxOQAAACBxCM6ADdHnjTIHG/IpMa3z32CLwtu3BDUR3k2NNbI3ow
AAAECidrOyYbTlYxyBSPP7W/UHk3Si2dgWSfkT+eEIETcvqHEIzoAN0eeNMgcb8ikxrfPf
YIvC27cENRHeTY01sjejAAAAFnRlc3RfY2xpZW50QGdvbGFuZy5vcmcBAgMEBQYH
-----END OPENSSH PRIVATE KEY-----`
// devCertClientPublic is a public SSH certificate to be used for development.
devCertClientPublic = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHEIzoAN0eeNMgcb8ikxrfPfYIvC27cENRHeTY01sjej test_client@golang.org`
// devCertAlternateClientPrivate is a private SSH certificate to be used for development.
devCertAlternateClientPrivate = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDOj8K2lbCSv+LojNcrUf0XH1vqknuEZBkAceiBHuNuEQAAAKDYNRtZ2DUb
WQAAAAtzc2gtZWQyNTUxOQAAACDOj8K2lbCSv+LojNcrUf0XH1vqknuEZBkAceiBHuNuEQ
AAAEDS4G3tQt5S4v7CD+DVyT/mwOKgIScIgFOpFt/EsCXL9M6PwraVsJK/4uiM1ytR/Rcf
W+qSe4RkGQBx6IEe424RAAAAF3Rlc3RfZGlzY2FyZEBnb2xhbmcub3JnAQIDBAUG
-----END OPENSSH PRIVATE KEY-----`
// devCertAlternateClientPublic is a public SSH to be used for development.
devCertAlternateClientPublic = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM6PwraVsJK/4uiM1ytR/RcfW+qSe4RkGQBx6IEe424R test_discard@golang.org`
)