@@ -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