@@ -13,10 +13,12 @@ import (
13
13
"errors"
14
14
"fmt"
15
15
"io"
16
+ "maps"
16
17
"net"
17
18
"os"
18
19
"os/exec"
19
20
"path/filepath"
21
+ "reflect"
20
22
"strconv"
21
23
"strings"
22
24
"sync"
@@ -33,9 +35,22 @@ import (
33
35
gossh "golang.org/x/crypto/ssh"
34
36
)
35
37
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"
39
54
40
55
func getExitStatusFromError (err error ) int {
41
56
if err == nil {
@@ -61,8 +76,32 @@ func getExitStatusFromError(err error) int {
61
76
return waitStatus .ExitStatus ()
62
77
}
63
78
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
+
64
100
func 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 ]
66
105
67
106
command := session .RawCommand ()
68
107
@@ -164,6 +203,23 @@ func sessionHandler(session ssh.Session) {
164
203
}
165
204
166
205
func 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
+
167
223
if log .IsDebug () { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary
168
224
log .Debug ("Handle Public Key: Fingerprint: %s from %s" , gossh .FingerprintSHA256 (key ), ctx .RemoteAddr ())
169
225
}
@@ -238,7 +294,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
238
294
if log .IsDebug () { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary
239
295
log .Debug ("Successfully authenticated: %s Certificate Fingerprint: %s Principal: %s" , ctx .RemoteAddr (), gossh .FingerprintSHA256 (key ), principal )
240
296
}
241
- ctx . SetValue ( giteaKeyID , pkey .ID )
297
+ setPermExt ( pkey .ID )
242
298
243
299
return true
244
300
}
@@ -266,7 +322,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
266
322
if log .IsDebug () { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary
267
323
log .Debug ("Successfully authenticated: %s Public Key Fingerprint: %s" , ctx .RemoteAddr (), gossh .FingerprintSHA256 (key ))
268
324
}
269
- ctx . SetValue ( giteaKeyID , pkey .ID )
325
+ setPermExt ( pkey .ID )
270
326
271
327
return true
272
328
}
0 commit comments