Files
42b1f95261 fix: optimize packages upload memory (#39366)
Bound the memory used when parsing uploaded packages, so instances with
low memory limits survive non-standard packages.

1. Metadata files are read through a size-limited reader, archives
themselves stay unbounded so large payloads keep working
2. zstd decoding caps the window at 128 MiB, the zstd CLI default
3. RPM headers go through a first-party reader with rpmlib's size and
array count limits that allocates only what the upload contains, until
https://github.com/sassoftware/go-rpmutils/pull/44 is released. Signing
still verifies the payload digest, and its signature header matches
rpmsign's byte for byte
4. Swift and NuGet read the zip entry they size-checked, a duplicate
name could stand in for it before
5. Terraform states check `LIMIT_SIZE_TERRAFORM_STATE` before parsing,
NuGet symbol PDBs share a 256 MiB budget
6. Oversized container configs are rejected instead of stored without
metadata, invalid Vagrant boxes answer 400 instead of 500

Signed-off-by: techknowlogick <git@tklk.dev>
Co-authored-by: techknowlogick <git@tklk.dev>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
2026-10-17 06:10:04 +00:00

127 lines
3.9 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package rpm
import (
"bytes"
"crypto"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"encoding/hex"
"hash"
"io"
"maps"
"slices"
"time"
"github.com/ProtonMail/go-crypto/openpgp/packet"
)
var payloadDigestHashes = map[uint64]func() hash.Hash{1: md5.New, 2: sha1.New, 8: sha256.New, 9: sha512.New384, 10: sha512.New, 11: sha256.New224} // rpm's PGPHASHALGO ids
// SignPackage verifies the payload and replaces all OpenPGP signatures with a header-only and a header+payload one, the latter needed by packages without payload digest
func SignPackage(r io.ReadSeeker, key *packet.PrivateKey) (io.Reader, error) {
lead, sig, h, err := readHeaders(r)
if err != nil {
return nil, err
}
payloadStart, err := r.Seek(0, io.SeekCurrent)
if err != nil {
return nil, err
}
payloadHash, payloadDigest, err := payloadVerifier(sig, h)
if err != nil {
return nil, err
}
headerHash, packageHash := crypto.SHA256.New(), crypto.SHA256.New()
headerHash.Write(h.raw)
packageHash.Write(h.raw)
if _, err := io.Copy(io.MultiWriter(packageHash, payloadHash), r); err != nil {
return nil, err
}
if hex.EncodeToString(payloadHash.Sum(nil)) != payloadDigest {
return nil, ErrInvalidPackage
}
if _, err := r.Seek(payloadStart, io.SeekStart); err != nil {
return nil, err
}
headerSignature, err := signOpenPGP(key, headerHash)
if err != nil {
return nil, err
}
packageSignature, err := signOpenPGP(key, packageHash)
if err != nil {
return nil, err
}
for _, tag := range []uint32{sigTagDSA, sigTagGPG, sigTagReservedSpace} {
delete(sig.entries, tag)
}
sig.entries[sigTagRSA] = headerEntry{typ: typeBin, count: uint32(len(headerSignature)), data: headerSignature}
sig.entries[sigTagPGP] = headerEntry{typ: typeBin, count: uint32(len(packageSignature)), data: packageSignature}
return io.MultiReader(bytes.NewReader(lead), bytes.NewReader(sig.marshalSignature()), bytes.NewReader(h.raw), r), nil
}
func payloadVerifier(sig, h *header) (hash.Hash, string, error) {
if newHash, ok := payloadDigestHashes[h.getUint(tagPayloadDigestAlgo)]; ok {
if digest := h.getString(tagPayloadDigest); digest != "" {
return newHash(), digest, nil
}
}
if md5sum := sig.entries[sigTagMD5]; md5sum.typ == typeBin && len(md5sum.data) > 0 {
hasher := md5.New()
hasher.Write(h.raw)
return hasher, hex.EncodeToString(md5sum.data), nil
}
return nil, "", ErrInvalidPackage
}
func signOpenPGP(key *packet.PrivateKey, digest hash.Hash) ([]byte, error) {
signature := &packet.Signature{
SigType: packet.SigTypeBinary,
CreationTime: time.Now(),
PubKeyAlgo: key.PubKeyAlgo,
Hash: crypto.SHA256,
IssuerKeyId: &key.KeyId,
}
if err := signature.Sign(digest, key, nil); err != nil {
return nil, err
}
var buf bytes.Buffer
err := signature.Serialize(&buf)
return buf.Bytes(), err
}
func (h *header) marshalSignature() []byte {
tags := slices.DeleteFunc(slices.Sorted(maps.Keys(h.entries)), func(tag uint32) bool { return tag <= maxRegionTag })
var index, store []byte
for _, tag := range tags {
entry := h.entries[tag]
if size := typeSizes[entry.typ]; size > 1 {
store = append(store, make([]byte, (size-len(store)%size)%size)...)
}
index = appendUint32s(index, tag, entry.typ, uint32(len(store)), entry.count)
store = append(store, entry.data...)
}
count := uint32(len(tags) + 1)
region := appendUint32s(nil, sigTagRegion, typeBin, uint32(len(store)), indexEntrySize)
store = appendUint32s(store, sigTagRegion, typeBin, -count*indexEntrySize, indexEntrySize)
out := appendUint32s(nil, headerMagic, 0, count, uint32(len(store)))
out = append(append(append(out, region...), index...), store...)
return append(out, make([]byte, signaturePadding(len(store)))...)
}
func appendUint32s(buf []byte, values ...uint32) []byte {
for _, value := range values {
buf = binary.BigEndian.AppendUint32(buf, value)
}
return buf
}