dashboard / erock/pico / feat(auth): authenticate key handler using ssh cert #90 rss

open · opened on 2025-12-14T01:53:24Z by erock
Help
checkout latest patchset:
ssh pr.pico.sh print pr-90 | 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 90
add review to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add --review 90
accept PR:
ssh pr.pico.sh pr accept 90
close PR:
ssh pr.pico.sh pr close 90

Logs

erock created pr with ps-166 on 2025-12-14T01:53:24Z

Patchsets

ps-166 by erock on 2025-12-14T01:53:24Z

+22 -5 pkg/apps/auth/api.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
diff --git a/pkg/apps/auth/api.go b/pkg/apps/auth/api.go
index 7e14321..b055990 100644
--- a/pkg/apps/auth/api.go
+++ b/pkg/apps/auth/api.go
@@ -23,6 +23,7 @@ import (
 	"github.com/picosh/utils/pipe"
 	"github.com/picosh/utils/pipe/metrics"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
+	"golang.org/x/crypto/ssh"
 )
 
 //go:embed html/* public/*
@@ -245,22 +246,38 @@ func keyHandler(apiConfig *shared.ApiConfig) http.HandlerFunc {
 
 		space := r.URL.Query().Get("space")
 
-		apiConfig.Cfg.Logger.Info(
-			"handle key",
+		log := apiConfig.Cfg.Logger.With(
 			"remoteAddress", data.RemoteAddress,
 			"user", data.Username,
 			"space", space,
 			"publicKey", data.PublicKey,
 		)
 
-		user, err := apiConfig.Dbpool.FindUserForKey(data.Username, data.PublicKey)
+		log.Info("handle key")
+
+		key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(data.PublicKey))
 		if err != nil {
-			apiConfig.Cfg.Logger.Error(err.Error())
+			log.Error("parse authorized key", "err", err)
+			http.Error(w, err.Error(), http.StatusBadRequest)
+			return
+		}
+
+		pubkey, err := shared.PubkeyCertVerify(key, space)
+		if err != nil {
+			log.Error("pubkey cert verify", "err", err)
+			http.Error(w, err.Error(), http.StatusBadRequest)
+			return
+		}
+
+		user, err := apiConfig.Dbpool.FindUserForKey(data.Username, pubkey)
+		if err != nil {
+			log.Error("find user for key", "err", err)
 			w.WriteHeader(http.StatusUnauthorized)
 			return
 		}
 
 		if !apiConfig.HasPlusOrSpace(user, space) {
+			log.Error("key handler unauthorized")
 			w.WriteHeader(http.StatusUnauthorized)
 			return
 		}
@@ -274,7 +291,7 @@ func keyHandler(apiConfig *shared.ApiConfig) http.HandlerFunc {
 		w.WriteHeader(http.StatusOK)
 		err = json.NewEncoder(w).Encode(user)
 		if err != nil {
-			apiConfig.Cfg.Logger.Error(err.Error())
+			log.Error("json encode", "err", err)
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 		}
 	}
+1 -1 pkg/apps/auth/api_test.go link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
diff --git a/pkg/apps/auth/api_test.go b/pkg/apps/auth/api_test.go
index f53d06c..7711783 100644
--- a/pkg/apps/auth/api_test.go
+++ b/pkg/apps/auth/api_test.go
@@ -97,7 +97,7 @@ func TestKey(t *testing.T) {
 
 	data := sishData{
 		Username:  testUsername,
-		PublicKey: "zzz",
+		PublicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxVPgEqtWOa5l0QHZV6TQKhV+l46SAXU07c9RuHlGka test@pico",
 	}
 	jso, err := json.Marshal(data)
 	bail(err)
+19 -14 pkg/shared/ssh.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
diff --git a/pkg/shared/ssh.go b/pkg/shared/ssh.go
index 234c0bc..83273ca 100644
--- a/pkg/shared/ssh.go
+++ b/pkg/shared/ssh.go
@@ -33,41 +33,46 @@ func NewSshAuthHandler(dbh AuthFindUser, logger *slog.Logger, principal string)
 	}
 }
 
-func (r *SshAuthHandler) PubkeyAuthHandler(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
-	log := r.Logger
-	var user *db.User
-	var err error
-	pubkey := ""
-
+func PubkeyCertVerify(key ssh.PublicKey, srcPrincipal string) (string, error) {
 	cert, ok := key.(*ssh.Certificate)
 	if ok {
 		if cert.CertType != ssh.UserCert {
-			return nil, fmt.Errorf("ssh-cert has type %d", cert.CertType)
+			return "", fmt.Errorf("ssh-cert has type %d", cert.CertType)
 		}
 
 		found := false
 		for _, princ := range cert.ValidPrincipals {
-			if princ == "admin" || princ == r.Principal {
+			if princ == "admin" || princ == srcPrincipal {
 				found = true
 				break
 			}
 		}
 		if !found {
-			return nil, fmt.Errorf("ssh-cert principals not valid")
+			return "", fmt.Errorf("ssh-cert principals not valid")
 		}
 
 		clock := time.Now
 		unixNow := clock().Unix()
 		if after := int64(cert.ValidAfter); after < 0 || unixNow < int64(cert.ValidAfter) {
-			return nil, fmt.Errorf("ssh-cert is not yet valid")
+			return "", fmt.Errorf("ssh-cert is not yet valid")
 		}
 		if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(ssh.CertTimeInfinity) && (unixNow >= before || before < 0) {
-			return nil, fmt.Errorf("ssh-cert has expired")
+			return "", fmt.Errorf("ssh-cert has expired")
 		}
 
-		pubkey = utils.KeyForKeyText(cert.SignatureKey)
-	} else {
-		pubkey = utils.KeyForKeyText(key)
+		return utils.KeyForKeyText(cert.SignatureKey), nil
+	}
+
+	return utils.KeyForKeyText(key), nil
+}
+
+func (r *SshAuthHandler) PubkeyAuthHandler(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
+	log := r.Logger
+	var user *db.User
+	var err error
+	pubkey, err := PubkeyCertVerify(key, r.Principal)
+	if err != nil {
+		return nil, err
 	}
 
 	user, err = r.DB.FindUserByPubkey(pubkey)