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