mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-28 02:38:44 +09:00
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. Backport #37707 Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
86 lines
2.7 KiB
Go
86 lines
2.7 KiB
Go
// Copyright 2025 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
actions_model "code.gitea.io/gitea/models/actions"
|
|
"code.gitea.io/gitea/modules/httplib"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/storage"
|
|
"code.gitea.io/gitea/services/context"
|
|
)
|
|
|
|
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)
|
|
}
|
|
|
|
// IsArtifactV4 detects whether the artifact is likely from v4.
|
|
// V4 backend stores the files as a single combined zip file per artifact, and ensures ContentEncoding contains a slash
|
|
// (otherwise this uses application/zip instead of the custom mime type), which is not the case for the old backend.
|
|
func IsArtifactV4(art *actions_model.ActionArtifact) bool {
|
|
return strings.Contains(art.ContentEncodingOrType, "/")
|
|
}
|
|
|
|
func GetArtifactV4ServeDirectURL(art *actions_model.ActionArtifact, method string) (string, error) {
|
|
contentType := art.ContentEncodingOrType
|
|
u, err := storage.ActionsArtifacts.ServeDirectURL(art.StoragePath, art.ArtifactPath, method, &storage.ServeDirectOptions{ContentType: contentType})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return u.String(), nil
|
|
}
|
|
|
|
func DownloadArtifactV4ServeDirect(ctx *context.Base, art *actions_model.ActionArtifact) bool {
|
|
if !setting.Actions.ArtifactStorage.ServeDirect() {
|
|
return false
|
|
}
|
|
u, err := GetArtifactV4ServeDirectURL(art, ctx.Req.Method)
|
|
if err != nil {
|
|
log.Error("GetArtifactV4ServeDirectURL: %v", err)
|
|
return false
|
|
}
|
|
ctx.Redirect(u, http.StatusFound)
|
|
return true
|
|
}
|
|
|
|
func DownloadArtifactV4ReadStorage(ctx *context.Base, art *actions_model.ActionArtifact) error {
|
|
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, f, httplib.ServeHeaderOptions{
|
|
Filename: art.ArtifactPath,
|
|
ContentType: art.ContentEncodingOrType, // v4 guarantees that the field is Content-Type
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func DownloadArtifactV4(ctx *context.Base, art *actions_model.ActionArtifact) error {
|
|
if DownloadArtifactV4ServeDirect(ctx, art) {
|
|
return nil
|
|
}
|
|
return DownloadArtifactV4ReadStorage(ctx, art)
|
|
}
|