loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'Highlight signed tags like signed commits' (#2534) from algernon/forgejo:message-in-a-bottle-ctrl-w-tag into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2534

+861 -503
+6 -486
models/asymkey/gpg_key_commit_verification.go
··· 5 5 6 6 import ( 7 7 "context" 8 - "fmt" 9 - "hash" 10 - "strings" 11 8 12 - "code.gitea.io/gitea/models/db" 13 9 repo_model "code.gitea.io/gitea/models/repo" 14 10 user_model "code.gitea.io/gitea/models/user" 15 11 "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 12 ) 21 13 22 14 // __________________ ________ ____ __. ··· 40 32 41 33 // This file provides functions relating commit verification 42 34 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 35 // SignCommit represents a commit with validation of signature. 57 36 type SignCommit struct { 58 - Verification *CommitVerification 37 + Verification *ObjectVerification 59 38 *user_model.UserCommit 60 39 } 61 40 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 41 // ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys. 74 42 func ParseCommitsWithSignature(ctx context.Context, oldCommits []*user_model.UserCommit, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error)) []*SignCommit { 75 43 newCommits := make([]*SignCommit, 0, len(oldCommits)) 76 44 keyMap := map[string]bool{} 77 45 78 46 for _, c := range oldCommits { 47 + o := commitToGitObject(c.Commit) 79 48 signCommit := &SignCommit{ 80 49 UserCommit: c, 81 - Verification: ParseCommitWithSignature(ctx, c.Commit), 50 + Verification: ParseObjectWithSignature(ctx, &o), 82 51 } 83 52 84 53 _ = CalculateTrustStatus(signCommit.Verification, repoTrustModel, isOwnerMemberCollaborator, &keyMap) ··· 88 57 return newCommits 89 58 } 90 59 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 - 117 - // If no signature just report the committer 118 - if c.Signature == nil { 119 - return &CommitVerification{ 120 - CommittingUser: committer, 121 - Verified: false, // Default value 122 - Reason: "gpg.error.not_signed_commit", // Default value 123 - } 124 - } 125 - 126 - // If this a SSH signature handle it differently 127 - if strings.HasPrefix(c.Signature.Signature, "-----BEGIN SSH SIGNATURE-----") { 128 - return ParseCommitWithSSHSignature(ctx, c, committer) 129 - } 130 - 131 - // Parsing signature 132 - sig, err := extractSignature(c.Signature.Signature) 133 - if err != nil { // Skipping failed to extract sign 134 - log.Error("SignatureRead err: %v", err) 135 - return &CommitVerification{ 136 - CommittingUser: committer, 137 - Verified: false, 138 - Reason: "gpg.error.extract_sign", 139 - } 140 - } 141 - 142 - keyID := "" 143 - if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 { 144 - keyID = fmt.Sprintf("%X", *sig.IssuerKeyId) 145 - } 146 - if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 { 147 - keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20]) 148 - } 149 - defaultReason := NoKeyFound 150 - 151 - // First check if the sig has a keyID and if so just look at that 152 - if commitVerification := hashAndVerifyForKeyID( 153 - ctx, 154 - sig, 155 - c.Signature.Payload, 156 - committer, 157 - keyID, 158 - setting.AppName, 159 - ""); commitVerification != nil { 160 - if commitVerification.Reason == BadSignature { 161 - defaultReason = BadSignature 162 - } else { 163 - return commitVerification 164 - } 165 - } 166 - 167 - // Now try to associate the signature with the committer, if present 168 - if committer.ID != 0 { 169 - keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{ 170 - OwnerID: committer.ID, 171 - }) 172 - if err != nil { // Skipping failed to get gpg keys of user 173 - log.Error("ListGPGKeys: %v", err) 174 - return &CommitVerification{ 175 - CommittingUser: committer, 176 - Verified: false, 177 - Reason: "gpg.error.failed_retrieval_gpg_keys", 178 - } 179 - } 180 - 181 - if err := GPGKeyList(keys).LoadSubKeys(ctx); err != nil { 182 - log.Error("LoadSubKeys: %v", err) 183 - return &CommitVerification{ 184 - CommittingUser: committer, 185 - Verified: false, 186 - Reason: "gpg.error.failed_retrieval_gpg_keys", 187 - } 188 - } 189 - 190 - committerEmailAddresses, _ := user_model.GetEmailAddresses(ctx, committer.ID) 191 - activated := false 192 - for _, e := range committerEmailAddresses { 193 - if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { 194 - activated = true 195 - break 196 - } 197 - } 198 - 199 - for _, k := range keys { 200 - // Pre-check (& optimization) that emails attached to key can be attached to the committer email and can validate 201 - canValidate := false 202 - email := "" 203 - if k.Verified && activated { 204 - canValidate = true 205 - email = c.Committer.Email 206 - } 207 - if !canValidate { 208 - for _, e := range k.Emails { 209 - if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { 210 - canValidate = true 211 - email = e.Email 212 - break 213 - } 214 - } 215 - } 216 - if !canValidate { 217 - continue // Skip this key 218 - } 219 - 220 - commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, c.Signature.Payload, k, committer, committer, email) 221 - if commitVerification != nil { 222 - return commitVerification 223 - } 224 - } 225 - } 226 - 227 - if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" { 228 - // OK we should try the default key 229 - gpgSettings := git.GPGSettings{ 230 - Sign: true, 231 - KeyID: setting.Repository.Signing.SigningKey, 232 - Name: setting.Repository.Signing.SigningName, 233 - Email: setting.Repository.Signing.SigningEmail, 234 - } 235 - if err := gpgSettings.LoadPublicKeyContent(); err != nil { 236 - log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err) 237 - } else if commitVerification := verifyWithGPGSettings(ctx, &gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { 238 - if commitVerification.Reason == BadSignature { 239 - defaultReason = BadSignature 240 - } else { 241 - return commitVerification 242 - } 243 - } 244 - } 245 - 246 - defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false) 247 - if err != nil { 248 - log.Error("Error getting default public gpg key: %v", err) 249 - } else if defaultGPGSettings == nil { 250 - log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String()) 251 - } else if defaultGPGSettings.Sign { 252 - if commitVerification := verifyWithGPGSettings(ctx, defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { 253 - if commitVerification.Reason == BadSignature { 254 - defaultReason = BadSignature 255 - } else { 256 - return commitVerification 257 - } 258 - } 259 - } 260 - 261 - return &CommitVerification{ // Default at this stage 262 - CommittingUser: committer, 263 - Verified: false, 264 - Warning: defaultReason != NoKeyFound, 265 - Reason: defaultReason, 266 - SigningKey: &GPGKey{ 267 - KeyID: keyID, 268 - }, 269 - } 270 - } 271 - 272 - func verifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *user_model.User, keyID string) *CommitVerification { 273 - // First try to find the key in the db 274 - if commitVerification := hashAndVerifyForKeyID(ctx, sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil { 275 - return commitVerification 276 - } 277 - 278 - // Otherwise we have to parse the key 279 - ekeys, err := checkArmoredGPGKeyString(gpgSettings.PublicKeyContent) 280 - if err != nil { 281 - log.Error("Unable to get default signing key: %v", err) 282 - return &CommitVerification{ 283 - CommittingUser: committer, 284 - Verified: false, 285 - Reason: "gpg.error.generate_hash", 286 - } 287 - } 288 - for _, ekey := range ekeys { 289 - pubkey := ekey.PrimaryKey 290 - content, err := base64EncPubKey(pubkey) 291 - if err != nil { 292 - return &CommitVerification{ 293 - CommittingUser: committer, 294 - Verified: false, 295 - Reason: "gpg.error.generate_hash", 296 - } 297 - } 298 - k := &GPGKey{ 299 - Content: content, 300 - CanSign: pubkey.CanSign(), 301 - KeyID: pubkey.KeyIdString(), 302 - } 303 - for _, subKey := range ekey.Subkeys { 304 - content, err := base64EncPubKey(subKey.PublicKey) 305 - if err != nil { 306 - return &CommitVerification{ 307 - CommittingUser: committer, 308 - Verified: false, 309 - Reason: "gpg.error.generate_hash", 310 - } 311 - } 312 - k.SubsKey = append(k.SubsKey, &GPGKey{ 313 - Content: content, 314 - CanSign: subKey.PublicKey.CanSign(), 315 - KeyID: subKey.PublicKey.KeyIdString(), 316 - }) 317 - } 318 - if commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, k, committer, &user_model.User{ 319 - Name: gpgSettings.Name, 320 - Email: gpgSettings.Email, 321 - }, gpgSettings.Email); commitVerification != nil { 322 - return commitVerification 323 - } 324 - if keyID == k.KeyID { 325 - // This is a bad situation ... We have a key id that matches our default key but the signature doesn't match. 326 - return &CommitVerification{ 327 - CommittingUser: committer, 328 - Verified: false, 329 - Warning: true, 330 - Reason: BadSignature, 331 - } 332 - } 333 - } 334 - return nil 335 - } 336 - 337 - func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error { 338 - // Check if key can sign 339 - if !k.CanSign { 340 - return fmt.Errorf("key can not sign") 341 - } 342 - // Decode key 343 - pkey, err := base64DecPubKey(k.Content) 344 - if err != nil { 345 - return err 346 - } 347 - return pkey.VerifySignature(h, s) 348 - } 349 - 350 - func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) { 351 - // Generating hash of commit 352 - hash, err := populateHash(sig.Hash, []byte(payload)) 353 - if err != nil { // Skipping as failed to generate hash 354 - log.Error("PopulateHash: %v", err) 355 - return nil, err 356 - } 357 - // We will ignore errors in verification as they don't need to be propagated up 358 - err = verifySign(sig, hash, k) 359 - if err != nil { 360 - return nil, nil 361 - } 362 - return k, nil 363 - } 364 - 365 - func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) { 366 - verified, err := hashAndVerify(sig, payload, k) 367 - if err != nil || verified != nil { 368 - return verified, err 369 - } 370 - for _, sk := range k.SubsKey { 371 - verified, err := hashAndVerify(sig, payload, sk) 372 - if err != nil || verified != nil { 373 - return verified, err 374 - } 375 - } 376 - return nil, nil 377 - } 378 - 379 - func hashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *user_model.User, email string) *CommitVerification { 380 - key, err := hashAndVerifyWithSubKeys(sig, payload, k) 381 - if err != nil { // Skipping failed to generate hash 382 - return &CommitVerification{ 383 - CommittingUser: committer, 384 - Verified: false, 385 - Reason: "gpg.error.generate_hash", 386 - } 387 - } 388 - 389 - if key != nil { 390 - return &CommitVerification{ // Everything is ok 391 - CommittingUser: committer, 392 - Verified: true, 393 - Reason: fmt.Sprintf("%s / %s", signer.Name, key.KeyID), 394 - SigningUser: signer, 395 - SigningKey: key, 396 - SigningEmail: email, 397 - } 398 - } 399 - return nil 400 - } 401 - 402 - func hashAndVerifyForKeyID(ctx context.Context, sig *packet.Signature, payload string, committer *user_model.User, keyID, name, email string) *CommitVerification { 403 - if keyID == "" { 404 - return nil 405 - } 406 - keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{ 407 - KeyID: keyID, 408 - IncludeSubKeys: true, 409 - }) 410 - if err != nil { 411 - log.Error("GetGPGKeysByKeyID: %v", err) 412 - return &CommitVerification{ 413 - CommittingUser: committer, 414 - Verified: false, 415 - Reason: "gpg.error.failed_retrieval_gpg_keys", 416 - } 417 - } 418 - if len(keys) == 0 { 419 - return nil 420 - } 421 - for _, key := range keys { 422 - var primaryKeys []*GPGKey 423 - if key.PrimaryKeyID != "" { 424 - primaryKeys, err = db.Find[GPGKey](ctx, FindGPGKeyOptions{ 425 - KeyID: key.PrimaryKeyID, 426 - IncludeSubKeys: true, 427 - }) 428 - if err != nil { 429 - log.Error("GetGPGKeysByKeyID: %v", err) 430 - return &CommitVerification{ 431 - CommittingUser: committer, 432 - Verified: false, 433 - Reason: "gpg.error.failed_retrieval_gpg_keys", 434 - } 435 - } 436 - } 437 - 438 - activated, email := checkKeyEmails(ctx, email, append([]*GPGKey{key}, primaryKeys...)...) 439 - if !activated { 440 - continue 441 - } 442 - 443 - signer := &user_model.User{ 444 - Name: name, 445 - Email: email, 446 - } 447 - if key.OwnerID != 0 { 448 - owner, err := user_model.GetUserByID(ctx, key.OwnerID) 449 - if err == nil { 450 - signer = owner 451 - } else if !user_model.IsErrUserNotExist(err) { 452 - log.Error("Failed to user_model.GetUserByID: %d for key ID: %d (%s) %v", key.OwnerID, key.ID, key.KeyID, err) 453 - return &CommitVerification{ 454 - CommittingUser: committer, 455 - Verified: false, 456 - Reason: "gpg.error.no_committer_account", 457 - } 458 - } 459 - } 460 - commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, key, committer, signer, email) 461 - if commitVerification != nil { 462 - return commitVerification 463 - } 464 - } 465 - // This is a bad situation ... We have a key id that is in our database but the signature doesn't match. 466 - return &CommitVerification{ 467 - CommittingUser: committer, 468 - Verified: false, 469 - Warning: true, 470 - Reason: BadSignature, 471 - } 472 - } 473 - 474 - // CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository 475 - // There are several trust models in Gitea 476 - func CalculateTrustStatus(verification *CommitVerification, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error), keyMap *map[string]bool) error { 477 - if !verification.Verified { 478 - return nil 479 - } 480 - 481 - // In the Committer trust model a signature is trusted if it matches the committer 482 - // - it doesn't matter if they're a collaborator, the owner, Gitea or Github 483 - // NB: This model is commit verification only 484 - if repoTrustModel == repo_model.CommitterTrustModel { 485 - // default to "unmatched" 486 - verification.TrustStatus = "unmatched" 487 - 488 - // We can only verify against users in our database but the default key will match 489 - // against by email if it is not in the db. 490 - if (verification.SigningUser.ID != 0 && 491 - verification.CommittingUser.ID == verification.SigningUser.ID) || 492 - (verification.SigningUser.ID == 0 && verification.CommittingUser.ID == 0 && 493 - verification.SigningUser.Email == verification.CommittingUser.Email) { 494 - verification.TrustStatus = "trusted" 495 - } 496 - return nil 497 - } 498 - 499 - // Now we drop to the more nuanced trust models... 500 - verification.TrustStatus = "trusted" 501 - 502 - if verification.SigningUser.ID == 0 { 503 - // This commit is signed by the default key - but this key is not assigned to a user in the DB. 504 - 505 - // However in the repo_model.CollaboratorCommitterTrustModel we cannot mark this as trusted 506 - // unless the default key matches the email of a non-user. 507 - if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && (verification.CommittingUser.ID != 0 || 508 - verification.SigningUser.Email != verification.CommittingUser.Email) { 509 - verification.TrustStatus = "untrusted" 510 - } 511 - return nil 512 - } 513 - 514 - // Check we actually have a GPG SigningKey 515 - var err error 516 - if verification.SigningKey != nil { 517 - var isMember bool 518 - if keyMap != nil { 519 - var has bool 520 - isMember, has = (*keyMap)[verification.SigningKey.KeyID] 521 - if !has { 522 - isMember, err = isOwnerMemberCollaborator(verification.SigningUser) 523 - (*keyMap)[verification.SigningKey.KeyID] = isMember 524 - } 525 - } else { 526 - isMember, err = isOwnerMemberCollaborator(verification.SigningUser) 527 - } 528 - 529 - if !isMember { 530 - verification.TrustStatus = "untrusted" 531 - if verification.CommittingUser.ID != verification.SigningUser.ID { 532 - // The committing user and the signing user are not the same 533 - // This should be marked as questionable unless the signing user is a collaborator/team member etc. 534 - verification.TrustStatus = "unmatched" 535 - } 536 - } else if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID { 537 - // The committing user and the signing user are not the same and our trustmodel states that they must match 538 - verification.TrustStatus = "unmatched" 539 - } 540 - } 541 - 542 - return err 60 + func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *ObjectVerification { 61 + o := commitToGitObject(c) 62 + return ParseObjectWithSignature(ctx, &o) 543 63 }
+527
models/asymkey/gpg_key_object_verification.go
··· 1 + // Copyright 2021 The Gitea Authors. All rights reserved. 2 + // Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. 3 + // SPDX-License-Identifier: MIT 4 + 5 + package asymkey 6 + 7 + import ( 8 + "context" 9 + "fmt" 10 + "hash" 11 + "strings" 12 + 13 + "code.gitea.io/gitea/models/db" 14 + repo_model "code.gitea.io/gitea/models/repo" 15 + user_model "code.gitea.io/gitea/models/user" 16 + "code.gitea.io/gitea/modules/git" 17 + "code.gitea.io/gitea/modules/log" 18 + "code.gitea.io/gitea/modules/setting" 19 + 20 + "github.com/keybase/go-crypto/openpgp/packet" 21 + ) 22 + 23 + // This file provides functions related to object (commit, tag) verification 24 + 25 + // ObjectVerification represents a commit validation of signature 26 + type ObjectVerification struct { 27 + Verified bool 28 + Warning bool 29 + Reason string 30 + SigningUser *user_model.User 31 + CommittingUser *user_model.User 32 + SigningEmail string 33 + SigningKey *GPGKey 34 + SigningSSHKey *PublicKey 35 + TrustStatus string 36 + } 37 + 38 + const ( 39 + // BadSignature is used as the reason when the signature has a KeyID that is in the db 40 + // but no key that has that ID verifies the signature. This is a suspicious failure. 41 + BadSignature = "gpg.error.probable_bad_signature" 42 + // BadDefaultSignature is used as the reason when the signature has a KeyID that matches the 43 + // default Key but is not verified by the default key. This is a suspicious failure. 44 + BadDefaultSignature = "gpg.error.probable_bad_default_signature" 45 + // NoKeyFound is used as the reason when no key can be found to verify the signature. 46 + NoKeyFound = "gpg.error.no_gpg_keys_found" 47 + ) 48 + 49 + type GitObject struct { 50 + ID git.ObjectID 51 + Committer *git.Signature 52 + Signature *git.ObjectSignature 53 + Commit *git.Commit 54 + } 55 + 56 + func commitToGitObject(c *git.Commit) GitObject { 57 + return GitObject{ 58 + ID: c.ID, 59 + Committer: c.Committer, 60 + Signature: c.Signature, 61 + Commit: c, 62 + } 63 + } 64 + 65 + func tagToGitObject(t *git.Tag, gitRepo *git.Repository) GitObject { 66 + commit, _ := t.Commit(gitRepo) 67 + return GitObject{ 68 + ID: t.ID, 69 + Committer: t.Tagger, 70 + Signature: t.Signature, 71 + Commit: commit, 72 + } 73 + } 74 + 75 + // ParseObjectWithSignature check if signature is good against keystore. 76 + func ParseObjectWithSignature(ctx context.Context, c *GitObject) *ObjectVerification { 77 + var committer *user_model.User 78 + if c.Committer != nil { 79 + var err error 80 + // Find Committer account 81 + 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 82 + if err != nil { // Skipping not user for committer 83 + committer = &user_model.User{ 84 + Name: c.Committer.Name, 85 + Email: c.Committer.Email, 86 + } 87 + // We can expect this to often be an ErrUserNotExist. in the case 88 + // it is not, however, it is important to log it. 89 + if !user_model.IsErrUserNotExist(err) { 90 + log.Error("GetUserByEmail: %v", err) 91 + return &ObjectVerification{ 92 + CommittingUser: committer, 93 + Verified: false, 94 + Reason: "gpg.error.no_committer_account", 95 + } 96 + } 97 + 98 + } 99 + } 100 + 101 + // If no signature just report the committer 102 + if c.Signature == nil { 103 + return &ObjectVerification{ 104 + CommittingUser: committer, 105 + Verified: false, // Default value 106 + Reason: "gpg.error.not_signed_commit", // Default value 107 + } 108 + } 109 + 110 + // If this a SSH signature handle it differently 111 + if strings.HasPrefix(c.Signature.Signature, "-----BEGIN SSH SIGNATURE-----") { 112 + return ParseObjectWithSSHSignature(ctx, c, committer) 113 + } 114 + 115 + // Parsing signature 116 + sig, err := extractSignature(c.Signature.Signature) 117 + if err != nil { // Skipping failed to extract sign 118 + log.Error("SignatureRead err: %v", err) 119 + return &ObjectVerification{ 120 + CommittingUser: committer, 121 + Verified: false, 122 + Reason: "gpg.error.extract_sign", 123 + } 124 + } 125 + 126 + keyID := "" 127 + if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 { 128 + keyID = fmt.Sprintf("%X", *sig.IssuerKeyId) 129 + } 130 + if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 { 131 + keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20]) 132 + } 133 + defaultReason := NoKeyFound 134 + 135 + // First check if the sig has a keyID and if so just look at that 136 + if commitVerification := hashAndVerifyForKeyID( 137 + ctx, 138 + sig, 139 + c.Signature.Payload, 140 + committer, 141 + keyID, 142 + setting.AppName, 143 + ""); commitVerification != nil { 144 + if commitVerification.Reason == BadSignature { 145 + defaultReason = BadSignature 146 + } else { 147 + return commitVerification 148 + } 149 + } 150 + 151 + // Now try to associate the signature with the committer, if present 152 + if committer.ID != 0 { 153 + keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{ 154 + OwnerID: committer.ID, 155 + }) 156 + if err != nil { // Skipping failed to get gpg keys of user 157 + log.Error("ListGPGKeys: %v", err) 158 + return &ObjectVerification{ 159 + CommittingUser: committer, 160 + Verified: false, 161 + Reason: "gpg.error.failed_retrieval_gpg_keys", 162 + } 163 + } 164 + 165 + if err := GPGKeyList(keys).LoadSubKeys(ctx); err != nil { 166 + log.Error("LoadSubKeys: %v", err) 167 + return &ObjectVerification{ 168 + CommittingUser: committer, 169 + Verified: false, 170 + Reason: "gpg.error.failed_retrieval_gpg_keys", 171 + } 172 + } 173 + 174 + committerEmailAddresses, _ := user_model.GetEmailAddresses(ctx, committer.ID) 175 + activated := false 176 + for _, e := range committerEmailAddresses { 177 + if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { 178 + activated = true 179 + break 180 + } 181 + } 182 + 183 + for _, k := range keys { 184 + // Pre-check (& optimization) that emails attached to key can be attached to the committer email and can validate 185 + canValidate := false 186 + email := "" 187 + if k.Verified && activated { 188 + canValidate = true 189 + email = c.Committer.Email 190 + } 191 + if !canValidate { 192 + for _, e := range k.Emails { 193 + if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { 194 + canValidate = true 195 + email = e.Email 196 + break 197 + } 198 + } 199 + } 200 + if !canValidate { 201 + continue // Skip this key 202 + } 203 + 204 + commitVerification := hashAndVerifyWithSubKeysObjectVerification(sig, c.Signature.Payload, k, committer, committer, email) 205 + if commitVerification != nil { 206 + return commitVerification 207 + } 208 + } 209 + } 210 + 211 + if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" { 212 + // OK we should try the default key 213 + gpgSettings := git.GPGSettings{ 214 + Sign: true, 215 + KeyID: setting.Repository.Signing.SigningKey, 216 + Name: setting.Repository.Signing.SigningName, 217 + Email: setting.Repository.Signing.SigningEmail, 218 + } 219 + if err := gpgSettings.LoadPublicKeyContent(); err != nil { 220 + log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err) 221 + } else if commitVerification := verifyWithGPGSettings(ctx, &gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { 222 + if commitVerification.Reason == BadSignature { 223 + defaultReason = BadSignature 224 + } else { 225 + return commitVerification 226 + } 227 + } 228 + } 229 + 230 + defaultGPGSettings, err := c.Commit.GetRepositoryDefaultPublicGPGKey(false) 231 + if err != nil { 232 + log.Error("Error getting default public gpg key: %v", err) 233 + } else if defaultGPGSettings == nil { 234 + log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.Commit.ID.String()) 235 + } else if defaultGPGSettings.Sign { 236 + if commitVerification := verifyWithGPGSettings(ctx, defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { 237 + if commitVerification.Reason == BadSignature { 238 + defaultReason = BadSignature 239 + } else { 240 + return commitVerification 241 + } 242 + } 243 + } 244 + 245 + return &ObjectVerification{ // Default at this stage 246 + CommittingUser: committer, 247 + Verified: false, 248 + Warning: defaultReason != NoKeyFound, 249 + Reason: defaultReason, 250 + SigningKey: &GPGKey{ 251 + KeyID: keyID, 252 + }, 253 + } 254 + } 255 + 256 + func verifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *user_model.User, keyID string) *ObjectVerification { 257 + // First try to find the key in the db 258 + if commitVerification := hashAndVerifyForKeyID(ctx, sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil { 259 + return commitVerification 260 + } 261 + 262 + // Otherwise we have to parse the key 263 + ekeys, err := checkArmoredGPGKeyString(gpgSettings.PublicKeyContent) 264 + if err != nil { 265 + log.Error("Unable to get default signing key: %v", err) 266 + return &ObjectVerification{ 267 + CommittingUser: committer, 268 + Verified: false, 269 + Reason: "gpg.error.generate_hash", 270 + } 271 + } 272 + for _, ekey := range ekeys { 273 + pubkey := ekey.PrimaryKey 274 + content, err := base64EncPubKey(pubkey) 275 + if err != nil { 276 + return &ObjectVerification{ 277 + CommittingUser: committer, 278 + Verified: false, 279 + Reason: "gpg.error.generate_hash", 280 + } 281 + } 282 + k := &GPGKey{ 283 + Content: content, 284 + CanSign: pubkey.CanSign(), 285 + KeyID: pubkey.KeyIdString(), 286 + } 287 + for _, subKey := range ekey.Subkeys { 288 + content, err := base64EncPubKey(subKey.PublicKey) 289 + if err != nil { 290 + return &ObjectVerification{ 291 + CommittingUser: committer, 292 + Verified: false, 293 + Reason: "gpg.error.generate_hash", 294 + } 295 + } 296 + k.SubsKey = append(k.SubsKey, &GPGKey{ 297 + Content: content, 298 + CanSign: subKey.PublicKey.CanSign(), 299 + KeyID: subKey.PublicKey.KeyIdString(), 300 + }) 301 + } 302 + if commitVerification := hashAndVerifyWithSubKeysObjectVerification(sig, payload, k, committer, &user_model.User{ 303 + Name: gpgSettings.Name, 304 + Email: gpgSettings.Email, 305 + }, gpgSettings.Email); commitVerification != nil { 306 + return commitVerification 307 + } 308 + if keyID == k.KeyID { 309 + // This is a bad situation ... We have a key id that matches our default key but the signature doesn't match. 310 + return &ObjectVerification{ 311 + CommittingUser: committer, 312 + Verified: false, 313 + Warning: true, 314 + Reason: BadSignature, 315 + } 316 + } 317 + } 318 + return nil 319 + } 320 + 321 + func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error { 322 + // Check if key can sign 323 + if !k.CanSign { 324 + return fmt.Errorf("key can not sign") 325 + } 326 + // Decode key 327 + pkey, err := base64DecPubKey(k.Content) 328 + if err != nil { 329 + return err 330 + } 331 + return pkey.VerifySignature(h, s) 332 + } 333 + 334 + func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) { 335 + // Generating hash of commit 336 + hash, err := populateHash(sig.Hash, []byte(payload)) 337 + if err != nil { // Skipping as failed to generate hash 338 + log.Error("PopulateHash: %v", err) 339 + return nil, err 340 + } 341 + // We will ignore errors in verification as they don't need to be propagated up 342 + err = verifySign(sig, hash, k) 343 + if err != nil { 344 + return nil, nil 345 + } 346 + return k, nil 347 + } 348 + 349 + func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) { 350 + verified, err := hashAndVerify(sig, payload, k) 351 + if err != nil || verified != nil { 352 + return verified, err 353 + } 354 + for _, sk := range k.SubsKey { 355 + verified, err := hashAndVerify(sig, payload, sk) 356 + if err != nil || verified != nil { 357 + return verified, err 358 + } 359 + } 360 + return nil, nil 361 + } 362 + 363 + func hashAndVerifyWithSubKeysObjectVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *user_model.User, email string) *ObjectVerification { 364 + key, err := hashAndVerifyWithSubKeys(sig, payload, k) 365 + if err != nil { // Skipping failed to generate hash 366 + return &ObjectVerification{ 367 + CommittingUser: committer, 368 + Verified: false, 369 + Reason: "gpg.error.generate_hash", 370 + } 371 + } 372 + 373 + if key != nil { 374 + return &ObjectVerification{ // Everything is ok 375 + CommittingUser: committer, 376 + Verified: true, 377 + Reason: fmt.Sprintf("%s / %s", signer.Name, key.KeyID), 378 + SigningUser: signer, 379 + SigningKey: key, 380 + SigningEmail: email, 381 + } 382 + } 383 + return nil 384 + } 385 + 386 + func hashAndVerifyForKeyID(ctx context.Context, sig *packet.Signature, payload string, committer *user_model.User, keyID, name, email string) *ObjectVerification { 387 + if keyID == "" { 388 + return nil 389 + } 390 + keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{ 391 + KeyID: keyID, 392 + IncludeSubKeys: true, 393 + }) 394 + if err != nil { 395 + log.Error("GetGPGKeysByKeyID: %v", err) 396 + return &ObjectVerification{ 397 + CommittingUser: committer, 398 + Verified: false, 399 + Reason: "gpg.error.failed_retrieval_gpg_keys", 400 + } 401 + } 402 + if len(keys) == 0 { 403 + return nil 404 + } 405 + for _, key := range keys { 406 + var primaryKeys []*GPGKey 407 + if key.PrimaryKeyID != "" { 408 + primaryKeys, err = db.Find[GPGKey](ctx, FindGPGKeyOptions{ 409 + KeyID: key.PrimaryKeyID, 410 + IncludeSubKeys: true, 411 + }) 412 + if err != nil { 413 + log.Error("GetGPGKeysByKeyID: %v", err) 414 + return &ObjectVerification{ 415 + CommittingUser: committer, 416 + Verified: false, 417 + Reason: "gpg.error.failed_retrieval_gpg_keys", 418 + } 419 + } 420 + } 421 + 422 + activated, email := checkKeyEmails(ctx, email, append([]*GPGKey{key}, primaryKeys...)...) 423 + if !activated { 424 + continue 425 + } 426 + 427 + signer := &user_model.User{ 428 + Name: name, 429 + Email: email, 430 + } 431 + if key.OwnerID != 0 { 432 + owner, err := user_model.GetUserByID(ctx, key.OwnerID) 433 + if err == nil { 434 + signer = owner 435 + } else if !user_model.IsErrUserNotExist(err) { 436 + log.Error("Failed to user_model.GetUserByID: %d for key ID: %d (%s) %v", key.OwnerID, key.ID, key.KeyID, err) 437 + return &ObjectVerification{ 438 + CommittingUser: committer, 439 + Verified: false, 440 + Reason: "gpg.error.no_committer_account", 441 + } 442 + } 443 + } 444 + commitVerification := hashAndVerifyWithSubKeysObjectVerification(sig, payload, key, committer, signer, email) 445 + if commitVerification != nil { 446 + return commitVerification 447 + } 448 + } 449 + // This is a bad situation ... We have a key id that is in our database but the signature doesn't match. 450 + return &ObjectVerification{ 451 + CommittingUser: committer, 452 + Verified: false, 453 + Warning: true, 454 + Reason: BadSignature, 455 + } 456 + } 457 + 458 + // CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository 459 + // There are several trust models in Gitea 460 + func CalculateTrustStatus(verification *ObjectVerification, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error), keyMap *map[string]bool) error { 461 + if !verification.Verified { 462 + return nil 463 + } 464 + 465 + // In the Committer trust model a signature is trusted if it matches the committer 466 + // - it doesn't matter if they're a collaborator, the owner, Gitea or Github 467 + // NB: This model is commit verification only 468 + if repoTrustModel == repo_model.CommitterTrustModel { 469 + // default to "unmatched" 470 + verification.TrustStatus = "unmatched" 471 + 472 + // We can only verify against users in our database but the default key will match 473 + // against by email if it is not in the db. 474 + if (verification.SigningUser.ID != 0 && 475 + verification.CommittingUser.ID == verification.SigningUser.ID) || 476 + (verification.SigningUser.ID == 0 && verification.CommittingUser.ID == 0 && 477 + verification.SigningUser.Email == verification.CommittingUser.Email) { 478 + verification.TrustStatus = "trusted" 479 + } 480 + return nil 481 + } 482 + 483 + // Now we drop to the more nuanced trust models... 484 + verification.TrustStatus = "trusted" 485 + 486 + if verification.SigningUser.ID == 0 { 487 + // This commit is signed by the default key - but this key is not assigned to a user in the DB. 488 + 489 + // However in the repo_model.CollaboratorCommitterTrustModel we cannot mark this as trusted 490 + // unless the default key matches the email of a non-user. 491 + if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && (verification.CommittingUser.ID != 0 || 492 + verification.SigningUser.Email != verification.CommittingUser.Email) { 493 + verification.TrustStatus = "untrusted" 494 + } 495 + return nil 496 + } 497 + 498 + // Check we actually have a GPG SigningKey 499 + var err error 500 + if verification.SigningKey != nil { 501 + var isMember bool 502 + if keyMap != nil { 503 + var has bool 504 + isMember, has = (*keyMap)[verification.SigningKey.KeyID] 505 + if !has { 506 + isMember, err = isOwnerMemberCollaborator(verification.SigningUser) 507 + (*keyMap)[verification.SigningKey.KeyID] = isMember 508 + } 509 + } else { 510 + isMember, err = isOwnerMemberCollaborator(verification.SigningUser) 511 + } 512 + 513 + if !isMember { 514 + verification.TrustStatus = "untrusted" 515 + if verification.CommittingUser.ID != verification.SigningUser.ID { 516 + // The committing user and the signing user are not the same 517 + // This should be marked as questionable unless the signing user is a collaborator/team member etc. 518 + verification.TrustStatus = "unmatched" 519 + } 520 + } else if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID { 521 + // The committing user and the signing user are not the same and our trustmodel states that they must match 522 + verification.TrustStatus = "unmatched" 523 + } 524 + } 525 + 526 + return err 527 + }
+15
models/asymkey/gpg_key_tag_verification.go
··· 1 + // Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package asymkey 5 + 6 + import ( 7 + "context" 8 + 9 + "code.gitea.io/gitea/modules/git" 10 + ) 11 + 12 + func ParseTagWithSignature(ctx context.Context, gitRepo *git.Repository, t *git.Tag) *ObjectVerification { 13 + o := tagToGitObject(t, gitRepo) 14 + return ParseObjectWithSignature(ctx, &o) 15 + }
+7 -8
models/asymkey/ssh_key_commit_verification.go models/asymkey/ssh_key_object_verification.go
··· 11 11 12 12 "code.gitea.io/gitea/models/db" 13 13 user_model "code.gitea.io/gitea/models/user" 14 - "code.gitea.io/gitea/modules/git" 15 14 "code.gitea.io/gitea/modules/log" 16 15 17 16 "github.com/42wim/sshsig" 18 17 ) 19 18 20 - // ParseCommitWithSSHSignature check if signature is good against keystore. 21 - func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *user_model.User) *CommitVerification { 19 + // ParseObjectWithSSHSignature check if signature is good against keystore. 20 + func ParseObjectWithSSHSignature(ctx context.Context, c *GitObject, committer *user_model.User) *ObjectVerification { 22 21 // Now try to associate the signature with the committer, if present 23 22 if committer.ID != 0 { 24 23 keys, err := db.Find[PublicKey](ctx, FindPublicKeyOptions{ ··· 27 26 }) 28 27 if err != nil { // Skipping failed to get ssh keys of user 29 28 log.Error("ListPublicKeys: %v", err) 30 - return &CommitVerification{ 29 + return &ObjectVerification{ 31 30 CommittingUser: committer, 32 31 Verified: false, 33 32 Reason: "gpg.error.failed_retrieval_gpg_keys", ··· 55 54 56 55 for _, k := range keys { 57 56 if k.Verified && activated { 58 - commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committer, committer, c.Committer.Email) 57 + commitVerification := verifySSHObjectVerification(c.Signature.Signature, c.Signature.Payload, k, committer, committer, c.Committer.Email) 59 58 if commitVerification != nil { 60 59 return commitVerification 61 60 } ··· 63 62 } 64 63 } 65 64 66 - return &CommitVerification{ 65 + return &ObjectVerification{ 67 66 CommittingUser: committer, 68 67 Verified: false, 69 68 Reason: NoKeyFound, 70 69 } 71 70 } 72 71 73 - func verifySSHCommitVerification(sig, payload string, k *PublicKey, committer, signer *user_model.User, email string) *CommitVerification { 72 + func verifySSHObjectVerification(sig, payload string, k *PublicKey, committer, signer *user_model.User, email string) *ObjectVerification { 74 73 if err := sshsig.Verify(bytes.NewBuffer([]byte(payload)), []byte(sig), []byte(k.Content), "git"); err != nil { 75 74 return nil 76 75 } 77 76 78 - return &CommitVerification{ // Everything is ok 77 + return &ObjectVerification{ // Everything is ok 79 78 CommittingUser: committer, 80 79 Verified: true, 81 80 Reason: fmt.Sprintf("%s / %s", signer.Name, k.Fingerprint),
+12 -6
models/asymkey/ssh_key_commit_verification_test.go models/asymkey/ssh_key_object_verification_test.go
··· 22 22 sshKey := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1000, OwnerID: 2}) 23 23 24 24 t.Run("No commiter", func(t *testing.T) { 25 - commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, &git.Commit{}, &user_model.User{}) 25 + o := commitToGitObject(&git.Commit{}) 26 + commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, &user_model.User{}) 26 27 assert.False(t, commitVerification.Verified) 27 28 assert.Equal(t, NoKeyFound, commitVerification.Reason) 28 29 }) ··· 30 31 t.Run("Commiter without keys", func(t *testing.T) { 31 32 user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) 32 33 33 - commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, &git.Commit{Committer: &git.Signature{Email: user.Email}}, user) 34 + o := commitToGitObject(&git.Commit{Committer: &git.Signature{Email: user.Email}}) 35 + commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user) 34 36 assert.False(t, commitVerification.Verified) 35 37 assert.Equal(t, NoKeyFound, commitVerification.Reason) 36 38 }) ··· 57 59 `, 58 60 }, 59 61 } 60 - commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2) 62 + o := commitToGitObject(gitCommit) 63 + commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user2) 61 64 assert.False(t, commitVerification.Verified) 62 65 assert.Equal(t, NoKeyFound, commitVerification.Reason) 63 66 }) ··· 79 82 }, 80 83 } 81 84 82 - commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2) 85 + o := commitToGitObject(gitCommit) 86 + commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user2) 83 87 assert.False(t, commitVerification.Verified) 84 88 assert.Equal(t, NoKeyFound, commitVerification.Reason) 85 89 }) ··· 107 111 }, 108 112 } 109 113 110 - commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2) 114 + o := commitToGitObject(gitCommit) 115 + commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user2) 111 116 assert.True(t, commitVerification.Verified) 112 117 assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason) 113 118 assert.Equal(t, sshKey, commitVerification.SigningSSHKey) ··· 138 143 }, 139 144 } 140 145 141 - commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2) 146 + o := commitToGitObject(gitCommit) 147 + commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user2) 142 148 assert.True(t, commitVerification.Verified) 143 149 assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason) 144 150 assert.Equal(t, sshKey, commitVerification.SigningSSHKey)
+1 -1
modules/gitgraph/graph_models.go
··· 238 238 type Commit struct { 239 239 Commit *git.Commit 240 240 User *user_model.User 241 - Verification *asymkey_model.CommitVerification 241 + Verification *asymkey_model.ObjectVerification 242 242 Status *git_model.CommitStatus 243 243 Flow int64 244 244 Row int
+43
routers/web/repo/release.go
··· 11 11 "strings" 12 12 13 13 "code.gitea.io/gitea/models" 14 + "code.gitea.io/gitea/models/asymkey" 14 15 "code.gitea.io/gitea/models/db" 15 16 git_model "code.gitea.io/gitea/models/git" 16 17 repo_model "code.gitea.io/gitea/models/repo" ··· 18 19 user_model "code.gitea.io/gitea/models/user" 19 20 "code.gitea.io/gitea/modules/base" 20 21 "code.gitea.io/gitea/modules/git" 22 + "code.gitea.io/gitea/modules/gitrepo" 21 23 "code.gitea.io/gitea/modules/log" 22 24 "code.gitea.io/gitea/modules/markup" 23 25 "code.gitea.io/gitea/modules/markup/markdown" ··· 192 194 } 193 195 194 196 ctx.Data["Releases"] = releases 197 + addVerifyTagToContext(ctx) 195 198 196 199 numReleases := ctx.Data["NumReleases"].(int64) 197 200 pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5) ··· 201 204 ctx.HTML(http.StatusOK, tplReleasesList) 202 205 } 203 206 207 + func verifyTagSignature(ctx *context.Context, r *repo_model.Release) (*asymkey.ObjectVerification, error) { 208 + if err := r.LoadAttributes(ctx); err != nil { 209 + return nil, err 210 + } 211 + gitRepo, err := gitrepo.OpenRepository(ctx, r.Repo) 212 + if err != nil { 213 + return nil, err 214 + } 215 + defer gitRepo.Close() 216 + 217 + tag, err := gitRepo.GetTag(r.TagName) 218 + if err != nil { 219 + return nil, err 220 + } 221 + if tag.Signature == nil { 222 + return nil, nil 223 + } 224 + 225 + verification := asymkey.ParseTagWithSignature(ctx, gitRepo, tag) 226 + return verification, nil 227 + } 228 + 229 + func addVerifyTagToContext(ctx *context.Context) { 230 + ctx.Data["VerifyTag"] = func(r *repo_model.Release) *asymkey.ObjectVerification { 231 + v, err := verifyTagSignature(ctx, r) 232 + if err != nil { 233 + return nil 234 + } 235 + return v 236 + } 237 + ctx.Data["HasSignature"] = func(verification *asymkey.ObjectVerification) bool { 238 + if verification == nil { 239 + return false 240 + } 241 + return verification.Reason != "gpg.error.not_signed_commit" 242 + } 243 + } 244 + 204 245 // TagsList render tags list page 205 246 func TagsList(ctx *context.Context) { 206 247 ctx.Data["PageIsTagList"] = true ··· 240 281 } 241 282 242 283 ctx.Data["Releases"] = releases 284 + addVerifyTagToContext(ctx) 243 285 244 286 numTags := ctx.Data["NumTags"].(int64) 245 287 pager := context.NewPagination(int(numTags), opts.PageSize, opts.Page, 5) ··· 304 346 if release.IsTag && release.Title == "" { 305 347 release.Title = release.TagName 306 348 } 349 + addVerifyTagToContext(ctx) 307 350 308 351 ctx.Data["PageIsSingleTag"] = release.IsTag 309 352 if release.IsTag {
+1
templates/repo/release/list.tmpl
··· 60 60 <div class="markup desc"> 61 61 {{$release.RenderedNote}} 62 62 </div> 63 + {{template "repo/tag/verification_line" (dict "ctxData" $ "release" $release)}} 63 64 <div class="divider"></div> 64 65 <details class="download" {{if eq $idx 0}}open{{end}}> 65 66 <summary class="tw-my-4">
+2 -1
templates/repo/tag/list.tmpl
··· 16 16 {{range $idx, $release := .Releases}} 17 17 <tr> 18 18 <td class="tag"> 19 - <h3 class="release-tag-name tw-mb-2"> 19 + <h3 class="release-tag-name tw-mb-2 tw-flex"> 20 20 {{if $canReadReleases}} 21 21 <a class="tw-flex tw-items-center" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a> 22 22 {{else}} 23 23 <a class="tw-flex tw-items-center" href="{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a> 24 24 {{end}} 25 + {{template "repo/tag/verification_box" (dict "ctxData" $ "release" $release)}} 25 26 </h3> 26 27 <div class="download tw-flex tw-items-center"> 27 28 {{if $.Permission.CanRead $.UnitTypeCode}}
+27
templates/repo/tag/verification_box.tmpl
··· 1 + {{$v := call .ctxData.VerifyTag .release}} 2 + {{if call .ctxData.HasSignature $v}} 3 + {{$class := "isSigned"}} 4 + {{$href := ""}} 5 + {{if $v.Verified}} 6 + {{$href = $v.SigningUser.HomeLink}} 7 + {{$class = (print $class " isVerified")}} 8 + {{else}} 9 + {{$class = (print $class " isWarning")}} 10 + {{end}} 11 + 12 + <a {{if $href}}href="{{$href}}"{{end}} class="ui label tw-ml-2 {{$class}}"> 13 + {{if $v.Verified}} 14 + <div title="{{$v.Reason}}"> 15 + {{if ne $v.SigningUser.ID 0}} 16 + {{svg "gitea-lock"}} 17 + {{ctx.AvatarUtils.Avatar $v.SigningUser 28 "signature"}} 18 + {{else}} 19 + <span title="{{ctx.Locale.Tr "gpg.default_key"}}">{{svg "gitea-lock-cog"}}</span> 20 + {{ctx.AvatarUtils.AvatarByEmail $v.Verification.SigningEmail "" 28 "signature"}} 21 + {{end}} 22 + </div> 23 + {{else}} 24 + <span title="{{ctx.Locale.Tr $v.Reason}}">{{svg "gitea-unlock"}}</span> 25 + {{end}} 26 + </a> 27 + {{end}}
+80
templates/repo/tag/verification_line.tmpl
··· 1 + {{$v := call .ctxData.VerifyTag .release}} 2 + {{if call .ctxData.HasSignature $v}} 3 + {{$class := "isSigned"}} 4 + {{$href := ""}} 5 + {{if $v.Verified}} 6 + {{$href = $v.SigningUser.HomeLink}} 7 + {{$class = (print $class " isVerified")}} 8 + {{else}} 9 + {{$class = (print $class " isWarning")}} 10 + {{end}} 11 + 12 + <div class="ui bottom attached message tw-text-left tw-flex tw-content-center tw-justify-between tag-signature-row tw-flex-wrap tw-mb-0 {{$class}}"> 13 + <div class="tw-flex tw-content-center"> 14 + {{if $v.Verified}} 15 + {{if ne $v.SigningUser.ID 0}} 16 + {{svg "gitea-lock" 16 "tw-mr-2"}} 17 + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by"}}</span> 18 + {{ctx.AvatarUtils.Avatar $v.SigningUser 28 "tw-mr-2"}} 19 + <a href="{{$v.SigningUser.HomeLink}}"><strong>{{$v.SigningUser.GetDisplayName}}</strong></a> 20 + {{else}} 21 + <span title="{{ctx.Locale.Tr "gpg.default_key"}}">{{svg "gitea-lock-cog" 16 "tw-mr-2"}}</span> 22 + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span> 23 + {{ctx.AvatarUtils.AvatarByEmail $v.SigningEmail "" 28 "tw-mr-2"}} 24 + <strong>{{$v.SigningUser.GetDisplayName}}</strong> 25 + {{end}} 26 + {{else}} 27 + {{svg "gitea-unlock" 16 "tw-mr-2"}} 28 + <span class="ui text">{{ctx.Locale.Tr $v.Reason}}</span> 29 + {{end}} 30 + </div> 31 + 32 + <div class="tw-flex tw-content-center"> 33 + {{if $v.Verified}} 34 + {{if ne $v.SigningUser.ID 0}} 35 + {{svg "octicon-verified" 16 "tw-mr-2"}} 36 + {{if $v.SigningSSHKey}} 37 + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> 38 + {{$v.SigningSSHKey.Fingerprint}} 39 + {{else}} 40 + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> 41 + {{$v.SigningKey.PaddedKeyID}} 42 + {{end}} 43 + {{else}} 44 + {{svg "octicon-unverified" 16 "tw-mr-2"}} 45 + {{if $v.SigningSSHKey}} 46 + <span class="ui text tw-mr-2" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> 47 + {{$v.SigningSSHKey.Fingerprint}} 48 + {{else}} 49 + <span class="ui text tw-mr-2" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> 50 + {{$v.SigningKey.PaddedKeyID}} 51 + {{end}} 52 + {{end}} 53 + {{else if $v.Warning}} 54 + {{svg "octicon-unverified" 16 "tw-mr-2"}} 55 + {{if $v.SigningSSHKey}} 56 + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> 57 + {{$v.SigningSSHKey.Fingerprint}} 58 + {{else}} 59 + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> 60 + {{$v.SigningKey.PaddedKeyID}} 61 + {{end}} 62 + {{else}} 63 + {{if $v.SigningKey}} 64 + {{if ne $v.SigningKey.KeyID ""}} 65 + {{svg "octicon-verified" 16 "tw-mr-2"}} 66 + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> 67 + {{$v.SigningKey.PaddedKeyID}} 68 + {{end}} 69 + {{end}} 70 + {{if $v.SigningSSHKey}} 71 + {{if ne $v.SigningSSHKey.Fingerprint ""}} 72 + {{svg "octicon-verified" 16 "tw-mr-2"}} 73 + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> 74 + {{$v.SigningSSHKey.Fingerprint}} 75 + {{end}} 76 + {{end}} 77 + {{end}} 78 + </div> 79 + </div> 80 + {{end}}
+107
tests/integration/repo_signed_tag_test.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package integration 5 + 6 + import ( 7 + "fmt" 8 + "net/http" 9 + "os" 10 + "os/exec" 11 + "testing" 12 + 13 + auth_model "code.gitea.io/gitea/models/auth" 14 + "code.gitea.io/gitea/models/db" 15 + "code.gitea.io/gitea/models/unittest" 16 + user_model "code.gitea.io/gitea/models/user" 17 + "code.gitea.io/gitea/modules/git" 18 + "code.gitea.io/gitea/modules/gitrepo" 19 + "code.gitea.io/gitea/modules/graceful" 20 + repo_module "code.gitea.io/gitea/modules/repository" 21 + api "code.gitea.io/gitea/modules/structs" 22 + "code.gitea.io/gitea/tests" 23 + 24 + "github.com/stretchr/testify/assert" 25 + ) 26 + 27 + func TestRepoSSHSignedTags(t *testing.T) { 28 + defer tests.PrepareTestEnv(t)() 29 + 30 + // Preparations 31 + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 32 + repo, _, f := CreateDeclarativeRepo(t, user, "", nil, nil, nil) 33 + defer f() 34 + 35 + // Set up an SSH key for the tagger 36 + tmpDir := t.TempDir() 37 + err := os.Chmod(tmpDir, 0o700) 38 + assert.NoError(t, err) 39 + 40 + signingKey := fmt.Sprintf("%s/ssh_key", tmpDir) 41 + 42 + cmd := exec.Command("ssh-keygen", "-t", "ed25519", "-N", "", "-f", signingKey) 43 + err = cmd.Run() 44 + assert.NoError(t, err) 45 + 46 + // Set up git config for the tagger 47 + _ = git.NewCommand(git.DefaultContext, "config", "user.name").AddDynamicArguments(user.Name).Run(&git.RunOpts{Dir: repo.RepoPath()}) 48 + _ = git.NewCommand(git.DefaultContext, "config", "user.email").AddDynamicArguments(user.Email).Run(&git.RunOpts{Dir: repo.RepoPath()}) 49 + _ = git.NewCommand(git.DefaultContext, "config", "gpg.format", "ssh").Run(&git.RunOpts{Dir: repo.RepoPath()}) 50 + _ = git.NewCommand(git.DefaultContext, "config", "user.signingkey").AddDynamicArguments(signingKey).Run(&git.RunOpts{Dir: repo.RepoPath()}) 51 + 52 + // Open the git repo 53 + gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo) 54 + defer gitRepo.Close() 55 + 56 + // Create a signed tag 57 + err = git.NewCommand(git.DefaultContext, "tag", "-s", "-m", "this is a signed tag", "ssh-signed-tag").Run(&git.RunOpts{Dir: repo.RepoPath()}) 58 + assert.NoError(t, err) 59 + 60 + // Sync the tag to the DB 61 + repo_module.SyncRepoTags(graceful.GetManager().ShutdownContext(), repo.ID) 62 + 63 + // Helper functions 64 + assertTagSignedStatus := func(t *testing.T, isSigned bool) { 65 + t.Helper() 66 + 67 + req := NewRequestf(t, "GET", "%s/releases/tag/ssh-signed-tag", repo.HTMLURL()) 68 + resp := MakeRequest(t, req, http.StatusOK) 69 + doc := NewHTMLParser(t, resp.Body) 70 + 71 + doc.AssertElement(t, ".tag-signature-row .gitea-unlock", !isSigned) 72 + doc.AssertElement(t, ".tag-signature-row .gitea-lock", isSigned) 73 + } 74 + 75 + t.Run("unverified", func(t *testing.T) { 76 + defer tests.PrintCurrentTest(t)() 77 + 78 + assertTagSignedStatus(t, false) 79 + }) 80 + 81 + t.Run("verified", func(t *testing.T) { 82 + defer tests.PrintCurrentTest(t)() 83 + 84 + // Upload the signing key 85 + keyData, err := os.ReadFile(fmt.Sprintf("%s.pub", signingKey)) 86 + assert.NoError(t, err) 87 + key := string(keyData) 88 + 89 + session := loginUser(t, user.Name) 90 + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser) 91 + 92 + req := NewRequestWithJSON(t, "POST", "/api/v1/user/keys", &api.CreateKeyOption{ 93 + Key: key, 94 + Title: "test key", 95 + }).AddTokenAuth(token) 96 + resp := MakeRequest(t, req, http.StatusCreated) 97 + 98 + var pubkey *api.PublicKey 99 + DecodeJSON(t, resp, &pubkey) 100 + 101 + // Mark the key as verified 102 + db.GetEngine(db.DefaultContext).Exec("UPDATE `public_key` SET verified = true WHERE id = ?", pubkey.ID) 103 + 104 + // Check the tag page 105 + assertTagSignedStatus(t, true) 106 + }) 107 + }
+33 -1
web_src/css/repo.css
··· 1878 1878 border-bottom: 1px solid var(--color-warning-border); 1879 1879 } 1880 1880 1881 + .repository .release-tag-name .ui.label.isSigned, 1882 + .repository .release-list-title .ui.label.isSigned { 1883 + padding: 0 0.5em; 1884 + box-shadow: none; 1885 + } 1886 + 1887 + .repository .release-tag-name .ui.label.isSigned .avatar, 1888 + .repository .release-list-title .ui.label.isSigned .avatar { 1889 + margin-left: .5rem; 1890 + } 1891 + 1892 + .repository .release-tag-name .ui.label.isSigned.isVerified, 1893 + .repository .release-list-title .ui.label.isSigned.isVerified { 1894 + border: 1px solid var(--color-success-border); 1895 + background-color: var(--color-success-bg); 1896 + color: var(--color-success-text); 1897 + } 1898 + 1899 + .repository .release-tag-name .ui.label.isSigned.isWarning, 1900 + .repository .release-list-title .ui.label.isSigned.isWarning { 1901 + border: 1px solid var(--color-warning-border); 1902 + background-color: var(--color-warning-bg); 1903 + color: var(--color-warning-text); 1904 + } 1905 + 1881 1906 .repository .segment.reactions.dropdown .menu, 1882 1907 .repository .select-reaction.dropdown .menu { 1883 1908 right: 0 !important; ··· 2111 2136 padding-top: 15px; 2112 2137 } 2113 2138 2114 - .commit-header-row { 2139 + .commit-header-row, 2140 + .tag-signature-row { 2115 2141 min-height: 50px !important; 2116 2142 padding-top: 0 !important; 2117 2143 padding-bottom: 0 !important; 2144 + } 2145 + 2146 + .tag-signature-row div { 2147 + margin-top: auto !important; 2148 + margin-bottom: auto !important; 2149 + display: inline-block !important; 2118 2150 } 2119 2151 2120 2152 .settings.webhooks .list > .item:not(:first-child),