tuns

created pr with 88.1 on 2025-12-13T18:19:19Z · by c8ef7d19
cmds
checkout latest patchset:
ssh pr.pico.sh print 88 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print 88.[rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 88
+124 -0 utils/authentication_key_request_test.go #
......@@ -11,7 +11,9 @@ import (
1111 "net/http"
1212 "net/http/httptest"
1313 "os"
14+ "strings"
1415 "testing"
16+ "time"
1517
1618 "github.com/spf13/viper"
1719 "golang.org/x/crypto/ssh"
......@@ -252,3 +254,125 @@ func TestAuthenticationKeyRequest(t *testing.T) {
252254 }
253255 }
254256 }
257+
258+// TestAuthenticationKeyRequestWithCertificate validates that when authenticating
259+// with an SSH certificate, the certificate (not the underlying public key) is sent
260+// to the authentication-key-request-url.
261+func TestAuthenticationKeyRequestWithCertificate(t *testing.T) {
262+ // Generate CA key for signing certificates
263+ caKey, err := rsa.GenerateKey(rand.Reader, 2048)
264+ if err != nil {
265+ t.Fatal(err)
266+ }
267+ caSigner, err := ssh.NewSignerFromKey(caKey)
268+ if err != nil {
269+ t.Fatal(err)
270+ }
271+
272+ // Generate user key
273+ userKey, err := rsa.GenerateKey(rand.Reader, 2048)
274+ if err != nil {
275+ t.Fatal(err)
276+ }
277+ userPubKey, err := ssh.NewPublicKey(&userKey.PublicKey)
278+ if err != nil {
279+ t.Fatal(err)
280+ }
281+
282+ // Create a certificate signed by the CA
283+ cert := &ssh.Certificate{
284+ Key: userPubKey,
285+ Serial: 1,
286+ CertType: ssh.UserCert,
287+ KeyId: "test-user",
288+ ValidPrincipals: []string{"ubuntu"},
289+ ValidAfter: uint64(time.Now().Add(-time.Hour).Unix()),
290+ ValidBefore: uint64(time.Now().Add(time.Hour).Unix()),
291+ }
292+ err = cert.SignCert(rand.Reader, caSigner)
293+ if err != nil {
294+ t.Fatal(err)
295+ }
296+
297+ // Create a signer that uses the certificate
298+ userSigner, err := ssh.NewSignerFromKey(userKey)
299+ if err != nil {
300+ t.Fatal(err)
301+ }
302+ certSigner, err := ssh.NewCertSigner(cert, userSigner)
303+ if err != nil {
304+ t.Fatal(err)
305+ }
306+
307+ // Give sish a temp directory to generate a server ssh host key
308+ dir, err := os.MkdirTemp("", "sish_keys_cert")
309+ if err != nil {
310+ t.Fatal(err)
311+ }
312+ defer os.RemoveAll(dir)
313+
314+ viper.Set("private-keys-directory", dir)
315+ viper.Set("authentication", true)
316+
317+ // Track what key was received by the auth server
318+ var receivedKey string
319+ httpSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
320+ body, err := io.ReadAll(r.Body)
321+ if err != nil {
322+ http.Error(w, err.Error(), http.StatusBadRequest)
323+ return
324+ }
325+ var reqBody AuthRequestBody
326+ err = json.Unmarshal(body, &reqBody)
327+ if err != nil {
328+ http.Error(w, err.Error(), http.StatusBadRequest)
329+ return
330+ }
331+ receivedKey = reqBody.PubKey
332+ // Accept the auth
333+ w.WriteHeader(http.StatusOK)
334+ }))
335+ defer httpSrv.Close()
336+
337+ viper.Set("authentication-key-request-url", httpSrv.URL)
338+
339+ sshListener, err := net.Listen("tcp", "localhost:0")
340+ if err != nil {
341+ t.Fatal(err)
342+ }
343+ defer sshListener.Close()
344+
345+ successAuth := make(chan bool)
346+ go HandleSSHConn(sshListener, &successAuth)
347+
348+ // Connect with the certificate
349+ clientConfig := &ssh.ClientConfig{
350+ Auth: []ssh.AuthMethod{
351+ ssh.PublicKeys(certSigner),
352+ },
353+ HostKeyCallback: ssh.InsecureIgnoreHostKey(),
354+ User: "ubuntu",
355+ }
356+
357+ client, err := ssh.Dial("tcp", sshListener.Addr().String(), clientConfig)
358+ if err != nil {
359+ t.Fatalf("ssh client connection failed: %v", err)
360+ }
361+ client.Close()
362+
363+ didAuth := <-successAuth
364+ if !didAuth {
365+ t.Error("Expected auth to succeed")
366+ }
367+
368+ // Verify that the received key is a certificate (starts with ssh-rsa-cert-v01@openssh.com or similar)
369+ if !strings.Contains(receivedKey, "-cert-") {
370+ t.Errorf("Expected certificate to be sent to auth URL, got: %s", receivedKey[:min(100, len(receivedKey))])
371+ }
372+
373+ // Verify it's not just the plain public key
374+ plainPubKey := string(ssh.MarshalAuthorizedKey(userPubKey))
375+ if strings.TrimSpace(receivedKey) == strings.TrimSpace(plainPubKey) {
376+ t.Error("Expected certificate to be sent, but received the underlying public key instead")
377+ }
378+}
+8 -1 utils/utils.go #
......@@ -515,7 +515,14 @@ func GetSSHConfig() *ssh.ServerConfig {
515515 // Allow validation of public keys via a sub-request to another service
516516 authUrl := viper.GetString("authentication-key-request-url")
517517 if authUrl != "" {
518- validKey, extensionsInfo, err := checkAuthenticationKeyRequest(authUrl, authKey, c.RemoteAddr(), c.User())
518+ // If the key is an SSH certificate, send the certificate instead of the underlying public key
519+ authKeyToSend := authKey
520+ if cert, ok := key.(*ssh.Certificate); ok {
521+ certKey := ssh.MarshalAuthorizedKey(cert)
522+ authKeyToSend = certKey[:len(certKey)-1]
523+ }
524+
525+ validKey, extensionsInfo, err := checkAuthenticationKeyRequest(authUrl, authKeyToSend, c.RemoteAddr(), c.User())
519526 if err != nil {
520527 slog.Error("error calling authentication key url", slog.String("authURL", authUrl), slog.Any("error", err))
521528 }
Back to top