Skip to content

Commit fc8fc5a

Browse files
committed
Improve instance wide ssh commit signing
* Signed SSH commits can look like on GitHub * No user account of the committer needed * SSH format can be added in gitea config * No gitconfig changes needed * Set gpg.format git key for signing command * Previously only the default gpg key had global trust in Gitea * SSH Signing worked before with DEFAULT_TRUST_MODEL=committer, but not with model default and manually changing the .gitconfig e.g. the following is all needed ``` [repository.signing] SIGNING_KEY = /data/id_ed25519.pub SIGNING_NAME = Gitea SIGNING_EMAIL = [email protected] SIGNING_FORMAT = ssh INITIAL_COMMIT = always CRUD_ACTIONS = always WIKI = always MERGES = always ``` `TRUSTED_SSH_KEYS` can be a list of additional ssh public keys to trust for every user of this instance
1 parent cbb2e52 commit fc8fc5a

File tree

19 files changed

+348
-93
lines changed

19 files changed

+348
-93
lines changed

models/asymkey/key.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package asymkey
2+
3+
type SigningKey struct {
4+
KeyID string
5+
Format string
6+
}

modules/git/command.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type Command struct {
4747
globalArgsLength int
4848
brokenArgs []string
4949
cmd *exec.Cmd // for debug purpose only
50+
configArgs []string
5051
}
5152

