1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-10 04:27:22 +00:00

Improve instance wide ssh commit signing (#34341)

* Signed SSH commits can look in the UI like on GitHub, just like gpg keys today in Gitea
* SSH format can be added in gitea config
* SSH Signing worked before with DEFAULT_TRUST_MODEL=committer

`TRUSTED_SSH_KEYS` can be a list of additional ssh public key contents
to trust for every user of this instance

Closes #34329
Related #31392

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
ChristopherHX
2025-06-11 12:32:55 +02:00
committed by GitHub
parent fbc3796f9e
commit c9505a26b9
22 changed files with 469 additions and 124 deletions

View File

@ -6,6 +6,7 @@ package asymkey
import (
"context"
"fmt"
"slices"
"strings"
asymkey_model "code.gitea.io/gitea/models/asymkey"
@ -359,24 +360,39 @@ func VerifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, si
return nil
}
func verifySSHCommitVerificationByInstanceKey(c *git.Commit, committerUser, signerUser *user_model.User, committerGitEmail, publicKeyContent string) *asymkey_model.CommitVerification {
fingerprint, err := asymkey_model.CalcFingerprint(publicKeyContent)
if err != nil {
log.Error("Error calculating the fingerprint public key %q, err: %v", publicKeyContent, err)
return nil
}
sshPubKey := &asymkey_model.PublicKey{
Verified: true,
Content: publicKeyContent,
Fingerprint: fingerprint,
HasUsed: true,
}
return verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, sshPubKey, committerUser, signerUser, committerGitEmail)
}
// ParseCommitWithSSHSignature check if signature is good against keystore.
func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *user_model.User) *asymkey_model.CommitVerification {
func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committerUser *user_model.User) *asymkey_model.CommitVerification {
// Now try to associate the signature with the committer, if present
if committer.ID != 0 {
if committerUser.ID != 0 {
keys, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
OwnerID: committer.ID,
OwnerID: committerUser.ID,
NotKeytype: asymkey_model.KeyTypePrincipal,
})
if err != nil { // Skipping failed to get ssh keys of user
log.Error("ListPublicKeys: %v", err)
return &asymkey_model.CommitVerification{
CommittingUser: committer,
CommittingUser: committerUser,
Verified: false,
Reason: "gpg.error.failed_retrieval_gpg_keys",
}
}
committerEmailAddresses, err := cache.GetWithContextCache(ctx, cachegroup.UserEmailAddresses, committer.ID, user_model.GetEmailAddresses)
committerEmailAddresses, err := cache.GetWithContextCache(ctx, cachegroup.UserEmailAddresses, committerUser.ID, user_model.GetEmailAddresses)
if err != nil {
log.Error("GetEmailAddresses: %v", err)
}
@ -391,7 +407,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 := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committerUser, committerUser, c.Committer.Email)
if commitVerification != nil {
return commitVerification
}
@ -399,8 +415,45 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *
}
}
// Try the pre-set trusted keys (for key-rotation purpose)
// At the moment, we still use the SigningName&SigningEmail for the rotated keys.
// Maybe in the future we can extend the key format to "ssh-xxx .... old-user@example.com" to support different signer emails.
for _, k := range setting.Repository.Signing.TrustedSSHKeys {
signerUser := &user_model.User{
Name: setting.Repository.Signing.SigningName,
Email: setting.Repository.Signing.SigningEmail,
}
commitVerification := verifySSHCommitVerificationByInstanceKey(c, committerUser, signerUser, c.Committer.Email, k)
if commitVerification != nil && commitVerification.Verified {
return commitVerification
}
}
// Try the configured instance-wide SSH public key
if setting.Repository.Signing.SigningFormat == git.SigningKeyFormatSSH && !slices.Contains([]string{"", "default", "none"}, setting.Repository.Signing.SigningKey) {
gpgSettings := git.GPGSettings{
Sign: true,
KeyID: setting.Repository.Signing.SigningKey,
Name: setting.Repository.Signing.SigningName,
Email: setting.Repository.Signing.SigningEmail,
Format: setting.Repository.Signing.SigningFormat,
}
signerUser := &user_model.User{
Name: gpgSettings.Name,
Email: gpgSettings.Email,
}
if err := gpgSettings.LoadPublicKeyContent(); err != nil {
log.Error("Error getting instance-wide SSH signing key %q, err: %v", gpgSettings.KeyID, err)
} else {
commitVerification := verifySSHCommitVerificationByInstanceKey(c, committerUser, signerUser, gpgSettings.Email, gpgSettings.PublicKeyContent)
if commitVerification != nil && commitVerification.Verified {
return commitVerification
}
}
}
return &asymkey_model.CommitVerification{
CommittingUser: committer,
CommittingUser: committerUser,
Verified: false,
Reason: asymkey_model.NoKeyFound,
}