github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/gateway/sig/v4_streaming_reader.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2016 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     https://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  // Package sig This file implements helper functions to validate Streaming AWS
    18  // Signature Version '4' authorization header.
    19  package sig
    20  
    21  import (
    22  	"bufio"
    23  	"bytes"
    24  	"crypto/sha256"
    25  	"encoding/hex"
    26  	"errors"
    27  	"hash"
    28  	"io"
    29  	"strings"
    30  	"time"
    31  
    32  	"github.com/treeverse/lakefs/pkg/auth/model"
    33  	gwerrors "github.com/treeverse/lakefs/pkg/gateway/errors"
    34  )
    35  
    36  // Streaming AWS Signature Version '4' constants.
    37  const (
    38  	emptySHA256            = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" //nolint:gosec
    39  	signV4ChunkedAlgorithm = "AWS4-HMAC-SHA256-PAYLOAD"                                         //nolint:gosec
    40  	SlashSeparator         = "/"
    41  )
    42  
    43  // getScope generate a string of a specific date, an AWS region, and a service.
    44  func getScope(t time.Time, region, service string) string {
    45  	scope := strings.Join([]string{
    46  		t.Format(v4shortTimeFormat),
    47  		region,
    48  		service,
    49  		v4scopeTerminator,
    50  	}, SlashSeparator)
    51  	return scope
    52  }
    53  
    54  // getChunkSignature - get chunk signature.
    55  func getChunkSignature(cred *model.Credential, seedSignature string, region string, service string, date time.Time, hashedChunk string) string {
    56  	// Calculate string to sign.
    57  	stringToSign := signV4ChunkedAlgorithm + "\n" +
    58  		date.Format(v4timeFormat) + "\n" +
    59  		getScope(date, region, service) + "\n" +
    60  		seedSignature + "\n" +
    61  		emptySHA256 + "\n" +
    62  		hashedChunk
    63  
    64  	// Get hmac signing key.
    65  	signingKey := createSignature(cred.SecretAccessKey, date.Format(v4shortTimeFormat), region, service)
    66  
    67  	// Calculate signature.
    68  	newSignature := hex.EncodeToString(sign(signingKey, stringToSign))
    69  
    70  	return newSignature
    71  }
    72  
    73  const maxLineLength = 4 * 1024
    74  
    75  var (
    76  	// lineTooLong is generated as chunk header is bigger than 4KiB.
    77  	errLineTooLong = errors.New("header line too long")
    78  
    79  	// Malformed encoding is generated when a chunk header is wrongly formed.
    80  	errMalformedEncoding = errors.New("malformed chunked encoding")
    81  
    82  	ErrInvalidByte   = errors.New("invalid byte in chunk length")
    83  	ErrChunkTooLarge = errors.New("http chunk length too large")
    84  )
    85  
    86  // newSignV4ChunkedReader returns a new s3ChunkedReader that translates the data read from r
    87  // out of HTTP "chunked" format before returning it.
    88  // The s3ChunkedReader returns io.EOF when the final 0-length chunk is read.
    89  //
    90  // NewChunkedReader is not needed by normal applications. The http package
    91  // automatically decodes chunking when reading response bodies.
    92  func newSignV4ChunkedReader(reader *bufio.Reader, amzDate string, auth V4Auth, creds *model.Credential) (io.ReadCloser, error) {
    93  	seedDate, err := time.Parse(v4timeFormat, amzDate)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  	return &s3ChunkedReader{
    98  		reader:            reader,
    99  		cred:              creds,
   100  		seedSignature:     auth.Signature,
   101  		seedDate:          seedDate,
   102  		region:            auth.Region,
   103  		service:           auth.Service,
   104  		chunkSHA256Writer: sha256.New(),
   105  		state:             readChunkHeader,
   106  	}, nil
   107  }
   108  
   109  // Represents the overall state that is required for decoding an
   110  // AWS Signature V4 chunked reader.
   111  type s3ChunkedReader struct {
   112  	reader            *bufio.Reader
   113  	cred              *model.Credential
   114  	seedSignature     string
   115  	seedDate          time.Time
   116  	region            string
   117  	service           string
   118  	state             chunkState
   119  	lastChunk         bool
   120  	chunkSignature    string
   121  	chunkSHA256Writer hash.Hash // Calculates sha256 of chunk data.
   122  	n                 uint64    // Unread bytes in chunk
   123  	err               error
   124  }
   125  
   126  // Read chunk reads the chunk token signature portion.
   127  func (cr *s3ChunkedReader) readS3ChunkHeader() {
   128  	// Read the first chunk line until CRLF.
   129  	var hexChunkSize, hexChunkSignature []byte
   130  	hexChunkSize, hexChunkSignature, cr.err = readChunkLine(cr.reader)
   131  	if cr.err != nil {
   132  		return
   133  	}
   134  	// <hex>;token=value - converts the hex into its uint64 form.
   135  	cr.n, cr.err = parseHexUint(hexChunkSize)
   136  	if cr.err != nil {
   137  		return
   138  	}
   139  	if cr.n == 0 {
   140  		cr.err = io.EOF
   141  	}
   142  	// Save the incoming chunk signature.
   143  	cr.chunkSignature = string(hexChunkSignature)
   144  }
   145  
   146  type chunkState int
   147  
   148  const (
   149  	readChunkHeader chunkState = iota
   150  	readChunkTrailer
   151  	readChunk
   152  	verifyChunk
   153  	eofChunk
   154  )
   155  
   156  func (cs chunkState) String() string {
   157  	stateString := ""
   158  	switch cs {
   159  	case readChunkHeader:
   160  		stateString = "readChunkHeader"
   161  	case readChunkTrailer:
   162  		stateString = "readChunkTrailer"
   163  	case readChunk:
   164  		stateString = "readChunk"
   165  	case verifyChunk:
   166  		stateString = "verifyChunk"
   167  	case eofChunk:
   168  		stateString = "eofChunk"
   169  	}
   170  	return stateString
   171  }
   172  
   173  func (cr *s3ChunkedReader) Close() (err error) {
   174  	return nil
   175  }
   176  
   177  // Read - implements `io.Reader`, which transparently decodes
   178  // the incoming AWS Signature V4 streaming signature.
   179  func (cr *s3ChunkedReader) Read(buf []byte) (n int, err error) {
   180  	for {
   181  		switch cr.state {
   182  		case readChunkHeader:
   183  			cr.readS3ChunkHeader()
   184  			// If we're at the end of a chunk.
   185  			if cr.n == 0 && cr.err == io.EOF {
   186  				cr.state = readChunkTrailer
   187  				cr.lastChunk = true
   188  				continue
   189  			}
   190  			if cr.err != nil {
   191  				return 0, cr.err
   192  			}
   193  			cr.state = readChunk
   194  		case readChunkTrailer:
   195  			cr.err = readCRLF(cr.reader)
   196  			if cr.err != nil {
   197  				return 0, errMalformedEncoding
   198  			}
   199  			cr.state = verifyChunk
   200  		case readChunk:
   201  			// There is no more space left in the request buffer.
   202  			if len(buf) == 0 {
   203  				return n, nil
   204  			}
   205  			rbuf := buf
   206  			// The request buffer is larger than the current chunk size.
   207  			// Read only the current chunk from the underlying reader.
   208  			if uint64(len(rbuf)) > cr.n {
   209  				rbuf = rbuf[:cr.n]
   210  			}
   211  			var n0 int
   212  			n0, cr.err = cr.reader.Read(rbuf)
   213  			if cr.err != nil {
   214  				// We have lesser than chunk size advertised in chunkHeader, this is 'unexpected'.
   215  				if cr.err == io.EOF {
   216  					cr.err = io.ErrUnexpectedEOF
   217  				}
   218  				return 0, cr.err
   219  			}
   220  
   221  			// Calculate sha256.
   222  			if _, err := cr.chunkSHA256Writer.Write(rbuf[:n0]); err != nil {
   223  				return 0, err
   224  			}
   225  			// Update the bytes read into request buffer so far.
   226  			n += n0
   227  			buf = buf[n0:]
   228  			// Update bytes to be read of the current chunk before verifying chunk's signature.
   229  			cr.n -= uint64(n0)
   230  
   231  			// If we're at the end of a chunk.
   232  			if cr.n == 0 {
   233  				cr.state = readChunkTrailer
   234  				continue
   235  			}
   236  		case verifyChunk:
   237  			// Calculate the hashed chunk.
   238  			hashedChunk := hex.EncodeToString(cr.chunkSHA256Writer.Sum(nil))
   239  			// Calculate the chunk signature.
   240  			newSignature := getChunkSignature(cr.cred, cr.seedSignature, cr.region, cr.service, cr.seedDate, hashedChunk)
   241  			if !Equal([]byte(cr.chunkSignature), []byte(newSignature)) {
   242  				return 0, gwerrors.ErrSignatureDoesNotMatch
   243  			}
   244  			// Newly calculated signature becomes the seed for the next chunk
   245  			// this follows the chaining.
   246  			cr.seedSignature = newSignature
   247  			cr.chunkSHA256Writer.Reset()
   248  			if cr.lastChunk {
   249  				cr.state = eofChunk
   250  			} else {
   251  				cr.state = readChunkHeader
   252  			}
   253  		case eofChunk:
   254  			return n, io.EOF
   255  		}
   256  	}
   257  }
   258  
   259  // readCRLF - check if reader only has '\r\n' CRLF character.
   260  // returns malformed encoding if it doesn't.
   261  func readCRLF(reader io.Reader) error {
   262  	buf := make([]byte, 2)                 //nolint: mnd
   263  	_, err := io.ReadFull(reader, buf[:2]) //nolint: mnd
   264  	if err != nil {
   265  		return err
   266  	}
   267  	if buf[0] != '\r' || buf[1] != '\n' {
   268  		return errMalformedEncoding
   269  	}
   270  	return nil
   271  }
   272  
   273  // Read a line of bytes (up to \n) from b.
   274  // Give up if the line exceeds maxLineLength.
   275  // The returned bytes are owned by the bufio.Reader
   276  // so they are only valid until the next bufio read.
   277  func readChunkLine(b *bufio.Reader) ([]byte, []byte, error) {
   278  	buf, err := b.ReadSlice('\n')
   279  	if err != nil {
   280  		// We always know when EOF is coming.
   281  		// If the caller asked for a line, there should be a line.
   282  		if err == io.EOF {
   283  			err = io.ErrUnexpectedEOF
   284  		} else if errors.Is(err, bufio.ErrBufferFull) {
   285  			err = errLineTooLong
   286  		}
   287  		return nil, nil, err
   288  	}
   289  	if len(buf) >= maxLineLength {
   290  		return nil, nil, errLineTooLong
   291  	}
   292  	// Parse s3 specific chunk extension and fetch the values.
   293  	hexChunkSize, hexChunkSignature := parseS3ChunkExtension(buf)
   294  	return hexChunkSize, hexChunkSignature, nil
   295  }
   296  
   297  // trimTrailingWhitespace - trim trailing white space.
   298  func trimTrailingWhitespace(b []byte) []byte {
   299  	for len(b) > 0 && isASCIISpace(b[len(b)-1]) {
   300  		b = b[:len(b)-1]
   301  	}
   302  	return b
   303  }
   304  
   305  // isASCIISpace - is ascii space?
   306  func isASCIISpace(b byte) bool {
   307  	return b == ' ' || b == '\t' || b == '\n' || b == '\r'
   308  }
   309  
   310  // Constant s3 chunk encoding signature.
   311  const s3ChunkSignatureStr = ";chunk-signature="
   312  
   313  // parses3ChunkExtension removes any s3 specific chunk-extension from buf.
   314  // For example,
   315  //
   316  //	"10000;chunk-signature=..." => "10000", "chunk-signature=..."
   317  func parseS3ChunkExtension(buf []byte) ([]byte, []byte) {
   318  	buf = trimTrailingWhitespace(buf)
   319  	semi := bytes.Index(buf, []byte(s3ChunkSignatureStr))
   320  	// Chunk signature not found, return the whole buffer.
   321  	if semi == -1 {
   322  		return buf, nil
   323  	}
   324  	return buf[:semi], parseChunkSignature(buf[semi:])
   325  }
   326  
   327  // parseChunkSignature - parse chunk signature.
   328  func parseChunkSignature(chunk []byte) []byte {
   329  	chunkSplits := bytes.SplitN(chunk, []byte(s3ChunkSignatureStr), 2) //nolint: mnd
   330  	return chunkSplits[1]
   331  }
   332  
   333  // parse hex to uint64.
   334  func parseHexUint(v []byte) (n uint64, err error) {
   335  	const maxChunkLength = 16
   336  	const letterOffset = 10
   337  	for i, b := range v {
   338  		switch {
   339  		case '0' <= b && b <= '9':
   340  			b -= '0'
   341  		case 'a' <= b && b <= 'f':
   342  			b -= 'a' - letterOffset
   343  		case 'A' <= b && b <= 'F':
   344  			b -= 'A' - letterOffset
   345  		default:
   346  			return 0, ErrInvalidByte
   347  		}
   348  		if i == maxChunkLength {
   349  			return 0, ErrChunkTooLarge
   350  		}
   351  		n <<= 4
   352  		n |= uint64(b)
   353  	}
   354  	return
   355  }