main pico / pkg / shared / ssh.go
Antonio Mika  ·  2026-10-25
  1package shared
  2
  3import (
  4	"fmt"
  5	"log/slog"
  6	"strings"
  7	"time"
  8
  9	"github.com/picosh/pico/pkg/db"
 10	"golang.org/x/crypto/ssh"
 11)
 12
 13const adminPrefix = "admin__"
 14
 15type SshAuthHandler struct {
 16	DB        db.AuthDB
 17	Logger    *slog.Logger
 18	Principal string
 19}
 20
 21func NewSshAuthHandler(dbh db.AuthDB, logger *slog.Logger, principal string) *SshAuthHandler {
 22	return &SshAuthHandler{
 23		DB:        dbh,
 24		Logger:    logger,
 25		Principal: principal,
 26	}
 27}
 28
 29type AuthedPubkey struct {
 30	OrigPubkey string
 31	Pubkey     string
 32	Identity   string
 33}
 34
 35func PubkeyCertVerify(key ssh.PublicKey, srcPrincipal string) (*AuthedPubkey, error) {
 36	origPubkey := KeyForKeyText(key)
 37	authed := &AuthedPubkey{
 38		OrigPubkey: origPubkey,
 39		Pubkey:     origPubkey,
 40		Identity:   "pubkey",
 41	}
 42
 43	cert, ok := key.(*ssh.Certificate)
 44	if ok {
 45		if cert.CertType != ssh.UserCert {
 46			return nil, fmt.Errorf("ssh-cert has type %d", cert.CertType)
 47		}
 48
 49		found := false
 50		for _, princ := range cert.ValidPrincipals {
 51			if princ == "admin" || princ == srcPrincipal {
 52				found = true
 53				break
 54			}
 55		}
 56		if !found {
 57			return nil, fmt.Errorf("ssh-cert principals not valid")
 58		}
 59
 60		clock := time.Now
 61		unixNow := clock().Unix()
 62		if after := int64(cert.ValidAfter); after < 0 || unixNow < int64(cert.ValidAfter) {
 63			return nil, fmt.Errorf("ssh-cert is not yet valid")
 64		}
 65		if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(ssh.CertTimeInfinity) && (unixNow >= before || before < 0) {
 66			return nil, fmt.Errorf("ssh-cert has expired")
 67		}
 68
 69		authed.Pubkey = KeyForKeyText(cert.SignatureKey)
 70		authed.Identity = cert.KeyId
 71		return authed, nil
 72	}
 73
 74	return authed, nil
 75}
 76
 77func (r *SshAuthHandler) PubkeyAuthHandler(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
 78	log := r.Logger
 79	var user *db.User
 80	var err error
 81	authed, err := PubkeyCertVerify(key, r.Principal)
 82	if err != nil {
 83		return nil, err
 84	}
 85
 86	user, err = r.DB.FindUserByPubkey(authed.Pubkey)
 87	if err != nil {
 88		log.Error(
 89			"could not find user for key",
 90			"keyType", key.Type(),
 91			"key", string(key.Marshal()),
 92			"err", err,
 93		)
 94		return nil, err
 95	}
 96
 97	if user.Name == "" {
 98		log.Error("username is not set")
 99		return nil, fmt.Errorf("username is not set")
100	}
101
102	if authed.Identity == "pubkey" && user.PublicKey != nil && user.PublicKey.Name != "" {
103		authed.Identity = user.PublicKey.Name
104	}
105
106	log.Info("inserting access log", "principal", r.Principal, "identity", authed.Identity)
107	err = r.DB.InsertAccessLog(&db.AccessLog{
108		UserID:   user.ID,
109		Service:  r.Principal,
110		Identity: authed.Identity,
111		Pubkey:   authed.OrigPubkey,
112	})
113	if err != nil {
114		log.Error("cannot insert access log", "err", err)
115	}
116
117	// impersonation
118	var impID string
119	usr := conn.User()
120	if strings.HasPrefix(usr, adminPrefix) {
121		ff, err := r.DB.FindFeature(user.ID, "admin")
122		if err == nil && ff.IsValid() {
123			impersonate := strings.TrimPrefix(usr, adminPrefix)
124			impersonatedUser, err := r.DB.FindUserByName(impersonate)
125			if err == nil {
126				impID = user.ID
127				user = impersonatedUser
128			}
129		}
130	}
131
132	perms := &ssh.Permissions{
133		Extensions: map[string]string{
134			"user_id":  user.ID,
135			"user":     user.Name,
136			"pubkey":   authed.Pubkey,
137			"identity": authed.Identity,
138		},
139	}
140
141	if impID != "" {
142		perms.Extensions["imp_id"] = impID
143	}
144
145	return perms, nil
146}