github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/signature-v4-parser.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 19 20 import ( 21 "net/http" 22 "net/url" 23 "strings" 24 "time" 25 26 "github.com/minio/minio/internal/auth" 27 xhttp "github.com/minio/minio/internal/http" 28 ) 29 30 // credentialHeader data type represents structured form of Credential 31 // string from authorization header. 32 type credentialHeader struct { 33 accessKey string 34 scope struct { 35 date time.Time 36 region string 37 service string 38 request string 39 } 40 } 41 42 // Return scope string. 43 func (c credentialHeader) getScope() string { 44 return strings.Join([]string{ 45 c.scope.date.Format(yyyymmdd), 46 c.scope.region, 47 c.scope.service, 48 c.scope.request, 49 }, SlashSeparator) 50 } 51 52 func getReqAccessKeyV4(r *http.Request, region string, stype serviceType) (auth.Credentials, bool, APIErrorCode) { 53 ch, s3Err := parseCredentialHeader("Credential="+r.Form.Get(xhttp.AmzCredential), region, stype) 54 if s3Err != ErrNone { 55 // Strip off the Algorithm prefix. 56 v4Auth := strings.TrimPrefix(r.Header.Get("Authorization"), signV4Algorithm) 57 authFields := strings.Split(strings.TrimSpace(v4Auth), ",") 58 if len(authFields) != 3 { 59 return auth.Credentials{}, false, ErrMissingFields 60 } 61 ch, s3Err = parseCredentialHeader(authFields[0], region, stype) 62 if s3Err != ErrNone { 63 return auth.Credentials{}, false, s3Err 64 } 65 } 66 return checkKeyValid(r, ch.accessKey) 67 } 68 69 // parse credentialHeader string into its structured form. 70 func parseCredentialHeader(credElement string, region string, stype serviceType) (ch credentialHeader, aec APIErrorCode) { 71 creds := strings.SplitN(strings.TrimSpace(credElement), "=", 2) 72 if len(creds) != 2 { 73 return ch, ErrMissingFields 74 } 75 if creds[0] != "Credential" { 76 return ch, ErrMissingCredTag 77 } 78 credElements := strings.Split(strings.TrimSpace(creds[1]), SlashSeparator) 79 if len(credElements) < 5 { 80 return ch, ErrCredMalformed 81 } 82 accessKey := strings.Join(credElements[:len(credElements)-4], SlashSeparator) // The access key may contain one or more `/` 83 if !auth.IsAccessKeyValid(accessKey) { 84 return ch, ErrInvalidAccessKeyID 85 } 86 // Save access key id. 87 cred := credentialHeader{ 88 accessKey: accessKey, 89 } 90 credElements = credElements[len(credElements)-4:] 91 var e error 92 cred.scope.date, e = time.Parse(yyyymmdd, credElements[0]) 93 if e != nil { 94 return ch, ErrMalformedCredentialDate 95 } 96 97 cred.scope.region = credElements[1] 98 // Verify if region is valid. 99 sRegion := cred.scope.region 100 // Region is set to be empty, we use whatever was sent by the 101 // request and proceed further. This is a work-around to address 102 // an important problem for ListBuckets() getting signed with 103 // different regions. 104 if region == "" { 105 region = sRegion 106 } 107 // Should validate region, only if region is set. 108 if !isValidRegion(sRegion, region) { 109 return ch, ErrAuthorizationHeaderMalformed 110 } 111 if credElements[2] != string(stype) { 112 if stype == serviceSTS { 113 return ch, ErrInvalidServiceSTS 114 } 115 return ch, ErrInvalidServiceS3 116 } 117 cred.scope.service = credElements[2] 118 if credElements[3] != "aws4_request" { 119 return ch, ErrInvalidRequestVersion 120 } 121 cred.scope.request = credElements[3] 122 return cred, ErrNone 123 } 124 125 // Parse signature from signature tag. 126 func parseSignature(signElement string) (string, APIErrorCode) { 127 signFields := strings.Split(strings.TrimSpace(signElement), "=") 128 if len(signFields) != 2 { 129 return "", ErrMissingFields 130 } 131 if signFields[0] != "Signature" { 132 return "", ErrMissingSignTag 133 } 134 if signFields[1] == "" { 135 return "", ErrMissingFields 136 } 137 signature := signFields[1] 138 return signature, ErrNone 139 } 140 141 // Parse slice of signed headers from signed headers tag. 142 func parseSignedHeader(signedHdrElement string) ([]string, APIErrorCode) { 143 signedHdrFields := strings.Split(strings.TrimSpace(signedHdrElement), "=") 144 if len(signedHdrFields) != 2 { 145 return nil, ErrMissingFields 146 } 147 if signedHdrFields[0] != "SignedHeaders" { 148 return nil, ErrMissingSignHeadersTag 149 } 150 if signedHdrFields[1] == "" { 151 return nil, ErrMissingFields 152 } 153 signedHeaders := strings.Split(signedHdrFields[1], ";") 154 return signedHeaders, ErrNone 155 } 156 157 // signValues data type represents structured form of AWS Signature V4 header. 158 type signValues struct { 159 Credential credentialHeader 160 SignedHeaders []string 161 Signature string 162 } 163 164 // preSignValues data type represents structured form of AWS Signature V4 query string. 165 type preSignValues struct { 166 signValues 167 Date time.Time 168 Expires time.Duration 169 } 170 171 // Parses signature version '4' query string of the following form. 172 // 173 // querystring = X-Amz-Algorithm=algorithm 174 // querystring += &X-Amz-Credential= urlencode(accessKey + '/' + credential_scope) 175 // querystring += &X-Amz-Date=date 176 // querystring += &X-Amz-Expires=timeout interval 177 // querystring += &X-Amz-SignedHeaders=signed_headers 178 // querystring += &X-Amz-Signature=signature 179 // 180 // verifies if any of the necessary query params are missing in the presigned request. 181 func doesV4PresignParamsExist(query url.Values) APIErrorCode { 182 v4PresignQueryParams := []string{xhttp.AmzAlgorithm, xhttp.AmzCredential, xhttp.AmzSignature, xhttp.AmzDate, xhttp.AmzSignedHeaders, xhttp.AmzExpires} 183 for _, v4PresignQueryParam := range v4PresignQueryParams { 184 if _, ok := query[v4PresignQueryParam]; !ok { 185 return ErrInvalidQueryParams 186 } 187 } 188 return ErrNone 189 } 190 191 // Parses all the presigned signature values into separate elements. 192 func parsePreSignV4(query url.Values, region string, stype serviceType) (psv preSignValues, aec APIErrorCode) { 193 // verify whether the required query params exist. 194 aec = doesV4PresignParamsExist(query) 195 if aec != ErrNone { 196 return psv, aec 197 } 198 199 // Verify if the query algorithm is supported or not. 200 if query.Get(xhttp.AmzAlgorithm) != signV4Algorithm { 201 return psv, ErrInvalidQuerySignatureAlgo 202 } 203 204 // Initialize signature version '4' structured header. 205 preSignV4Values := preSignValues{} 206 207 // Save credential. 208 preSignV4Values.Credential, aec = parseCredentialHeader("Credential="+query.Get(xhttp.AmzCredential), region, stype) 209 if aec != ErrNone { 210 return psv, aec 211 } 212 213 var e error 214 // Save date in native time.Time. 215 preSignV4Values.Date, e = time.Parse(iso8601Format, query.Get(xhttp.AmzDate)) 216 if e != nil { 217 return psv, ErrMalformedPresignedDate 218 } 219 220 // Save expires in native time.Duration. 221 preSignV4Values.Expires, e = time.ParseDuration(query.Get(xhttp.AmzExpires) + "s") 222 if e != nil { 223 return psv, ErrMalformedExpires 224 } 225 226 if preSignV4Values.Expires < 0 { 227 return psv, ErrNegativeExpires 228 } 229 230 // Check if Expiry time is less than 7 days (value in seconds). 231 if preSignV4Values.Expires.Seconds() > 604800 { 232 return psv, ErrMaximumExpires 233 } 234 235 if preSignV4Values.Date.IsZero() || preSignV4Values.Date.Equal(timeSentinel) { 236 return psv, ErrMalformedPresignedDate 237 } 238 239 // Save signed headers. 240 preSignV4Values.SignedHeaders, aec = parseSignedHeader("SignedHeaders=" + query.Get(xhttp.AmzSignedHeaders)) 241 if aec != ErrNone { 242 return psv, aec 243 } 244 245 // Save signature. 246 preSignV4Values.Signature, aec = parseSignature("Signature=" + query.Get(xhttp.AmzSignature)) 247 if aec != ErrNone { 248 return psv, aec 249 } 250 251 // Return structured form of signature query string. 252 return preSignV4Values, ErrNone 253 } 254 255 // Parses signature version '4' header of the following form. 256 // 257 // Authorization: algorithm Credential=accessKeyID/credScope, \ 258 // SignedHeaders=signedHeaders, Signature=signature 259 func parseSignV4(v4Auth string, region string, stype serviceType) (sv signValues, aec APIErrorCode) { 260 // credElement is fetched first to skip replacing the space in access key. 261 credElement := strings.TrimPrefix(strings.Split(strings.TrimSpace(v4Auth), ",")[0], signV4Algorithm) 262 // Replace all spaced strings, some clients can send spaced 263 // parameters and some won't. So we pro-actively remove any spaces 264 // to make parsing easier. 265 v4Auth = strings.ReplaceAll(v4Auth, " ", "") 266 if v4Auth == "" { 267 return sv, ErrAuthHeaderEmpty 268 } 269 270 // Verify if the header algorithm is supported or not. 271 if !strings.HasPrefix(v4Auth, signV4Algorithm) { 272 return sv, ErrSignatureVersionNotSupported 273 } 274 275 // Strip off the Algorithm prefix. 276 v4Auth = strings.TrimPrefix(v4Auth, signV4Algorithm) 277 authFields := strings.Split(strings.TrimSpace(v4Auth), ",") 278 if len(authFields) != 3 { 279 return sv, ErrMissingFields 280 } 281 282 // Initialize signature version '4' structured header. 283 signV4Values := signValues{} 284 285 var s3Err APIErrorCode 286 // Save credential values. 287 signV4Values.Credential, s3Err = parseCredentialHeader(strings.TrimSpace(credElement), region, stype) 288 if s3Err != ErrNone { 289 return sv, s3Err 290 } 291 292 // Save signed headers. 293 signV4Values.SignedHeaders, s3Err = parseSignedHeader(authFields[1]) 294 if s3Err != ErrNone { 295 return sv, s3Err 296 } 297 298 // Save signature. 299 signV4Values.Signature, s3Err = parseSignature(authFields[2]) 300 if s3Err != ErrNone { 301 return sv, s3Err 302 } 303 304 // Return the structure here. 305 return signV4Values, ErrNone 306 }