storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/streaming-signature-v4.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   *     http://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 cmd This file implements helper functions to validate Streaming AWS
    18  // Signature Version '4' authorization header.
    19  package cmd
    20  
    21  import (
    22  	"bufio"
    23  	"bytes"
    24  	"crypto/sha256"
    25  	"encoding/hex"
    26  	"errors"
    27  	"hash"
    28  	"io"
    29  	"net/http"
    30  	"time"
    31  
    32  	humanize "github.com/dustin/go-humanize"
    33  
    34  	xhttp "storj.io/minio/cmd/http"
    35  	"storj.io/minio/pkg/auth"
    36  )
    37  
    38  // Streaming AWS Signature Version '4' constants.
    39  const (
    40  	emptySHA256              = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    41  	streamingContentSHA256   = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
    42  	signV4ChunkedAlgorithm   = "AWS4-HMAC-SHA256-PAYLOAD"
    43  	streamingContentEncoding = "aws-chunked"
    44  )
    45  
    46  // getChunkSignature - get chunk signature.
    47  func getChunkSignature(cred auth.Credentials, seedSignature string, region string, date time.Time, hashedChunk string) string {
    48  	// Calculate string to sign.
    49  	stringToSign := signV4ChunkedAlgorithm + "\n" +
    50  		date.Format(iso8601Format) + "\n" +
    51  		getScope(date, region) + "\n" +
    52  		seedSignature + "\n" +
    53  		emptySHA256 + "\n" +
    54  		hashedChunk
    55  
    56  	// Get hmac signing key.
    57  	signingKey := getSigningKey(cred.SecretKey, date, region, serviceS3)
    58  
    59  	// Calculate signature.
    60  	newSignature := getSignature(signingKey, stringToSign)
    61  
    62  	return newSignature
    63  }
    64  
    65  // calculateSeedSignature - Calculate seed signature in accordance with
    66  //     - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
    67  // returns signature, error otherwise if the signature mismatches or any other
    68  // error while parsing and validating.
    69  func calculateSeedSignature(r *http.Request) (cred auth.Credentials, signature string, region string, date time.Time, errCode APIErrorCode) {
    70  	// Copy request.
    71  	req := *r
    72  
    73  	// Save authorization header.
    74  	v4Auth := req.Header.Get(xhttp.Authorization)
    75  
    76  	// Parse signature version '4' header.
    77  	signV4Values, errCode := parseSignV4(v4Auth, globalServerRegion, serviceS3)
    78  	if errCode != ErrNone {
    79  		return cred, "", "", time.Time{}, errCode
    80  	}
    81  
    82  	// Payload streaming.
    83  	payload := streamingContentSHA256
    84  
    85  	// Payload for STREAMING signature should be 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD'
    86  	if payload != req.Header.Get(xhttp.AmzContentSha256) {
    87  		return cred, "", "", time.Time{}, ErrContentSHA256Mismatch
    88  	}
    89  
    90  	// Extract all the signed headers along with its values.
    91  	extractedSignedHeaders, errCode := extractSignedHeaders(signV4Values.SignedHeaders, r)
    92  	if errCode != ErrNone {
    93  		return cred, "", "", time.Time{}, errCode
    94  	}
    95  
    96  	cred, _, errCode = checkKeyValid(req.Context(), signV4Values.Credential.accessKey)
    97  	if errCode != ErrNone {
    98  		return cred, "", "", time.Time{}, errCode
    99  	}
   100  
   101  	// Verify if region is valid.
   102  	region = signV4Values.Credential.scope.region
   103  
   104  	// Extract date, if not present throw error.
   105  	var dateStr string
   106  	if dateStr = req.Header.Get("x-amz-date"); dateStr == "" {
   107  		if dateStr = r.Header.Get("Date"); dateStr == "" {
   108  			return cred, "", "", time.Time{}, ErrMissingDateHeader
   109  		}
   110  	}
   111  
   112  	// Parse date header.
   113  	var err error
   114  	date, err = time.Parse(iso8601Format, dateStr)
   115  	if err != nil {
   116  		return cred, "", "", time.Time{}, ErrMissingDateHeader
   117  	}
   118  
   119  	// Query string.
   120  	queryStr := req.URL.Query().Encode()
   121  
   122  	// Get canonical request.
   123  	canonicalRequest := getCanonicalRequest(extractedSignedHeaders, payload, queryStr, req.URL.Path, req.Method)
   124  
   125  	// Get string to sign from canonical request.
   126  	stringToSign := getStringToSign(canonicalRequest, date, signV4Values.Credential.getScope())
   127  
   128  	// Get hmac signing key.
   129  	signingKey := getSigningKey(cred.SecretKey, signV4Values.Credential.scope.date, region, serviceS3)
   130  
   131  	// Calculate signature.
   132  	newSignature := getSignature(signingKey, stringToSign)
   133  
   134  	// Verify if signature match.
   135  	if !compareSignatureV4(newSignature, signV4Values.Signature) {
   136  		return cred, "", "", time.Time{}, ErrSignatureDoesNotMatch
   137  	}
   138  
   139  	// Return caculated signature.
   140  	return cred, newSignature, region, date, ErrNone
   141  }
   142  
   143  const maxLineLength = 4 * humanize.KiByte // assumed <= bufio.defaultBufSize 4KiB
   144  
   145  // lineTooLong is generated as chunk header is bigger than 4KiB.
   146  var errLineTooLong = errors.New("header line too long")
   147  
   148  // Malformed encoding is generated when chunk header is wrongly formed.
   149  var errMalformedEncoding = errors.New("malformed chunked encoding")
   150  
   151  // newSignV4ChunkedReader returns a new s3ChunkedReader that translates the data read from r
   152  // out of HTTP "chunked" format before returning it.
   153  // The s3ChunkedReader returns io.EOF when the final 0-length chunk is read.
   154  //
   155  // NewChunkedReader is not needed by normal applications. The http package
   156  // automatically decodes chunking when reading response bodies.
   157  func newSignV4ChunkedReader(req *http.Request) (io.ReadCloser, APIErrorCode) {
   158  	cred, seedSignature, region, seedDate, errCode := calculateSeedSignature(req)
   159  	if errCode != ErrNone {
   160  		return nil, errCode
   161  	}
   162  
   163  	return &s3ChunkedReader{
   164  		reader:            bufio.NewReader(req.Body),
   165  		cred:              cred,
   166  		seedSignature:     seedSignature,
   167  		seedDate:          seedDate,
   168  		region:            region,
   169  		chunkSHA256Writer: sha256.New(),
   170  		buffer:            make([]byte, 64*1024),
   171  	}, ErrNone
   172  }
   173  
   174  // Represents the overall state that is required for decoding a
   175  // AWS Signature V4 chunked reader.
   176  type s3ChunkedReader struct {
   177  	reader        *bufio.Reader
   178  	cred          auth.Credentials
   179  	seedSignature string
   180  	seedDate      time.Time
   181  	region        string
   182  
   183  	chunkSHA256Writer hash.Hash // Calculates sha256 of chunk data.
   184  	buffer            []byte
   185  	offset            int
   186  	err               error
   187  }
   188  
   189  func (cr *s3ChunkedReader) Close() (err error) {
   190  	return nil
   191  }
   192  
   193  // Read - implements `io.Reader`, which transparently decodes
   194  // the incoming AWS Signature V4 streaming signature.
   195  func (cr *s3ChunkedReader) Read(buf []byte) (n int, err error) {
   196  	// First, if there is any unread data, copy it to the client
   197  	// provided buffer.
   198  	if cr.offset > 0 {
   199  		n = copy(buf, cr.buffer[cr.offset:])
   200  		if n == len(buf) {
   201  			cr.offset += n
   202  			return n, nil
   203  		}
   204  		cr.offset = 0
   205  		buf = buf[n:]
   206  	}
   207  
   208  	// Now, we read one chunk from the underlying reader.
   209  	// A chunk has the following format:
   210  	//   <chunk-size-as-hex> + ";chunk-signature=" + <signature-as-hex> + "\r\n" + <payload> + "\r\n"
   211  	//
   212  	// Frist, we read the chunk size but fail if it is larger
   213  	// than 1 MB. We must not accept arbitrary large chunks.
   214  	// One 1 MB is a reasonable max limit.
   215  	//
   216  	// Then we read the signature and payload data. We compute the SHA256 checksum
   217  	// of the payload and verify that it matches the expected signature value.
   218  	//
   219  	// The last chunk is *always* 0-sized. So, we must only return io.EOF if we have encountered
   220  	// a chunk with a chunk size = 0. However, this chunk still has a signature and we must
   221  	// verify it.
   222  	const MaxSize = 1 << 20 // 1 MB
   223  	var size int
   224  	for {
   225  		b, err := cr.reader.ReadByte()
   226  		if err == io.EOF {
   227  			err = io.ErrUnexpectedEOF
   228  		}
   229  		if err != nil {
   230  			cr.err = err
   231  			return n, cr.err
   232  		}
   233  		if b == ';' { // separating character
   234  			break
   235  		}
   236  
   237  		// Manually deserialize the size since AWS specified
   238  		// the chunk size to be of variable width. In particular,
   239  		// a size of 16 is encoded as `10` while a size of 64 KB
   240  		// is `10000`.
   241  		switch {
   242  		case b >= '0' && b <= '9':
   243  			size = size<<4 | int(b-'0')
   244  		case b >= 'a' && b <= 'f':
   245  			size = size<<4 | int(b-('a'-10))
   246  		case b >= 'A' && b <= 'F':
   247  			size = size<<4 | int(b-('A'-10))
   248  		default:
   249  			cr.err = errMalformedEncoding
   250  			return n, cr.err
   251  		}
   252  		if size > MaxSize {
   253  			cr.err = errMalformedEncoding
   254  			return n, cr.err
   255  		}
   256  	}
   257  
   258  	// Now, we read the signature of the following payload and expect:
   259  	//   chunk-signature=" + <signature-as-hex> + "\r\n"
   260  	//
   261  	// The signature is 64 bytes long (hex-encoded SHA256 hash) and
   262  	// starts with a 16 byte header: len("chunk-signature=") + 64 == 80.
   263  	var signature [80]byte
   264  	_, err = io.ReadFull(cr.reader, signature[:])
   265  	if err == io.EOF {
   266  		err = io.ErrUnexpectedEOF
   267  	}
   268  	if err != nil {
   269  		cr.err = err
   270  		return n, cr.err
   271  	}
   272  	if !bytes.HasPrefix(signature[:], []byte("chunk-signature=")) {
   273  		cr.err = errMalformedEncoding
   274  		return n, cr.err
   275  	}
   276  	b, err := cr.reader.ReadByte()
   277  	if err == io.EOF {
   278  		err = io.ErrUnexpectedEOF
   279  	}
   280  	if err != nil {
   281  		cr.err = err
   282  		return n, cr.err
   283  	}
   284  	if b != '\r' {
   285  		cr.err = errMalformedEncoding
   286  		return n, cr.err
   287  	}
   288  	b, err = cr.reader.ReadByte()
   289  	if err == io.EOF {
   290  		err = io.ErrUnexpectedEOF
   291  	}
   292  	if err != nil {
   293  		cr.err = err
   294  		return n, cr.err
   295  	}
   296  	if b != '\n' {
   297  		cr.err = errMalformedEncoding
   298  		return n, cr.err
   299  	}
   300  
   301  	if cap(cr.buffer) < size {
   302  		cr.buffer = make([]byte, size)
   303  	} else {
   304  		cr.buffer = cr.buffer[:size]
   305  	}
   306  
   307  	// Now, we read the payload and compute its SHA-256 hash.
   308  	_, err = io.ReadFull(cr.reader, cr.buffer)
   309  	if err == io.EOF && size != 0 {
   310  		err = io.ErrUnexpectedEOF
   311  	}
   312  	if err != nil && err != io.EOF {
   313  		cr.err = err
   314  		return n, cr.err
   315  	}
   316  	b, err = cr.reader.ReadByte()
   317  	if b != '\r' {
   318  		cr.err = errMalformedEncoding
   319  		return n, cr.err
   320  	}
   321  	b, err = cr.reader.ReadByte()
   322  	if err == io.EOF {
   323  		err = io.ErrUnexpectedEOF
   324  	}
   325  	if err != nil {
   326  		cr.err = err
   327  		return n, cr.err
   328  	}
   329  	if b != '\n' {
   330  		cr.err = errMalformedEncoding
   331  		return n, cr.err
   332  	}
   333  
   334  	// Once we have read the entire chunk successfully, we verify
   335  	// that the received signature matches our computed signature.
   336  	cr.chunkSHA256Writer.Write(cr.buffer)
   337  	newSignature := getChunkSignature(cr.cred, cr.seedSignature, cr.region, cr.seedDate, hex.EncodeToString(cr.chunkSHA256Writer.Sum(nil)))
   338  	if !compareSignatureV4(string(signature[16:]), newSignature) {
   339  		cr.err = errSignatureMismatch
   340  		return n, cr.err
   341  	}
   342  	cr.seedSignature = newSignature
   343  	cr.chunkSHA256Writer.Reset()
   344  
   345  	// If the chunk size is zero we return io.EOF. As specified by AWS,
   346  	// only the last chunk is zero-sized.
   347  	if size == 0 {
   348  		cr.err = io.EOF
   349  		return n, cr.err
   350  	}
   351  
   352  	cr.offset = copy(buf, cr.buffer)
   353  	n += cr.offset
   354  	return n, err
   355  }
   356  
   357  // readCRLF - check if reader only has '\r\n' CRLF character.
   358  // returns malformed encoding if it doesn't.
   359  func readCRLF(reader io.Reader) error {
   360  	buf := make([]byte, 2)
   361  	_, err := io.ReadFull(reader, buf[:2])
   362  	if err != nil {
   363  		return err
   364  	}
   365  	if buf[0] != '\r' || buf[1] != '\n' {
   366  		return errMalformedEncoding
   367  	}
   368  	return nil
   369  }
   370  
   371  // Read a line of bytes (up to \n) from b.
   372  // Give up if the line exceeds maxLineLength.
   373  // The returned bytes are owned by the bufio.Reader
   374  // so they are only valid until the next bufio read.
   375  func readChunkLine(b *bufio.Reader) ([]byte, []byte, error) {
   376  	buf, err := b.ReadSlice('\n')
   377  	if err != nil {
   378  		// We always know when EOF is coming.
   379  		// If the caller asked for a line, there should be a line.
   380  		if err == io.EOF {
   381  			err = io.ErrUnexpectedEOF
   382  		} else if err == bufio.ErrBufferFull {
   383  			err = errLineTooLong
   384  		}
   385  		return nil, nil, err
   386  	}
   387  	if len(buf) >= maxLineLength {
   388  		return nil, nil, errLineTooLong
   389  	}
   390  	// Parse s3 specific chunk extension and fetch the values.
   391  	hexChunkSize, hexChunkSignature := parseS3ChunkExtension(buf)
   392  	return hexChunkSize, hexChunkSignature, nil
   393  }
   394  
   395  // trimTrailingWhitespace - trim trailing white space.
   396  func trimTrailingWhitespace(b []byte) []byte {
   397  	for len(b) > 0 && isASCIISpace(b[len(b)-1]) {
   398  		b = b[:len(b)-1]
   399  	}
   400  	return b
   401  }
   402  
   403  // isASCIISpace - is ascii space?
   404  func isASCIISpace(b byte) bool {
   405  	return b == ' ' || b == '\t' || b == '\n' || b == '\r'
   406  }
   407  
   408  // Constant s3 chunk encoding signature.
   409  const s3ChunkSignatureStr = ";chunk-signature="
   410  
   411  // parses3ChunkExtension removes any s3 specific chunk-extension from buf.
   412  // For example,
   413  //     "10000;chunk-signature=..." => "10000", "chunk-signature=..."
   414  func parseS3ChunkExtension(buf []byte) ([]byte, []byte) {
   415  	buf = trimTrailingWhitespace(buf)
   416  	semi := bytes.Index(buf, []byte(s3ChunkSignatureStr))
   417  	// Chunk signature not found, return the whole buffer.
   418  	if semi == -1 {
   419  		return buf, nil
   420  	}
   421  	return buf[:semi], parseChunkSignature(buf[semi:])
   422  }
   423  
   424  // parseChunkSignature - parse chunk signature.
   425  func parseChunkSignature(chunk []byte) []byte {
   426  	chunkSplits := bytes.SplitN(chunk, []byte(s3ChunkSignatureStr), 2)
   427  	return chunkSplits[1]
   428  }
   429  
   430  // parse hex to uint64.
   431  func parseHexUint(v []byte) (n uint64, err error) {
   432  	for i, b := range v {
   433  		switch {
   434  		case '0' <= b && b <= '9':
   435  			b = b - '0'
   436  		case 'a' <= b && b <= 'f':
   437  			b = b - 'a' + 10
   438  		case 'A' <= b && b <= 'F':
   439  			b = b - 'A' + 10
   440  		default:
   441  			return 0, errors.New("invalid byte in chunk length")
   442  		}
   443  		if i == 16 {
   444  			return 0, errors.New("http chunk length too large")
   445  		}
   446  		n <<= 4
   447  		n |= uint64(b)
   448  	}
   449  	return
   450  }