dashboard / erock/tuns / feat: use cert pubkey instead of underlying pubkey #88 rss

closed · opened on 2025-12-13T18:19:19Z by erock
Help
checkout latest patchset:
ssh pr.pico.sh print pr-88 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print ps-X | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 88
add review to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add --review 88
accept PR:
ssh pr.pico.sh pr accept 88
close PR:
ssh pr.pico.sh pr close 88

Logs

erock created pr with ps-164 on 2025-12-13T18:19:19Z
erock changed status on 2025-12-14T01:49:38Z {"status":"closed"}

Patchsets

ps-164 by erock on 2025-12-13T18:19:19Z

+124 -0 utils/authentication_key_request_test.go link
  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
diff --git a/utils/authentication_key_request_test.go b/utils/authentication_key_request_test.go
index 01f0657..c0069cb 100644
--- a/utils/authentication_key_request_test.go
+++ b/utils/authentication_key_request_test.go
@@ -11,7 +11,9 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"os"
+	"strings"
 	"testing"
+	"time"
 
 	"github.com/spf13/viper"
 	"golang.org/x/crypto/ssh"
@@ -252,3 +254,125 @@ func TestAuthenticationKeyRequest(t *testing.T) {
 		}
 	}
 }
+
+// TestAuthenticationKeyRequestWithCertificate validates that when authenticating
+// with an SSH certificate, the certificate (not the underlying public key) is sent
+// to the authentication-key-request-url.
+func TestAuthenticationKeyRequestWithCertificate(t *testing.T) {
+	// Generate CA key for signing certificates
+	caKey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		t.Fatal(err)
+	}
+	caSigner, err := ssh.NewSignerFromKey(caKey)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Generate user key
+	userKey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		t.Fatal(err)
+	}
+	userPubKey, err := ssh.NewPublicKey(&userKey.PublicKey)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Create a certificate signed by the CA
+	cert := &ssh.Certificate{
+		Key:             userPubKey,
+		Serial:          1,
+		CertType:        ssh.UserCert,
+		KeyId:           "test-user",
+		ValidPrincipals: []string{"ubuntu"},
+		ValidAfter:      uint64(time.Now().Add(-time.Hour).Unix()),
+		ValidBefore:     uint64(time.Now().Add(time.Hour).Unix()),
+	}
+	err = cert.SignCert(rand.Reader, caSigner)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Create a signer that uses the certificate
+	userSigner, err := ssh.NewSignerFromKey(userKey)
+	if err != nil {
+		t.Fatal(err)
+	}
+	certSigner, err := ssh.NewCertSigner(cert, userSigner)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Give sish a temp directory to generate a server ssh host key
+	dir, err := os.MkdirTemp("", "sish_keys_cert")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+
+	viper.Set("private-keys-directory", dir)
+	viper.Set("authentication", true)
+
+	// Track what key was received by the auth server
+	var receivedKey string
+	httpSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		body, err := io.ReadAll(r.Body)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusBadRequest)
+			return
+		}
+		var reqBody AuthRequestBody
+		err = json.Unmarshal(body, &reqBody)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusBadRequest)
+			return
+		}
+		receivedKey = reqBody.PubKey
+		// Accept the auth
+		w.WriteHeader(http.StatusOK)
+	}))
+	defer httpSrv.Close()
+
+	viper.Set("authentication-key-request-url", httpSrv.URL)
+
+	sshListener, err := net.Listen("tcp", "localhost:0")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer sshListener.Close()
+
+	successAuth := make(chan bool)
+	go HandleSSHConn(sshListener, &successAuth)
+
+	// Connect with the certificate
+	clientConfig := &ssh.ClientConfig{
+		Auth: []ssh.AuthMethod{
+			ssh.PublicKeys(certSigner),
+		},
+		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+		User:            "ubuntu",
+	}
+
+	client, err := ssh.Dial("tcp", sshListener.Addr().String(), clientConfig)
+	if err != nil {
+		t.Fatalf("ssh client connection failed: %v", err)
+	}
+	client.Close()
+
+	didAuth := <-successAuth
+	if !didAuth {
+		t.Error("Expected auth to succeed")
+	}
+
+	// Verify that the received key is a certificate (starts with ssh-rsa-cert-v01@openssh.com or similar)
+	if !strings.Contains(receivedKey, "-cert-") {
+		t.Errorf("Expected certificate to be sent to auth URL, got: %s", receivedKey[:min(100, len(receivedKey))])
+	}
+
+	// Verify it's not just the plain public key
+	plainPubKey := string(ssh.MarshalAuthorizedKey(userPubKey))
+	if strings.TrimSpace(receivedKey) == strings.TrimSpace(plainPubKey) {
+		t.Error("Expected certificate to be sent, but received the underlying public key instead")
+	}
+}
+8 -1 utils/utils.go link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
diff --git a/utils/utils.go b/utils/utils.go
index 2d5792c..95d203d 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -515,7 +515,14 @@ func GetSSHConfig() *ssh.ServerConfig {
 			// Allow validation of public keys via a sub-request to another service
 			authUrl := viper.GetString("authentication-key-request-url")
 			if authUrl != "" {
-				validKey, extensionsInfo, err := checkAuthenticationKeyRequest(authUrl, authKey, c.RemoteAddr(), c.User())
+				// If the key is an SSH certificate, send the certificate instead of the underlying public key
+				authKeyToSend := authKey
+				if cert, ok := key.(*ssh.Certificate); ok {
+					certKey := ssh.MarshalAuthorizedKey(cert)
+					authKeyToSend = certKey[:len(certKey)-1]
+				}
+
+				validKey, extensionsInfo, err := checkAuthenticationKeyRequest(authUrl, authKeyToSend, c.RemoteAddr(), c.User())
 				if err != nil {
 					slog.Error("error calling authentication key url", slog.String("authURL", authUrl), slog.Any("error", err))
 				}