Commit 59aba61
Eric Bower
·
2025-12-13 20:49:35 -0500 EST
parent bb8ef88
feat(auth): authenticate key handler using ssh cert
3 files changed,
+42,
-20
+22,
-5
| ... | ... | @@ -23,6 +23,7 @@ import ( | |
| 23 | 23 | "github.com/picosh/utils/pipe" | |
| 24 | 24 | "github.com/picosh/utils/pipe/metrics" | |
| 25 | 25 | "github.com/prometheus/client_golang/prometheus/promhttp" | |
| 26 | + | "golang.org/x/crypto/ssh" | |
| 26 | 27 | ) | |
| 27 | 28 | ||
| 28 | 29 | //go:embed html/* public/* |
| ... | ... | @@ -245,22 +246,38 @@ func keyHandler(apiConfig *shared.ApiConfig) http.HandlerFunc { | |
| 245 | 246 | ||
| 246 | 247 | space := r.URL.Query().Get("space") | |
| 247 | 248 | ||
| 248 | - | apiConfig.Cfg.Logger.Info( | |
| 249 | - | "handle key", | |
| 249 | + | log := apiConfig.Cfg.Logger.With( | |
| 250 | 250 | "remoteAddress", data.RemoteAddress, | |
| 251 | 251 | "user", data.Username, | |
| 252 | 252 | "space", space, | |
| 253 | 253 | "publicKey", data.PublicKey, | |
| 254 | 254 | ) | |
| 255 | 255 | ||
| 256 | - | user, err := apiConfig.Dbpool.FindUserForKey(data.Username, data.PublicKey) | |
| 256 | + | log.Info("handle key") | |
| 257 | + | ||
| 258 | + | key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(data.PublicKey)) | |
| 257 | 259 | if err != nil { | |
| 258 | - | apiConfig.Cfg.Logger.Error(err.Error()) | |
| 260 | + | log.Error("parse authorized key", "err", err) | |
| 261 | + | http.Error(w, err.Error(), http.StatusBadRequest) | |
| 262 | + | return | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | pubkey, err := shared.PubkeyCertVerify(key, space) | |
| 266 | + | if err != nil { | |
| 267 | + | log.Error("pubkey cert verify", "err", err) | |
| 268 | + | http.Error(w, err.Error(), http.StatusBadRequest) | |
| 269 | + | return | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | user, err := apiConfig.Dbpool.FindUserForKey(data.Username, pubkey) | |
| 273 | + | if err != nil { | |
| 274 | + | log.Error("find user for key", "err", err) | |
| 259 | 275 | w.WriteHeader(http.StatusUnauthorized) | |
| 260 | 276 | return | |
| 261 | 277 | } | |
| 262 | 278 | ||
| 263 | 279 | if !apiConfig.HasPlusOrSpace(user, space) { | |
| 280 | + | log.Error("key handler unauthorized") | |
| 264 | 281 | w.WriteHeader(http.StatusUnauthorized) | |
| 265 | 282 | return | |
| 266 | 283 | } |
| ... | ... | @@ -274,7 +291,7 @@ func keyHandler(apiConfig *shared.ApiConfig) http.HandlerFunc { | |
| 274 | 291 | w.WriteHeader(http.StatusOK) | |
| 275 | 292 | err = json.NewEncoder(w).Encode(user) | |
| 276 | 293 | if err != nil { | |
| 277 | - | apiConfig.Cfg.Logger.Error(err.Error()) | |
| 294 | + | log.Error("json encode", "err", err) | |
| 278 | 295 | http.Error(w, err.Error(), http.StatusInternalServerError) | |
| 279 | 296 | } | |
| 280 | 297 | } |
+1,
-1
| ... | ... | @@ -97,7 +97,7 @@ func TestKey(t *testing.T) { | |
| 97 | 97 | ||
| 98 | 98 | data := sishData{ | |
| 99 | 99 | Username: testUsername, | |
| 100 | - | PublicKey: "zzz", | |
| 100 | + | PublicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxVPgEqtWOa5l0QHZV6TQKhV+l46SAXU07c9RuHlGka test@pico", | |
| 101 | 101 | } | |
| 102 | 102 | jso, err := json.Marshal(data) | |
| 103 | 103 | bail(err) |
| ... | ... | @@ -33,41 +33,46 @@ func NewSshAuthHandler(dbh AuthFindUser, logger *slog.Logger, principal string) | |
| 33 | 33 | } | |
| 34 | 34 | } | |
| 35 | 35 | ||
| 36 | - | func (r *SshAuthHandler) PubkeyAuthHandler(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { | |
| 37 | - | log := r.Logger | |
| 38 | - | var user *db.User | |
| 39 | - | var err error | |
| 40 | - | pubkey := "" | |
| 41 | - | ||
| 36 | + | func PubkeyCertVerify(key ssh.PublicKey, srcPrincipal string) (string, error) { | |
| 42 | 37 | cert, ok := key.(*ssh.Certificate) | |
| 43 | 38 | if ok { | |
| 44 | 39 | if cert.CertType != ssh.UserCert { | |
| 45 | - | return nil, fmt.Errorf("ssh-cert has type %d", cert.CertType) | |
| 40 | + | return "", fmt.Errorf("ssh-cert has type %d", cert.CertType) | |
| 46 | 41 | } | |
| 47 | 42 | ||
| 48 | 43 | found := false | |
| 49 | 44 | for _, princ := range cert.ValidPrincipals { | |
| 50 | - | if princ == "admin" || princ == r.Principal { | |
| 45 | + | if princ == "admin" || princ == srcPrincipal { | |
| 51 | 46 | found = true | |
| 52 | 47 | break | |
| 53 | 48 | } | |
| 54 | 49 | } | |
| 55 | 50 | if !found { | |
| 56 | - | return nil, fmt.Errorf("ssh-cert principals not valid") | |
| 51 | + | return "", fmt.Errorf("ssh-cert principals not valid") | |
| 57 | 52 | } | |
| 58 | 53 | ||
| 59 | 54 | clock := time.Now | |
| 60 | 55 | unixNow := clock().Unix() | |
| 61 | 56 | if after := int64(cert.ValidAfter); after < 0 || unixNow < int64(cert.ValidAfter) { | |
| 62 | - | return nil, fmt.Errorf("ssh-cert is not yet valid") | |
| 57 | + | return "", fmt.Errorf("ssh-cert is not yet valid") | |
| 63 | 58 | } | |
| 64 | 59 | if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(ssh.CertTimeInfinity) && (unixNow >= before || before < 0) { | |
| 65 | - | return nil, fmt.Errorf("ssh-cert has expired") | |
| 60 | + | return "", fmt.Errorf("ssh-cert has expired") | |
| 66 | 61 | } | |
| 67 | 62 | ||
| 68 | - | pubkey = utils.KeyForKeyText(cert.SignatureKey) | |
| 69 | - | } else { | |
| 70 | - | pubkey = utils.KeyForKeyText(key) | |
| 63 | + | return utils.KeyForKeyText(cert.SignatureKey), nil | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | return utils.KeyForKeyText(key), nil | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | func (r *SshAuthHandler) PubkeyAuthHandler(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { | |
| 70 | + | log := r.Logger | |
| 71 | + | var user *db.User | |
| 72 | + | var err error | |
| 73 | + | pubkey, err := PubkeyCertVerify(key, r.Principal) | |
| 74 | + | if err != nil { | |
| 75 | + | return nil, err | |
| 71 | 76 | } | |
| 72 | 77 | ||
| 73 | 78 | user, err = r.DB.FindUserByPubkey(pubkey) |