From 4dd475dfe53204999b41f1094c25bdd693cce789 Mon Sep 17 00:00:00 2001
From: Gergely Nagy <forgejo@gergo.csillger.hu>
Date: Mon, 4 Mar 2024 19:11:42 +0100
Subject: [PATCH] models/asymkey: Implement Tag verification

This is, in large part, a refactoring: we rename `CommitVerification` to
`ObjectVerification`, and adjust `ParseObjectWithSignature` (previously
`ParseCommitWithSignature`) to work on an object, rather than a commit.

This in turn, lets us implement `ParseTagWithSignature` on top of it, so
commit & tag signature verification will share most of the code.

Work sponsored by @glts.

Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
---
 models/asymkey/gpg_key_commit_verification.go | 492 +---------------
 models/asymkey/gpg_key_object_verification.go | 527 ++++++++++++++++++
 models/asymkey/gpg_key_tag_verification.go    |  15 +
 ...tion.go => ssh_key_object_verification.go} |  15 +-
 ...go => ssh_key_object_verification_test.go} |  18 +-
 modules/gitgraph/graph_models.go              |   2 +-
 6 files changed, 568 insertions(+), 501 deletions(-)
 create mode 100644 models/asymkey/gpg_key_object_verification.go
 create mode 100644 models/asymkey/gpg_key_tag_verification.go
 rename models/asymkey/{ssh_key_commit_verification.go => ssh_key_object_verification.go} (79%)
 rename models/asymkey/{ssh_key_commit_verification_test.go => ssh_key_object_verification_test.go} (85%)

diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go
index 83fbab5d36..9aa606405e 100644
--- a/models/asymkey/gpg_key_commit_verification.go
+++ b/models/asymkey/gpg_key_commit_verification.go
@@ -5,18 +5,10 @@ package asymkey
 
 import (
 	"context"
-	"fmt"
-	"hash"
-	"strings"
 
-	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/setting"
-
-	"github.com/keybase/go-crypto/openpgp/packet"
 )
 
 //   __________________  ________   ____  __.
