mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-03 21:08:25 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			185 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			185 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
						|
// SPDX-License-Identifier: MIT
 | 
						|
 | 
						|
package asymkey
 | 
						|
 | 
						|
import (
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"hash"
 | 
						|
 | 
						|
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						|
	user_model "code.gitea.io/gitea/models/user"
 | 
						|
	"code.gitea.io/gitea/modules/log"
 | 
						|
 | 
						|
	"github.com/ProtonMail/go-crypto/openpgp/packet"
 | 
						|
)
 | 
						|
 | 
						|
// This file provides functions relating commit verification
 | 
						|
 | 
						|
// CommitVerification represents a commit validation of signature
 | 
						|
type CommitVerification struct {
 | 
						|
	Verified       bool
 | 
						|
	Warning        bool
 | 
						|
	Reason         string
 | 
						|
	SigningUser    *user_model.User // if Verified, then SigningUser is non-nil
 | 
						|
	CommittingUser *user_model.User // if Verified, then CommittingUser is non-nil
 | 
						|
	SigningEmail   string
 | 
						|
	SigningKey     *GPGKey // FIXME: need to refactor it to a new name like "SigningGPGKey", it is also used in some templates
 | 
						|
	SigningSSHKey  *PublicKey
 | 
						|
	TrustStatus    string
 | 
						|
}
 | 
						|
 | 
						|
// SignCommit represents a commit with validation of signature.
 | 
						|
type SignCommit struct {
 | 
						|
	Verification *CommitVerification
 | 
						|
	*user_model.UserCommit
 | 
						|
}
 | 
						|
 | 
						|
const (
 | 
						|
	// BadSignature is used as the reason when the signature has a KeyID that is in the db
 | 
						|
	// but no key that has that ID verifies the signature. This is a suspicious failure.
 | 
						|
	BadSignature = "gpg.error.probable_bad_signature"
 | 
						|
	// BadDefaultSignature is used as the reason when the signature has a KeyID that matches the
 | 
						|
	// default Key but is not verified by the default key. This is a suspicious failure.
 | 
						|
	BadDefaultSignature = "gpg.error.probable_bad_default_signature"
 | 
						|
	// NoKeyFound is used as the reason when no key can be found to verify the signature.
 | 
						|
	NoKeyFound = "gpg.error.no_gpg_keys_found"
 | 
						|
)
 | 
						|
 | 
						|
func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
 | 
						|
	// Check if key can sign
 | 
						|
	if !k.CanSign {
 | 
						|
		return errors.New("key can not sign")
 | 
						|
	}
 | 
						|
	// Decode key
 | 
						|
	pkey, err := base64DecPubKey(k.Content)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	return pkey.VerifySignature(h, s)
 | 
						|
}
 | 
						|
 | 
						|
