Skip to content

Commit 6fe756d

Browse files
42wimGustedwxiaoguanglunnytechknowlogick
authored
Add support for ssh commit signing (#17743)
* Add support for ssh commit signing * Split out ssh verification to separate file * Show ssh key fingerprint on commit page * Update sshsig lib * Make sure we verify against correct namespace * Add ssh public key verification via ssh signatures When adding a public ssh key also validate that this user actually owns the key by signing a token with the private key. * Remove some gpg references and make verify key optional * Fix spaces indentation * Update options/locale/locale_en-US.ini Co-authored-by: Gusted <[email protected]> * Update templates/user/settings/keys_ssh.tmpl Co-authored-by: Gusted <[email protected]> * Update options/locale/locale_en-US.ini Co-authored-by: Gusted <[email protected]> * Update options/locale/locale_en-US.ini Co-authored-by: Gusted <[email protected]> * Update models/ssh_key_commit_verification.go Co-authored-by: Gusted <[email protected]> * Reword ssh/gpg_key_success message * Change Badsignature to NoKeyFound * Add sign/verify tests * Fix upstream api changes to user_model User * Match exact on SSH signature * Fix code review remarks Co-authored-by: Gusted <[email protected]> Co-authored-by: wxiaoguang <[email protected]> Co-authored-by: Lunny Xiao <[email protected]> Co-authored-by: techknowlogick <[email protected]>
1 parent f1e8562 commit 6fe756d

File tree

182 files changed

+17444
-15178
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

182 files changed

+17444
-15178
lines changed

go.mod

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
gitea.com/go-chi/captcha v0.0.0-20211013065431-70641c1a35d5
1212
gitea.com/go-chi/session v0.0.0-20211013065435-7d334f340c09
1313
gitea.com/lunny/levelqueue v0.4.1
14+
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
1415
github.com/Microsoft/go-winio v0.5.0 // indirect
1516
github.com/NYTimes/gziphandler v1.1.1
1617
github.com/ProtonMail/go-crypto v0.0.0-20210705153151-cc34b1f6908b // indirect
@@ -121,10 +122,10 @@ require (
121122
go.uber.org/atomic v1.9.0 // indirect
122123
go.uber.org/multierr v1.7.0 // indirect
123124
go.uber.org/zap v1.19.0 // indirect
124-
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
125-
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
125+
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
126+
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
126127
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914
127-
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf
128+
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1
128129
golang.org/x/text v0.3.7
129130
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect
130131
golang.org/x/tools v0.1.0

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ gitea.com/lunny/levelqueue v0.4.1 h1:RZ+AFx5gBsZuyqCvofhAkPQ9uaVDPJnsULoJZIYaJNw
5454
gitea.com/lunny/levelqueue v0.4.1/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU=
5555
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
5656
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
57+
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 h1:r3qt8PCHnfjOv9PN3H+XXKmDA1dfFMIN1AislhlA/ps=
58+
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121/go.mod h1:Ock8XgA7pvULhIaHGAk/cDnRfNrF9Jey81nPcc403iU=
5759
github.com/6543/go-version v1.3.1 h1:HvOp+Telns7HWJ2Xo/05YXQSB2bE0WmVgbHqwMPZT4U=
5860
github.com/6543/go-version v1.3.1/go.mod h1:oqFAHCwtLVUTLdhQmVZWYvaHXTdsbB4SY85at64SQEo=
5961
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
@@ -1261,6 +1263,8 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5
12611263
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
12621264
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
12631265
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
1266+
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI=
1267+
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
12641268
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
12651269
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
12661270
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1355,6 +1359,8 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx
13551359
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
13561360
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
13571361
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
1362+
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
1363+
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
13581364
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
13591365
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
13601366
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1462,6 +1468,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
14621468
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
14631469
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k=
14641470
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1471+
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI=
1472+
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
14651473
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
14661474
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
14671475
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

models/asymkey/error.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,19 @@ func IsErrDeployKeyNameAlreadyUsed(err error) bool {
246246
func (err ErrDeployKeyNameAlreadyUsed) Error() string {
247247
return fmt.Sprintf("public key with name already exists [repo_id: %d, name: %s]", err.RepoID, err.Name)
248248
}
249+
250+
// ErrSSHInvalidTokenSignature represents a "ErrSSHInvalidTokenSignature" kind of error.
251+
type ErrSSHInvalidTokenSignature struct {
252+
Wrapped error
253+
Fingerprint string
254+
}
255+
256+
// IsErrSSHInvalidTokenSignature checks if an error is a ErrSSHInvalidTokenSignature.
257+
func IsErrSSHInvalidTokenSignature(err error) bool {
258+
_, ok := err.(ErrSSHInvalidTokenSignature)
259+
return ok
260+
}
261+
262+
func (err ErrSSHInvalidTokenSignature) Error() string {
263+
return "the provided signature does not sign the token with the provided key"
264+
}

models/asymkey/gpg_key_commit_verification.go

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type CommitVerification struct {
4949
CommittingUser *user_model.User
5050
SigningEmail string
5151
SigningKey *GPGKey
52+
SigningSSHKey *PublicKey
5253
TrustStatus string
5354
}
5455

@@ -122,6 +123,11 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
122123
}
123124
}
124125

126+
// If this a SSH signature handle it differently
127+
if strings.HasPrefix(c.Signature.Signature, "-----BEGIN SSH SIGNATURE-----") {
128+
return ParseCommitWithSSHSignature(c, committer)
129+
}
130+
125131
// Parsing signature
126132
sig, err := extractSignature(c.Signature.Signature)
127133
if err != nil { // Skipping failed to extract sign
@@ -487,28 +493,31 @@ func CalculateTrustStatus(verification *CommitVerification, repoTrustModel repo_
487493
return
488494
}
489495

490-
var isMember bool
491-
if keyMap != nil {
492-
var has bool
493-
isMember, has = (*keyMap)[verification.SigningKey.KeyID]
494-
if !has {
496+
// Check we actually have a GPG SigningKey
497+
if verification.SigningKey != nil {
498+
var isMember bool
499+
if keyMap != nil {
500+
var has bool
501+
isMember, has = (*keyMap)[verification.SigningKey.KeyID]
502+
if !has {
503+
isMember, err = isCodeReader(verification.SigningUser)
504+
(*keyMap)[verification.SigningKey.KeyID] = isMember
505+
}
506+
} else {
495507
isMember, err = isCodeReader(verification.SigningUser)
496-
(*keyMap)[verification.SigningKey.KeyID] = isMember
497508
}
498-
} else {
499-
isMember, err = isCodeReader(verification.SigningUser)
500-
}
501509

502-
if !isMember {
503-
verification.TrustStatus = "untrusted"
504-
if verification.CommittingUser.ID != verification.SigningUser.ID {
505-
// The committing user and the signing user are not the same
506-
// This should be marked as questionable unless the signing user is a collaborator/team member etc.
510+
if !isMember {
511+
verification.TrustStatus = "untrusted"
512+
if verification.CommittingUser.ID != verification.SigningUser.ID {
513+
// The committing user and the signing user are not the same
514+
// This should be marked as questionable unless the signing user is a collaborator/team member etc.
515+
verification.TrustStatus = "unmatched"
516+
}
517+
} else if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID {
518+
// The committing user and the signing user are not the same and our trustmodel states that they must match
507519
verification.TrustStatus = "unmatched"
508520
}
509-
} else if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID {
510-
// The committing user and the signing user are not the same and our trustmodel states that they must match
511-
verification.TrustStatus = "unmatched"
512521
}
513522

514523
return

models/asymkey/ssh_key.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type PublicKey struct {
5050
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
5151
HasRecentActivity bool `xorm:"-"`
5252
HasUsed bool `xorm:"-"`
53+
Verified bool `xorm:"NOT NULL DEFAULT false"`
5354
}
5455

5556
func init() {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package asymkey
6+
7+
import (
8+
"bytes"
9+
"fmt"
10+
"strings"
11+
12+
"code.gitea.io/gitea/models/db"
13+
user_model "code.gitea.io/gitea/models/user"
14+
"code.gitea.io/gitea/modules/git"
15+
"code.gitea.io/gitea/modules/log"
16+
17+
"github.com/42wim/sshsig"
18+
)
19+
20+
// ParseCommitWithSSHSignature check if signature is good against keystore.
21+
func ParseCommitWithSSHSignature(c *git.Commit, committer *user_model.User) *CommitVerification {
22+
// Now try to associate the signature with the committer, if present
23+
if committer.ID != 0 {
24+
keys, err := ListPublicKeys(committer.ID, db.ListOptions{})
25+
if err != nil { // Skipping failed to get ssh keys of user
26+
log.Error("ListPublicKeys: %v", err)
27+
return &CommitVerification{
28+
CommittingUser: committer,
29+
Verified: false,
30+
Reason: "gpg.error.failed_retrieval_gpg_keys",
31+
}
32+
}
33+
34+
committerEmailAddresses, err := user_model.GetEmailAddresses(committer.ID)
35+
if err != nil {
36+
log.Error("GetEmailAddresses: %v", err)
37+
}
38+
39+
activated := false
40+
for _, e := range committerEmailAddresses {
41+
if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
42+
activated = true
43+
break
44+
}
45+
}
46+
47+
for _, k := range keys {
48+
if k.Verified && activated {
49+
commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committer, committer, c.Committer.Email)
50+
if commitVerification != nil {
51+
return commitVerification
52+
}
53+
}
54+
}
55+
}
56+
57+
return &CommitVerification{
58+
CommittingUser: committer,
59+
Verified: false,
60+
Reason: NoKeyFound,
61+
}
62+
}
63+
64+
func verifySSHCommitVerification(sig, payload string, k *PublicKey, committer, signer *user_model.User, email string) *CommitVerification {
65+
if err := sshsig.Verify(bytes.NewBuffer([]byte(payload)), []byte(sig), []byte(k.Content), "git"); err != nil {
66+
return nil
67+
}
68+
69+
return &CommitVerification{ // Everything is ok
70+
CommittingUser: committer,
71+
Verified: true,
72+
Reason: fmt.Sprintf("%s / %s", signer.Name, k.Fingerprint),
73+
SigningUser: signer,
74+
SigningSSHKey: k,
75+
SigningEmail: email,
76+
}
77+
}

0 commit comments

Comments
 (0)