From 81cc017f9146956decb838408a7aeec58126f4d6 Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Wed, 30 Sep 2015 22:59:52 -0700 Subject: [PATCH 1/2] Implement presigned signature v4 support --- pkg/donut/donut-v2.go | 22 ++++++-- pkg/donut/errors.go | 14 +++++ pkg/donut/signature-v4.go | 101 ++++++++++++++++++++++++++++++---- server-api-object-handlers.go | 18 +++++- server-api-signature.go | 38 ++++++++++++- 5 files changed, 173 insertions(+), 20 deletions(-) diff --git a/pkg/donut/donut-v2.go b/pkg/donut/donut-v2.go index 41fbb2fa3..ce8ecdbdc 100644 --- a/pkg/donut/donut-v2.go +++ b/pkg/donut/donut-v2.go @@ -626,12 +626,22 @@ func (donut API) GetObjectMetadata(bucket, key string, signature *Signature) (Ob defer donut.lock.Unlock() if signature != nil { - ok, err := signature.DoesSignatureMatch("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") - if err != nil { - return ObjectMetadata{}, err.Trace() - } - if !ok { - return ObjectMetadata{}, probe.NewError(SignatureDoesNotMatch{}) + if signature.Presigned { + ok, err := signature.DoesPresignedSignatureMatch() + if err != nil { + return ObjectMetadata{}, err.Trace() + } + if !ok { + return ObjectMetadata{}, probe.NewError(SignatureDoesNotMatch{}) + } + } else { + ok, err := signature.DoesSignatureMatch("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + if err != nil { + return ObjectMetadata{}, err.Trace() + } + if !ok { + return ObjectMetadata{}, probe.NewError(SignatureDoesNotMatch{}) + } } } diff --git a/pkg/donut/errors.go b/pkg/donut/errors.go index 479202780..411c80709 100644 --- a/pkg/donut/errors.go +++ b/pkg/donut/errors.go @@ -328,6 +328,20 @@ func (e SignatureDoesNotMatch) Error() string { return "The request signature we calculated does not match the signature you provided" } +// ExpiredPresignedRequest request already expired +type ExpiredPresignedRequest struct{} + +func (e ExpiredPresignedRequest) Error() string { + return "Presigned request already expired" +} + +// MissingExpiresQuery expires query string missing +type MissingExpiresQuery struct{} + +func (e MissingExpiresQuery) Error() string { + return "Missing expires query string" +} + // MissingDateHeader date header missing type MissingDateHeader struct{} diff --git a/pkg/donut/signature-v4.go b/pkg/donut/signature-v4.go index 6d48c7fff..9adb0c904 100644 --- a/pkg/donut/signature-v4.go +++ b/pkg/donut/signature-v4.go @@ -21,8 +21,10 @@ import ( "crypto/hmac" "encoding/hex" "net/http" + "net/url" "regexp" "sort" + "strconv" "strings" "time" "unicode/utf8" @@ -35,7 +37,10 @@ import ( type Signature struct { AccessKeyID string SecretAccessKey string - AuthHeader string + Presigned bool + PresignedPolicy bool + SignedHeaders []string + Signature string Request *http.Request } @@ -135,11 +140,9 @@ func (r *Signature) getSignedHeaders(signedHeaders map[string][]string) string { } // extractSignedHeaders extract signed headers from Authorization header -func (r *Signature) extractSignedHeaders() map[string][]string { - authFields := strings.Split(strings.TrimSpace(r.AuthHeader), ",") - extractedHeaders := strings.Split(strings.Split(strings.TrimSpace(authFields[1]), "=")[1], ";") +func (r Signature) extractSignedHeaders() map[string][]string { extractedSignedHeadersMap := make(map[string][]string) - for _, header := range extractedHeaders { + for _, header := range r.SignedHeaders { val, ok := r.Request.Header[http.CanonicalHeaderKey(header)] if !ok { // if not found continue, we will fail later @@ -161,6 +164,7 @@ func (r *Signature) extractSignedHeaders() map[string][]string { // // func (r *Signature) getCanonicalRequest() string { + payload := r.Request.Header.Get(http.CanonicalHeaderKey("x-amz-content-sha256")) r.Request.URL.RawQuery = strings.Replace(r.Request.URL.Query().Encode(), "+", "%20", -1) encodedPath, _ := urlEncodeName(r.Request.URL.Path) // convert any space strings back to "+" @@ -171,7 +175,33 @@ func (r *Signature) getCanonicalRequest() string { r.Request.URL.RawQuery, r.getCanonicalHeaders(r.extractSignedHeaders()), r.getSignedHeaders(r.extractSignedHeaders()), - r.Request.Header.Get(http.CanonicalHeaderKey("x-amz-content-sha256")), + payload, + }, "\n") + return canonicalRequest +} + +// getCanonicalRequest generate a canonical request of style +// +// canonicalRequest = +// \n +// \n +// \n +// \n +// \n +// +// +func (r *Signature) getPresignedCanonicalRequest(presignedQuery string) string { + rawQuery := strings.Replace(presignedQuery, "+", "%20", -1) + encodedPath, _ := urlEncodeName(r.Request.URL.Path) + // convert any space strings back to "+" + encodedPath = strings.Replace(encodedPath, "+", "%20", -1) + canonicalRequest := strings.Join([]string{ + r.Request.Method, + encodedPath, + rawQuery, + r.getCanonicalHeaders(r.extractSignedHeaders()), + r.getSignedHeaders(r.extractSignedHeaders()), + "UNSIGNED-PAYLOAD", }, "\n") return canonicalRequest } @@ -210,13 +240,62 @@ func (r *Signature) getSignature(signingKey []byte, stringToSign string) string return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign))) } -// DoesSignatureMatch - Verify authorization header with calculated header in accordance with - http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html -// returns true if matches, false other wise if error is not nil then it is always false +// DoesPolicySignatureMatch - Verify query headers with post policy +// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html +// returns true if matches, false otherwise. if error is not nil then it is always false +func (r *Signature) DoesPolicySignatureMatch() (bool, *probe.Error) { + // FIXME: Implement this + return true, nil +} + +// DoesPresignedSignatureMatch - Verify query headers with presigned signature +// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html +// returns true if matches, false otherwise. if error is not nil then it is always false +func (r *Signature) DoesPresignedSignatureMatch() (bool, *probe.Error) { + query := make(url.Values) + query.Set("X-Amz-Algorithm", authHeaderPrefix) + + var date string + if date = r.Request.URL.Query().Get("X-Amz-Date"); date == "" { + return false, probe.NewError(MissingDateHeader{}) + } + t, err := time.Parse(iso8601Format, date) + if err != nil { + return false, probe.NewError(err) + } + if _, ok := r.Request.URL.Query()["X-Amz-Expires"]; !ok { + return false, probe.NewError(MissingExpiresQuery{}) + } + expireSeconds, err := strconv.Atoi(r.Request.URL.Query().Get("X-Amz-Expires")) + if err != nil { + return false, probe.NewError(err) + } + if time.Now().UTC().Sub(t) > time.Duration(expireSeconds)*time.Second { + return false, probe.NewError(ExpiredPresignedRequest{}) + } + query.Set("X-Amz-Date", t.Format(iso8601Format)) + query.Set("X-Amz-Expires", strconv.Itoa(expireSeconds)) + query.Set("X-Amz-SignedHeaders", r.getSignedHeaders(r.extractSignedHeaders())) + query.Set("X-Amz-Credential", r.AccessKeyID+"/"+r.getScope(t)) + + encodedQuery := query.Encode() + newSignature := r.getSignature(r.getSigningKey(t), r.getStringToSign(r.getPresignedCanonicalRequest(encodedQuery), t)) + encodedQuery += "&X-Amz-Signature=" + newSignature + + if encodedQuery != r.Request.URL.RawQuery { + return false, nil + } + return true, nil +} + +// DoesSignatureMatch - Verify authorization header with calculated header in accordance with +// - http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html +// returns true if matches, false otherwise. if error is not nil then it is always false func (r *Signature) DoesSignatureMatch(hashedPayload string) (bool, *probe.Error) { // set new calulated payload r.Request.Header.Set("X-Amz-Content-Sha256", hashedPayload) - // Add date if not present + // Add date if not present throw error var date string if date = r.Request.Header.Get(http.CanonicalHeaderKey("x-amz-date")); date == "" { if date = r.Request.Header.Get("Date"); date == "" { @@ -232,9 +311,7 @@ func (r *Signature) DoesSignatureMatch(hashedPayload string) (bool, *probe.Error signingKey := r.getSigningKey(t) newSignature := r.getSignature(signingKey, stringToSign) - authFields := strings.Split(strings.TrimSpace(r.AuthHeader), ",") - signature := strings.Split(strings.TrimSpace(authFields[2]), "=")[1] - if newSignature != signature { + if newSignature != r.Signature { return false, nil } return true, nil diff --git a/server-api-object-handlers.go b/server-api-object-handlers.go index 071477b73..6b10a281f 100644 --- a/server-api-object-handlers.go +++ b/server-api-object-handlers.go @@ -63,8 +63,24 @@ func (api MinioAPI) GetObjectHandler(w http.ResponseWriter, req *http.Request) { writeErrorResponse(w, req, InternalError, acceptsContentType, req.URL.Path) return } + } else { + if _, ok := req.URL.Query()["X-Amz-Credential"]; ok { + var err *probe.Error + signature, err = initPresignedSignatureV4(req) + if err != nil { + switch err.ToGoError() { + case errAccessKeyIDInvalid: + errorIf(err.Trace(), "Invalid access key id requested.", nil) + writeErrorResponse(w, req, InvalidAccessKeyID, acceptsContentType, req.URL.Path) + return + default: + errorIf(err.Trace(), "Initializing signature v4 failed.", nil) + writeErrorResponse(w, req, InternalError, acceptsContentType, req.URL.Path) + return + } + } + } } - metadata, err := api.Donut.GetObjectMetadata(bucket, object, signature) if err != nil { errorIf(err.Trace(), "GetObject failed.", nil) diff --git a/server-api-signature.go b/server-api-signature.go index 3d1b4b0b8..8e976bc2b 100644 --- a/server-api-signature.go +++ b/server-api-signature.go @@ -106,12 +106,16 @@ func initSignatureV4(req *http.Request) (*donut.Signature, *probe.Error) { if err != nil { return nil, err.Trace() } + authFields := strings.Split(strings.TrimSpace(authHeaderValue), ",") + signedHeaders := strings.Split(strings.Split(strings.TrimSpace(authFields[1]), "=")[1], ";") + signature := strings.Split(strings.TrimSpace(authFields[2]), "=")[1] for _, user := range authConfig.Users { if user.AccessKeyID == accessKeyID { signature := &donut.Signature{ AccessKeyID: user.AccessKeyID, SecretAccessKey: user.SecretAccessKey, - AuthHeader: authHeaderValue, + Signature: signature, + SignedHeaders: signedHeaders, Request: req, } return signature, nil @@ -119,3 +123,35 @@ func initSignatureV4(req *http.Request) (*donut.Signature, *probe.Error) { } return nil, probe.NewError(errors.New("AccessKeyID not found")) } + +// initPresignedSignatureV4 initializing presigned signature verification +func initPresignedSignatureV4(req *http.Request) (*donut.Signature, *probe.Error) { + credentialElements := strings.Split(strings.TrimSpace(req.URL.Query().Get("X-Amz-Credential")), "/") + if len(credentialElements) != 5 { + return nil, probe.NewError(errCredentialTagMalformed) + } + accessKeyID := credentialElements[0] + if !auth.IsValidAccessKey(accessKeyID) { + return nil, probe.NewError(errAccessKeyIDInvalid) + } + authConfig, err := auth.LoadConfig() + if err != nil { + return nil, err.Trace() + } + signedHeaders := strings.Split(strings.TrimSpace(req.URL.Query().Get("X-Amz-SignedHeaders")), ";") + signature := strings.TrimSpace(req.URL.Query().Get("X-Amz-Signature")) + for _, user := range authConfig.Users { + if user.AccessKeyID == accessKeyID { + signature := &donut.Signature{ + AccessKeyID: user.AccessKeyID, + SecretAccessKey: user.SecretAccessKey, + Signature: signature, + SignedHeaders: signedHeaders, + Presigned: true, + Request: req, + } + return signature, nil + } + } + return nil, probe.NewError(errAccessKeyIDInvalid) +} From 3b070dee169001e58cc3fa3cf7760358d2123764 Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Thu, 1 Oct 2015 10:16:14 -0700 Subject: [PATCH 2/2] Fix an important metadata getObject bug in donut --- pkg/donut/bucket.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/donut/bucket.go b/pkg/donut/bucket.go index cae76163e..4d29e1279 100644 --- a/pkg/donut/bucket.go +++ b/pkg/donut/bucket.go @@ -135,7 +135,7 @@ func (b bucket) getBucketMetadata() (*AllBuckets, *probe.Error) { func (b bucket) GetObjectMetadata(objectName string) (ObjectMetadata, *probe.Error) { b.lock.Lock() defer b.lock.Unlock() - return b.readObjectMetadata(objectName) + return b.readObjectMetadata(normalizeObjectName(objectName)) } // ListObjects - list all objects