mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
[v15.0/forgejo] fix: get tag must return the tag signature instead of commit signature (#12395)
**Backport:** https://codeberg.org/forgejo/forgejo/pulls/12351 ## Fix: `GET /api/v1/repos/{owner}/{repo}/git/tags/{sha}` returns empty verification for signed tags ### Problem When an annotated tag is signed (GPG or SSH) but the underlying commit is **not** signed, the API endpoint `GET /repos/{owner}/{repo}/git/tags/{sha}` returns an empty `verification.signature` field. This is because `ToAnnotatedTag` was calling `ToVerification(ctx, c)` with the **commit** object, which checks the commit's signature — not the tag's own signature. Since the commit is unsigned, the API returns `signature: ""` and `verified: false`. This causes issues for tools that rely on the tag signature from the API to validate that a tag push event is from a trusted source. ### Fix `ToAnnotatedTag` now checks if the tag has its own signature (`t.Signature != nil`). If so, it uses `ParseTagWithSignature` to verify the tag's signature and populates the `verification` field from the tag. Otherwise, it falls back to the commit signature (existing behavior for unsigned/lightweight tags). Co-authored-by: steven.guiheux <steven.guiheux@ovhcloud.com> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12395 Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
This commit is contained in:
parent
b2c9c14dea
commit
d3dd397001
3 changed files with 122 additions and 3 deletions
|
|
@ -122,7 +122,7 @@ func GetAnnotatedTag(ctx *context.APIContext) {
|
|||
ctx.Error(http.StatusBadRequest, "GetAnnotatedTag", err)
|
||||
}
|
||||
|
||||
convertedAnnotatedTag, err := convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit)
|
||||
convertedAnnotatedTag, err := convert.ToAnnotatedTag(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, tag, commit)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ToAnnotatedTag", err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -397,12 +397,32 @@ func ToTeams(ctx context.Context, teams []*organization.Team, loadOrgs bool) ([]
|
|||
}
|
||||
|
||||
// ToAnnotatedTag convert git.Tag to api.AnnotatedTag
|
||||
func ToAnnotatedTag(ctx context.Context, repo *repo_model.Repository, t *git.Tag, c *git.Commit) (*api.AnnotatedTag, error) {
|
||||
func ToAnnotatedTag(ctx context.Context, gitRepo *git.Repository, repo *repo_model.Repository, t *git.Tag, c *git.Commit) (*api.AnnotatedTag, error) {
|
||||
archiveDownloadCount, err := repo_model.GetArchiveDownloadCountForTagName(ctx, repo.ID, t.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Use the tag's own signature if the tag is signed, otherwise fall back to commit signature.
|
||||
var verification *api.PayloadCommitVerification
|
||||
if t.Signature != nil {
|
||||
verif := asymkey_model.ParseTagWithSignature(ctx, gitRepo, t)
|
||||
verification = &api.PayloadCommitVerification{
|
||||
Verified: verif.Verified,
|
||||
Reason: verif.Reason,
|
||||
Signature: t.Signature.Signature,
|
||||
Payload: t.Signature.Payload,
|
||||
}
|
||||
if verif.SigningUser != nil {
|
||||
verification.Signer = &api.PayloadUser{
|
||||
Name: verif.SigningUser.Name,
|
||||
Email: verif.SigningEmail,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
verification = ToVerification(ctx, c)
|
||||
}
|
||||
|
||||
return &api.AnnotatedTag{
|
||||
Tag: t.Name,
|
||||
SHA: t.ID.String(),
|
||||
|
|
@ -410,7 +430,7 @@ func ToAnnotatedTag(ctx context.Context, repo *repo_model.Repository, t *git.Tag
|
|||
Message: t.Message,
|
||||
URL: util.URLJoin(repo.APIURL(), "git/tags", t.ID.String()),
|
||||
Tagger: ToCommitUser(t.Tagger),
|
||||
Verification: ToVerification(ctx, c),
|
||||
Verification: verification,
|
||||
ArchiveDownloadCount: archiveDownloadCount,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,17 @@ package convert
|
|||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
"forgejo.org/models/db"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/git"
|
||||
api "forgejo.org/modules/structs"
|
||||
"forgejo.org/modules/timeutil"
|
||||
"forgejo.org/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -106,6 +109,102 @@ uf51WIBywxztet6vi+jYJK1jFoY4iA==
|
|||
})
|
||||
}
|
||||
|
||||
func TestToAnnotatedTag(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("models/fixtures/TestParseCommitWithSSHSignature")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// Align user email for predictable test results (same as TestToVerification).
|
||||
userModel := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
userModel.Email = "secret-email@example.com"
|
||||
db.GetEngine(t.Context()).ID(userModel.ID).Cols("email").Update(userModel)
|
||||
|
||||
headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
sha1 := git.Sha1ObjectFormat
|
||||
|
||||
tagSHA := sha1.EmptyObjectID()
|
||||
commitSHA := git.MustIDFromString("e20aa0bcd2878f65a93de68a3eed9045d6efdd74")
|
||||
tagger := &git.Signature{Name: "user2", Email: "user2@example.com", When: time.Unix(1699707877, 0)}
|
||||
|
||||
t.Run("Unsigned tag falls back to commit signature (GPG)", func(t *testing.T) {
|
||||
tag := &git.Tag{
|
||||
Name: "v2.0.0",
|
||||
ID: tagSHA,
|
||||
Object: commitSHA,
|
||||
Type: "commit",
|
||||
Tagger: tagger,
|
||||
Message: "Lightweight tag\n",
|
||||
// No Signature → unsigned tag
|
||||
}
|
||||
|
||||
commitPayload := `tree e20aa0bcd2878f65a93de68a3eed9045d6efdd74
|
||||
parent 5cd9b9847563eb730d63d23c1f1b84868e52ae7d
|
||||
author user2 <user2+committer@example.com> 1759956520 -0600
|
||||
committer user2 <user2+committer@example.com> 1759956520 -0600
|
||||
|
||||
Add content
|
||||
`
|
||||
commitGPGSig := `-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQEzBAABCgAdFiEEdlqhn25IEoMmvK5vmDaXTfEZWRMFAmjmzigACgkQmDaXTfEZ
|
||||
WROC4ggAs8mD8csA6FV5e2v/4HcxuaZKCN+D8Gvku2JUigODQCA+NOX0FF2jDnCh
|
||||
tXylBPB4HJw1spKkDLtOpnCUSOniBdl9NcZjnBt6sP/OSnEfLznXFra+9fCHzsu0
|
||||
9uhDn3Wn1iHWXQ2ZglUwVS0ja6pNgEip8wNZBysv8+XbO1CEEW0m7zQA6tunzIwp
|
||||
yiPZDUJrKtpKAK0+v19EccT2VjYAa+Vo+p3/E0piaTYNbsTqtFRy63tdjDkf+mo+
|
||||
l/PaPhrMqdnbxv3/sd/63VCNdvPH3f0+OuydcC7mXyysmvap99EC+QKnpsrm7RAP
|
||||
uf51WIBywxztet6vi+jYJK1jFoY4iA==
|
||||
=Lnrt
|
||||
-----END PGP SIGNATURE-----`
|
||||
|
||||
commit := &git.Commit{
|
||||
ID: commitSHA,
|
||||
Committer: &git.Signature{
|
||||
Email: "user2@example.com",
|
||||
},
|
||||
Signature: &git.ObjectSignature{
|
||||
Payload: commitPayload,
|
||||
Signature: commitGPGSig,
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ToAnnotatedTag(t.Context(), nil, headRepo, tag, commit)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
|
||||
// Should fall back to commit verification (tag has no signature)
|
||||
assert.Equal(t, commitGPGSig, result.Verification.Signature, "should use the commit GPG signature")
|
||||
assert.Equal(t, commitPayload, result.Verification.Payload, "should use the commit payload")
|
||||
assert.True(t, result.Verification.Verified, "commit signature should be verified")
|
||||
assert.Equal(t, "v2.0.0", result.Tag)
|
||||
assert.Equal(t, tagSHA.String(), result.SHA)
|
||||
assert.Equal(t, util.URLJoin(headRepo.APIURL(), "git/tags", tagSHA.String()), result.URL)
|
||||
})
|
||||
|
||||
t.Run("Unsigned tag, unsigned commit", func(t *testing.T) {
|
||||
tag := &git.Tag{
|
||||
Name: "v3.0.0",
|
||||
ID: tagSHA,
|
||||
Object: commitSHA,
|
||||
Type: "commit",
|
||||
Tagger: tagger,
|
||||
Message: "No signature\n",
|
||||
}
|
||||
|
||||
commit := &git.Commit{
|
||||
ID: commitSHA,
|
||||
Committer: &git.Signature{Email: "user2@example.com"},
|
||||
// No Signature
|
||||
}
|
||||
|
||||
result, err := ToAnnotatedTag(t.Context(), nil, headRepo, tag, commit)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
|
||||
assert.False(t, result.Verification.Verified, "should not be verified")
|
||||
assert.Empty(t, result.Verification.Signature, "should have no signature")
|
||||
assert.Equal(t, "v3.0.0", result.Tag)
|
||||
})
|
||||
}
|
||||
|
||||
func TestToActionRunner(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue