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 }