@@ -40,45 +32,22 @@ import (
 
 // 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
-	CommittingUser *user_model.User
-	SigningEmail   string
-	SigningKey     *GPGKey
-	SigningSSHKey  *PublicKey
-	TrustStatus    string
-}
-
 // SignCommit represents a commit with validation of signature.
 type SignCommit struct {
-	Verification *CommitVerification
+	Verification *ObjectVerification
 	*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"
-)
-
 // ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
 func ParseCommitsWithSignature(ctx context.Context, oldCommits []*user_model.UserCommit, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error)) []*SignCommit {
 	newCommits := make([]*SignCommit, 0, len(oldCommits))
 	keyMap := map[string]bool{}
 
 	for _, c := range oldCommits {
+		o := commitToGitObject(c.Commit)
 		signCommit := &SignCommit{
 			UserCommit:   c,
-			Verification: ParseCommitWithSignature(ctx, c.Commit),
+			Verification: ParseObjectWithSignature(ctx, &o),
 		}
 
 		_ = CalculateTrustStatus(signCommit.Verification, repoTrustModel, isOwnerMemberCollaborator, &keyMap)
@@ -88,456 +57,7 @@ func ParseCommitsWithSignature(ctx context.Context, oldCommits []*user_model.Use
 	return newCommits
 }
 
-// ParseCommitWithSignature check if signature is good against keystore.
-func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerification {
-	var committer *user_model.User
-	if c.Committer != nil {
-		var err error
-		// Find Committer account
-		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
-		if err != nil {                                                    // Skipping not user for committer
-			committer = &user_model.User{
-				Name:  c.Committer.Name,
-				Email: c.Committer.Email,
-			}
-			// We can expect this to often be an ErrUserNotExist. in the case
-			// it is not, however, it is important to log it.
-			if !user_model.IsErrUserNotExist(err) {
-				log.Error("GetUserByEmail: %v", err)
-				return &CommitVerification{
-					CommittingUser: committer,
-					Verified:       false,
-					Reason:         "gpg.error.no_committer_account",
-				}
-			}
-
-		}
-	}
-
-	// If no signature just report the committer
-	if c.Signature == nil {
-		return &CommitVerification{
-			CommittingUser: committer,
-			Verified:       false,                         // Default value
-			Reason:         "gpg.error.not_signed_commit", // Default value
-		}
-	}
-
-	// If this a SSH signature handle it differently
-	if strings.HasPrefix(c.Signature.Signature, "-----BEGIN SSH SIGNATURE-----") {
-		return ParseCommitWithSSHSignature(ctx, c, committer)
-	}
-
-	// Parsing signature
-	sig, err := extractSignature(c.Signature.Signature)
-	if err != nil { // Skipping failed to extract sign
-		log.Error("SignatureRead err: %v", err)
-		return &CommitVerification{
-			CommittingUser: committer,
-			Verified:       false,
-			Reason:         "gpg.error.extract_sign",
-		}
-	}
-
-	keyID := ""
-	if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
-		keyID = fmt.Sprintf("%X", *sig.IssuerKeyId)
-	}
-	if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 {
-		keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20])
-	}
-	defaultReason := NoKeyFound
-
-	// First check if the sig has a keyID and if so just look at that
-	if commitVerification := hashAndVerifyForKeyID(
-		ctx,
-		sig,
-		c.Signature.Payload,
-		committer,
-		keyID,
-		setting.AppName,
-		""); commitVerification != nil {
-		if commitVerification.Reason == BadSignature {
-			defaultReason = BadSignature
-		} else {
-			return commitVerification
-		}
-	}
-
-	// Now try to associate the signature with the committer, if present
-	if committer.ID != 0 {
-		keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{
-			OwnerID: committer.ID,
-		})
-		if err != nil { // Skipping failed to get gpg keys of user
-			log.Error("ListGPGKeys: %v", err)
-			return &CommitVerification{
-				CommittingUser: committer,
-				Verified:       false,
-				Reason:         "gpg.error.failed_retrieval_gpg_keys",
-			}
-		}
-
-		if err := GPGKeyList(keys).LoadSubKeys(ctx); err != nil {
-			log.Error("LoadSubKeys: %v", err)
-			return &CommitVerification{
-				CommittingUser: committer,
-				Verified:       false,
-				Reason:         "gpg.error.failed_retrieval_gpg_keys",
-			}
-		}
-
-		committerEmailAddresses, _ := user_model.GetEmailAddresses(ctx, committer.ID)
-		activated := false
-		for _, e := range committerEmailAddresses {
-			if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
-				activated = true
-				break
-			}
-		}
-
-		for _, k := range keys {
-			// Pre-check (& optimization) that emails attached to key can be attached to the committer email and can validate
-			canValidate := false
-			email := ""
-			if k.Verified && activated {
-				canValidate = true
-				email = c.Committer.Email
-			}
-			if !canValidate {
-				for _, e := range k.Emails {
-					if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
-						canValidate = true
-						email = e.Email
-						break
-					}
-				}
-			}
-			if !canValidate {
-				continue // Skip this key
-			}
-
-			commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, c.Signature.Payload, k, committer, committer, email)
-			if commitVerification != nil {
-				return commitVerification
-			}
-		}
-	}
-
-	if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" {
-		// OK we should try the default key
-		gpgSettings := git.GPGSettings{
-			Sign:  true,
-			KeyID: setting.Repository.Signing.SigningKey,
-			Name:  setting.Repository.Signing.SigningName,
-			Email: setting.Repository.Signing.SigningEmail,
-		}
-		if err := gpgSettings.LoadPublicKeyContent(); err != nil {
-			log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err)
-		} else if commitVerification := verifyWithGPGSettings(ctx, &gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
-			if commitVerification.Reason == BadSignature {
-				defaultReason = BadSignature
-			} else {
-				return commitVerification
-			}
-		}
-	}
-
-	defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false)
-	if err != nil {
-		log.Error("Error getting default public gpg key: %v", err)
-	} else if defaultGPGSettings == nil {
-		log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String())
-	} else if defaultGPGSettings.Sign {
-		if commitVerification := verifyWithGPGSettings(ctx, defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
-			if commitVerification.Reason == BadSignature {
-				defaultReason = BadSignature
-			} else {
-				return commitVerification
-			}
-		}
-	}
-
-	return &CommitVerification{ // Default at this stage
-		CommittingUser: committer,
-		Verified:       false,
-		Warning:        defaultReason != NoKeyFound,
-		Reason:         defaultReason,
-		SigningKey: &GPGKey{
-			KeyID: keyID,
-		},
-	}
-}
-
-func verifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *user_model.User, keyID string) *CommitVerification {
-	// First try to find the key in the db
-	if commitVerification := hashAndVerifyForKeyID(ctx, sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil {
-		return commitVerification
-	}
-
-	// Otherwise we have to parse the key
-	ekeys, err := checkArmoredGPGKeyString(gpgSettings.PublicKeyContent)
-	if err != nil {
-		log.Error("Unable to get default signing key: %v", err)
-		return &CommitVerification{
-			CommittingUser: committer,
-			Verified:       false,
-			Reason:         "gpg.error.generate_hash",
-		}
-	}
-	for _, ekey := range ekeys {
-		pubkey := ekey.PrimaryKey
-		content, err := base64EncPubKey(pubkey)
-		if err != nil {
-			return &CommitVerification{
-				CommittingUser: committer,
-				Verified:       false,
-				Reason:         "gpg.error.generate_hash",
-			}
-		}
-		k := &GPGKey{
-			Content: content,
-			CanSign: pubkey.CanSign(),
-			KeyID:   pubkey.KeyIdString(),
-		}
-		for _, subKey := range ekey.Subkeys {
-			content, err := base64EncPubKey(subKey.PublicKey)
-			if err != nil {
-				return &CommitVerification{
-					CommittingUser: committer,
-					Verified:       false,
-					Reason:         "gpg.error.generate_hash",
-				}
-			}
-			k.SubsKey = append(k.SubsKey, &GPGKey{
-				Content: content,
-				CanSign: subKey.PublicKey.CanSign(),
-				KeyID:   subKey.PublicKey.KeyIdString(),
-			})
-		}
-		if commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, k, committer, &user_model.User{
-			Name:  gpgSettings.Name,
-			Email: gpgSettings.Email,
-		}, gpgSettings.Email); commitVerification != nil {
-			return commitVerification
-		}
-		if keyID == k.KeyID {
-			// This is a bad situation ... We have a key id that matches our default key but the signature doesn't match.
-			return &CommitVerification{
-				CommittingUser: committer,
-				Verified:       false,
-				Warning:        true,
-				Reason:         BadSignature,
-			}
-		}
-	}
-	return nil
-}
-
-func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
-	// Check if key can sign
-	if !k.CanSign {
-		return fmt.Errorf("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
-}
-
-func hashAndVerifyForKeyID(ctx context.Context, sig *packet.Signature, payload string, committer *user_model.User, keyID, name, email string) *CommitVerification {
-	if keyID == "" {
-		return nil
-	}
-	keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{
-		KeyID:          keyID,
-		IncludeSubKeys: true,
-	})
-	if err != nil {
-		log.Error("GetGPGKeysByKeyID: %v", err)
-		return &CommitVerification{
-			CommittingUser: committer,
-			Verified:       false,
-			Reason:         "gpg.error.failed_retrieval_gpg_keys",
-		}
-	}
-	if len(keys) == 0 {
-		return nil
-	}
-	for _, key := range keys {
-		var primaryKeys []*GPGKey
-		if key.PrimaryKeyID != "" {
-			primaryKeys, err = db.Find[GPGKey](ctx, FindGPGKeyOptions{
-				KeyID:          key.PrimaryKeyID,
-				IncludeSubKeys: true,
-			})
-			if err != nil {
-				log.Error("GetGPGKeysByKeyID: %v", err)
-				return &CommitVerification{
-					CommittingUser: committer,
-					Verified:       false,
-					Reason:         "gpg.error.failed_retrieval_gpg_keys",
-				}
-			}
-		}
-
-		activated, email := checkKeyEmails(ctx, email, append([]*GPGKey{key}, primaryKeys...)...)
-		if !activated {
-			continue
-		}
-
-		signer := &user_model.User{
-			Name:  name,
-			Email: email,
-		}
-		if key.OwnerID != 0 {
-			owner, err := user_model.GetUserByID(ctx, key.OwnerID)
-			if err == nil {
-				signer = owner
-			} else if !user_model.IsErrUserNotExist(err) {
-				log.Error("Failed to user_model.GetUserByID: %d for key ID: %d (%s) %v", key.OwnerID, key.ID, key.KeyID, err)
-				return &CommitVerification{
-					CommittingUser: committer,
-					Verified:       false,
-					Reason:         "gpg.error.no_committer_account",
-				}
-			}
-		}
-		commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, key, committer, signer, email)
-		if commitVerification != nil {
-			return commitVerification
-		}
-	}
-	// This is a bad situation ... We have a key id that is in our database but the signature doesn't match.
-	return &CommitVerification{
-		CommittingUser: committer,
-		Verified:       false,
-		Warning:        true,
-		Reason:         BadSignature,
-	}
-}
-
-// 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
+func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *ObjectVerification {
+	o := commitToGitObject(c)
+	return ParseObjectWithSignature(ctx, &o)
 }
