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}