Skip to content

Commit e147a82

Browse files
GiteaBotpeterverraedtwxiaoguang
authored
Fix regression in writing authorized principals (#36213) (#36218)
Backport #36213 by peterverraedt Fixes: #36212 Signed-off-by: Peter Verraedt <peter.verraedt@kuleuven.be> Co-authored-by: Peter Verraedt <peter.verraedt@kuleuven.be> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
1 parent 9a7cfd8 commit e147a82

File tree

2 files changed

+126
-7
lines changed

2 files changed

+126
-7
lines changed

models/asymkey/ssh_key_authorized_keys.go

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"io"
1111
"os"
1212
"path/filepath"
13+
"regexp"
1314
"strings"
1415
"sync"
1516

@@ -50,12 +51,42 @@ func WriteAuthorizedStringForValidKey(key *PublicKey, w io.Writer) error {
5051
return err
5152
}
5253

54+
var globalVars = sync.OnceValue(func() (ret struct {
55+
principalRegexp *regexp.Regexp
56+
},
57+
) {
58+
// principalRegexp expresses whether a principal is considered valid.
59+
// This reverse engineers how sshd parses the authorized keys file,
60+
// see e.g. https://github.com/openssh/openssh-portable/blob/32deb00b38b4ee2b3302f261ea1e68c04e020a08/auth2-pubkeyfile.c#L221-L256
61+
// Any newline or # comment will be stripped when parsing, so don't allow
62+
// those. Also, if any space or tab is present in the principal, the part
63+
// proceeding this would be parsed as an option, so just avoid any whitespace
64+
// altogether.
65+
ret.principalRegexp = regexp.MustCompile(`^[^\s#]+$`)
66+
return ret
67+
})
68+
5369
func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, err error) {
54-
const tpl = AuthorizedStringCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s %s` + "\n"
55-
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
56-
if err != nil {
57-
return false, err
70+
const tpl = AuthorizedStringCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s` + "\n"
71+
72+
var sshKey string
73+
74+
if key.Type == KeyTypePrincipal {
75+
// TODO: actually using PublicKey to store "principal" is an abuse
76+
if !globalVars().principalRegexp.MatchString(key.Content) {
77+
return false, fmt.Errorf("invalid principal key: %s", key.Content)
78+
}
79+
sshKey = fmt.Sprintf("%s # user-%d", key.Content, key.OwnerID)
80+
} else {
81+
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
82+
if err != nil {
83+
return false, err
84+
}
85+
86+
sshKeyMarshalled := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
87+
sshKey = fmt.Sprintf("%s user-%d", sshKeyMarshalled, key.OwnerID)
5888
}
89+
5990
// now the key is valid, the code below could only return template/IO related errors
6091
sbCmd := &strings.Builder{}
6192
err = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sbCmd, map[string]any{
@@ -69,9 +100,7 @@ func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, er
69100
return true, err
70101
}
71102
sshCommandEscaped := util.ShellEscape(sbCmd.String())
72-
sshKeyMarshalled := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
73-
sshKeyComment := fmt.Sprintf("user-%d", key.OwnerID)
74-
_, err = fmt.Fprintf(w, tpl, sshCommandEscaped, sshKeyMarshalled, sshKeyComment)
103+
_, err = fmt.Fprintf(w, tpl, sshCommandEscaped, sshKey)
75104
return true, err
76105
}
77106

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package asymkey
5+
6+
import (
7+
"strings"
8+
"testing"
9+
10+
"code.gitea.io/gitea/modules/setting"
11+
"code.gitea.io/gitea/modules/test"
12+
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
func TestWriteAuthorizedStringForKey(t *testing.T) {
17+
defer test.MockVariableValue(&setting.AppPath, "/tmp/gitea")()
18+
defer test.MockVariableValue(&setting.CustomConf, "/tmp/app.ini")()
19+
writeKey := func(t *testing.T, key *PublicKey) (bool, string, error) {
20+
sb := &strings.Builder{}
21+
valid, err := writeAuthorizedStringForKey(key, sb)
22+
return valid, sb.String(), err
23+
}
24+
const validKeyContent = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf`
25+
26+
testValid := func(t *testing.T, key *PublicKey, expected string) {
27+
valid, content, err := writeKey(t, key)
28+
assert.True(t, valid)
29+
assert.Equal(t, expected, content)
30+
assert.NoError(t, err)
31+
}
32+
33+
testInvalid := func(t *testing.T, key *PublicKey) {
34+
valid, content, err := writeKey(t, key)
35+
assert.False(t, valid)
36+
assert.Empty(t, content)
37+
assert.Error(t, err)
38+
}
39+
40+
t.Run("PublicKey", func(t *testing.T) {
41+
testValid(t, &PublicKey{
42+
OwnerID: 123,
43+
Content: validKeyContent + " any-comment",
44+
Type: KeyTypeUser,
45+
}, `# gitea public key
46+
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf user-123
47+
`)
48+
})
49+
50+
t.Run("PublicKeyWithNewLine", func(t *testing.T) {
51+
testValid(t, &PublicKey{
52+
OwnerID: 123,
53+
Content: validKeyContent + "\nany-more", // the new line should be ignored
54+
Type: KeyTypeUser,
55+
}, `# gitea public key
56+
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf user-123
57+
`)
58+
})
59+
60+
t.Run("PublicKeyInvalid", func(t *testing.T) {
61+
testInvalid(t, &PublicKey{
62+
OwnerID: 123,
63+
Content: validKeyContent + "any-more",
64+
Type: KeyTypeUser,
65+
})
66+
})
67+
68+
t.Run("Principal", func(t *testing.T) {
69+
testValid(t, &PublicKey{
70+
OwnerID: 123,
71+
Content: "any-content",
72+
Type: KeyTypePrincipal,
73+
}, `# gitea public key
74+
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict any-content # user-123
75+
`)
76+
})
77+
78+
t.Run("PrincipalInvalid", func(t *testing.T) {
79+
testInvalid(t, &PublicKey{
80+
OwnerID: 123,
81+
Content: "a b",
82+
Type: KeyTypePrincipal,
83+
})
84+
testInvalid(t, &PublicKey{
85+
OwnerID: 123,
86+
Content: "a\nb",
87+
Type: KeyTypePrincipal,
88+
})
89+
})
90+
}

0 commit comments

Comments
 (0)