code.gitea.io/gitea@v1.22.3/models/asymkey/gpg_key_commit_verification.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 "hash" 10 "strings" 11 12 "code.gitea.io/gitea/models/db" 13 repo_model "code.gitea.io/gitea/models/repo" 14 user_model "code.gitea.io/gitea/models/user" 15 "code.gitea.io/gitea/modules/git" 16 "code.gitea.io/gitea/modules/log" 17 "code.gitea.io/gitea/modules/setting" 18 19 "github.com/keybase/go-crypto/openpgp/packet" 20 ) 21 22 // __________________ ________ ____ __. 23 // / _____/\______ \/ _____/ | |/ _|____ ___.__. 24 // / \ ___ | ___/ \ ___ | <_/ __ < | | 25 // \ \_\ \| | \ \_\ \ | | \ ___/\___ | 26 // \______ /|____| \______ / |____|__ \___ > ____| 27 // \/ \/ \/ \/\/ 28 // _________ .__ __ 29 // \_ ___ \ ____ _____ _____ |__|/ |_ 30 // / \ \/ / _ \ / \ / \| \ __\ 31 // \ \___( <_> ) Y Y \ Y Y \ || | 32 // \______ /\____/|__|_| /__|_| /__||__| 33 // \/ \/ \/ 34 // ____ ____ .__ _____.__ __ .__ 35 // \ \ / /___________|__|/ ____\__| ____ _____ _/ |_|__| ____ ____ 36 // \ Y // __ \_ __ \ \ __\| |/ ___\\__ \\ __\ |/ _ \ / \ 37 // \ /\ ___/| | \/ || | | \ \___ / __ \| | | ( <_> ) | \ 38 // \___/ \___ >__| |__||__| |__|\___ >____ /__| |__|\____/|___| / 39 // \/ \/ \/ \/ 40 41 // This file provides functions relating commit verification 42 43 // CommitVerification represents a commit validation of signature 44 type CommitVerification struct { 45 Verified bool 46 Warning bool 47 Reason string 48 SigningUser *user_model.User 49 CommittingUser *user_model.User 50 SigningEmail string 51 SigningKey *GPGKey 52 SigningSSHKey *PublicKey 53 TrustStatus string 54 } 55 56 // SignCommit represents a commit with validation of signature. 57 type SignCommit struct { 58 Verification *CommitVerification 59 *user_model.UserCommit 60 } 61 62 const ( 63 // BadSignature is used as the reason when the signature has a KeyID that is in the db 64 // but no key that has that ID verifies the signature. This is a suspicious failure. 65 BadSignature = "gpg.error.probable_bad_signature" 66 // BadDefaultSignature is used as the reason when the signature has a KeyID that matches the 67 // default Key but is not verified by the default key. This is a suspicious failure. 68 BadDefaultSignature = "gpg.error.probable_bad_default_signature" 69 // NoKeyFound is used as the reason when no key can be found to verify the signature. 70 NoKeyFound = "gpg.error.no_gpg_keys_found" 71 ) 72 73 // ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys. 74 func ParseCommitsWithSignature(ctx context.Context, oldCommits []*user_model.UserCommit, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error)) []*SignCommit { 75 newCommits := make([]*SignCommit, 0, len(oldCommits)) 76 keyMap := map[string]bool{} 77 78 for _, c := range oldCommits { 79 signCommit := &SignCommit{ 80 UserCommit: c, 81 Verification: ParseCommitWithSignature(ctx, c.Commit), 82 } 83 84 _ = CalculateTrustStatus(signCommit.Verification, repoTrustModel, isOwnerMemberCollaborator, &keyMap) 85 86 newCommits = append(newCommits, signCommit) 87 } 88 return newCommits 89 } 90 91 // ParseCommitWithSignature check if signature is good against keystore. 92 func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerification { 93 var committer *user_model.User 94 if c.Committer != nil { 95 var err error 96 // Find Committer account 97 committer, err = user_model.GetUserByEmail(ctx, c.Committer.Email) // This finds the user by primary email or activated email so commit will not be valid if email is not 98 if err != nil { // Skipping not user for committer 99 committer = &user_model.User{ 100 Name: c.Committer.Name, 101 Email: c.Committer.Email, 102 } 103 // We can expect this to often be an ErrUserNotExist. in the case 104 // it is not, however, it is important to log it. 105 if !user_model.IsErrUserNotExist(err) { 106 log.Error("GetUserByEmail: %v", err) 107 return &CommitVerification{ 108 CommittingUser: committer, 109 Verified: false, 110 Reason: "gpg.error.no_committer_account", 111 } 112 } 113 } 114 } 115 116 // If no signature just report the committer 117 if c.Signature == nil { 118 return &CommitVerification{ 119 CommittingUser: committer, 120 Verified: false, // Default value 121 Reason: "gpg.error.not_signed_commit", // Default value 122 } 123 } 124 125 // If this a SSH signature handle it differently 126 if strings.HasPrefix(c.Signature.Signature, "-----BEGIN SSH SIGNATURE-----") { 127 return ParseCommitWithSSHSignature(ctx, c, committer) 128 } 129 130 // Parsing signature 131 sig, err := extractSignature(c.Signature.Signature) 132 if err != nil { // Skipping failed to extract sign 133 log.Error("SignatureRead err: %v", err) 134 return &CommitVerification{ 135 CommittingUser: committer, 136 Verified: false, 137 Reason: "gpg.error.extract_sign", 138 } 139 } 140 141 keyID := tryGetKeyIDFromSignature(sig) 142 defaultReason := NoKeyFound 143 144 // First check if the sig has a keyID and if so just look at that 145 if commitVerification := hashAndVerifyForKeyID( 146 ctx, 147 sig, 148 c.Signature.Payload, 149 committer, 150 keyID, 151 setting.AppName, 152 ""); commitVerification != nil { 153 if commitVerification.Reason == BadSignature { 154 defaultReason = BadSignature 155 } else { 156 return commitVerification 157 } 158 } 159 160 // Now try to associate the signature with the committer, if present 161 if committer.ID != 0 { 162 keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{ 163 OwnerID: committer.ID, 164 }) 165 if err != nil { // Skipping failed to get gpg keys of user 166 log.Error("ListGPGKeys: %v", err) 167 return &CommitVerification{ 168 CommittingUser: committer, 169 Verified: false, 170 Reason: "gpg.error.failed_retrieval_gpg_keys", 171 } 172 } 173 174 if err := GPGKeyList(keys).LoadSubKeys(ctx); err != nil { 175 log.Error("LoadSubKeys: %v", err) 176 return &CommitVerification{ 177 CommittingUser: committer, 178 Verified: false, 179 Reason: "gpg.error.failed_retrieval_gpg_keys", 180 } 181 } 182 183 committerEmailAddresses, _ := user_model.GetEmailAddresses(ctx, committer.ID) 184 activated := false 185 for _, e := range committerEmailAddresses { 186 if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { 187 activated = true 188 break 189 } 190 } 191 192 for _, k := range keys { 193 // Pre-check (& optimization) that emails attached to key can be attached to the committer email and can validate 194 canValidate := false 195 email := "" 196 if k.Verified && activated { 197 canValidate = true 198 email = c.Committer.Email 199 } 200 if !canValidate { 201 for _, e := range k.Emails { 202 if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { 203 canValidate = true 204 email = e.Email 205 break 206 } 207 } 208 } 209 if !canValidate { 210 continue // Skip this key 211 } 212 213 commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, c.Signature.Payload, k, committer, committer, email) 214 if commitVerification != nil { 215 return commitVerification 216 } 217 } 218 } 219 220 if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" { 221 // OK we should try the default key 222 gpgSettings := git.GPGSettings{ 223 Sign: true, 224 KeyID: setting.Repository.Signing.SigningKey, 225 Name: setting.Repository.Signing.SigningName, 226 Email: setting.Repository.Signing.SigningEmail, 227 } 228 if err := gpgSettings.LoadPublicKeyContent(); err != nil { 229 log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err) 230 } else if commitVerification := verifyWithGPGSettings(ctx, &gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { 231 if commitVerification.Reason == BadSignature { 232 defaultReason = BadSignature 233 } else { 234 return commitVerification 235 } 236 } 237 } 238 239 defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false) 240 if err != nil { 241 log.Error("Error getting default public gpg key: %v", err) 242 } else if defaultGPGSettings == nil { 243 log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String()) 244 } else if defaultGPGSettings.Sign { 245 if commitVerification := verifyWithGPGSettings(ctx, defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { 246 if commitVerification.Reason == BadSignature { 247 defaultReason = BadSignature 248 } else { 249 return commitVerification 250 } 251 } 252 } 253 254 return &CommitVerification{ // Default at this stage 255 CommittingUser: committer, 256 Verified: false, 257 Warning: defaultReason != NoKeyFound, 258 Reason: defaultReason, 259 SigningKey: &GPGKey{ 260 KeyID: keyID, 261 }, 262 } 263 } 264 265 func verifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *user_model.User, keyID string) *CommitVerification { 266 // First try to find the key in the db 267 if commitVerification := hashAndVerifyForKeyID(ctx, sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil { 268 return commitVerification 269 } 270 271 // Otherwise we have to parse the key 272 ekeys, err := checkArmoredGPGKeyString(gpgSettings.PublicKeyContent) 273 if err != nil { 274 log.Error("Unable to get default signing key: %v", err) 275 return &CommitVerification{ 276 CommittingUser: committer, 277 Verified: false, 278 Reason: "gpg.error.generate_hash", 279 } 280 } 281 for _, ekey := range ekeys { 282 pubkey := ekey.PrimaryKey 283 content, err := base64EncPubKey(pubkey) 284 if err != nil { 285 return &CommitVerification{ 286 CommittingUser: committer, 287 Verified: false, 288 Reason: "gpg.error.generate_hash", 289 } 290 } 291 k := &GPGKey{ 292 Content: content, 293 CanSign: pubkey.CanSign(), 294 KeyID: pubkey.KeyIdString(), 295 } 296 for _, subKey := range ekey.Subkeys { 297 content, err := base64EncPubKey(subKey.PublicKey) 298 if err != nil { 299 return &CommitVerification{ 300 CommittingUser: committer, 301 Verified: false, 302 Reason: "gpg.error.generate_hash", 303 } 304 } 305 k.SubsKey = append(k.SubsKey, &GPGKey{ 306 Content: content, 307 CanSign: subKey.PublicKey.CanSign(), 308 KeyID: subKey.PublicKey.KeyIdString(), 309 }) 310 } 311 if commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, k, committer, &user_model.User{ 312 Name: gpgSettings.Name, 313 Email: gpgSettings.Email, 314 }, gpgSettings.Email); commitVerification != nil { 315 return commitVerification 316 } 317 if keyID == k.KeyID { 318 // This is a bad situation ... We have a key id that matches our default key but the signature doesn't match. 319 return &CommitVerification{ 320 CommittingUser: committer, 321 Verified: false, 322 Warning: true, 323 Reason: BadSignature, 324 } 325 } 326 } 327 return nil 328 } 329 330 func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error { 331 // Check if key can sign 332 if !k.CanSign { 333 return fmt.Errorf("key can not sign") 334 } 335 // Decode key 336 pkey, err := base64DecPubKey(k.Content) 337 if err != nil { 338 return err 339 } 340 return pkey.VerifySignature(h, s) 341 } 342 343 func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) { 344 // Generating hash of commit 345 hash, err := populateHash(sig.Hash, []byte(payload)) 346 if err != nil { // Skipping as failed to generate hash 347 log.Error("PopulateHash: %v", err) 348 return nil, err 349 } 350 // We will ignore errors in verification as they don't need to be propagated up 351 err = verifySign(sig, hash, k) 352 if err != nil { 353 return nil, nil 354 } 355 return k, nil 356 } 357 358 func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) { 359 verified, err := hashAndVerify(sig, payload, k) 360 if err != nil || verified != nil { 361 return verified, err 362 } 363 for _, sk := range k.SubsKey { 364 verified, err := hashAndVerify(sig, payload, sk) 365 if err != nil || verified != nil { 366 return verified, err 367 } 368 } 369 return nil, nil 370 } 371 372 func hashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *user_model.User, email string) *CommitVerification { 373 key, err := hashAndVerifyWithSubKeys(sig, payload, k) 374 if err != nil { // Skipping failed to generate hash 375 return &CommitVerification{ 376 CommittingUser: committer, 377 Verified: false, 378 Reason: "gpg.error.generate_hash", 379 } 380 } 381 382 if key != nil { 383 return &CommitVerification{ // Everything is ok 384 CommittingUser: committer, 385 Verified: true, 386 Reason: fmt.Sprintf("%s / %s", signer.Name, key.KeyID), 387 SigningUser: signer, 388 SigningKey: key, 389 SigningEmail: email, 390 } 391 } 392 return nil 393 } 394 395 func hashAndVerifyForKeyID(ctx context.Context, sig *packet.Signature, payload string, committer *user_model.User, keyID, name, email string) *CommitVerification { 396 if keyID == "" { 397 return nil 398 } 399 keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{ 400 KeyID: keyID, 401 IncludeSubKeys: true, 402 }) 403 if err != nil { 404 log.Error("GetGPGKeysByKeyID: %v", err) 405 return &CommitVerification{ 406 CommittingUser: committer, 407 Verified: false, 408 Reason: "gpg.error.failed_retrieval_gpg_keys", 409 } 410 } 411 if len(keys) == 0 { 412 return nil 413 } 414 for _, key := range keys { 415 var primaryKeys []*GPGKey 416 if key.PrimaryKeyID != "" { 417 primaryKeys, err = db.Find[GPGKey](ctx, FindGPGKeyOptions{ 418 KeyID: key.PrimaryKeyID, 419 IncludeSubKeys: true, 420 }) 421 if err != nil { 422 log.Error("GetGPGKeysByKeyID: %v", err) 423 return &CommitVerification{ 424 CommittingUser: committer, 425 Verified: false, 426 Reason: "gpg.error.failed_retrieval_gpg_keys", 427 } 428 } 429 } 430 431 activated, email := checkKeyEmails(ctx, email, append([]*GPGKey{key}, primaryKeys...)...) 432 if !activated { 433 continue 434 } 435 436 signer := &user_model.User{ 437 Name: name, 438 Email: email, 439 } 440 if key.OwnerID != 0 { 441 owner, err := user_model.GetUserByID(ctx, key.OwnerID) 442 if err == nil { 443 signer = owner 444 } else if !user_model.IsErrUserNotExist(err) { 445 log.Error("Failed to user_model.GetUserByID: %d for key ID: %d (%s) %v", key.OwnerID, key.ID, key.KeyID, err) 446 return &CommitVerification{ 447 CommittingUser: committer, 448 Verified: false, 449 Reason: "gpg.error.no_committer_account", 450 } 451 } 452 } 453 commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, key, committer, signer, email) 454 if commitVerification != nil { 455 return commitVerification 456 } 457 } 458 // This is a bad situation ... We have a key id that is in our database but the signature doesn't match. 459 return &CommitVerification{ 460 CommittingUser: committer, 461 Verified: false, 462 Warning: true, 463 Reason: BadSignature, 464 } 465 } 466 467 // CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository 468 // There are several trust models in Gitea 469 func CalculateTrustStatus(verification *CommitVerification, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error), keyMap *map[string]bool) error { 470 if !verification.Verified { 471 return nil 472 } 473 474 // In the Committer trust model a signature is trusted if it matches the committer 475 // - it doesn't matter if they're a collaborator, the owner, Gitea or Github 476 // NB: This model is commit verification only 477 if repoTrustModel == repo_model.CommitterTrustModel { 478 // default to "unmatched" 479 verification.TrustStatus = "unmatched" 480 481 // We can only verify against users in our database but the default key will match 482 // against by email if it is not in the db. 483 if (verification.SigningUser.ID != 0 && 484 verification.CommittingUser.ID == verification.SigningUser.ID) || 485 (verification.SigningUser.ID == 0 && verification.CommittingUser.ID == 0 && 486 verification.SigningUser.Email == verification.CommittingUser.Email) { 487 verification.TrustStatus = "trusted" 488 } 489 return nil 490 } 491 492 // Now we drop to the more nuanced trust models... 493 verification.TrustStatus = "trusted" 494 495 if verification.SigningUser.ID == 0 { 496 // This commit is signed by the default key - but this key is not assigned to a user in the DB. 497 498 // However in the repo_model.CollaboratorCommitterTrustModel we cannot mark this as trusted 499 // unless the default key matches the email of a non-user. 500 if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && (verification.CommittingUser.ID != 0 || 501 verification.SigningUser.Email != verification.CommittingUser.Email) { 502 verification.TrustStatus = "untrusted" 503 } 504 return nil 505 } 506 507 // Check we actually have a GPG SigningKey 508 var err error 509 if verification.SigningKey != nil { 510 var isMember bool 511 if keyMap != nil { 512 var has bool 513 isMember, has = (*keyMap)[verification.SigningKey.KeyID] 514 if !has { 515 isMember, err = isOwnerMemberCollaborator(verification.SigningUser) 516 (*keyMap)[verification.SigningKey.KeyID] = isMember 517 } 518 } else { 519 isMember, err = isOwnerMemberCollaborator(verification.SigningUser) 520 } 521 522 if !isMember { 523 verification.TrustStatus = "untrusted" 524 if verification.CommittingUser.ID != verification.SigningUser.ID { 525 // The committing user and the signing user are not the same 526 // This should be marked as questionable unless the signing user is a collaborator/team member etc. 527 verification.TrustStatus = "unmatched" 528 } 529 } else if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID { 530 // The committing user and the signing user are not the same and our trustmodel states that they must match 531 verification.TrustStatus = "unmatched" 532 } 533 } 534 535 return err 536 }