func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) {
 | 
						|
	// Generating hash of commit
 | 
						|
	hash, err := populateHash(sig.Hash, []byte(payload))
 | 
						|
	if err != nil { // Skipping as failed to generate hash
 | 
						|
		log.Error("PopulateHash: %v", err)
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	// We will ignore errors in verification as they don't need to be propagated up
 | 
						|
	err = verifySign(sig, hash, k)
 | 
						|
	if err != nil {
 | 
						|
		return nil, nil
 | 
						|
	}
 | 
						|
	return k, nil
 | 
						|
}
 | 
						|
 | 
						|
func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) {
 | 
						|
	verified, err := hashAndVerify(sig, payload, k)
 | 
						|
	if err != nil || verified != nil {
 | 
						|
		return verified, err
 | 
						|
	}
 | 
						|
	for _, sk := range k.SubsKey {
 | 
						|
		verified, err := hashAndVerify(sig, payload, sk)
 | 
						|
		if err != nil || verified != nil {
 | 
						|
			return verified, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil, nil
 | 
						|
}
 | 
						|
 | 
						|
func HashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *user_model.User, email string) *CommitVerification {
 | 
						|
	key, err := hashAndVerifyWithSubKeys(sig, payload, k)
 | 
						|
	if err != nil { // Skipping failed to generate hash
 | 
						|
		return &CommitVerification{
 | 
						|
			CommittingUser: committer,
 | 
						|
			Verified:       false,
 | 
						|
			Reason:         "gpg.error.generate_hash",
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if key != nil {
 | 
						|
		return &CommitVerification{ // Everything is ok
 | 
						|
			CommittingUser: committer,
 | 
						|
			Verified:       true,
 | 
						|
			Reason:         fmt.Sprintf("%s / %s", signer.Name, key.KeyID),
 | 
						|
			SigningUser:    signer,
 | 
						|
			SigningKey:     key,
 | 
						|
			SigningEmail:   email,
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository
 | 
						|
// There are several trust models in Gitea
 | 
						|
func CalculateTrustStatus(verification *CommitVerification, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error), keyMap *map[string]bool) error {
 | 
						|
	if !verification.Verified {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	// In the Committer trust model a signature is trusted if it matches the committer
 | 
						|
	// - it doesn't matter if they're a collaborator, the owner, Gitea or Github
 | 
						|
	// NB: This model is commit verification only
 | 
						|
	if repoTrustModel == repo_model.CommitterTrustModel {
 | 
						|
		// default to "unmatched"
 | 
						|
		verification.TrustStatus = "unmatched"
 | 
						|
 | 
						|
		// We can only verify against users in our database but the default key will match
 | 
						|
		// against by email if it is not in the db.
 | 
						|
		if (verification.SigningUser.ID != 0 &&
 | 
						|
			verification.CommittingUser.ID == verification.SigningUser.ID) ||
 | 
						|
			(verification.SigningUser.ID == 0 && verification.CommittingUser.ID == 0 &&
 | 
						|
				verification.SigningUser.Email == verification.CommittingUser.Email) {
 | 
						|
			verification.TrustStatus = "trusted"
 | 
						|
		}
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	// Now we drop to the more nuanced trust models...
 | 
						|
	verification.TrustStatus = "trusted"
 | 
						|
 | 
						|
	if verification.SigningUser.ID == 0 {
 | 
						|
		// This commit is signed by the default key - but this key is not assigned to a user in the DB.
 | 
						|
 | 
						|
		// However in the repo_model.CollaboratorCommitterTrustModel we cannot mark this as trusted
 | 
						|
		// unless the default key matches the email of a non-user.
 | 
						|
		if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && (verification.CommittingUser.ID != 0 ||
 | 
						|
			verification.SigningUser.Email != verification.CommittingUser.Email) {
 | 
						|
			verification.TrustStatus = "untrusted"
 | 
						|
		}
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	// Check we actually have a GPG SigningKey
 | 
						|
	var err error
 | 
						|
	if verification.SigningKey != nil {
 | 
						|
		var isMember bool
 | 
						|
		if keyMap != nil {
 | 
						|
			var has bool
 | 
						|
			isMember, has = (*keyMap)[verification.SigningKey.KeyID]
 | 
						|
			if !has {
 | 
						|
				isMember, err = isOwnerMemberCollaborator(verification.SigningUser)
 | 
						|
				(*keyMap)[verification.SigningKey.KeyID] = isMember
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			isMember, err = isOwnerMemberCollaborator(verification.SigningUser)
 | 
						|
		}
 | 
						|
 | 
						|
		if !isMember {
 | 
						|
			verification.TrustStatus = "untrusted"
 | 
						|
			if verification.CommittingUser.ID != verification.SigningUser.ID {
 | 
						|
				// The committing user and the signing user are not the same
 | 
						|
				// This should be marked as questionable unless the signing user is a collaborator/team member etc.
 | 
						|
				verification.TrustStatus = "unmatched"
 | 
						|
			}
 | 
						|
		} else if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID {
 | 
						|
			// The committing user and the signing user are not the same and our trustmodel states that they must match
 | 
						|
			verification.TrustStatus = "unmatched"
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return err
 | 
						|
}
 |