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