5253
func logArgSanitize(arg string) string {
@@ -196,6 +197,15 @@ func (c *Command) AddDashesAndList(list ...string) *Command {
196197
return c
197198
}
198199

200+
func (c *Command) AddConfig(key string, value string) *Command {
201+
kv := key + "=" + value
202+
if !isSafeArgumentValue(kv) {
203+
c.brokenArgs = append(c.brokenArgs, key)
204+
}
205+
c.configArgs = append(c.configArgs, "-c", kv)
206+
return c
207+
}
208+
199209
// ToTrustedCmdArgs converts a list of strings (trusted as argument) to TrustedCmdArgs
200210
// In most cases, it shouldn't be used. Use NewCommand().AddXxx() function instead
201211
func ToTrustedCmdArgs(args []string) TrustedCmdArgs {
@@ -321,7 +331,7 @@ func (c *Command) run(ctx context.Context, skip int, opts *RunOpts) error {
321331

322332
startTime := time.Now()
323333

324-
cmd := exec.CommandContext(ctx, c.prog, c.args...)
334+
cmd := exec.CommandContext(ctx, c.prog, append(append([]string{}, c.configArgs...), c.args...)...)
325335
c.cmd = cmd // for debug purpose only
326336
if opts.Env == nil {
327337
cmd.Env = os.Environ()

modules/git/repo.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type GPGSettings struct {
2828
Email string
2929
Name string
3030
PublicKeyContent string
31+
Format string
3132
}
3233

3334
const prettyLogFormat = `--pretty=format:%H`

modules/git/repo_gpg.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,22 @@ package git
66

77
import (
88
"fmt"
9+
"os"
910
"strings"
1011

1112
"code.gitea.io/gitea/modules/process"
1213
)
1314

1415
// LoadPublicKeyContent will load the key from gpg
1516
func (gpgSettings *GPGSettings) LoadPublicKeyContent() error {
17+
if gpgSettings.Format == "ssh" {
18+
content, err := os.ReadFile(gpgSettings.KeyID)
19+
if err != nil {
20+
return fmt.Errorf("unable to read SSH public key file: %s, %w", gpgSettings.KeyID, err)
21+
}
22+
gpgSettings.PublicKeyContent = string(content)
23+
return nil
24+
}
1625
content, stderr, err := process.GetManager().Exec(
1726
"gpg -a --export",
1827
"gpg", "-a", "--export", gpgSettings.KeyID)
@@ -44,6 +53,9 @@ func (repo *Repository) GetDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings,
4453
signingKey, _, _ := NewCommand("config", "--get", "user.signingkey").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
4554
gpgSettings.KeyID = strings.TrimSpace(signingKey)
4655

56+
format, _, _ := NewCommand("config", "--get", "gpg.format").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
57+
gpgSettings.Format = strings.TrimSpace(format)
58+
4759
defaultEmail, _, _ := NewCommand("config", "--get", "user.email").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
4860
gpgSettings.Email = strings.TrimSpace(defaultEmail)
4961

modules/git/repo_tree.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type CommitTreeOpts struct {
1616
Parents []string
1717
Message string
1818
KeyID string
19+
KeyFormat string
1920
NoGPGSign bool
2021
AlwaysSign bool
2122
}
@@ -44,6 +45,9 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt
4445
_, _ = messageBytes.WriteString("\n")
4546

4647
if opts.KeyID != "" || opts.AlwaysSign {
48+
if opts.KeyFormat != "" {
49+
cmd.AddConfig("gpg.format", opts.KeyFormat)
50+
}
4751
cmd.AddOptionFormat("-S%s", opts.KeyID)
4852
}
4953

modules/setting/repository.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,13 @@ var (
100100
SigningKey string
101101
SigningName string
102102
SigningEmail string
103+
SigningFormat string
103104
InitialCommit []string
104105
CRUDActions []string `ini:"CRUD_ACTIONS"`
105106
Merges []string
106107
Wiki []string
107108
DefaultTrustModel string
109+
TrustedSSHKeys []string `ini:"TRUSTED_SSH_KEYS"`
108110
} `ini:"repository.signing"`
109111
}{
110112
DetectedCharsetsOrder: []string{
@@ -242,11 +244,13 @@ var (
242244
SigningKey string
243245
SigningName string
244246
SigningEmail string
247+
SigningFormat string
245248
InitialCommit []string
246249
CRUDActions []string `ini:"CRUD_ACTIONS"`
247250
Merges []string
248251
Wiki []string
249252
DefaultTrustModel string
253+
TrustedSSHKeys []string `ini:"TRUSTED_SSH_KEYS"`
250254
}{
251255
SigningKey: "default",
252256
SigningName: "",
@@ -256,6 +260,7 @@ var (
256260
Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"},
257261
Wiki: []string{"never"},
258262
DefaultTrustModel: "collaborator",
263+
TrustedSSHKeys: []string{},
259264
},
260265
}
261266
RepoRootPath string

routers/api/v1/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,7 @@ func Routes() *web.Router {
972972
m.Group("", func() {
973973
m.Get("/version", misc.Version)
974974
m.Get("/signing-key.gpg", misc.SigningKey)
975+
m.Get("/signing-key.pub", misc.SigningKeySSH)
975976
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
976977
m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)
977978
m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw)

routers/api/v1/misc/signing.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,70 @@ func SigningKey(ctx *context.APIContext) {
5050
path = ctx.Repo.Repository.RepoPath()
5151
}
5252

53-
content, err := asymkey_service.PublicSigningKey(ctx, path)
53+
content, format, err := asymkey_service.PublicSigningKey(ctx, path)
5454
if err != nil {
5555
ctx.APIErrorInternal(err)
5656
return
5757
}
58+
if format == "ssh" {
59+
ctx.APIErrorNotFound(fmt.Errorf("SSH keys are used for signing, not GPG"))
60+
return
61+
}
62+
_, err = ctx.Write([]byte(content))
63+
if err != nil {
64+
ctx.APIErrorInternal(fmt.Errorf("Error writing key content %w", err))
65+
}
66+
}
67+
68+
// SigningKey returns the public key of the default signing key if it exists
69+
func SigningKeySSH(ctx *context.APIContext) {
70+
// swagger:operation GET /signing-key.pub miscellaneous getSigningKeySSH
71+
// ---
72+
// summary: Get default signing-key.pub
73+
// produces:
74+
// - text/plain
75+
// responses:
76+
// "200":
77+
// description: "ssh public key"
78+
// schema:
79+
// type: string
80+
81+
// swagger:operation GET /repos/{owner}/{repo}/signing-key.pub repository repoSigningKeySSH
82+
// ---
83+
// summary: Get signing-key.pub for given repository
84+
// produces:
85+
// - text/plain
86+
// parameters:
87+
// - name: owner
88+
// in: path
89+
// description: owner of the repo
90+
// type: string
91+
// required: true
92+
// - name: repo
93+
// in: path
94+
// description: name of the repo
95+
// type: string
96+
// required: true
97+
// responses:
98+
// "200":
99+
// description: "ssh public key"
100+
// schema:
101+
// type: string
102+
103+
path := ""
104+
if ctx.Repo != nil && ctx.Repo.Repository != nil {
105+
path = ctx.Repo.Repository.RepoPath()
106+
}
107+
108+
content, format, err := asymkey_service.PublicSigningKey(ctx, path)
109+
if err != nil {
110+
ctx.APIErrorInternal(err)
111+
return
112+
}
113+
if format != "ssh" {
114+
ctx.APIErrorNotFound(fmt.Errorf("GPG keys are used for signing, not SSH"))
115+
return
116+
}
58117
_, err = ctx.Write([]byte(content))
59118
if err != nil {
60119
ctx.APIErrorInternal(fmt.Errorf("Error writing key content %w", err))

routers/web/repo/setting/setting.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func SettingsCtxData(ctx *context.Context) {
6262
ctx.Data["CanConvertFork"] = ctx.Repo.Repository.IsFork && ctx.Doer.CanCreateRepoIn(ctx.Repo.Repository.Owner)
6363

6464
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
65-
ctx.Data["SigningKeyAvailable"] = len(signing) > 0
65+
ctx.Data["SigningKeyAvailable"] = len(signing.KeyID) > 0
6666
ctx.Data["SigningSettings"] = setting.Repository.Signing
6767
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
6868

@@ -105,7 +105,7 @@ func SettingsPost(ctx *context.Context) {
105105
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
106106

107107
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
108-
ctx.Data["SigningKeyAvailable"] = len(signing) > 0
108+
ctx.Data["SigningKeyAvailable"] = len(signing.KeyID) > 0
109109
ctx.Data["SigningSettings"] = setting.Repository.Signing
110110
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
111111

services/asymkey/commit.go

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,11 +398,79 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *
398398
}
399399
}
400400
}
401+
// Trust more than one key for every User
402+
for _, k := range setting.Repository.Signing.TrustedSSHKeys {
403+
fingerprint, _ := asymkey_model.CalcFingerprint(k)
404+
commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, &asymkey_model.PublicKey{
405+
Verified: true,
406+
Content: k,
407+
Fingerprint: fingerprint,
408+
HasUsed: true,
409+
}, committer, committer, c.Committer.Email)
410+
if commitVerification != nil {
411+
return commitVerification
412+
}
413+
}
414+
415+
defaultReason := asymkey_model.NoKeyFound
416+
417+
if setting.Repository.Signing.SigningFormat == "ssh" && setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" {
418+
// OK we should try the default key
419+
gpgSettings := git.GPGSettings{
420+
Sign: true,
421+
KeyID: setting.Repository.Signing.SigningKey,
422+
Name: setting.Repository.Signing.SigningName,
423+
Email: setting.Repository.Signing.SigningEmail,
424+
Format: setting.Repository.Signing.SigningFormat,
425+
}
426+
if err := gpgSettings.LoadPublicKeyContent(); err != nil {
427+
log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err)
428+
}
429+
fingerprint, _ := asymkey_model.CalcFingerprint(gpgSettings.PublicKeyContent)
430+
if commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, &asymkey_model.PublicKey{
431+
Verified: true,
432+
Content: gpgSettings.PublicKeyContent,
433+
Fingerprint: fingerprint,
434+
HasUsed: true,
435+
}, committer, committer, committer.Email); commitVerification != nil {
436+
if commitVerification.Reason == asymkey_model.BadSignature {
437+
defaultReason = asymkey_model.BadSignature
438+
} else {
439+
return commitVerification
440+
}
441+
}
442+
}
443+
444+
defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false)
445+
if defaultGPGSettings.Format == "ssh" {
446+
if err != nil {
447+
log.Error("Error getting default public gpg key: %v", err)
448+
} else if defaultGPGSettings == nil {
449+
log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String())
450+
} else if defaultGPGSettings.Sign {
451+
if err := defaultGPGSettings.LoadPublicKeyContent(); err != nil {
452+
log.Error("Error getting default signing key: %s %v", defaultGPGSettings.KeyID, err)
453+
}
454+
fingerprint, _ := asymkey_model.CalcFingerprint(defaultGPGSettings.PublicKeyContent)
455+
if commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, &asymkey_model.PublicKey{
456+
Verified: true,
457+
Content: defaultGPGSettings.PublicKeyContent,
458+
Fingerprint: fingerprint,
459+
HasUsed: true,
460+
}, committer, committer, committer.Email); commitVerification != nil {
461+
if commitVerification.Reason == asymkey_model.BadSignature {
462+
defaultReason = asymkey_model.BadSignature
463+
} else {
464+
return commitVerification
465+
}
466+
}
467+
}
468+
}
401469

402470
return &asymkey_model.CommitVerification{
403471
CommittingUser: committer,
404472
Verified: false,
405-
Reason: asymkey_model.NoKeyFound,
473+
Reason: defaultReason,
406474
}
407475
}
408476

0 commit comments

Comments
 (0)