diff --git a/models/asymkey/gpg_key_object_verification.go b/models/asymkey/gpg_key_object_verification.go
new file mode 100644
index 0000000000..cbae280c6a
--- /dev/null
+++ b/models/asymkey/gpg_key_object_verification.go
@@ -0,0 +1,527 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+	"context"
+	"fmt"
+	"hash"
+	"strings"
+
+	"code.gitea.io/gitea/models/db"
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/keybase/go-crypto/openpgp/packet"
+)
+
+// This file provides functions related to object (commit, tag) verification
+
+// ObjectVerification represents a commit validation of signature
+type ObjectVerification struct {
+	Verified       bool
+	Warning        bool
+	Reason         string
+	SigningUser    *user_model.User
+	CommittingUser *user_model.User
+	SigningEmail   string
+	SigningKey     *GPGKey
+	SigningSSHKey  *PublicKey
+	TrustStatus    string
+}
+
+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"
+)
+
+type GitObject struct {
+	ID        git.ObjectID
+	Committer *git.Signature
+	Signature *git.ObjectSignature
+	Commit    *git.Commit
+}
+
+func commitToGitObject(c *git.Commit) GitObject {
+	return GitObject{
+		ID:        c.ID,
+		Committer: c.Committer,
+		Signature: c.Signature,
+		Commit:    c,
+	}
+}
+
+func tagToGitObject(t *git.Tag, gitRepo *git.Repository) GitObject {
+	commit, _ := t.Commit(gitRepo)
+	return GitObject{
+		ID:        t.ID,
+		Committer: t.Tagger,
+		Signature: t.Signature,
+		Commit:    commit,
+	}
+}
+
+// ParseObjectWithSignature check if signature is good against keystore.
+func ParseObjectWithSignature(ctx context.Context, c *GitObject) *ObjectVerification {
+	var committer *user_model.User
+	if c.Committer != nil {
+		var err error
+		// Find Committer account
+		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
+		if err != nil {                                                    // Skipping not user for committer
+			committer = &user_model.User{
+				Name:  c.Committer.Name,
+				Email: c.Committer.Email,
+			}
+			// We can expect this to often be an ErrUserNotExist. in the case
+			// it is not, however, it is important to log it.
+			if !user_model.IsErrUserNotExist(err) {
+				log.Error("GetUserByEmail: %v", err)
+				return &ObjectVerification{
+					CommittingUser: committer,
+					Verified:       false,
+					Reason:         "gpg.error.no_committer_account",
+				}
+			}
+
+		}
+	}
+
+	// If no signature just report the committer
+	if c.Signature == nil {
+		return &ObjectVerification{
+			CommittingUser: committer,
+			Verified:       false,                         // Default value
+			Reason:         "gpg.error.not_signed_commit", // Default value
+		}
+	}
+
+	// If this a SSH signature handle it differently
+	if strings.HasPrefix(c.Signature.Signature, "-----BEGIN SSH SIGNATURE-----") {
+		return ParseObjectWithSSHSignature(ctx, c, committer)
+	}
+
+	// Parsing signature
+	sig, err := extractSignature(c.Signature.Signature)
+	if err != nil { // Skipping failed to extract sign
+		log.Error("SignatureRead err: %v", err)
+		return &ObjectVerification{
+			CommittingUser: committer,
+			Verified:       false,
+			Reason:         "gpg.error.extract_sign",
+		}
+	}
+
+	keyID := ""
+	if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
+		keyID = fmt.Sprintf("%X", *sig.IssuerKeyId)
+	}
+	if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 {
+		keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20])
+	}
+	defaultReason := NoKeyFound
+
+	// First check if the sig has a keyID and if so just look at that
+	if commitVerification := hashAndVerifyForKeyID(
+		ctx,
+		sig,
+		c.Signature.Payload,
+		committer,
+		keyID,
+		setting.AppName,
+		""); commitVerification != nil {
+		if commitVerification.Reason == BadSignature {
+			defaultReason = BadSignature
+		} else {
+			return commitVerification
+		}
+	}
+
+	// Now try to associate the signature with the committer, if present
+	if committer.ID != 0 {
+		keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{
+			OwnerID: committer.ID,
+		})
+		if err != nil { // Skipping failed to get gpg keys of user
+			log.Error("ListGPGKeys: %v", err)
+			return &ObjectVerification{
+				CommittingUser: committer,
+				Verified:       false,
+				Reason:         "gpg.error.failed_retrieval_gpg_keys",
+			}
+		}
+
+		if err := GPGKeyList(keys).LoadSubKeys(ctx); err != nil {
+			log.Error("LoadSubKeys: %v", err)
+			return &ObjectVerification{
+				CommittingUser: committer,
+				Verified:       false,
+				Reason:         "gpg.error.failed_retrieval_gpg_keys",
+			}
+		}
+
+		committerEmailAddresses, _ := user_model.GetEmailAddresses(ctx, committer.ID)
+		activated := false
+		for _, e := range committerEmailAddresses {
+			if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
+				activated = true
+				break
+			}
+		}
+
+		for _, k := range keys {
+			// Pre-check (& optimization) that emails attached to key can be attached to the committer email and can validate
+			canValidate := false
+			email := ""
+			if k.Verified && activated {
+				canValidate = true
+				email = c.Committer.Email
+			}
+			if !canValidate {
+				for _, e := range k.Emails {
+					if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
+						canValidate = true
+						email = e.Email
+						break
+					}
+				}
+			}
+			if !canValidate {
+				continue // Skip this key
+			}
+
+			commitVerification := hashAndVerifyWithSubKeysObjectVerification(sig, c.Signature.Payload, k, committer, committer, email)
+			if commitVerification != nil {
+				return commitVerification
+			}
+		}
+	}
+
+	if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" {
+		// OK we should try the default key
+		gpgSettings := git.GPGSettings{
+			Sign:  true,
+			KeyID: setting.Repository.Signing.SigningKey,
+			Name:  setting.Repository.Signing.SigningName,
+			Email: setting.Repository.Signing.SigningEmail,
+		}
+		if err := gpgSettings.LoadPublicKeyContent(); err != nil {
+			log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err)
+		} else if commitVerification := verifyWithGPGSettings(ctx, &gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
+			if commitVerification.Reason == BadSignature {
+				defaultReason = BadSignature
+			} else {
+				return commitVerification
+			}
+		}
+	}
+
+	defaultGPGSettings, err := c.Commit.GetRepositoryDefaultPublicGPGKey(false)
+	if err != nil {
+		log.Error("Error getting default public gpg key: %v", err)
+	} else if defaultGPGSettings == nil {
+		log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.Commit.ID.String())
+	} else if defaultGPGSettings.Sign {
+		if commitVerification := verifyWithGPGSettings(ctx, defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
+			if commitVerification.Reason == BadSignature {
+				defaultReason = BadSignature
+			} else {
+				return commitVerification
+			}
+		}
+	}
+
+	return &ObjectVerification{ // Default at this stage
+		CommittingUser: committer,
+		Verified:       false,
+		Warning:        defaultReason != NoKeyFound,
+		Reason:         defaultReason,
+		SigningKey: &GPGKey{
+			KeyID: keyID,
+		},
+	}
+}
+
+func verifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *user_model.User, keyID string) *ObjectVerification {
+	// First try to find the key in the db
+	if commitVerification := hashAndVerifyForKeyID(ctx, sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil {
+		return commitVerification
+	}
+
+	// Otherwise we have to parse the key
+	ekeys, err := checkArmoredGPGKeyString(gpgSettings.PublicKeyContent)
+	if err != nil {
+		log.Error("Unable to get default signing key: %v", err)
+		return &ObjectVerification{
+			CommittingUser: committer,
+			Verified:       false,
+			Reason:         "gpg.error.generate_hash",
+		}
+	}
+	for _, ekey := range ekeys {
+		pubkey := ekey.PrimaryKey
+		content, err := base64EncPubKey(pubkey)
+		if err != nil {
+			return &ObjectVerification{
+				CommittingUser: committer,
+				Verified:       false,
+				Reason:         "gpg.error.generate_hash",
+			}
+		}
+		k := &GPGKey{
+			Content: content,
+			CanSign: pubkey.CanSign(),
+			KeyID:   pubkey.KeyIdString(),
+		}
+		for _, subKey := range ekey.Subkeys {
+			content, err := base64EncPubKey(subKey.PublicKey)
+			if err != nil {
+				return &ObjectVerification{
+					CommittingUser: committer,
+					Verified:       false,
+					Reason:         "gpg.error.generate_hash",
+				}
+			}
+			k.SubsKey = append(k.SubsKey, &GPGKey{
+				Content: content,
+				CanSign: subKey.PublicKey.CanSign(),
+				KeyID:   subKey.PublicKey.KeyIdString(),
+			})
+		}
+		if commitVerification := hashAndVerifyWithSubKeysObjectVerification(sig, payload, k, committer, &user_model.User{
+			Name:  gpgSettings.Name,
+			Email: gpgSettings.Email,
+		}, gpgSettings.Email); commitVerification != nil {
+			return commitVerification
+		}
+		if keyID == k.KeyID {
+			// This is a bad situation ... We have a key id that matches our default key but the signature doesn't match.
+			return &ObjectVerification{
+				CommittingUser: committer,
+				Verified:       false,
+				Warning:        true,
+				Reason:         BadSignature,
+			}
+		}
+	}
+	return nil
+}
+
+func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
+	// Check if key can sign
+	if !k.CanSign {
+		return fmt.Errorf("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 hashAndVerifyWithSubKeysObjectVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *user_model.User, email string) *ObjectVerification {
+	key, err := hashAndVerifyWithSubKeys(sig, payload, k)
+	if err != nil { // Skipping failed to generate hash
+		return &ObjectVerification{
+			CommittingUser: committer,
+			Verified:       false,
+			Reason:         "gpg.error.generate_hash",
+		}
+	}
+
+	if key != nil {
+		return &ObjectVerification{ // Everything is ok
+			CommittingUser: committer,
+			Verified:       true,
+			Reason:         fmt.Sprintf("%s / %s", signer.Name, key.KeyID),
+			SigningUser:    signer,
+			SigningKey:     key,
+			SigningEmail:   email,
+		}
+	}
+	return nil
+}
+
+func hashAndVerifyForKeyID(ctx context.Context, sig *packet.Signature, payload string, committer *user_model.User, keyID, name, email string) *ObjectVerification {
+	if keyID == "" {
+		return nil
+	}
+	keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{
+		KeyID:          keyID,
+		IncludeSubKeys: true,
+	})
+	if err != nil {
+		log.Error("GetGPGKeysByKeyID: %v", err)
+		return &ObjectVerification{
+			CommittingUser: committer,
+			Verified:       false,
+			Reason:         "gpg.error.failed_retrieval_gpg_keys",
+		}
+	}
+	if len(keys) == 0 {
+		return nil
+	}
+	for _, key := range keys {
+		var primaryKeys []*GPGKey
+		if key.PrimaryKeyID != "" {
+			primaryKeys, err = db.Find[GPGKey](ctx, FindGPGKeyOptions{
+				KeyID:          key.PrimaryKeyID,
+				IncludeSubKeys: true,
+			})
+			if err != nil {
+				log.Error("GetGPGKeysByKeyID: %v", err)
+				return &ObjectVerification{
+					CommittingUser: committer,
+					Verified:       false,
+					Reason:         "gpg.error.failed_retrieval_gpg_keys",
+				}
+			}
+		}
+
+		activated, email := checkKeyEmails(ctx, email, append([]*GPGKey{key}, primaryKeys...)...)
+		if !activated {
+			continue
+		}
+
+		signer := &user_model.User{
+			Name:  name,
+			Email: email,
+		}
+		if key.OwnerID != 0 {
+			owner, err := user_model.GetUserByID(ctx, key.OwnerID)
+			if err == nil {
+				signer = owner
+			} else if !user_model.IsErrUserNotExist(err) {
+				log.Error("Failed to user_model.GetUserByID: %d for key ID: %d (%s) %v", key.OwnerID, key.ID, key.KeyID, err)
+				return &ObjectVerification{
+					CommittingUser: committer,
+					Verified:       false,
+					Reason:         "gpg.error.no_committer_account",
+				}
+			}
+		}
+		commitVerification := hashAndVerifyWithSubKeysObjectVerification(sig, payload, key, committer, signer, email)
+		if commitVerification != nil {
+			return commitVerification
+		}
+	}
+	// This is a bad situation ... We have a key id that is in our database but the signature doesn't match.
+	return &ObjectVerification{
+		CommittingUser: committer,
+		Verified:       false,
+		Warning:        true,
+		Reason:         BadSignature,
+	}
+}
+
+// CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository
+// There are several trust models in Gitea
+func CalculateTrustStatus(verification *ObjectVerification, 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
+}
diff --git a/models/asymkey/gpg_key_tag_verification.go b/models/asymkey/gpg_key_tag_verification.go
new file mode 100644
index 0000000000..5fd3983e54
--- /dev/null
+++ b/models/asymkey/gpg_key_tag_verification.go
@@ -0,0 +1,15 @@
+// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+	"context"
+
+	"code.gitea.io/gitea/modules/git"
+)
+
+func ParseTagWithSignature(ctx context.Context, gitRepo *git.Repository, t *git.Tag) *ObjectVerification {
+	o := tagToGitObject(t, gitRepo)
+	return ParseObjectWithSignature(ctx, &o)
+}
diff --git a/models/asymkey/ssh_key_commit_verification.go b/models/asymkey/ssh_key_object_verification.go
similarity index 79%
rename from models/asymkey/ssh_key_commit_verification.go
rename to models/asymkey/ssh_key_object_verification.go
index 2b802710a8..5ad6fdb0a9 100644
--- a/models/asymkey/ssh_key_commit_verification.go
+++ b/models/asymkey/ssh_key_object_verification.go
@@ -11,14 +11,13 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 
 	"github.com/42wim/sshsig"
 )
 
