@@ -1,5 +1,5 @@
11--
2−-- MinIO Haskell SDK, (C) 2017 MinIO, Inc.
2+-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
33--
44-- Licensed under the Apache License, Version 2.0 (the "License");
55-- you may not use this file except in compliance with the License.
@@ -14,21 +14,9 @@
1414-- limitations under the License.
1515--
1616
17−module Network.Minio.Sign.V4
18− (
19− signV4
20− , signV4PostPolicy
21− , mkScope
22− , getHeadersToSign
23− , mkCanonicalRequest
24− , mkStringToSign
25− , mkSigningKey
26− , computeSignature
27− , SignV4Data(..)
28− , SignParams(..)
29− , debugPrintSignV4Data
30− ) where
17+module Network.Minio.Sign.V4 where
3118
19+import qualified Conduit as C
3220import qualified Data.ByteString as B
3321import qualified Data.ByteString.Base64 as Base64
3422import qualified Data.ByteString.Char8 as B8
@@ -39,13 +27,15 @@ import qualified Data.Set as Set
3927import qualified Data.Time as Time
4028import qualified Network.HTTP.Conduit as NC
4129import Network.HTTP.Types (Header, parseQuery)
42−import qualified Network.HTTP.Types.Header as H
30+import qualified Network.HTTP.Types as H
31+import Text.Printf (printf)
4332
4433import Lib.Prelude
4534
4635import Network.Minio.Data.ByteString
4736import Network.Minio.Data.Crypto
4837import Network.Minio.Data.Time
38+import Network.Minio.Errors
4939
5040-- these headers are not included in the string to sign when signing a
5141-- request
@@ -53,7 +43,6 @@ ignoredHeaders :: Set ByteString
5343ignoredHeaders = Set.fromList $ map CI.foldedCase
5444 [ H.hAuthorization
5545 , H.hContentType
56− , H.hContentLength
5746 , H.hUserAgent
5847 ]
5948
@@ -93,6 +82,20 @@ debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
9382 mapM_ (\x -> B.putStr $ B.concat [show x, " "]) $ B.unpack b
9483 B8.putStrLn ""
9584
85+mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
86+mkAuthHeader accessKey scope signedHeaderKeys sign =
87+ let authValue = B.concat
88+ [ "AWS4-HMAC-SHA256 Credential="
89+ , toS accessKey
90+ , "/"
91+ , scope
92+ , ", SignedHeaders="
93+ , signedHeaderKeys
94+ , ", Signature="
95+ , sign
96+ ]
97+ in (H.hAuthorization, authValue)
98+
9699-- | Given SignParams and request details, including request method,
97100-- request path, headers, query params and payload hash, generates an
98101-- updated set of headers, including the x-amz-date header and the
@@ -105,7 +108,6 @@ debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
105108-- is being created. The expiry is interpreted as an integer number of
106109-- seconds. The output will be the list of query-parameters to add to
107110-- the request.
108−
109111signV4 :: SignParams -> NC.Request -> [(ByteString, ByteString)]
110112signV4 !sp !req =
111113 let
@@ -139,7 +141,8 @@ signV4 !sp !req =
139141 else []
140142
141143 -- 1. compute canonical request
142− canonicalRequest = mkCanonicalRequest sp (NC.setQueryString finalQP req)
144+ canonicalRequest = mkCanonicalRequest False sp
145+ (NC.setQueryString finalQP req)
143146 headersToSign
144147
145148 -- 2. compute string to sign
@@ -152,23 +155,15 @@ signV4 !sp !req =
152155 signature = computeSignature stringToSign signingKey
153156
154157 -- 4. compute auth header
155− authValue = B.concat
156− [ "AWS4-HMAC-SHA256 Credential="
157− , accessKey
158− , "/"
159− , scope
160− , ", SignedHeaders="
161− , signedHeaderKeys
162− , ", Signature="
163− , signature
164− ]
165− authHeader = (H.hAuthorization, authValue)
158+ authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
166159
167160 -- finally compute output pairs
161+ sha256Hdr = ("x-amz-content-sha256",
162+ fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp)
168163 output = if isJust expiry
169164 then ("X-Amz-Signature", signature) : authQP
170165 else [(\(x, y) -> (CI.foldedCase x, y)) authHeader,
171− datePair]
166+ datePair, sha256Hdr]
172167
173168 in output
174169
@@ -186,9 +181,9 @@ getHeadersToSign !h =
186181 filter (flip Set.notMember ignoredHeaders . fst) $
187182 map (\(x, y) -> (CI.foldedCase x, stripBS y)) h
188183
189−mkCanonicalRequest :: SignParams -> NC.Request -> [(ByteString, ByteString)]
190− -> ByteString
191−mkCanonicalRequest !sp !req !headersForSign =
184+mkCanonicalRequest :: Bool -> SignParams -> NC.Request -> [(ByteString, ByteString)]
185+ -> ByteString
186+mkCanonicalRequest !isStreaming !sp !req !headersForSign =
192187 let
193188 canonicalQueryString = B.intercalate "&" $
194189 map (\(x, y) -> B.concat [x, "=", y]) $
@@ -203,6 +198,10 @@ mkCanonicalRequest !sp !req !headersForSign =
203198
204199 signedHeaders = B.intercalate ";" $ map fst sortedHeaders
205200
201+ payloadHashStr =
202+ if isStreaming
203+ then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
204+ else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
206205 in
207206 B.intercalate "\n"
208207 [ NC.method req
@@ -210,7 +209,7 @@ mkCanonicalRequest !sp !req !headersForSign =
210209 , canonicalQueryString
211210 , canonicalHeaders
212211 , signedHeaders
213− , maybe "UNSIGNED-PAYLOAD" identity $ spPayloadHash sp
212+ , payloadHashStr
214213 ]
215214
216215mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString
@@ -246,3 +245,147 @@ signV4PostPolicy !postPolicyJSON !sp =
246245 Map.fromList [ ("x-amz-signature", signature)
247246 , ("policy", stringToSign)
248247 ]
248+
249+chunkSizeConstant :: Int
250+chunkSizeConstant = 64 * 1024
251+
252+-- base16Len computes the number of bytes required to represent @n (> 0)@ in
253+-- hexadecimal.
254+base16Len :: Integral a => a -> Int
255+base16Len n | n == 0 = 0
256+ | otherwise = 1 + base16Len (n `div` 16)
257+
258+signedStreamLength :: Int64 -> Int64
259+signedStreamLength dataLen =
260+ let
261+ chunkSzInt = fromIntegral chunkSizeConstant
262+ (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt
263+
264+
265+ -- Structure of a chunk:
266+ -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n
267+ encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2
268+ fullChunkSize = encodedChunkLen chunkSzInt
269+ lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0
270+ finalChunkSize = 1 + 17 + 64 + 2 + 2
271+ in
272+ numChunks * fullChunkSize + lastChunkSize + finalChunkSize
273+
274+signV4Stream :: Int64 -> SignParams -> NC.Request
275+ -> (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
276+ -- -> ([Header], C.ConduitT () ByteString (C.ResourceT IO) () -> NC.RequestBody)
277+signV4Stream !payloadLength !sp !req =
278+ let
279+ ts = spTimeStamp sp
280+
281+ addContentEncoding hs =
282+ let ceMay = headMay $ filter (\(x, _) -> x == "content-encoding") hs
283+ in case ceMay of
284+ Nothing -> ("content-encoding", "aws-chunked") : hs
285+ Just (_, ce) -> ("content-encoding", ce <> ",aws-chunked") :
286+ filter (\(x, _) -> x /= "content-encoding") hs
287+
288+ -- headers to be added to the request
289+ datePair = ("X-Amz-Date", awsTimeFormatBS ts)
290+ computedHeaders = addContentEncoding $
291+ datePair : NC.requestHeaders req
292+
293+ -- headers specific to streaming signature
294+ signedContentLength = signedStreamLength payloadLength
295+ streamingHeaders :: [Header]
296+ streamingHeaders =
297+ [ ("x-amz-decoded-content-length", show payloadLength)
298+ , ("content-length", show signedContentLength )
299+ , ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
300+ ]
301+ headersToSign = getHeadersToSign $ computedHeaders ++ streamingHeaders
302+ signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
303+ finalQP = parseQuery (NC.queryString req)
304+
305+ -- 1. Compute Seed Signature
306+ -- 1.1 Canonical Request
307+ canonicalReq = mkCanonicalRequest True sp
308+ (NC.setQueryString finalQP req)
309+ headersToSign
310+
311+ region = fromMaybe "" $ spRegion sp
312+ scope = mkScope ts region
313+ accessKey = spAccessKey sp
314+ secretKey = spSecretKey sp
315+
316+ -- 1.2 String toSign
317+ stringToSign = mkStringToSign ts scope canonicalReq
318+
319+ -- 1.3 Compute signature
320+ -- 1.3.1 compute signing key
321+ signingKey = mkSigningKey ts region $ toS secretKey
322+
323+ -- 1.3.2 Compute signature
324+ seedSignature = computeSignature stringToSign signingKey
325+
326+ -- 1.3.3 Compute Auth Header
327+ authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature
328+
329+ -- 1.4 Updated headers for the request
330+ finalReqHeaders = authHeader : (computedHeaders ++ streamingHeaders)
331+ -- headersToAdd = authHeader : datePair : streamingHeaders
332+
333+ toHexStr n = B8.pack $ printf "%x" n
334+
335+ (numParts, lastPSize) = payloadLength `quotRem` fromIntegral chunkSizeConstant
336+
337+ -- Function to compute string to sign for each chunk.
338+ chunkStrToSign prevSign currChunkHash =
339+ B.intercalate "\n"
340+ [ "AWS4-HMAC-SHA256-PAYLOAD"
341+ , awsTimeFormatBS ts
342+ , scope
343+ , prevSign
344+ , hashSHA256 ""
345+ , currChunkHash
346+ ]
347+
348+ -- Read n byte from upstream and return a strict bytestring.
349+ mustTakeN n = do
350+ bs <- toS <$> (C.takeCE n C..| C.sinkLazy)
351+ when (B.length bs /= n) $
352+ throwIO MErrVStreamingBodyUnexpectedEOF
353+ return bs
354+
355+ signerConduit n lps prevSign =
356+ -- First case encodes a full chunk of length
357+ -- 'chunkSizeConstant'.
358+ if | n > 0 -> do
359+ bs <- mustTakeN chunkSizeConstant
360+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
361+ nextSign = computeSignature strToSign signingKey
362+ chunkBS = toHexStr chunkSizeConstant
363+ <> ";chunk-signature="
364+ <> nextSign <> "\r\n" <> bs <> "\r\n"
365+ C.yield chunkBS
366+ signerConduit (n-1) lps nextSign
367+
368+ -- Second case encodes the last chunk which is smaller than
369+ -- 'chunkSizeConstant'
370+ | lps > 0 -> do
371+ bs <- mustTakeN $ fromIntegral lps
372+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
373+ nextSign = computeSignature strToSign signingKey
374+ chunkBS = toHexStr lps <> ";chunk-signature="
375+ <> nextSign <> "\r\n" <> bs <> "\r\n"
376+ C.yield chunkBS
377+ signerConduit 0 0 nextSign
378+
379+ -- Last case encodes the final signature chunk that has no
380+ -- data.
381+ | otherwise -> do
382+ let strToSign = chunkStrToSign prevSign (hashSHA256 "")
383+ nextSign = computeSignature strToSign signingKey
384+ lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
385+ C.yield lastChunkBS
386+ in
387+ \src -> req { NC.requestHeaders = finalReqHeaders
388+ , NC.requestBody =
389+ NC.requestBodySource signedContentLength $
390+ src C..| signerConduit numParts lastPSize seedSignature
391+ }