mirror of
https://github.com/pgsty/minio.git
synced 2026-09-05 18:16:16 +03:00
7c103389f5
Reject unimplemented x-amz-checksum value and trailer names instead of accepting uploads without verification. Apply the same contract to PutObject, multipart initiation and parts, CopyObject, and UploadPartCopy while preserving the five supported algorithms. Signed-off-by: Feng Ruohang <rh@vonng.com>
247 lines
8.4 KiB
Go
247 lines
8.4 KiB
Go
// Copyright (c) 2015-2025 MinIO, Inc.
|
|
//
|
|
// This file is part of MinIO Object Storage stack
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package hash
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
xhttp "github.com/minio/minio/internal/http"
|
|
)
|
|
|
|
func TestGetContentChecksumRejectsUnsupportedHeaders(t *testing.T) {
|
|
unsupported := []string{
|
|
"x-amz-checksum-md5",
|
|
"x-amz-checksum-sha512",
|
|
"x-amz-checksum-xxhash64",
|
|
"x-amz-checksum-xxhash3",
|
|
"x-amz-checksum-xxhash128",
|
|
"x-amz-checksum-future",
|
|
}
|
|
for _, header := range unsupported {
|
|
t.Run("header/"+header, func(t *testing.T) {
|
|
h := http.Header{header: {"AA=="}}
|
|
if _, err := GetContentChecksum(h); !errors.Is(err, ErrInvalidChecksum) {
|
|
t.Fatalf("GetContentChecksum(%s) error = %v, want ErrInvalidChecksum", header, err)
|
|
}
|
|
})
|
|
t.Run("trailer/"+header, func(t *testing.T) {
|
|
h := http.Header{xhttp.AmzTrailer: {header}}
|
|
if _, err := GetContentChecksum(h); !errors.Is(err, ErrInvalidChecksum) {
|
|
t.Fatalf("GetContentChecksum(trailer %s) error = %v, want ErrInvalidChecksum", header, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
for header, value := range map[string]string{
|
|
xhttp.AmzChecksumAlgo: "CRC32",
|
|
xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite,
|
|
xhttp.AmzChecksumMode: "ENABLED",
|
|
"x-amz-sdk-checksum-algorithm": "SHA512",
|
|
} {
|
|
t.Run("control/"+header, func(t *testing.T) {
|
|
h := http.Header{header: {value}}
|
|
if _, err := GetContentChecksum(h); errors.Is(err, ErrInvalidChecksum) {
|
|
t.Fatalf("control header %s was rejected", header)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestChecksumAddToHeader tests that adding and retrieving a checksum on a header works
|
|
func TestChecksumAddToHeader(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
checksum ChecksumType
|
|
fullobj bool
|
|
wantErr bool
|
|
}{
|
|
{"CRC32-composite", ChecksumCRC32, false, false},
|
|
{"CRC32-full-object", ChecksumCRC32, true, false},
|
|
{"CRC32C-composite", ChecksumCRC32C, false, false},
|
|
{"CRC32C-full-object", ChecksumCRC32C, true, false},
|
|
{"CRC64NVME-full-object", ChecksumCRC64NVME, false, false}, // CRC64NVME is always full object
|
|
{"ChecksumSHA1-composite", ChecksumSHA1, false, false},
|
|
{"ChecksumSHA256-composite", ChecksumSHA256, false, false},
|
|
{"ChecksumSHA1-full-object", ChecksumSHA1, true, true}, // SHA1 does not support full object
|
|
{"ChecksumSHA256-full-object", ChecksumSHA256, true, true}, // SHA256 does not support full object
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Skip invalid cases where SHA1 or SHA256 is used with full object
|
|
if (tt.checksum.Is(ChecksumSHA1) || tt.checksum.Is(ChecksumSHA256)) && tt.fullobj {
|
|
// Validate that NewChecksumType correctly marks these as invalid
|
|
alg := tt.checksum.String()
|
|
typ := NewChecksumType(alg, xhttp.AmzChecksumTypeFullObject)
|
|
if !typ.Is(ChecksumInvalid) {
|
|
t.Fatalf("Expected ChecksumInvalid for %s with full object, got %s", tt.name, typ.StringFull())
|
|
}
|
|
return
|
|
}
|
|
myData := []byte("this-is-a-checksum-data-test")
|
|
chksm := NewChecksumFromData(tt.checksum, myData)
|
|
if chksm == nil {
|
|
t.Fatalf("NewChecksumFromData failed for %s", tt.name)
|
|
}
|
|
if tt.fullobj {
|
|
chksm.Type |= ChecksumFullObject
|
|
}
|
|
|
|
// CRC64NVME is always full object
|
|
if chksm.Type.Base().Is(ChecksumCRC64NVME) {
|
|
chksm.Type |= ChecksumFullObject
|
|
}
|
|
|
|
// Prepare the checksum map with appropriate headers
|
|
m := chksm.AsMap()
|
|
m[xhttp.AmzChecksumAlgo] = chksm.Type.String() // Set the algorithm explicitly
|
|
if chksm.Type.FullObjectRequested() {
|
|
m[xhttp.AmzChecksumType] = xhttp.AmzChecksumTypeFullObject
|
|
} else {
|
|
m[xhttp.AmzChecksumType] = xhttp.AmzChecksumTypeComposite
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
AddChecksumHeader(w, m)
|
|
gotChksm, err := GetContentChecksum(w.Result().Header)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("Expected error for %s, got none", tt.name)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("GetContentChecksum failed for %s: %v", tt.name, err)
|
|
}
|
|
|
|
if gotChksm == nil {
|
|
t.Fatalf("Got nil checksum for %s", tt.name)
|
|
}
|
|
// Compare the full checksum structs
|
|
if !chksm.Equal(gotChksm) {
|
|
t.Errorf("Checksum mismatch for %s: expected %+v, got %+v", tt.name, chksm, gotChksm)
|
|
}
|
|
// Verify the checksum type
|
|
expectedType := chksm.Type
|
|
if gotChksm.Type != expectedType {
|
|
t.Errorf("Type mismatch for %s: expected %s, got %s", tt.name, expectedType.StringFull(), gotChksm.Type.StringFull())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestChecksumSerializeDeserialize checks AppendTo can be reversed by ChecksumFromBytes
|
|
func TestChecksumSerializeDeserialize(t *testing.T) {
|
|
myData := []byte("this-is-a-checksum-data-test")
|
|
chksm := NewChecksumFromData(ChecksumCRC32, myData)
|
|
if chksm == nil {
|
|
t.Fatal("NewChecksumFromData returned nil")
|
|
}
|
|
// Serialize the checksum to bytes
|
|
b := chksm.AppendTo(nil, nil)
|
|
if b == nil {
|
|
t.Fatal("AppendTo returned nil")
|
|
}
|
|
|
|
// Deserialize the checksum from bytes
|
|
chksmOut := ChecksumFromBytes(b)
|
|
if chksmOut == nil {
|
|
t.Fatal("ChecksumFromBytes returned nil")
|
|
}
|
|
|
|
// Assert new checksum matches the content
|
|
matchError := chksmOut.Matches(myData, 0)
|
|
if matchError != nil {
|
|
t.Fatalf("Checksum mismatch on chksmOut: %v", matchError)
|
|
}
|
|
|
|
// Assert they are exactly equal
|
|
if !chksmOut.Equal(chksm) {
|
|
t.Fatalf("Checksum mismatch: expected %+v, got %+v", chksm, chksmOut)
|
|
}
|
|
}
|
|
|
|
// TestChecksumSerializeDeserializeMultiPart checks AppendTo can be reversed by ChecksumFromBytes
|
|
// for multipart checksum
|
|
func TestChecksumSerializeDeserializeMultiPart(t *testing.T) {
|
|
// Create dummy data that we'll split into 3 parts
|
|
dummyData := []byte("The quick brown fox jumps over the lazy dog. " +
|
|
"Pack my box with five dozen brown eggs. " +
|
|
"Have another go it will all make sense in the end!")
|
|
|
|
// Split data into 3 parts
|
|
partSize := len(dummyData) / 3
|
|
part1Data := dummyData[0:partSize]
|
|
part2Data := dummyData[partSize : 2*partSize]
|
|
part3Data := dummyData[2*partSize:]
|
|
|
|
// Calculate CRC32C checksum for each part using NewChecksumFromData
|
|
checksumType := ChecksumCRC32C
|
|
|
|
part1Checksum := NewChecksumFromData(checksumType, part1Data)
|
|
part2Checksum := NewChecksumFromData(checksumType, part2Data)
|
|
part3Checksum := NewChecksumFromData(checksumType, part3Data)
|
|
|
|
// Combine the raw checksums (this is what happens in CompleteMultipartUpload)
|
|
var checksumCombined []byte
|
|
checksumCombined = append(checksumCombined, part1Checksum.Raw...)
|
|
checksumCombined = append(checksumCombined, part2Checksum.Raw...)
|
|
checksumCombined = append(checksumCombined, part3Checksum.Raw...)
|
|
|
|
// Create the final checksum (checksum of the combined checksums)
|
|
// Add BOTH the multipart flag AND the includes-multipart flag
|
|
finalChecksumType := checksumType | ChecksumMultipart | ChecksumIncludesMultipart
|
|
finalChecksum := NewChecksumFromData(finalChecksumType, checksumCombined)
|
|
|
|
// Set WantParts to indicate 3 parts
|
|
finalChecksum.WantParts = 3
|
|
|
|
// Test AppendTo serialization
|
|
var serialized []byte
|
|
serialized = finalChecksum.AppendTo(serialized, checksumCombined)
|
|
|
|
// Use ChecksumFromBytes to deserialize the final checksum
|
|
chksmOut := ChecksumFromBytes(serialized)
|
|
if chksmOut == nil {
|
|
t.Fatal("ChecksumFromBytes returned nil")
|
|
}
|
|
|
|
// Assert they are exactly equal
|
|
if !chksmOut.Equal(finalChecksum) {
|
|
t.Fatalf("Checksum mismatch: expected %+v, got %+v", finalChecksum, chksmOut)
|
|
}
|
|
|
|
// Serialize what we got from ChecksumFromBytes
|
|
serializedOut := chksmOut.AppendTo(nil, checksumCombined)
|
|
|
|
// Read part checksums from serializedOut
|
|
readParts := ReadPartCheckSums(serializedOut)
|
|
expectedChecksums := []string{
|
|
part1Checksum.Encoded,
|
|
part2Checksum.Encoded,
|
|
part3Checksum.Encoded,
|
|
}
|
|
for i, expected := range expectedChecksums {
|
|
if got := readParts[i][ChecksumCRC32C.String()]; got != expected {
|
|
t.Fatalf("want part%dChecksum.Encoded %s, got %s", i+1, expected, got)
|
|
}
|
|
}
|
|
}
|