Skip to content

Commit

Permalink
auth: Read aws-chunked data properly
Browse files Browse the repository at this point in the history
closes #913.

Signed-off-by: Evgenii Baidakov <[email protected]>
  • Loading branch information
smallhive committed Dec 1, 2023
1 parent 9bf4dd1 commit 32f233e
Show file tree
Hide file tree
Showing 4 changed files with 459 additions and 42 deletions.
30 changes: 22 additions & 8 deletions api/auth/center.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,17 @@ const (
authHeaderPartsNum = 6
maxFormSizeMemory = 50 * 1048576 // 50 MB

AmzAlgorithm = "X-Amz-Algorithm"
AmzCredential = "X-Amz-Credential"
AmzSignature = "X-Amz-Signature"
AmzSignedHeaders = "X-Amz-SignedHeaders"
AmzExpires = "X-Amz-Expires"
AmzDate = "X-Amz-Date"
AuthorizationHdr = "Authorization"
ContentTypeHdr = "Content-Type"
AmzAlgorithm = "X-Amz-Algorithm"
AmzCredential = "X-Amz-Credential"
AmzSignature = "X-Amz-Signature"
AmzSignedHeaders = "X-Amz-SignedHeaders"
AmzExpires = "X-Amz-Expires"
AmzDate = "X-Amz-Date"
AuthorizationHdr = "Authorization"
ContentTypeHdr = "Content-Type"
ContentEncodingHdr = "Content-Encoding"
ContentEncodingAwsChunked = "aws-chunked"

timeFormatISO8601 = "20060102T150405Z"
)

Expand Down Expand Up @@ -207,6 +210,17 @@ func (c *center) Authenticate(r *http.Request) (*Box, error) {
return nil, err
}

if hdr := r.Header.Get(ContentEncodingHdr); hdr == ContentEncodingAwsChunked {
sig, err := hex.DecodeString(authHdr.SignatureV4)
if err != nil {
return nil, fmt.Errorf("DecodeString: %w", err)
}

awsCreds := credentials.NewStaticCredentials(authHdr.AccessKeyID, box.Gate.AccessKey, "")
streamSigner := v4.NewChunkSigner(authHdr.Region, authHdr.Service, sig, signatureDateTime, awsCreds)
r.Body = v4.NewChunkedReader(r.Body, streamSigner)
}

