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