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
5package asymkey
6
7import (
8	"fmt"
9	"strings"
10
11	"code.gitea.io/gitea/models"
12	asymkey_model "code.gitea.io/gitea/models/asymkey"
13	"code.gitea.io/gitea/models/auth"
14	"code.gitea.io/gitea/models/db"
15	user_model "code.gitea.io/gitea/models/user"
16	"code.gitea.io/gitea/modules/git"
17	"code.gitea.io/gitea/modules/log"
18	"code.gitea.io/gitea/modules/process"
19	"code.gitea.io/gitea/modules/setting"
20)
21
22type signingMode string
23
24const (
25	never         signingMode = "never"
26	always        signingMode = "always"
27	pubkey        signingMode = "pubkey"
28	twofa         signingMode = "twofa"
29	parentSigned  signingMode = "parentsigned"
30	baseSigned    signingMode = "basesigned"
31	headSigned    signingMode = "headsigned"
32	commitsSigned signingMode = "commitssigned"
33	approved      signingMode = "approved"
34	noKey         signingMode = "nokey"
35)
36
37func signingModeFromStrings(modeStrings []string) []signingMode {
38	returnable := make([]signingMode, 0, len(modeStrings))
39	for _, mode := range modeStrings {
40		signMode := signingMode(strings.ToLower(strings.TrimSpace(mode)))
41		switch signMode {
42		case never:
43			return []signingMode{never}
44		case always:
45			return []signingMode{always}
46		case pubkey:
47			fallthrough
48		case twofa:
49			fallthrough
50		case parentSigned:
51			fallthrough
52		case baseSigned:
53			fallthrough
54		case headSigned:
55			fallthrough
56		case approved:
57			fallthrough
58		case commitsSigned:
59			returnable = append(returnable, signMode)
60		}
61	}
62	if len(returnable) == 0 {
63		return []signingMode{never}
64	}
65	return returnable
66}
67
68// ErrWontSign explains the first reason why a commit would not be signed
69// There may be other reasons - this is just the first reason found
70type ErrWontSign struct {
71	Reason signingMode
72}
73
74func (e *ErrWontSign) Error() string {
75	return fmt.Sprintf("wont sign: %s", e.Reason)
76}
77
78// IsErrWontSign checks if an error is a ErrWontSign
79func IsErrWontSign(err error) bool {
80	_, ok := err.(*ErrWontSign)
81	return ok
82}
83
84// SigningKey returns the KeyID and git Signature for the repo
85func SigningKey(repoPath string) (string, *git.Signature) {
86	if setting.Repository.Signing.SigningKey == "none" {
87		return "", nil
88	}
89
90	if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" {
91		// Can ignore the error here as it means that commit.gpgsign is not set
92		value, _ := git.NewCommand("config", "--get", "commit.gpgsign").RunInDir(repoPath)
93		sign, valid := git.ParseBool(strings.TrimSpace(value))
94		if !sign || !valid {
95			return "", nil
96		}
97
98		signingKey, _ := git.NewCommand("config", "--get", "user.signingkey").RunInDir(repoPath)
99		signingName, _ := git.NewCommand("config", "--get", "user.name").RunInDir(repoPath)
100		signingEmail, _ := git.NewCommand("config", "--get", "user.email").RunInDir(repoPath)
101		return strings.TrimSpace(signingKey), &git.Signature{
102			Name:  strings.TrimSpace(signingName),
103			Email: strings.TrimSpace(signingEmail),
104		}
105	}
106
107	return setting.Repository.Signing.SigningKey, &git.Signature{
108		Name:  setting.Repository.Signing.SigningName,
109		Email: setting.Repository.Signing.SigningEmail,
110	}
111}
112
113// PublicSigningKey gets the public signing key within a provided repository directory
114func PublicSigningKey(repoPath string) (string, error) {
115	signingKey, _ := SigningKey(repoPath)
116	if signingKey == "" {
117		return "", nil
118	}
119
120	content, stderr, err := process.GetManager().ExecDir(-1, repoPath,
121		"gpg --export -a", "gpg", "--export", "-a", signingKey)
122	if err != nil {
123		log.Error("Unable to get default signing key in %s: %s, %s, %v", repoPath, signingKey, stderr, err)
124		return "", err
125	}
126	return content, nil
127}
128
129// SignInitialCommit determines if we should sign the initial commit to this repository
130func SignInitialCommit(repoPath string, u *user_model.User) (bool, string, *git.Signature, error) {
131	rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit)
132	signingKey, sig := SigningKey(repoPath)
133	if signingKey == "" {
134		return false, "", nil, &ErrWontSign{noKey}
135	}
136
137Loop:
138	for _, rule := range rules {
139		switch rule {
140		case never:
141			return false, "", nil, &ErrWontSign{never}
142		case always:
143			break Loop
144		case pubkey:
145			keys, err := asymkey_model.ListGPGKeys(db.DefaultContext, u.ID, db.ListOptions{})
146			if err != nil {
147				return false, "", nil, err
148			}
149			if len(keys) == 0 {
150				return false, "", nil, &ErrWontSign{pubkey}
151			}
152		case twofa:
153			twofaModel, err := auth.GetTwoFactorByUID(u.ID)
154			if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
155				return false, "", nil, err
156			}
157			if twofaModel == nil {
158				return false, "", nil, &ErrWontSign{twofa}
159			}
160		}
161	}
162	return true, signingKey, sig, nil
163}
164
165// SignWikiCommit determines if we should sign the commits to this repository wiki
166func SignWikiCommit(repoWikiPath string, u *user_model.User) (bool, string, *git.Signature, error) {
167	rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
168	signingKey, sig := SigningKey(repoWikiPath)
169	if signingKey == "" {
170		return false, "", nil, &ErrWontSign{noKey}
171	}
172
173Loop:
174	for _, rule := range rules {
175		switch rule {
176		case never:
177			return false, "", nil, &ErrWontSign{never}
178		case always:
179			break Loop
180		case pubkey:
181			keys, err := asymkey_model.ListGPGKeys(db.DefaultContext, u.ID, db.ListOptions{})
182			if err != nil {
183				return false, "", nil, err
184			}
185			if len(keys) == 0 {
186				return false, "", nil, &ErrWontSign{pubkey}
187			}
188		case twofa:
189			twofaModel, err := auth.GetTwoFactorByUID(u.ID)
190			if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
191				return false, "", nil, err
192			}
193			if twofaModel == nil {
194				return false, "", nil, &ErrWontSign{twofa}
195			}
196		case parentSigned:
197			gitRepo, err := git.OpenRepository(repoWikiPath)
198			if err != nil {
199				return false, "", nil, err
200			}
201			defer gitRepo.Close()
202			commit, err := gitRepo.GetCommit("HEAD")
203			if err != nil {
204				return false, "", nil, err
205			}
206			if commit.Signature == nil {
207				return false, "", nil, &ErrWontSign{parentSigned}
208			}
209			verification := asymkey_model.ParseCommitWithSignature(commit)
210			if !verification.Verified {
211				return false, "", nil, &ErrWontSign{parentSigned}
212			}
213		}
214	}
215	return true, signingKey, sig, nil
216}
217
218// SignCRUDAction determines if we should sign a CRUD commit to this repository
219func SignCRUDAction(repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, string, *git.Signature, error) {
220	rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
221	signingKey, sig := SigningKey(repoPath)
222	if signingKey == "" {
223		return false, "", nil, &ErrWontSign{noKey}
224	}
225
226Loop:
227	for _, rule := range rules {
228		switch rule {
229		case never:
230			return false, "", nil, &ErrWontSign{never}
231		case always:
232			break Loop
233		case pubkey:
234			keys, err := asymkey_model.ListGPGKeys(db.DefaultContext, u.ID, db.ListOptions{})
235			if err != nil {
236				return false, "", nil, err
237			}
238			if len(keys) == 0 {
239				return false, "", nil, &ErrWontSign{pubkey}
240			}
241		case twofa:
242			twofaModel, err := auth.GetTwoFactorByUID(u.ID)
243			if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
244				return false, "", nil, err
245			}
246			if twofaModel == nil {
247				return false, "", nil, &ErrWontSign{twofa}
248			}
249		case parentSigned:
250			gitRepo, err := git.OpenRepository(tmpBasePath)
251			if err != nil {
252				return false, "", nil, err
253			}
254			defer gitRepo.Close()
255			commit, err := gitRepo.GetCommit(parentCommit)
256			if err != nil {
257				return false, "", nil, err
258			}
259			if commit.Signature == nil {
260				return false, "", nil, &ErrWontSign{parentSigned}
261			}
262			verification := asymkey_model.ParseCommitWithSignature(commit)
263			if !verification.Verified {
264				return false, "", nil, &ErrWontSign{parentSigned}
265			}
266		}
267	}
268	return true, signingKey, sig, nil
269}
270
271// SignMerge determines if we should sign a PR merge commit to the base repository
272func SignMerge(pr *models.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) {
273	if err := pr.LoadBaseRepo(); err != nil {
274		log.Error("Unable to get Base Repo for pull request")
275		return false, "", nil, err
276	}
277	repo := pr.BaseRepo
278
279	signingKey, signer := SigningKey(repo.RepoPath())
280	if signingKey == "" {
281		return false, "", nil, &ErrWontSign{noKey}
282	}
283	rules := signingModeFromStrings(setting.Repository.Signing.Merges)
284
285	var gitRepo *git.Repository
286	var err error
287
288Loop:
289	for _, rule := range rules {
290		switch rule {
291		case never:
292			return false, "", nil, &ErrWontSign{never}
293		case always:
294			break Loop
295		case pubkey:
296			keys, err := asymkey_model.ListGPGKeys(db.DefaultContext, u.ID, db.ListOptions{})
297			if err != nil {
298				return false, "", nil, err
299			}
300			if len(keys) == 0 {
301				return false, "", nil, &ErrWontSign{pubkey}
302			}
303		case twofa:
304			twofaModel, err := auth.GetTwoFactorByUID(u.ID)
305			if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
306				return false, "", nil, err
307			}
308			if twofaModel == nil {
309				return false, "", nil, &ErrWontSign{twofa}
310			}
311		case approved:
312			protectedBranch, err := models.GetProtectedBranchBy(repo.ID, pr.BaseBranch)
313			if err != nil {
314				return false, "", nil, err
315			}
316			if protectedBranch == nil {
317				return false, "", nil, &ErrWontSign{approved}
318			}
319			if protectedBranch.GetGrantedApprovalsCount(pr) < 1 {
320				return false, "", nil, &ErrWontSign{approved}
321			}
322		case baseSigned:
323			if gitRepo == nil {
324				gitRepo, err = git.OpenRepository(tmpBasePath)
325				if err != nil {
326					return false, "", nil, err
327				}
328				defer gitRepo.Close()
329			}
330			commit, err := gitRepo.GetCommit(baseCommit)
331			if err != nil {
332				return false, "", nil, err
333			}
334			verification := asymkey_model.ParseCommitWithSignature(commit)
335			if !verification.Verified {
336				return false, "", nil, &ErrWontSign{baseSigned}
337			}
338		case headSigned:
339			if gitRepo == nil {
340				gitRepo, err = git.OpenRepository(tmpBasePath)
341				if err != nil {
342					return false, "", nil, err
343				}
344				defer gitRepo.Close()
345			}
346			commit, err := gitRepo.GetCommit(headCommit)
347			if err != nil {
348				return false, "", nil, err
349			}
350			verification := asymkey_model.ParseCommitWithSignature(commit)
351			if !verification.Verified {
352				return false, "", nil, &ErrWontSign{headSigned}
353			}
354		case commitsSigned:
355			if gitRepo == nil {
356				gitRepo, err = git.OpenRepository(tmpBasePath)
357				if err != nil {
358					return false, "", nil, err
359				}
360				defer gitRepo.Close()
361			}
362			commit, err := gitRepo.GetCommit(headCommit)
363			if err != nil {
364				return false, "", nil, err
365			}
366			verification := asymkey_model.ParseCommitWithSignature(commit)
367			if !verification.Verified {
368				return false, "", nil, &ErrWontSign{commitsSigned}
369			}
370			// need to work out merge-base
371			mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit)
372			if err != nil {
373				return false, "", nil, err
374			}
375			commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit)
376			if err != nil {
377				return false, "", nil, err
378			}
379			for _, commit := range commitList {
380				verification := asymkey_model.ParseCommitWithSignature(commit)
381				if !verification.Verified {
382					return false, "", nil, &ErrWontSign{commitsSigned}
383				}
384			}
385		}
386	}
387	return true, signingKey, signer, nil
388}
389