From 304f8a551a2fe9956aa0fc6175c382127217a96c Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 19 May 2026 20:56:34 -0700 Subject: [PATCH 1/2] chore: Update giteabot to fix failure when backport (#37789) --- .github/workflows/giteabot-backport.yml | 2 +- .github/workflows/giteabot.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/giteabot-backport.yml b/.github/workflows/giteabot-backport.yml index be4b3d42a1c..9a9c244b0c8 100644 --- a/.github/workflows/giteabot-backport.yml +++ b/.github/workflows/giteabot-backport.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: go-gitea/giteabot@40d7c74f93d479578978c4ef47a655a467b8dab1 # Add config options (#5) + - uses: go-gitea/giteabot@d4f19d5b4a88059d8c3ca78d660631506fc0c286 # add retry logic to giteabot with: github_token: ${{ secrets.GITEABOT_TOKEN }} gitea_fork: giteabot/gitea diff --git a/.github/workflows/giteabot.yml b/.github/workflows/giteabot.yml index b9e3cc1651b..91043988898 100644 --- a/.github/workflows/giteabot.yml +++ b/.github/workflows/giteabot.yml @@ -45,7 +45,7 @@ jobs: steps: # pull_request_review runs without repository secrets on fork PRs, so fall # back to the workflow token for the non-backport checks handled here. - - uses: go-gitea/giteabot@40d7c74f93d479578978c4ef47a655a467b8dab1 # Add config options (#5) + - uses: go-gitea/giteabot@d4f19d5b4a88059d8c3ca78d660631506fc0c286 # add retry logic to giteabot with: github_token: ${{ secrets.GITEABOT_TOKEN || github.token }} checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }} From 552c29a2592d091830e982a89351859d12b6b887 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 20 May 2026 09:34:27 -0700 Subject: [PATCH 2/2] fix(actions): make artifact signature payloads unambiguous (#37707) This PR hardens artifact URL signing by encoding signature inputs in an unambiguous binary payload before computing the HMAC. What it changes: - replace direct concatenation-style signing inputs with explicit payload builders - encode string fields with a length prefix before appending their bytes - encode integer fields as fixed-width binary values instead of decimal text - apply the same hardening to both: - Actions Artifact V4 signing in `routers/api/actions/artifactsv4.go` - artifact download signing in `routers/api/v1/repo/action.go` - add regression tests that verify distinct field combinations produce distinct payloads and signatures Why: The previous signing logic built HMAC inputs by appending multiple fields without a strongly structured representation. That kind of construction can create ambiguity at field boundaries, where different parameter combinations may serialize into the same byte stream for signing. This change removes that ambiguity by constructing a deterministic payload format with explicit boundaries between fields. --------- Co-authored-by: wxiaoguang Co-authored-by: Nicolas Co-authored-by: silverwind Co-authored-by: Claude (Opus 4.7) --- modules/actions/artifacts.go | 29 ++++++++++++++++++++++++ modules/actions/artifacts_test.go | 36 ++++++++++++++++++++++++++++++ routers/api/actions/artifactsv4.go | 9 ++------ routers/api/v1/repo/action.go | 9 ++------ 4 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 modules/actions/artifacts.go create mode 100644 modules/actions/artifacts_test.go diff --git a/modules/actions/artifacts.go b/modules/actions/artifacts.go new file mode 100644 index 00000000000..743e2f00fe2 --- /dev/null +++ b/modules/actions/artifacts.go @@ -0,0 +1,29 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/binary" + "io" + + "code.gitea.io/gitea/modules/setting" +) + +type tagType string + +// BuildSignature builds a hmac signature for the input values. +// "tag" is an internal pre-defined static string to distinguish the signatures for different purpose. +func BuildSignature(tag tagType, vals ...string) []byte { + m := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret()) + _, _ = io.WriteString(m, string(tag)) + var buf8 [8]byte + for _, v := range vals { + binary.LittleEndian.PutUint64(buf8[:], uint64(len(v))) + _, _ = m.Write(buf8[:]) + _, _ = io.WriteString(m, v) + } + return m.Sum(nil) +} diff --git a/modules/actions/artifacts_test.go b/modules/actions/artifacts_test.go new file mode 100644 index 00000000000..4c038436539 --- /dev/null +++ b/modules/actions/artifacts_test.go @@ -0,0 +1,36 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBuildSignature(t *testing.T) { + a := BuildSignature("v0", "x") + b := BuildSignature("v0", "x") + assert.Equal(t, a, b) + + a = BuildSignature("v0", "x", "yz") + b = BuildSignature("v0", "xy", "z") + assert.NotEqual(t, a, b) + + a = BuildSignature("v1", "x") + b = BuildSignature("v2", "x") + assert.NotEqual(t, a, b) + + a = BuildSignature("v0", "x") + b = BuildSignature("v0x") + assert.NotEqual(t, a, b) + + a = BuildSignature("v0", "", "x") + b = BuildSignature("v0", "x", "") + assert.NotEqual(t, a, b) + + a = BuildSignature("v0") + b = BuildSignature("v0") + assert.Equal(t, a, b) +} diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index f1f33424edd..a50d9c9ffd5 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -104,6 +104,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + actions_module "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" @@ -162,13 +163,7 @@ func ArtifactsV4Routes(prefix string) *web.Router { } func (r *artifactV4Routes) buildSignature(endpoint, expires, artifactName string, taskID, artifactID int64) []byte { - mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret()) - mac.Write([]byte(endpoint)) - mac.Write([]byte(expires)) - mac.Write([]byte(artifactName)) - _, _ = fmt.Fprint(mac, taskID) - _, _ = fmt.Fprint(mac, artifactID) - return mac.Sum(nil) + return actions_module.BuildSignature("v4", endpoint, expires, artifactName, strconv.FormatInt(taskID, 10), strconv.FormatInt(artifactID, 10)) } func (r *artifactV4Routes) buildArtifactURL(ctx *ArtifactContext, endpoint, artifactName string, taskID, artifactID int64) string { diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 666d4f98ef0..b70cdafe242 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -6,7 +6,6 @@ package repo import ( go_context "context" "crypto/hmac" - "crypto/sha256" "encoding/base64" "errors" "fmt" @@ -21,9 +20,9 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" secret_model "code.gitea.io/gitea/models/secret" + "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/optional" - "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" @@ -1959,11 +1958,7 @@ func DeleteArtifact(ctx *context.APIContext) { } func buildSignature(endp string, expires, artifactID int64) []byte { - mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret()) - mac.Write([]byte(endp)) - fmt.Fprint(mac, expires) - fmt.Fprint(mac, artifactID) - return mac.Sum(nil) + return actions.BuildSignature("api", endp, strconv.FormatInt(expires, 10), strconv.FormatInt(artifactID, 10)) } func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) string {