result := &Box{AccessBox: box}
if needClientTime {
result.ClientTime = signatureDateTime
Expand Down
245 changes: 245 additions & 0 deletions api/auth/center_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package auth

import (
"bytes"
"encoding/hex"
"io"
"strings"
"testing"
"time"

"github.com/aws/aws-sdk-go/aws/credentials"
v4 "github.com/nspcc-dev/neofs-s3-gw/api/auth/signer/v4"
"github.com/nspcc-dev/neofs-s3-gw/api/s3errors"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -99,3 +104,243 @@ func TestSignature(t *testing.T) {
signature := signStr(secret, "s3", "us-east-1", signTime, strToSign)
require.Equal(t, "dfbe886241d9e369cf4b329ca0f15eb27306c97aa1022cc0bb5a914c4ef87634", signature)
}

// TestAwsEncodedChunkReader checks example from https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
func TestAwsEncodedChunkReader(t *testing.T) {
chunkOnePayload := make([]byte, 65536)
for i := 0; i < 65536; i++ {
chunkOnePayload[i] = 'a'
}

chunkOneBody := append([]byte("10000;chunk-signature=ad80c730a21e5b8d04586a2213dd63b9a0e99e0e2307b0ade35a65485a288648\n"), chunkOnePayload...)
awsCreds := credentials.NewStaticCredentials("AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "")

ts, err := time.Parse(timeFormatISO8601, "20130524T000000Z")
require.NoError(t, err)

seedSignature, err := hex.DecodeString("4f232c4386841ef735655705268965c44a0e4690baa4adea153f7db9fa80a0a9")
require.NoError(t, err)

chunkTwoPayload := make([]byte, 1024)
for i := 0; i < 1024; i++ {
chunkTwoPayload[i] = 'a'
}

chunkTwoBody := append([]byte("400;chunk-signature=0055627c9e194cb4542bae2aa5492e3c1575bbb81b612b7d234b86a503ef5497\n"), chunkTwoPayload...)

t.Run("correct signature", func(t *testing.T) {
streamSigner := v4.NewChunkSigner("us-east-1", "s3", seedSignature, ts, awsCreds)
chunkThreeBody := []byte("0;chunk-signature=b6c6ea8a5354eaf15b3cb7646744f4275b71ea724fed81ceb9323e279d449df9\n")

buf := bytes.NewBuffer(nil)
chunks := [][]byte{chunkOneBody, chunkTwoBody, chunkThreeBody}

for _, chunk := range chunks {
_, err = buf.Write(chunk)
require.NoError(t, err)
_, err = buf.Write([]byte{'\r', '\n'})
require.NoError(t, err)
}

chunkedReader := v4.NewChunkedReader(io.NopCloser(buf), streamSigner)
defer func() {
_ = chunkedReader.Close()
}()

chunk := make([]byte, 4096)
payload := bytes.NewBuffer(nil)
_, err = io.CopyBuffer(payload, chunkedReader, chunk)

require.NoError(t, err)

require.Equal(t, append(chunkOnePayload, chunkTwoPayload...), payload.Bytes())
})

t.Run("err invalid chunk signature", func(t *testing.T) {
streamSigner := v4.NewChunkSigner("us-east-1", "s3", seedSignature, ts, awsCreds)
chunkThreeBody := []byte("0;chunk-signature=000\n")

buf := bytes.NewBuffer(nil)
chunks := [][]byte{chunkOneBody, chunkTwoBody, chunkThreeBody}

for _, chunk := range chunks {
_, err = buf.Write(chunk)
require.NoError(t, err)
_, err = buf.Write([]byte{'\r', '\n'})
require.NoError(t, err)
}

chunkedReader := v4.NewChunkedReader(io.NopCloser(buf), streamSigner)
defer func() {
_ = chunkedReader.Close()
}()

chunk := make([]byte, 4096)
payload := bytes.NewBuffer(nil)
_, err = io.CopyBuffer(payload, chunkedReader, chunk)

require.ErrorIs(t, err, v4.ErrInvalidChunkSignature)
})

t.Run("err missing separator", func(t *testing.T) {
streamSigner := v4.NewChunkSigner("us-east-1", "s3", seedSignature, ts, awsCreds)
chunkThreeBody := []byte("0chunk-signature=b6c6ea8a5354eaf15b3cb7646744f4275b71ea724fed81ceb9323e279d449df9\n")

buf := bytes.NewBuffer(nil)
chunks := [][]byte{chunkOneBody, chunkTwoBody, chunkThreeBody}

for _, chunk := range chunks {
_, err = buf.Write(chunk)
require.NoError(t, err)
_, err = buf.Write([]byte{'\r', '\n'})
require.NoError(t, err)
}

chunkedReader := v4.NewChunkedReader(io.NopCloser(buf), streamSigner)
defer func() {
_ = chunkedReader.Close()
}()

chunk := make([]byte, 4096)
payload := bytes.NewBuffer(nil)
_, err = io.CopyBuffer(payload, chunkedReader, chunk)

require.ErrorIs(t, err, v4.ErrMissingSeparator)
})

t.Run("err missing equality byte", func(t *testing.T) {
streamSigner := v4.NewChunkSigner("us-east-1", "s3", seedSignature, ts, awsCreds)
chunkThreeBody := []byte("0;chunk-signatureb6c6ea8a5354eaf15b3cb7646744f4275b71ea724fed81ceb9323e279d449df9\n")

buf := bytes.NewBuffer(nil)
chunks := [][]byte{chunkOneBody, chunkTwoBody, chunkThreeBody}

for _, chunk := range chunks {
_, err = buf.Write(chunk)
require.NoError(t, err)
_, err = buf.Write([]byte{'\r', '\n'})
require.NoError(t, err)
}

chunkedReader := v4.NewChunkedReader(io.NopCloser(buf), streamSigner)
defer func() {
_ = chunkedReader.Close()
}()

chunk := make([]byte, 4096)
payload := bytes.NewBuffer(nil)
_, err = io.CopyBuffer(payload, chunkedReader, chunk)

require.Error(t, err)
})

t.Run("invalid hex byte", func(t *testing.T) {
streamSigner := v4.NewChunkSigner("us-east-1", "s3", seedSignature, ts, awsCreds)
chunkThreeBody := []byte("h;chunk-signature=b6c6ea8a5354eaf15b3cb7646744f4275b71ea724fed81ceb9323e279d449df9\n")

buf := bytes.NewBuffer(nil)
chunks := [][]byte{chunkOneBody, chunkTwoBody, chunkThreeBody}

for _, chunk := range chunks {
_, err = buf.Write(chunk)
require.NoError(t, err)
_, err = buf.Write([]byte{'\r', '\n'})
require.NoError(t, err)
}

chunkedReader := v4.NewChunkedReader(io.NopCloser(buf), streamSigner)
defer func() {
_ = chunkedReader.Close()
}()

chunk := make([]byte, 4096)
payload := bytes.NewBuffer(nil)
_, err = io.CopyBuffer(payload, chunkedReader, chunk)

require.Error(t, err)
})

t.Run("invalid hex length", func(t *testing.T) {
streamSigner := v4.NewChunkSigner("us-east-1", "s3", seedSignature, ts, awsCreds)
chunkThreeBody := []byte("11111111111111111;chunk-signature=b6c6ea8a5354eaf15b3cb7646744f4275b71ea724fed81ceb9323e279d449df9\n")

buf := bytes.NewBuffer(nil)
chunks := [][]byte{chunkOneBody, chunkTwoBody, chunkThreeBody}

for _, chunk := range chunks {
_, err = buf.Write(chunk)
require.NoError(t, err)
_, err = buf.Write([]byte{'\r', '\n'})
require.NoError(t, err)
}

chunkedReader := v4.NewChunkedReader(io.NopCloser(buf), streamSigner)
defer func() {
_ = chunkedReader.Close()
}()

chunk := make([]byte, 4096)
payload := bytes.NewBuffer(nil)
_, err = io.CopyBuffer(payload, chunkedReader, chunk)

require.Error(t, err)
})

t.Run("err missing between chunks separator", func(t *testing.T) {
streamSigner := v4.NewChunkSigner("us-east-1", "s3", seedSignature, ts, awsCreds)
chunkThreeBody := []byte("0;chunk-signatureb6c6ea8a5354eaf15b3cb7646744f4275b71ea724fed81ceb9323e279d449df9\n")

buf := bytes.NewBuffer(nil)
chunks := [][]byte{chunkOneBody, chunkTwoBody, chunkThreeBody}

for _, chunk := range chunks {
_, err = buf.Write(chunk)
require.NoError(t, err)
_, err = buf.Write([]byte{'\n'})
require.NoError(t, err)
}

chunkedReader := v4.NewChunkedReader(io.NopCloser(buf), streamSigner)
defer func() {
_ = chunkedReader.Close()
}()

chunk := make([]byte, 4096)
payload := bytes.NewBuffer(nil)
_, err = io.CopyBuffer(payload, chunkedReader, chunk)

require.ErrorIs(t, err, v4.ErrNoChunksSeparator)
})

t.Run("err chunk header too long", func(t *testing.T) {
streamSigner := v4.NewChunkSigner("us-east-1", "s3", seedSignature, ts, awsCreds)
chunkThreeBody := make([]byte, 4097)
for i := 0; i < len(chunkThreeBody); i++ {
chunkThreeBody[i] = 'a'
}

chunkThreeBody[4] = ';'
chunkThreeBody[len(chunkThreeBody)-1] = '\n'

buf := bytes.NewBuffer(nil)
chunks := [][]byte{chunkOneBody, chunkTwoBody, chunkThreeBody}

for _, chunk := range chunks {
_, err = buf.Write(chunk)
require.NoError(t, err)
_, err = buf.Write([]byte{'\r', '\n'})
require.NoError(t, err)
}

chunkedReader := v4.NewChunkedReader(io.NopCloser(buf), streamSigner)
defer func() {
_ = chunkedReader.Close()
}()

chunk := make([]byte, 4096)
payload := bytes.NewBuffer(nil)
_, err = io.CopyBuffer(payload, chunkedReader, chunk)

require.ErrorIs(t, err, v4.ErrLineTooLong)
})
}
76 changes: 76 additions & 0 deletions api/auth/signer/v4/chunk_signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package v4

import (
"encoding/hex"
"hash"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws/credentials"
)

const (
// precalculated hash for the zero chunk length.
emptyChunkSHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
)

// ChunkSigner implements signing of aws-chunked payloads.
type ChunkSigner struct {
region string
service string

credentials credentialValueProvider

prevSig []byte
seedDate time.Time
}

// NewChunkSigner creates a SigV4 signer used to sign Event Stream encoded messages.
func NewChunkSigner(region, service string, seedSignature []byte, seedDate time.Time, credentials *credentials.Credentials) *ChunkSigner {
return &ChunkSigner{
region: region,
service: service,
credentials: credentials,
seedDate: seedDate,
prevSig: seedSignature,
}
}

// GetSignature takes an event stream encoded headers and payload and returns a signature.
func (s *ChunkSigner) GetSignature(payload []byte) ([]byte, error) {
return s.getSignature(hashSHA256(payload))
}

// GetSignatureByHash takes an event stream encoded headers and payload and returns a signature.
func (s *ChunkSigner) GetSignatureByHash(payloadHash hash.Hash) ([]byte, error) {
return s.getSignature(payloadHash.Sum(nil))
}

func (s *ChunkSigner) getSignature(payloadHash []byte) ([]byte, error) {
credValue, err := s.credentials.Get()
if err != nil {
return nil, err
}

sigKey := deriveSigningKey(s.region, s.service, credValue.SecretAccessKey, s.seedDate)

keyPath := buildSigningScope(s.region, s.service, s.seedDate)

stringToSign := buildStringToSign(payloadHash, s.prevSig, keyPath, s.seedDate)

signature := hmacSHA256(sigKey, []byte(stringToSign))
s.prevSig = signature

return signature, nil
}

func buildStringToSign(payloadHash, prevSig []byte, scope string, date time.Time) string {
return strings.Join([]string{
"AWS4-HMAC-SHA256-PAYLOAD",
formatTime(date),
scope,
hex.EncodeToString(prevSig),
emptyChunkSHA256,
hex.EncodeToString(payloadHash),
}, "\n")
}
Loading

0 comments on commit 32f233e

Please sign in to comment.