github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/streaming-signature-v4.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  // Package cmd This file implements helper functions to validate Streaming AWS
    19  // Signature Version '4' authorization header.
    20  package cmd
    21  
    22  import (
    23  	"bufio"
    24  	"bytes"
    25  	"encoding/hex"
    26  	"errors"
    27  	"fmt"
    28  	"hash"
    29  	"io"
    30  	"net/http"
    31  	"strings"
    32  	"time"
    33  
    34  	"github.com/dustin/go-humanize"
    35  	"github.com/minio/minio/internal/auth"
    36  	"github.com/minio/minio/internal/hash/sha256"
    37  	xhttp "github.com/minio/minio/internal/http"
    38  )
    39  
    40  // Streaming AWS Signature Version '4' constants.
    41  const (
    42  	emptySHA256                   = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    43  	streamingContentSHA256        = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
    44  	streamingContentSHA256Trailer = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER"
    45  	signV4ChunkedAlgorithm        = "AWS4-HMAC-SHA256-PAYLOAD"
    46  	signV4ChunkedAlgorithmTrailer = "AWS4-HMAC-SHA256-TRAILER"
    47  	streamingContentEncoding      = "aws-chunked"
    48  	awsTrailerHeader              = "X-Amz-Trailer"
    49  	trailerKVSeparator            = ":"
    50  )
    51  
    52  // getChunkSignature - get chunk signature.
    53  // Does not update anything in cr.
    54  func (cr *s3ChunkedReader) getChunkSignature() string {
    55  	hashedChunk := hex.EncodeToString(cr.chunkSHA256Writer.Sum(nil))
    56  
    57  	// Calculate string to sign.
    58  	alg := signV4ChunkedAlgorithm + "\n"
    59  	stringToSign := alg +
    60  		cr.seedDate.Format(iso8601Format) + "\n" +
    61  		getScope(cr.seedDate, cr.region) + "\n" +
    62  		cr.seedSignature + "\n" +
    63  		emptySHA256 + "\n" +
    64  		hashedChunk
    65  
    66  	// Get hmac signing key.
    67  	signingKey := getSigningKey(cr.cred.SecretKey, cr.seedDate, cr.region, serviceS3)
    68  
    69  	// Calculate signature.
    70  	newSignature := getSignature(signingKey, stringToSign)
    71  
    72  	return newSignature
    73  }
    74  
    75  // getTrailerChunkSignature - get trailer chunk signature.
    76  func (cr *s3ChunkedReader) getTrailerChunkSignature() string {
    77  	hashedChunk := hex.EncodeToString(cr.chunkSHA256Writer.Sum(nil))
    78  
    79  	// Calculate string to sign.
    80  	alg := signV4ChunkedAlgorithmTrailer + "\n"
    81  	stringToSign := alg +
    82  		cr.seedDate.Format(iso8601Format) + "\n" +
    83  		getScope(cr.seedDate, cr.region) + "\n" +
    84  		cr.seedSignature + "\n" +
    85  		hashedChunk
    86  
    87  	// Get hmac signing key.
    88  	signingKey := getSigningKey(cr.cred.SecretKey, cr.seedDate, cr.region, serviceS3)
    89  
    90  	// Calculate signature.
    91  	newSignature := getSignature(signingKey, stringToSign)
    92  
    93  	return newSignature
    94  }
    95  
    96  // calculateSeedSignature - Calculate seed signature in accordance with
    97  //   - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
    98  //
    99  // returns signature, error otherwise if the signature mismatches or any other
   100  // error while parsing and validating.
   101  func calculateSeedSignature(r *http.Request, trailers bool) (cred auth.Credentials, signature string, region string, date time.Time, errCode APIErrorCode) {
   102  	// Copy request.
   103  	req := *r
   104  
   105  	// Save authorization header.
   106  	v4Auth := req.Header.Get(xhttp.Authorization)
   107  
   108  	// Parse signature version '4' header.
   109  	signV4Values, errCode := parseSignV4(v4Auth, globalSite.Region, serviceS3)
   110  	if errCode != ErrNone {
   111  		return cred, "", "", time.Time{}, errCode
   112  	}
   113  
   114  	// Payload streaming.
   115  	payload := streamingContentSHA256
   116  	if trailers {
   117  		payload = streamingContentSHA256Trailer
   118  	}
   119  
   120  	// Payload for STREAMING signature should be 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD'
   121  	if payload != req.Header.Get(xhttp.AmzContentSha256) {
   122  		return cred, "", "", time.Time{}, ErrContentSHA256Mismatch
   123  	}
   124  
   125  	// Extract all the signed headers along with its values.
   126  	extractedSignedHeaders, errCode := extractSignedHeaders(signV4Values.SignedHeaders, r)
   127  	if errCode != ErrNone {
   128  		return cred, "", "", time.Time{}, errCode
   129  	}
   130  
   131  	cred, _, errCode = checkKeyValid(r, signV4Values.Credential.accessKey)
   132  	if errCode != ErrNone {
   133  		return cred, "", "", time.Time{}, errCode
   134  	}
   135  
   136  	// Verify if region is valid.
   137  	region = signV4Values.Credential.scope.region
   138  
   139  	// Extract date, if not present throw error.
   140  	var dateStr string
   141  	if dateStr = req.Header.Get("x-amz-date"); dateStr == "" {
   142  		if dateStr = r.Header.Get("Date"); dateStr == "" {
   143  			return cred, "", "", time.Time{}, ErrMissingDateHeader
   144  		}
   145  	}
   146  
   147  	// Parse date header.
   148  	var err error
   149  	date, err = time.Parse(iso8601Format, dateStr)
   150  	if err != nil {
   151  		return cred, "", "", time.Time{}, ErrMalformedDate
   152  	}
   153  
   154  	// Query string.
   155  	queryStr := req.Form.Encode()
   156  
   157  	// Get canonical request.
   158  	canonicalRequest := getCanonicalRequest(extractedSignedHeaders, payload, queryStr, req.URL.Path, req.Method)
   159  
   160  	// Get string to sign from canonical request.
   161  	stringToSign := getStringToSign(canonicalRequest, date, signV4Values.Credential.getScope())
   162  
   163  	// Get hmac signing key.
   164  	signingKey := getSigningKey(cred.SecretKey, signV4Values.Credential.scope.date, region, serviceS3)
   165  
   166  	// Calculate signature.
   167  	newSignature := getSignature(signingKey, stringToSign)
   168  
   169  	// Verify if signature match.
   170  	if !compareSignatureV4(newSignature, signV4Values.Signature) {
   171  		return cred, "", "", time.Time{}, ErrSignatureDoesNotMatch
   172  	}
   173  
   174  	// Return calculated signature.
   175  	return cred, newSignature, region, date, ErrNone
   176  }
   177  
   178  const maxLineLength = 4 * humanize.KiByte // assumed <= bufio.defaultBufSize 4KiB
   179  
   180  // lineTooLong is generated as chunk header is bigger than 4KiB.
   181  var errLineTooLong = errors.New("header line too long")
   182  
   183  // malformed encoding is generated when chunk header is wrongly formed.
   184  var errMalformedEncoding = errors.New("malformed chunked encoding")
   185  
   186  // chunk is considered too big if its bigger than > 16MiB.
   187  var errChunkTooBig = errors.New("chunk too big: choose chunk size <= 16MiB")
   188  
   189  // newSignV4ChunkedReader returns a new s3ChunkedReader that translates the data read from r
   190  // out of HTTP "chunked" format before returning it.
   191  // The s3ChunkedReader returns io.EOF when the final 0-length chunk is read.
   192  //
   193  // NewChunkedReader is not needed by normal applications. The http package
   194  // automatically decodes chunking when reading response bodies.
   195  func newSignV4ChunkedReader(req *http.Request, trailer bool) (io.ReadCloser, APIErrorCode) {
   196  	cred, seedSignature, region, seedDate, errCode := calculateSeedSignature(req, trailer)
   197  	if errCode != ErrNone {
   198  		return nil, errCode
   199  	}
   200  
   201  	if trailer {
   202  		// Discard anything unsigned.
   203  		req.Trailer = make(http.Header)
   204  		trailers := req.Header.Values(awsTrailerHeader)
   205  		for _, key := range trailers {
   206  			req.Trailer.Add(key, "")
   207  		}
   208  	} else {
   209  		req.Trailer = nil
   210  	}
   211  	return &s3ChunkedReader{
   212  		trailers:          req.Trailer,
   213  		reader:            bufio.NewReader(req.Body),
   214  		cred:              cred,
   215  		seedSignature:     seedSignature,
   216  		seedDate:          seedDate,
   217  		region:            region,
   218  		chunkSHA256Writer: sha256.New(),
   219  		buffer:            make([]byte, 64*1024),
   220  		debug:             false,
   221  	}, ErrNone
   222  }
   223  
   224  // Represents the overall state that is required for decoding a
   225  // AWS Signature V4 chunked reader.
   226  type s3ChunkedReader struct {
   227  	reader        *bufio.Reader
   228  	cred          auth.Credentials
   229  	seedSignature string
   230  	seedDate      time.Time
   231  	region        string
   232  	trailers      http.Header
   233  
   234  	chunkSHA256Writer hash.Hash // Calculates sha256 of chunk data.
   235  	buffer            []byte
   236  	offset            int
   237  	err               error
   238  	debug             bool // Print details on failure. Add your own if more are needed.
   239  }
   240  
   241  func (cr *s3ChunkedReader) Close() (err error) {
   242  	return nil
   243  }
   244  
   245  // Now, we read one chunk from the underlying reader.
   246  // A chunk has the following format:
   247  //
   248  //	<chunk-size-as-hex> + ";chunk-signature=" + <signature-as-hex> + "\r\n" + <payload> + "\r\n"
   249  //
   250  // First, we read the chunk size but fail if it is larger
   251  // than 16 MiB. We must not accept arbitrary large chunks.
   252  // One 16 MiB is a reasonable max limit.
   253  //
   254  // Then we read the signature and payload data. We compute the SHA256 checksum
   255  // of the payload and verify that it matches the expected signature value.
   256  //
   257  // The last chunk is *always* 0-sized. So, we must only return io.EOF if we have encountered
   258  // a chunk with a chunk size = 0. However, this chunk still has a signature and we must
   259  // verify it.
   260  const maxChunkSize = 16 << 20 // 16 MiB
   261  
   262  // Read - implements `io.Reader`, which transparently decodes
   263  // the incoming AWS Signature V4 streaming signature.
   264  func (cr *s3ChunkedReader) Read(buf []byte) (n int, err error) {
   265  	if cr.err != nil {
   266  		if cr.debug {
   267  			fmt.Printf("s3ChunkedReader: Returning err: %v (%T)\n", cr.err, cr.err)
   268  		}
   269  		return 0, cr.err
   270  	}
   271  	defer func() {
   272  		if err != nil && err != io.EOF {
   273  			if cr.debug {
   274  				fmt.Println("Read err:", err)
   275  			}
   276  		}
   277  	}()
   278  	// First, if there is any unread data, copy it to the client
   279  	// provided buffer.
   280  	if cr.offset > 0 {
   281  		n = copy(buf, cr.buffer[cr.offset:])
   282  		if n == len(buf) {
   283  			cr.offset += n
   284  			return n, nil
   285  		}
   286  		cr.offset = 0
   287  		buf = buf[n:]
   288  	}
   289  
   290  	var size int
   291  	for {
   292  		b, err := cr.reader.ReadByte()
   293  		if err == io.EOF {
   294  			err = io.ErrUnexpectedEOF
   295  		}
   296  		if err != nil {
   297  			cr.err = err
   298  			return n, cr.err
   299  		}
   300  		if b == ';' { // separating character
   301  			break
   302  		}
   303  
   304  		// Manually deserialize the size since AWS specified
   305  		// the chunk size to be of variable width. In particular,
   306  		// a size of 16 is encoded as `10` while a size of 64 KB
   307  		// is `10000`.
   308  		switch {
   309  		case b >= '0' && b <= '9':
   310  			size = size<<4 | int(b-'0')
   311  		case b >= 'a' && b <= 'f':
   312  			size = size<<4 | int(b-('a'-10))
   313  		case b >= 'A' && b <= 'F':
   314  			size = size<<4 | int(b-('A'-10))
   315  		default:
   316  			cr.err = errMalformedEncoding
   317  			return n, cr.err
   318  		}
   319  		if size > maxChunkSize {
   320  			cr.err = errChunkTooBig
   321  			return n, cr.err
   322  		}
   323  	}
   324  
   325  	// Now, we read the signature of the following payload and expect:
   326  	//   chunk-signature=" + <signature-as-hex> + "\r\n"
   327  	//
   328  	// The signature is 64 bytes long (hex-encoded SHA256 hash) and
   329  	// starts with a 16 byte header: len("chunk-signature=") + 64 == 80.
   330  	var signature [80]byte
   331  	_, err = io.ReadFull(cr.reader, signature[:])
   332  	if err == io.EOF {
   333  		err = io.ErrUnexpectedEOF
   334  	}
   335  	if err != nil {
   336  		cr.err = err
   337  		return n, cr.err
   338  	}
   339  	if !bytes.HasPrefix(signature[:], []byte("chunk-signature=")) {
   340  		cr.err = errMalformedEncoding
   341  		return n, cr.err
   342  	}
   343  	b, err := cr.reader.ReadByte()
   344  	if err == io.EOF {
   345  		err = io.ErrUnexpectedEOF
   346  	}
   347  	if err != nil {
   348  		cr.err = err
   349  		return n, cr.err
   350  	}
   351  	if b != '\r' {
   352  		cr.err = errMalformedEncoding
   353  		return n, cr.err
   354  	}
   355  	b, err = cr.reader.ReadByte()
   356  	if err == io.EOF {
   357  		err = io.ErrUnexpectedEOF
   358  	}
   359  	if err != nil {
   360  		cr.err = err
   361  		return n, cr.err
   362  	}
   363  	if b != '\n' {
   364  		cr.err = errMalformedEncoding
   365  		return n, cr.err
   366  	}
   367  
   368  	if cap(cr.buffer) < size {
   369  		cr.buffer = make([]byte, size)
   370  	} else {
   371  		cr.buffer = cr.buffer[:size]
   372  	}
   373  
   374  	// Now, we read the payload and compute its SHA-256 hash.
   375  	_, err = io.ReadFull(cr.reader, cr.buffer)
   376  	if err == io.EOF && size != 0 {
   377  		err = io.ErrUnexpectedEOF
   378  	}
   379  	if err != nil && err != io.EOF {
   380  		cr.err = err
   381  		return n, cr.err
   382  	}
   383  
   384  	// Once we have read the entire chunk successfully, we verify
   385  	// that the received signature matches our computed signature.
   386  	cr.chunkSHA256Writer.Write(cr.buffer)
   387  	newSignature := cr.getChunkSignature()
   388  	if !compareSignatureV4(string(signature[16:]), newSignature) {
   389  		cr.err = errSignatureMismatch
   390  		return n, cr.err
   391  	}
   392  	cr.seedSignature = newSignature
   393  	cr.chunkSHA256Writer.Reset()
   394  
   395  	// If the chunk size is zero we return io.EOF. As specified by AWS,
   396  	// only the last chunk is zero-sized.
   397  	if len(cr.buffer) == 0 {
   398  		if cr.debug {
   399  			fmt.Println("EOF. Reading Trailers:", cr.trailers)
   400  		}
   401  		if cr.trailers != nil {
   402  			err = cr.readTrailers()
   403  			if cr.debug {
   404  				fmt.Println("trailers returned:", err, "now:", cr.trailers)
   405  			}
   406  			if err != nil {
   407  				cr.err = err
   408  				return 0, err
   409  			}
   410  		}
   411  		cr.err = io.EOF
   412  		return n, cr.err
   413  	}
   414  
   415  	b, err = cr.reader.ReadByte()
   416  	if b != '\r' || err != nil {
   417  		if cr.debug {
   418  			fmt.Printf("want %q, got %q\n", "\r", string(b))
   419  		}
   420  		cr.err = errMalformedEncoding
   421  		return n, cr.err
   422  	}
   423  	b, err = cr.reader.ReadByte()
   424  	if err == io.EOF {
   425  		err = io.ErrUnexpectedEOF
   426  	}
   427  	if err != nil {
   428  		cr.err = err
   429  		return n, cr.err
   430  	}
   431  	if b != '\n' {
   432  		if cr.debug {
   433  			fmt.Printf("want %q, got %q\n", "\r", string(b))
   434  		}
   435  		cr.err = errMalformedEncoding
   436  		return n, cr.err
   437  	}
   438  
   439  	cr.offset = copy(buf, cr.buffer)
   440  	n += cr.offset
   441  	return n, err
   442  }
   443  
   444  // readTrailers will read all trailers and populate cr.trailers with actual values.
   445  func (cr *s3ChunkedReader) readTrailers() error {
   446  	if cr.debug {
   447  		fmt.Printf("pre trailer sig: %s\n", cr.seedSignature)
   448  	}
   449  	var valueBuffer bytes.Buffer
   450  	// Read value
   451  	for {
   452  		v, err := cr.reader.ReadByte()
   453  		if err != nil {
   454  			if err == io.EOF {
   455  				return io.ErrUnexpectedEOF
   456  			}
   457  		}
   458  		if v != '\r' {
   459  			valueBuffer.WriteByte(v)
   460  			continue
   461  		}
   462  		// End of buffer, do not add to value.
   463  		v, err = cr.reader.ReadByte()
   464  		if err != nil {
   465  			if err == io.EOF {
   466  				return io.ErrUnexpectedEOF
   467  			}
   468  		}
   469  		if v != '\n' {
   470  			return errMalformedEncoding
   471  		}
   472  		break
   473  	}
   474  
   475  	// Read signature
   476  	var signatureBuffer bytes.Buffer
   477  	for {
   478  		v, err := cr.reader.ReadByte()
   479  		if err != nil {
   480  			if err == io.EOF {
   481  				return io.ErrUnexpectedEOF
   482  			}
   483  		}
   484  		if v != '\r' {
   485  			signatureBuffer.WriteByte(v)
   486  			continue
   487  		}
   488  		var tmp [3]byte
   489  		_, err = io.ReadFull(cr.reader, tmp[:])
   490  		if err != nil {
   491  			if err == io.EOF {
   492  				return io.ErrUnexpectedEOF
   493  			}
   494  		}
   495  		if string(tmp[:]) != "\n\r\n" {
   496  			if cr.debug {
   497  				fmt.Printf("signature, want %q, got %q", "\n\r\n", string(tmp[:]))
   498  			}
   499  			return errMalformedEncoding
   500  		}
   501  		// No need to write final newlines to buffer.
   502  		break
   503  	}
   504  
   505  	// Verify signature.
   506  	sig := signatureBuffer.Bytes()
   507  	if !bytes.HasPrefix(sig, []byte("x-amz-trailer-signature:")) {
   508  		if cr.debug {
   509  			fmt.Printf("prefix, want prefix %q, got %q", "x-amz-trailer-signature:", string(sig))
   510  		}
   511  		return errMalformedEncoding
   512  	}
   513  
   514  	// TODO: It seems like we may have to be prepared to rewrite and sort trailing headers:
   515  	// https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html
   516  
   517  	// Any value must end with a newline.
   518  	// Not all clients send that.
   519  	trailerRaw := valueBuffer.Bytes()
   520  	if len(trailerRaw) > 0 && trailerRaw[len(trailerRaw)-1] != '\n' {
   521  		valueBuffer.Write([]byte{'\n'})
   522  	}
   523  	sig = sig[len("x-amz-trailer-signature:"):]
   524  	sig = bytes.TrimSpace(sig)
   525  	cr.chunkSHA256Writer.Write(valueBuffer.Bytes())
   526  	wantSig := cr.getTrailerChunkSignature()
   527  	if !compareSignatureV4(string(sig), wantSig) {
   528  		if cr.debug {
   529  			fmt.Printf("signature, want: %q, got %q\nSignature buffer: %q\n", wantSig, string(sig), valueBuffer.String())
   530  		}
   531  		return errSignatureMismatch
   532  	}
   533  
   534  	// Parse trailers.
   535  	wantTrailers := make(map[string]struct{}, len(cr.trailers))
   536  	for k := range cr.trailers {
   537  		wantTrailers[strings.ToLower(k)] = struct{}{}
   538  	}
   539  	input := bufio.NewScanner(bytes.NewReader(valueBuffer.Bytes()))
   540  	for input.Scan() {
   541  		line := strings.TrimSpace(input.Text())
   542  		if line == "" {
   543  			continue
   544  		}
   545  		// Find first separator.
   546  		idx := strings.IndexByte(line, trailerKVSeparator[0])
   547  		if idx <= 0 || idx >= len(line) {
   548  			if cr.debug {
   549  				fmt.Printf("index, ':' not found in %q\n", line)
   550  			}
   551  			return errMalformedEncoding
   552  		}
   553  		key := line[:idx]
   554  		value := line[idx+1:]
   555  		if _, ok := wantTrailers[key]; !ok {
   556  			if cr.debug {
   557  				fmt.Printf("%q not found in %q\n", key, cr.trailers)
   558  			}
   559  			return errMalformedEncoding
   560  		}
   561  		cr.trailers.Set(key, value)
   562  		delete(wantTrailers, key)
   563  	}
   564  
   565  	// Check if we got all we want.
   566  	if len(wantTrailers) > 0 {
   567  		return io.ErrUnexpectedEOF
   568  	}
   569  	return nil
   570  }
   571  
   572  // readCRLF - check if reader only has '\r\n' CRLF character.
   573  // returns malformed encoding if it doesn't.
   574  func readCRLF(reader io.Reader) error {
   575  	buf := make([]byte, 2)
   576  	_, err := io.ReadFull(reader, buf[:2])
   577  	if err != nil {
   578  		return err
   579  	}
   580  	if buf[0] != '\r' || buf[1] != '\n' {
   581  		return errMalformedEncoding
   582  	}
   583  	return nil
   584  }
   585  
   586  // Read a line of bytes (up to \n) from b.
   587  // Give up if the line exceeds maxLineLength.
   588  // The returned bytes are owned by the bufio.Reader
   589  // so they are only valid until the next bufio read.
   590  func readChunkLine(b *bufio.Reader) ([]byte, []byte, error) {
   591  	buf, err := b.ReadSlice('\n')
   592  	if err != nil {
   593  		// We always know when EOF is coming.
   594  		// If the caller asked for a line, there should be a line.
   595  		if err == io.EOF {
   596  			err = io.ErrUnexpectedEOF
   597  		} else if err == bufio.ErrBufferFull {
   598  			err = errLineTooLong
   599  		}
   600  		return nil, nil, err
   601  	}
   602  	if len(buf) >= maxLineLength {
   603  		return nil, nil, errLineTooLong
   604  	}
   605  	// Parse s3 specific chunk extension and fetch the values.
   606  	hexChunkSize, hexChunkSignature := parseS3ChunkExtension(buf)
   607  	return hexChunkSize, hexChunkSignature, nil
   608  }
   609  
   610  // trimTrailingWhitespace - trim trailing white space.
   611  func trimTrailingWhitespace(b []byte) []byte {
   612  	for len(b) > 0 && isASCIISpace(b[len(b)-1]) {
   613  		b = b[:len(b)-1]
   614  	}
   615  	return b
   616  }
   617  
   618  // isASCIISpace - is ascii space?
   619  func isASCIISpace(b byte) bool {
   620  	return b == ' ' || b == '\t' || b == '\n' || b == '\r'
   621  }
   622  
   623  // Constant s3 chunk encoding signature.
   624  const s3ChunkSignatureStr = ";chunk-signature="
   625  
   626  // parses3ChunkExtension removes any s3 specific chunk-extension from buf.
   627  // For example,
   628  //
   629  //	"10000;chunk-signature=..." => "10000", "chunk-signature=..."
   630  func parseS3ChunkExtension(buf []byte) ([]byte, []byte) {
   631  	buf = trimTrailingWhitespace(buf)
   632  	semi := bytes.Index(buf, []byte(s3ChunkSignatureStr))
   633  	// Chunk signature not found, return the whole buffer.
   634  	if semi == -1 {
   635  		return buf, nil
   636  	}
   637  	return buf[:semi], parseChunkSignature(buf[semi:])
   638  }
   639  
   640  // parseChunkSignature - parse chunk signature.
   641  func parseChunkSignature(chunk []byte) []byte {
   642  	chunkSplits := bytes.SplitN(chunk, []byte(s3ChunkSignatureStr), 2)
   643  	return chunkSplits[1]
   644  }
   645  
   646  // parse hex to uint64.
   647  func parseHexUint(v []byte) (n uint64, err error) {
   648  	for i, b := range v {
   649  		switch {
   650  		case '0' <= b && b <= '9':
   651  			b -= '0'
   652  		case 'a' <= b && b <= 'f':
   653  			b = b - 'a' + 10
   654  		case 'A' <= b && b <= 'F':
   655  			b = b - 'A' + 10
   656  		default:
   657  			return 0, errors.New("invalid byte in chunk length")
   658  		}
   659  		if i == 16 {
   660  			return 0, errors.New("http chunk length too large")
   661  		}
   662  		n <<= 4
   663  		n |= uint64(b)
   664  	}
   665  	return
   666  }