Merge pull request #1689 from versity/sis/signed-streaming-trailer-test-script

feat: adds STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER option in test generation script
This commit is contained in:
Ben McClelland
2025-12-11 10:30:33 -07:00
committed by GitHub
4 changed files with 127 additions and 16 deletions

View File

@@ -2,14 +2,16 @@ package command
import ( import (
"encoding/hex" "encoding/hex"
"github.com/versity/versitygw/tests/rest_scripts/logger"
"strings" "strings"
"github.com/versity/versitygw/tests/rest_scripts/logger"
) )
type PayloadChunkedAWS struct { type PayloadChunkedAWS struct {
*PayloadChunked *PayloadChunked
serviceString string serviceString string
currentDateTime string currentDateTime string
yyyymmdd string
lastSignature string lastSignature string
emptyByteSignature string emptyByteSignature string
signingKey []byte signingKey []byte

View File

@@ -5,22 +5,28 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"hash"
"io" "io"
"os" "os"
) )
const (
streamPayloadTrailerAlgo = "AWS4-HMAC-SHA256-TRAILER"
)
type PayloadStreamingAWS4HMACSHA256 struct { type PayloadStreamingAWS4HMACSHA256 struct {
*PayloadChunkedAWS *PayloadChunkedAWS
hasher hash.Hash
} }
func NewPayloadStreamingAWS4HMACSHA256(source DataSource, chunkSize int64, serviceString, currentDateTime string) *PayloadStreamingAWS4HMACSHA256 { func NewPayloadStreamingAWS4HMACSHA256(source DataSource, chunkSize int64, payloadType PayloadType, serviceString string, currentDateTime, yyyymmdd, checksumType string) *PayloadStreamingAWS4HMACSHA256 {
return &PayloadStreamingAWS4HMACSHA256{ return &PayloadStreamingAWS4HMACSHA256{
PayloadChunkedAWS: &PayloadChunkedAWS{ PayloadChunkedAWS: &PayloadChunkedAWS{
PayloadChunked: &PayloadChunked{ PayloadChunked: &PayloadChunked{
Payload: &Payload{ Payload: &Payload{
dataSource: source, dataSource: source,
payloadType: StreamingAWS4HMACSHA256Payload, payloadType: payloadType,
checksumType: "", checksumType: checksumType,
dataSizeCalculated: false, dataSizeCalculated: false,
dataSize: 0, dataSize: 0,
}, },
@@ -28,6 +34,7 @@ func NewPayloadStreamingAWS4HMACSHA256(source DataSource, chunkSize int64, servi
}, },
serviceString: serviceString, serviceString: serviceString,
currentDateTime: currentDateTime, currentDateTime: currentDateTime,
yyyymmdd: yyyymmdd,
lastSignature: "", lastSignature: "",
emptyByteSignature: SHA256HashZeroBytes, emptyByteSignature: SHA256HashZeroBytes,
signingKey: nil, signingKey: nil,
@@ -41,7 +48,19 @@ func (s *PayloadStreamingAWS4HMACSHA256) AddInitialSignatureAndSigningKey(initia
} }
func (s *PayloadStreamingAWS4HMACSHA256) GetContentLength() (int64, error) { func (s *PayloadStreamingAWS4HMACSHA256) GetContentLength() (int64, error) {
return s.getChunkedPayloadContentLength(83, 85) var trailerSize int64 = 85
if s.payloadType == StreamingAWS4HMACSHA256PayloadTrailer {
chLength, err := GetBase64ChecksumLength(s.checksumType)
if err != nil {
return 0, err
}
trailerSize += chLength + 92
// sum the checksum length
trailerSize += 16 + int64(len(s.checksumType))
}
return s.getChunkedPayloadContentLength(83, trailerSize)
} }
func (s *PayloadStreamingAWS4HMACSHA256) addSignature(chunk []byte, outFile *os.File) error { func (s *PayloadStreamingAWS4HMACSHA256) addSignature(chunk []byte, outFile *os.File) error {
@@ -55,10 +74,70 @@ func (s *PayloadStreamingAWS4HMACSHA256) addSignature(chunk []byte, outFile *os.
return nil return nil
} }
func (s *PayloadStreamingAWS4HMACSHA256) addTrailer(outFile *os.File) error {
checksum, err := s.getBase64Checksum(s.hasher)
if err != nil {
return fmt.Errorf("failed to calculated the trailing checksum: %w", err)
}
tr := fmt.Sprintf("x-amz-checksum-%s", s.checksumType)
trailer := fmt.Sprintf("%s:%s", tr, checksum)
finalSig := s.calculateTrailerSignature(trailer)
trailerStr := fmt.Sprintf(
"\r\n%s\r\nx-amz-trailer-signature:%s",
trailer,
finalSig,
)
if _, err := outFile.Write([]byte(trailerStr)); err != nil {
return fmt.Errorf("error writing final chunk trailer: %w", err)
}
s.lastSignature = finalSig
return nil
}
func (s *PayloadStreamingAWS4HMACSHA256) calculateTrailerSignature(trailer string) string {
trailer += "\n"
strToSign := s.getTrailerChunkStringToSign(trailer)
return hex.EncodeToString(hmacSHA256(s.signingKey, strToSign))
}
func (s *PayloadStreamingAWS4HMACSHA256) getTrailerChunkStringToSign(trailer string) string {
hsh := sha256.Sum256([]byte(trailer))
sig := hex.EncodeToString(hsh[:])
prefix := s.getStringToSignPrefix(streamPayloadTrailerAlgo)
strToSign := fmt.Sprintf("%s\n%s\n%s",
prefix,
s.lastSignature,
sig,
)
return strToSign
}
func (s PayloadStreamingAWS4HMACSHA256) getStringToSignPrefix(algo string) string {
return fmt.Sprintf("%s\n%s\n%s",
algo,
s.currentDateTime,
s.serviceString,
)
}
func (s *PayloadStreamingAWS4HMACSHA256) getReader() (io.Reader, error) { func (s *PayloadStreamingAWS4HMACSHA256) getReader() (io.Reader, error) {
sourceFile, err := s.dataSource.GetReader() sourceFile, err := s.dataSource.GetReader()
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating tee reader: %w", err) return nil, fmt.Errorf("error creating reader: %w", err)
}
if s.payloadType == StreamingAWS4HMACSHA256PayloadTrailer && s.checksumType != "" {
s.hasher = s.getChecksumHasher()
sourceFile, err = s.dataSource.GetTeeReader(s.hasher)
if err != nil {
return nil, fmt.Errorf("error creating tee reader: %w", err)
}
} }
return bufio.NewReader(sourceFile), nil return bufio.NewReader(sourceFile), nil
} }
@@ -66,8 +145,12 @@ func (s *PayloadStreamingAWS4HMACSHA256) getReader() (io.Reader, error) {
func (s *PayloadStreamingAWS4HMACSHA256) WritePayload(filePath string) error { func (s *PayloadStreamingAWS4HMACSHA256) WritePayload(filePath string) error {
s.addSignatureFunc = s.addSignature s.addSignatureFunc = s.addSignature
s.getReaderFunc = s.getReader s.getReaderFunc = s.getReader
s.addTrailerFunc = func(outFile *os.File) error { if s.payloadType == StreamingAWS4HMACSHA256PayloadTrailer {
return nil s.addTrailerFunc = s.addTrailer
} else {
s.addTrailerFunc = func(outFile *os.File) error {
return nil
}
} }
return s.writeChunkedPayload(filePath) return s.writeChunkedPayload(filePath)
} }

View File

@@ -7,11 +7,12 @@ import (
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
logger "github.com/versity/versitygw/tests/rest_scripts/logger"
"os" "os"
"sort" "sort"
"strings" "strings"
"time" "time"
logger "github.com/versity/versitygw/tests/rest_scripts/logger"
) )
const ( const (
@@ -128,10 +129,11 @@ func (s *S3Command) CurlShellCommand() (string, error) {
} }
func (s *S3Command) prepareForBuild() error { func (s *S3Command) prepareForBuild() error {
now := time.Now().UTC()
if s.IncorrectYearMonthDay { if s.IncorrectYearMonthDay {
s.currentDateTime = time.Now().Add(-48 * time.Hour).UTC().Format("20060102T150405Z") s.currentDateTime = now.Add(-48 * time.Hour).Format("20060102T150405Z")
} else { } else {
s.currentDateTime = time.Now().UTC().Format("20060102T150405Z") s.currentDateTime = now.Format("20060102T150405Z")
} }
protocolAndHost := strings.Split(s.Url, "://") protocolAndHost := strings.Split(s.Url, "://")
if len(protocolAndHost) != 2 { if len(protocolAndHost) != 2 {
@@ -184,9 +186,9 @@ func (s *S3Command) preparePayload() error {
func (s *S3Command) initializeOpenSSLPayloadAndGetContentLength() error { func (s *S3Command) initializeOpenSSLPayloadAndGetContentLength() error {
switch s.PayloadType { switch s.PayloadType {
case StreamingAWS4HMACSHA256Payload: case StreamingAWS4HMACSHA256Payload, StreamingAWS4HMACSHA256PayloadTrailer:
serviceString := fmt.Sprintf("%s/%s/%s/aws4_request", s.yearMonthDay, s.AwsRegion, s.ServiceName) serviceString := fmt.Sprintf("%s/%s/%s/aws4_request", s.yearMonthDay, s.AwsRegion, s.ServiceName)
s.payloadOpenSSL = NewPayloadStreamingAWS4HMACSHA256(s.dataSource, int64(s.ChunkSize), serviceString, s.currentDateTime) s.payloadOpenSSL = NewPayloadStreamingAWS4HMACSHA256(s.dataSource, int64(s.ChunkSize), PayloadType(s.PayloadType), serviceString, s.currentDateTime, s.yearMonthDay, s.ChecksumType)
case StreamingUnsignedPayloadTrailer: case StreamingUnsignedPayloadTrailer:
streamingUnsignedPayloadTrailerImpl := NewStreamingUnsignedPayloadWithTrailer(s.dataSource, int64(s.ChunkSize), s.ChecksumType) streamingUnsignedPayloadTrailerImpl := NewStreamingUnsignedPayloadWithTrailer(s.dataSource, int64(s.ChunkSize), s.ChecksumType)
streamingUnsignedPayloadTrailerImpl.OmitTrailerOrKey(s.OmitPayloadTrailer, s.OmitPayloadTrailerKey) streamingUnsignedPayloadTrailerImpl.OmitTrailerOrKey(s.OmitPayloadTrailer, s.OmitPayloadTrailerKey)
@@ -214,6 +216,9 @@ func (s *S3Command) addHeaderValues() error {
} else { } else {
s.headerValues = append(s.headerValues, []string{"host", s.host}) s.headerValues = append(s.headerValues, []string{"host", s.host})
} }
if s.PayloadType == StreamingAWS4HMACSHA256PayloadTrailer && s.ChecksumType != "" {
s.headerValues = append(s.headerValues, []string{"x-amz-trailer", fmt.Sprintf("x-amz-checksum-%s", s.ChecksumType)})
}
s.headerValues = append(s.headerValues, s.headerValues = append(s.headerValues,
[]string{"x-amz-content-sha256", s.payloadHash}, []string{"x-amz-content-sha256", s.payloadHash},
[]string{"x-amz-date", s.currentDateTime}, []string{"x-amz-date", s.currentDateTime},
@@ -418,7 +423,7 @@ func (s *S3Command) writeOpenSSLPayload(file *os.File) error {
awsPayload.AddInitialSignatureAndSigningKey(s.signature, s.signingKey) awsPayload.AddInitialSignatureAndSigningKey(s.signature, s.signingKey)
} }
switch s.PayloadType { switch s.PayloadType {
case UnsignedPayload, "", StreamingUnsignedPayloadTrailer, StreamingAWS4HMACSHA256Payload: case UnsignedPayload, "", StreamingUnsignedPayloadTrailer, StreamingAWS4HMACSHA256Payload, StreamingAWS4HMACSHA256PayloadTrailer:
if err := s.payloadOpenSSL.WritePayload(s.FilePath); err != nil { if err := s.payloadOpenSSL.WritePayload(s.FilePath); err != nil {
return fmt.Errorf("error writing payload to openssl file: %w", err) return fmt.Errorf("error writing payload to openssl file: %w", err)
} }

View File

@@ -4,10 +4,11 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"github.com/versity/versitygw/tests/rest_scripts/command"
logger "github.com/versity/versitygw/tests/rest_scripts/logger"
"log" "log"
"strings" "strings"
"github.com/versity/versitygw/tests/rest_scripts/command"
logger "github.com/versity/versitygw/tests/rest_scripts/logger"
) )
const ( const (
@@ -271,5 +272,25 @@ func validateConfig() error {
if *client == command.CURL && *filePath != "" { if *client == command.CURL && *filePath != "" {
return fmt.Errorf("writing to file not currently supported for curl commands") return fmt.Errorf("writing to file not currently supported for curl commands")
} }
if !isValidChecksumType(checksumType) {
return fmt.Errorf("invalid checksum type: %s", *checksumType)
}
return nil return nil
} }
func isValidChecksumType(input *string) bool {
if input == nil {
return true
}
// make is case insensitive
chType := strings.ToLower(*input)
checksumType = &chType
switch chType {
case "", command.ChecksumCRC32, command.ChecksumCRC32C, command.ChecksumCRC64NVME, command.ChecksumSHA1, command.ChecksumSHA256:
return true
default:
return false
}
}