Fix signature v2 and presigned query unescaping. (#4936)

Simplifies the testing code by using s3signer
package from minio-go library.

Fixes #4927
This commit is contained in:
Harshavardhana 2017-09-24 14:20:12 -07:00 committed by Dee Koder
parent aceaa1ec48
commit c3ff402fcb
5 changed files with 119 additions and 165 deletions

View File

@ -455,7 +455,6 @@ func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName s
// verify response for V2 signed HTTP request. // verify response for V2 signed HTTP request.
reqV2, err := newTestSignedRequestV2("GET", u, 0, nil, testCase.accessKey, testCase.secretKey) reqV2, err := newTestSignedRequestV2("GET", u, 0, nil, testCase.accessKey, testCase.secretKey)
if err != nil { if err != nil {
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err) t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
} }

View File

@ -2813,6 +2813,7 @@ func testAPIPutObjectPartHandlerPreSign(obj ObjectLayer, instanceType, bucketNam
t.Fatalf("[%s] - Failed to create an unsigned request to put object part for %s/%s <ERROR> %v", t.Fatalf("[%s] - Failed to create an unsigned request to put object part for %s/%s <ERROR> %v",
instanceType, bucketName, testObject, err) instanceType, bucketName, testObject, err)
} }
err = preSignV2(req, credentials.AccessKey, credentials.SecretKey, int64(10*60*60)) err = preSignV2(req, credentials.AccessKey, credentials.SecretKey, int64(10*60*60))
if err != nil { if err != nil {
t.Fatalf("[%s] - Failed to presign an unsigned request to put object part for %s/%s <ERROR> %v", t.Fatalf("[%s] - Failed to presign an unsigned request to put object part for %s/%s <ERROR> %v",
@ -3301,6 +3302,7 @@ func testAPIListObjectPartsHandlerPreSign(obj ObjectLayer, instanceType, bucketN
instanceType, bucketName, mpartResp.UploadID) instanceType, bucketName, mpartResp.UploadID)
} }
req.Header = http.Header{}
err = preSignV2(req, credentials.AccessKey, credentials.SecretKey, int64(10*60*60)) err = preSignV2(req, credentials.AccessKey, credentials.SecretKey, int64(10*60*60))
if err != nil { if err != nil {
t.Fatalf("[%s] - Failed to presignV2 an unsigned request to list object parts for bucket %s, uploadId %s", t.Fatalf("[%s] - Failed to presignV2 an unsigned request to list object parts for bucket %s, uploadId %s",

View File

@ -77,6 +77,20 @@ func doesPolicySignatureV2Match(formValues http.Header) APIErrorCode {
return ErrNone return ErrNone
} }
// Escape encodedQuery string into unescaped list of query params, returns error
// if any while unescaping the values.
func unescapeQueries(encodedQuery string) (unescapedQueries []string, err error) {
for _, query := range strings.Split(encodedQuery, "&") {
var unescapedQuery string
unescapedQuery, err = url.QueryUnescape(query)
if err != nil {
return nil, err
}
unescapedQueries = append(unescapedQueries, unescapedQuery)
}
return unescapedQueries, nil
}
// doesPresignV2SignatureMatch - Verify query headers with presigned signature // doesPresignV2SignatureMatch - Verify query headers with presigned signature
// - http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth // - http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth
// returns ErrNone if matches. S3 errors otherwise. // returns ErrNone if matches. S3 errors otherwise.
@ -92,38 +106,38 @@ func doesPresignV2SignatureMatch(r *http.Request) APIErrorCode {
encodedQuery = tokens[1] encodedQuery = tokens[1]
} }
queries := strings.Split(encodedQuery, "&") var (
var filteredQueries []string filteredQueries []string
var gotSignature string gotSignature string
var expires string expires string
var accessKey string accessKey string
var err error err error
for _, query := range queries { )
keyval := strings.Split(query, "=")
var unescapedQueries []string
unescapedQueries, err = unescapeQueries(encodedQuery)
if err != nil {
errorIf(err, "Unable to unescape (%s)", encodedQuery)
return ErrInvalidQueryParams
}
// Extract the necessary values from presigned query, construct a list of new filtered queries.
for _, query := range unescapedQueries {
keyval := strings.SplitN(query, "=", 2)
switch keyval[0] { switch keyval[0] {
case "AWSAccessKeyId": case "AWSAccessKeyId":
accessKey, err = url.QueryUnescape(keyval[1]) accessKey = keyval[1]
case "Signature": case "Signature":
gotSignature, err = url.QueryUnescape(keyval[1]) gotSignature = keyval[1]
case "Expires": case "Expires":
expires, err = url.QueryUnescape(keyval[1]) expires = keyval[1]
default: default:
unescapedQuery, qerr := url.QueryUnescape(query) filteredQueries = append(filteredQueries, query)
if qerr == nil {
filteredQueries = append(filteredQueries, unescapedQuery)
} else {
err = qerr
}
}
// Check if the query unescaped properly.
if err != nil {
errorIf(err, "Unable to unescape query values", queries)
return ErrInvalidQueryParams
} }
} }
// Invalid access key. // Invalid values returns error.
if accessKey == "" { if accessKey == "" || gotSignature == "" || expires == "" {
return ErrInvalidQueryParams return ErrInvalidQueryParams
} }
@ -217,7 +231,13 @@ func doesSignV2Match(r *http.Request) APIErrorCode {
encodedQuery = tokens[1] encodedQuery = tokens[1]
} }
expectedAuth := signatureV2(r.Method, encodedResource, encodedQuery, r.Header) unescapedQueries, err := unescapeQueries(encodedQuery)
if err != nil {
errorIf(err, "Unable to unescape (%s)", encodedQuery)
return ErrInvalidQueryParams
}
expectedAuth := signatureV2(r.Method, encodedResource, strings.Join(unescapedQueries, "&"), r.Header)
if v2Auth != expectedAuth { if v2Auth != expectedAuth {
return ErrSignatureDoesNotMatch return ErrSignatureDoesNotMatch
} }
@ -234,14 +254,14 @@ func calculateSignatureV2(stringToSign string, secret string) string {
// Return signature-v2 for the presigned request. // Return signature-v2 for the presigned request.
func preSignatureV2(method string, encodedResource string, encodedQuery string, headers http.Header, expires string) string { func preSignatureV2(method string, encodedResource string, encodedQuery string, headers http.Header, expires string) string {
cred := serverConfig.GetCredential() cred := serverConfig.GetCredential()
stringToSign := presignV2STS(method, encodedResource, encodedQuery, headers, expires) stringToSign := getStringToSignV2(method, encodedResource, encodedQuery, headers, expires)
return calculateSignatureV2(stringToSign, cred.SecretKey) return calculateSignatureV2(stringToSign, cred.SecretKey)
} }
// Return signature-v2 authrization header. // Return signature-v2 authrization header.
func signatureV2(method string, encodedResource string, encodedQuery string, headers http.Header) string { func signatureV2(method string, encodedResource string, encodedQuery string, headers http.Header) string {
cred := serverConfig.GetCredential() cred := serverConfig.GetCredential()
stringToSign := signV2STS(method, encodedResource, encodedQuery, headers) stringToSign := getStringToSignV2(method, encodedResource, encodedQuery, headers, "")
signature := calculateSignatureV2(stringToSign, cred.SecretKey) signature := calculateSignatureV2(stringToSign, cred.SecretKey)
return fmt.Sprintf("%s %s:%s", signV2Algorithm, cred.AccessKey, signature) return fmt.Sprintf("%s %s:%s", signV2Algorithm, cred.AccessKey, signature)
} }
@ -267,7 +287,7 @@ func canonicalizedAmzHeadersV2(headers http.Header) string {
} }
// Return canonical resource string. // Return canonical resource string.
func canonicalizedResourceV2(encodedPath string, encodedQuery string) string { func canonicalizedResourceV2(encodedQuery string) string {
queries := strings.Split(encodedQuery, "&") queries := strings.Split(encodedQuery, "&")
keyval := make(map[string]string) keyval := make(map[string]string)
for _, query := range queries { for _, query := range queries {
@ -280,6 +300,7 @@ func canonicalizedResourceV2(encodedPath string, encodedQuery string) string {
} }
keyval[key] = val keyval[key] = val
} }
var canonicalQueries []string var canonicalQueries []string
for _, key := range resourceList { for _, key := range resourceList {
val, ok := keyval[key] val, ok := keyval[key]
@ -290,68 +311,55 @@ func canonicalizedResourceV2(encodedPath string, encodedQuery string) string {
canonicalQueries = append(canonicalQueries, key) canonicalQueries = append(canonicalQueries, key)
continue continue
} }
// Resources values should be unescaped canonicalQueries = append(canonicalQueries, key+"="+val)
unescapedVal, err := url.QueryUnescape(val)
if err != nil {
errorIf(err, "Unable to unescape query value (query = `%s`, value = `%s`)", key, val)
continue
}
canonicalQueries = append(canonicalQueries, key+"="+unescapedVal)
} }
if len(canonicalQueries) == 0 {
return encodedPath // The queries will be already sorted as resourceList is sorted, if canonicalQueries
} // is empty strings.Join returns empty.
// the queries will be already sorted as resourceList is sorted. return strings.Join(canonicalQueries, "&")
return encodedPath + "?" + strings.Join(canonicalQueries, "&")
} }
// Return string to sign for authz header calculation. // Return string to sign under two different conditions.
func signV2STS(method string, encodedResource string, encodedQuery string, headers http.Header) string { // - if expires string is set then string to sign includes date instead of the Date header.
// - if expires string is empty then string to sign includes date header instead.
func getStringToSignV2(method string, encodedResource, encodedQuery string, headers http.Header, expires string) string {
canonicalHeaders := canonicalizedAmzHeadersV2(headers) canonicalHeaders := canonicalizedAmzHeadersV2(headers)
if len(canonicalHeaders) > 0 { if len(canonicalHeaders) > 0 {
canonicalHeaders += "\n" canonicalHeaders += "\n"
} }
date := expires // Date is set to expires date for presign operations.
if date == "" {
// If expires date is empty then request header Date is used.
date = headers.Get("Date")
}
// From the Amazon docs: // From the Amazon docs:
// //
// StringToSign = HTTP-Verb + "\n" + // StringToSign = HTTP-Verb + "\n" +
// Content-Md5 + "\n" + // Content-Md5 + "\n" +
// Content-Type + "\n" + // Content-Type + "\n" +
// Date + "\n" + // Date/Expires + "\n" +
// CanonicalizedProtocolHeaders + // CanonicalizedProtocolHeaders +
// CanonicalizedResource; // CanonicalizedResource;
stringToSign := strings.Join([]string{ stringToSign := strings.Join([]string{
method, method,
headers.Get("Content-MD5"), headers.Get("Content-MD5"),
headers.Get("Content-Type"), headers.Get("Content-Type"),
headers.Get("Date"), date,
canonicalHeaders, canonicalHeaders,
}, "\n") + canonicalizedResourceV2(encodedResource, encodedQuery) }, "\n")
return stringToSign // For presigned signature no need to filter out based on resourceList,
} // just sign whatever is with the request.
if expires != "" {
// Return string to sign for pre-sign signature calculation. return stringToSign + encodedResource + "?" + encodedQuery
func presignV2STS(method string, encodedResource string, encodedQuery string, headers http.Header, expires string) string {
canonicalHeaders := canonicalizedAmzHeadersV2(headers)
if len(canonicalHeaders) > 0 {
canonicalHeaders += "\n"
} }
// From the Amazon docs: canonicalResource := canonicalizedResourceV2(encodedQuery)
// if canonicalResource != "" {
// StringToSign = HTTP-Verb + "\n" + return stringToSign + encodedResource + "?" + canonicalResource
// Content-Md5 + "\n" + }
// Content-Type + "\n" +
// Expires + "\n" + return stringToSign + encodedResource
// CanonicalizedProtocolHeaders +
// CanonicalizedResource;
stringToSign := strings.Join([]string{
method,
headers.Get("Content-MD5"),
headers.Get("Content-Type"),
expires,
canonicalHeaders,
}, "\n") + canonicalizedResourceV2(encodedResource, encodedQuery)
return stringToSign
} }