-// ParseCommitWithSSHSignature check if signature is good against keystore.
-func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *user_model.User) *CommitVerification {
+// ParseObjectWithSSHSignature check if signature is good against keystore.
+func ParseObjectWithSSHSignature(ctx context.Context, c *GitObject, committer *user_model.User) *ObjectVerification {
 	// Now try to associate the signature with the committer, if present
 	if committer.ID != 0 {
 		keys, err := db.Find[PublicKey](ctx, FindPublicKeyOptions{
@@ -27,7 +26,7 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *
 		})
 		if err != nil { // Skipping failed to get ssh keys of user
 			log.Error("ListPublicKeys: %v", err)
-			return &CommitVerification{
+			return &ObjectVerification{
 				CommittingUser: committer,
 				Verified:       false,
 				Reason:         "gpg.error.failed_retrieval_gpg_keys",
@@ -55,7 +54,7 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *
 
 		for _, k := range keys {
 			if k.Verified && activated {
-				commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committer, committer, c.Committer.Email)
+				commitVerification := verifySSHObjectVerification(c.Signature.Signature, c.Signature.Payload, k, committer, committer, c.Committer.Email)
 				if commitVerification != nil {
 					return commitVerification
 				}
@@ -63,19 +62,19 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *
 		}
 	}
 
-	return &CommitVerification{
+	return &ObjectVerification{
 		CommittingUser: committer,
 		Verified:       false,
 		Reason:         NoKeyFound,
 	}
 }
 
-func verifySSHCommitVerification(sig, payload string, k *PublicKey, committer, signer *user_model.User, email string) *CommitVerification {
+func verifySSHObjectVerification(sig, payload string, k *PublicKey, committer, signer *user_model.User, email string) *ObjectVerification {
 	if err := sshsig.Verify(bytes.NewBuffer([]byte(payload)), []byte(sig), []byte(k.Content), "git"); err != nil {
 		return nil
 	}
 
-	return &CommitVerification{ // Everything is ok
+	return &ObjectVerification{ // Everything is ok
 		CommittingUser: committer,
 		Verified:       true,
 		Reason:         fmt.Sprintf("%s / %s", signer.Name, k.Fingerprint),
diff --git a/models/asymkey/ssh_key_commit_verification_test.go b/models/asymkey/ssh_key_object_verification_test.go
similarity index 85%
rename from models/asymkey/ssh_key_commit_verification_test.go
rename to models/asymkey/ssh_key_object_verification_test.go
index 320e114b3d..4e229c9b13 100644
--- a/models/asymkey/ssh_key_commit_verification_test.go
+++ b/models/asymkey/ssh_key_object_verification_test.go
@@ -22,7 +22,8 @@ func TestParseCommitWithSSHSignature(t *testing.T) {
 	sshKey := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1000, OwnerID: 2})
 
 	t.Run("No commiter", func(t *testing.T) {
-		commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, &git.Commit{}, &user_model.User{})
+		o := commitToGitObject(&git.Commit{})
+		commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, &user_model.User{})
 		assert.False(t, commitVerification.Verified)
 		assert.Equal(t, NoKeyFound, commitVerification.Reason)
 	})
@@ -30,7 +31,8 @@ func TestParseCommitWithSSHSignature(t *testing.T) {
 	t.Run("Commiter without keys", func(t *testing.T) {
 		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 
-		commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, &git.Commit{Committer: &git.Signature{Email: user.Email}}, user)
+		o := commitToGitObject(&git.Commit{Committer: &git.Signature{Email: user.Email}})
+		commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user)
 		assert.False(t, commitVerification.Verified)
 		assert.Equal(t, NoKeyFound, commitVerification.Reason)
 	})
@@ -57,7 +59,8 @@ AAAAQIMufOuSjZeDUujrkVK4sl7ICa0WwEftas8UAYxx0Thdkiw2qWjR1U1PKfTLm16/w8
 `,
 			},
 		}
-		commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
+		o := commitToGitObject(gitCommit)
+		commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user2)
 		assert.False(t, commitVerification.Verified)
 		assert.Equal(t, NoKeyFound, commitVerification.Reason)
 	})
@@ -79,7 +82,8 @@ Add content
 			},
 		}
 
-		commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
+		o := commitToGitObject(gitCommit)
+		commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user2)
 		assert.False(t, commitVerification.Verified)
 		assert.Equal(t, NoKeyFound, commitVerification.Reason)
 	})
@@ -107,7 +111,8 @@ fs9cMpZVM9BfIKNUSO8QY=
 			},
 		}
 
-		commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
+		o := commitToGitObject(gitCommit)
+		commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user2)
 		assert.True(t, commitVerification.Verified)
 		assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason)
 		assert.Equal(t, sshKey, commitVerification.SigningSSHKey)
@@ -138,7 +143,8 @@ muPLbvEduU+Ze/1Ol1pgk=
 			},
 		}
 
-		commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
+		o := commitToGitObject(gitCommit)
+		commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user2)
 		assert.True(t, commitVerification.Verified)
 		assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason)
 		assert.Equal(t, sshKey, commitVerification.SigningSSHKey)
diff --git a/modules/gitgraph/graph_models.go b/modules/gitgraph/graph_models.go
index e48fef8b9d..e9c1001964 100644
--- a/modules/gitgraph/graph_models.go
+++ b/modules/gitgraph/graph_models.go
@@ -238,7 +238,7 @@ func newRefsFromRefNames(refNames []byte) []git.Reference {
 type Commit struct {
 	Commit       *git.Commit
 	User         *user_model.User
-	Verification *asymkey_model.CommitVerification
+	Verification *asymkey_model.ObjectVerification
 	Status       *git_model.CommitStatus
 	Flow         int64
 	Row          int