@@ -13,10 +13,12 @@ import (
1313 "errors"
1414 "fmt"
1515 "io"
16+ "maps"
1617 "net"
1718 "os"
1819 "os/exec"
1920 "path/filepath"
21+ "reflect"
2022 "strconv"
2123 "strings"
2224 "sync"
@@ -33,9 +35,22 @@ import (
3335 gossh "golang.org/x/crypto/ssh"
3436)
3537
36- type contextKey string
37-
38- const giteaKeyID = contextKey ("gitea-key-id" )
38+ // The ssh auth overall works like this:
39+ // NewServerConn:
40+ // serverHandshake+serverAuthenticate:
41+ // PublicKeyCallback:
42+ // PublicKeyHandler (our code):
43+ // reset(ctx.Permissions) and set ctx.Permissions.giteaKeyID = keyID
44+ // pubKey.Verify
45+ // return ctx.Permissions // only reaches here, the pub key is really authenticated
46+ // set conn.Permissions from serverAuthenticate
47+ // sessionHandler(conn)
48+ //
49+ // Then sessionHandler should only use the "verified keyID" from the original ssh conn, but not the ctx one.
50+ // Otherwise, if a user provides 2 keys A (a correct one) and B (public key matches but no private key),
51+ // then only A succeeds to authenticate, sessionHandler will see B's keyID
52+
53+ const giteaPermissionExtensionKeyID = "gitea-perm-ext-key-id"
3954
4055func getExitStatusFromError (err error ) int {
4156 if err == nil {
@@ -61,8 +76,32 @@ func getExitStatusFromError(err error) int {
6176 return waitStatus .ExitStatus ()
6277}
6378
79+ // sessionPartial is the private struct from "gliderlabs/ssh/session.go"
80+ // We need to read the original "conn" field from "ssh.Session interface" which contains the "*session pointer"
81+ // https://github.com/gliderlabs/ssh/blob/d137aad99cd6f2d9495bfd98c755bec4e5dffb8c/session.go#L109-L113
82+ // If upstream fixes the problem and/or changes the struct, we need to follow.
83+ // If the struct mismatches, the builtin ssh server will fail during integration tests.
84+ type sessionPartial struct {
85+ sync.Mutex
86+ gossh.Channel
87+ conn * gossh.ServerConn
88+ }
89+
90+ func ptr [T any ](intf any ) * T {
91+ // https://pkg.go.dev/unsafe#Pointer
92+ // (1) Conversion of a *T1 to Pointer to *T2.
93+ // Provided that T2 is no larger than T1 and that the two share an equivalent memory layout,
94+ // this conversion allows reinterpreting data of one type as data of another type.
95+ v := reflect .ValueOf (intf )
96+ p := v .UnsafePointer ()
97+ return (* T )(p )
98+ }
99+
64100func sessionHandler (session ssh.Session ) {
65- keyID := fmt .Sprintf ("%d" , session .Context ().Value (giteaKeyID ).(int64 ))
101+ // here can't use session.Permissions() because it only uses the value from ctx, which might not be the authenticated one.
102+ // so we must use the original ssh conn, which always contains the correct (verified) keyID.
103+ sshConn := ptr [sessionPartial ](session )
104+ keyID := sshConn .conn .Permissions .Extensions [giteaPermissionExtensionKeyID ]
66105
67106 command := session .RawCommand ()
68107
@@ -164,6 +203,23 @@ func sessionHandler(session ssh.Session) {
164203}
165204
166205func publicKeyHandler (ctx ssh.Context , key ssh.PublicKey ) bool {
206+ // The publicKeyHandler (PublicKeyCallback) only helps to provide the candidate keys to authenticate,
207+ // It does NOT really verify here, so we could only record the related information here.
208+ // After authentication (Verify), the "Permissions" will be assigned to the ssh conn,
209+ // then we can use it in the "session handler"
210+
211+ // first, reset the ctx permissions (just like https://github.com/gliderlabs/ssh/pull/243 does)
212+ // it shouldn't be reused across different ssh conn (sessions), each pub key should have its own "Permissions"
213+ oldCtxPerm := ctx .Permissions ().Permissions
214+ ctx .Permissions ().Permissions = & gossh.Permissions {}
215+ ctx .Permissions ().Permissions .CriticalOptions = maps .Clone (oldCtxPerm .CriticalOptions )
216+
217+ setPermExt := func (keyID int64 ) {
218+ ctx .Permissions ().Permissions .Extensions = map [string ]string {
219+ giteaPermissionExtensionKeyID : fmt .Sprint (keyID ),
220+ }
221+ }
222+
167223 if log .IsDebug () { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary
168224 log .Debug ("Handle Public Key: Fingerprint: %s from %s" , gossh .FingerprintSHA256 (key ), ctx .RemoteAddr ())
169225 }
@@ -238,8 +294,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
238294 if log .IsDebug () { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary
239295 log .Debug ("Successfully authenticated: %s Certificate Fingerprint: %s Principal: %s" , ctx .RemoteAddr (), gossh .FingerprintSHA256 (key ), principal )
240296 }
241- ctx .SetValue (giteaKeyID , pkey .ID )
242-
297+ setPermExt (pkey .ID )
243298 return true
244299 }
245300
@@ -266,8 +321,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
266321 if log .IsDebug () { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary
267322 log .Debug ("Successfully authenticated: %s Public Key Fingerprint: %s" , ctx .RemoteAddr (), gossh .FingerprintSHA256 (key ))
268323 }
269- ctx .SetValue (giteaKeyID , pkey .ID )
270-
324+ setPermExt (pkey .ID )
271325 return true
272326}
273327
0 commit comments