View File

@ -48,9 +48,12 @@ func TestDoesPresignedV2SignatureMatch(t *testing.T) {
now := UTCNow() now := UTCNow()
var (
accessKey = serverConfig.GetCredential().AccessKey
secretKey = serverConfig.GetCredential().SecretKey
)
testCases := []struct { testCases := []struct {
queryParams map[string]string queryParams map[string]string
headers map[string]string
expected APIErrorCode expected APIErrorCode
}{ }{
// (0) Should error without a set URL query. // (0) Should error without a set URL query.
@ -71,7 +74,7 @@ func TestDoesPresignedV2SignatureMatch(t *testing.T) {
queryParams: map[string]string{ queryParams: map[string]string{
"Expires": "60s", "Expires": "60s",
"Signature": "badsignature", "Signature": "badsignature",
"AWSAccessKeyId": serverConfig.GetCredential().AccessKey, "AWSAccessKeyId": accessKey,
}, },
expected: ErrMalformedExpires, expected: ErrMalformedExpires,
}, },
@ -80,7 +83,7 @@ func TestDoesPresignedV2SignatureMatch(t *testing.T) {
queryParams: map[string]string{ queryParams: map[string]string{
"Expires": "60", "Expires": "60",
"Signature": "badsignature", "Signature": "badsignature",
"AWSAccessKeyId": serverConfig.GetCredential().AccessKey, "AWSAccessKeyId": accessKey,
}, },
expected: ErrExpiredPresignRequest, expected: ErrExpiredPresignRequest,
}, },
@ -89,7 +92,7 @@ func TestDoesPresignedV2SignatureMatch(t *testing.T) {
queryParams: map[string]string{ queryParams: map[string]string{
"Expires": fmt.Sprintf("%d", now.Unix()+60), "Expires": fmt.Sprintf("%d", now.Unix()+60),
"Signature": "badsignature", "Signature": "badsignature",
"AWSAccessKeyId": serverConfig.GetCredential().AccessKey, "AWSAccessKeyId": accessKey,
}, },
expected: ErrSignatureDoesNotMatch, expected: ErrSignatureDoesNotMatch,
}, },
@ -98,10 +101,17 @@ func TestDoesPresignedV2SignatureMatch(t *testing.T) {
queryParams: map[string]string{ queryParams: map[string]string{
"Expires": fmt.Sprintf("%d", now.Unix()+60), "Expires": fmt.Sprintf("%d", now.Unix()+60),
"Signature": "zOM2YrY/yAQe15VWmT78OlBrK6g=", "Signature": "zOM2YrY/yAQe15VWmT78OlBrK6g=",
"AWSAccessKeyId": serverConfig.GetCredential().AccessKey, "AWSAccessKeyId": accessKey,
}, },
expected: ErrSignatureDoesNotMatch, expected: ErrSignatureDoesNotMatch,
}, },
// (6) Should error when the signature does not match.
{
queryParams: map[string]string{
"response-content-disposition": "attachment; filename=\"4K%2d4M.txt\"",
},
expected: ErrNone,
},
} }
// Run each test case individually. // Run each test case individually.
@ -111,25 +121,32 @@ func TestDoesPresignedV2SignatureMatch(t *testing.T) {
for key, value := range testCase.queryParams { for key, value := range testCase.queryParams {
query.Set(key, value) query.Set(key, value)
} }
// Create a request to use. // Create a request to use.
req, e := http.NewRequest(http.MethodGet, "http://host/a/b?"+query.Encode(), nil) req, err := http.NewRequest(http.MethodGet, "http://host/a/b?"+query.Encode(), nil)
if e != nil { if err != nil {
t.Errorf("(%d) failed to create http.Request, got %v", i, e) t.Errorf("(%d) failed to create http.Request, got %v", i, err)
} }
// Should be set since we are simulating a http server. if testCase.expected != ErrNone {
req.RequestURI = req.URL.RequestURI() // Should be set since we are simulating a http server.
req.RequestURI = req.URL.RequestURI()
// Do the same for the headers. // Check if it matches!
for key, value := range testCase.headers { errCode := doesPresignV2SignatureMatch(req)
req.Header.Set(key, value) if errCode != testCase.expected {
t.Errorf("(%d) expected to get %s, instead got %s", i, niceError(testCase.expected), niceError(errCode))
}
} else {
err = preSignV2(req, accessKey, secretKey, now.Unix()+60)
if err != nil {
t.Fatalf("(%d) failed to preSignV2 http request, got %v", i, err)
}
// Should be set since we are simulating a http server.
req.RequestURI = req.URL.RequestURI()
errCode := doesPresignV2SignatureMatch(req)
if errCode != testCase.expected {
t.Errorf("(%d) expected to get success, instead got %s", i, niceError(errCode))
}
} }
// Check if it matches!
err := doesPresignV2SignatureMatch(req)
if err != testCase.expected {
t.Errorf("(%d) expected to get %s, instead got %s", i, niceError(testCase.expected), niceError(err))
}
} }
} }

