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 }