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  }