View File

@ -20,14 +20,11 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/hmac"
crand "crypto/rand" crand "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/sha1"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"encoding/pem" "encoding/pem"
@ -52,6 +49,7 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
router "github.com/gorilla/mux" router "github.com/gorilla/mux"
"github.com/minio/minio-go/pkg/s3signer"
) )
// Tests should initNSLock only once. // Tests should initNSLock only once.
@ -882,84 +880,14 @@ func preSignV2(req *http.Request, accessKeyID, secretAccessKey string, expires i
if accessKeyID == "" || secretAccessKey == "" { if accessKeyID == "" || secretAccessKey == "" {
return errors.New("Presign cannot be generated without access and secret keys") return errors.New("Presign cannot be generated without access and secret keys")
} }
d := UTCNow()
// Find epoch expires when the request will expire.
epochExpires := d.Unix() + expires
// Add expires header if not present.
expiresStr := req.Header.Get("Expires")
if expiresStr == "" {
expiresStr = strconv.FormatInt(epochExpires, 10)
req.Header.Set("Expires", expiresStr)
}
// url.RawPath will be valid if path has any encoded characters, if not it will
// be empty - in which case we need to consider url.Path (bug in net/http?)
encodedResource := req.URL.RawPath
encodedQuery := req.URL.RawQuery
if encodedResource == "" {
splits := strings.Split(req.URL.Path, "?")
if len(splits) > 0 {
encodedResource = splits[0]
}
}
// Get presigned string to sign.
stringToSign := presignV2STS(req.Method, encodedResource, encodedQuery, req.Header, expiresStr)
hm := hmac.New(sha1.New, []byte(secretAccessKey))
hm.Write([]byte(stringToSign))
// Calculate signature.
signature := base64.StdEncoding.EncodeToString(hm.Sum(nil))
query := req.URL.Query()
// Handle specially for Google Cloud Storage.
query.Set("AWSAccessKeyId", accessKeyID)
// Fill in Expires for presigned query.
query.Set("Expires", strconv.FormatInt(epochExpires, 10))
// Encode query and save.
req.URL.RawQuery = query.Encode()
// Save signature finally.
req.URL.RawQuery += "&Signature=" + url.QueryEscape(signature)
// Success. // Success.
req = s3signer.PreSignV2(*req, accessKeyID, secretAccessKey, expires)
return nil return nil
} }
// Sign given request using Signature V2. // Sign given request using Signature V2.
func signRequestV2(req *http.Request, accessKey, secretKey string) error { func signRequestV2(req *http.Request, accessKey, secretKey string) error {
// Initial time. req = s3signer.SignV2(*req, accessKey, secretKey)
d := UTCNow()
// Add date if not present.
if date := req.Header.Get("Date"); date == "" {
req.Header.Set("Date", d.Format(http.TimeFormat))
}
splits := strings.Split(req.URL.Path, "?")
var encodedResource string
if len(splits) > 0 {
encodedResource = getURLEncodedName(splits[0])
}
encodedQuery := req.URL.Query().Encode()
// Calculate HMAC for secretAccessKey.
stringToSign := signV2STS(req.Method, encodedResource, encodedQuery, req.Header)
hm := hmac.New(sha1.New, []byte(secretKey))
hm.Write([]byte(stringToSign))
// Prepare auth header.
authHeader := new(bytes.Buffer)
authHeader.WriteString(fmt.Sprintf("%s %s:", signV2Algorithm, accessKey))
encoder := base64.NewEncoder(base64.StdEncoding, authHeader)
encoder.Write(hm.Sum(nil))
encoder.Close()
// Set Authorization header.
req.Header.Set("Authorization", authHeader.String())
return nil return nil
} }