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