Merge pull request #879 from harshavardhana/presigned-signature-v4

Implement presigned signature v4 support
This commit is contained in:
Harshavardhana 2015-10-01 10:21:05 -07:00
commit 62e31e7eb0
6 changed files with 174 additions and 21 deletions

View File

@ -135,7 +135,7 @@ func (b bucket) getBucketMetadata() (*AllBuckets, *probe.Error) {
func (b bucket) GetObjectMetadata(objectName string) (ObjectMetadata, *probe.Error) { func (b bucket) GetObjectMetadata(objectName string) (ObjectMetadata, *probe.Error) {
b.lock.Lock() b.lock.Lock()
defer b.lock.Unlock() defer b.lock.Unlock()
return b.readObjectMetadata(objectName) return b.readObjectMetadata(normalizeObjectName(objectName))
} }
// ListObjects - list all objects // ListObjects - list all objects

View File

@ -626,12 +626,22 @@ func (donut API) GetObjectMetadata(bucket, key string, signature *Signature) (Ob
defer donut.lock.Unlock() defer donut.lock.Unlock()
if signature != nil { if signature != nil {
ok, err := signature.DoesSignatureMatch("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") if signature.Presigned {
if err != nil { ok, err := signature.DoesPresignedSignatureMatch()
return ObjectMetadata{}, err.Trace() if err != nil {
} return ObjectMetadata{}, err.Trace()
if !ok { }
return ObjectMetadata{}, probe.NewError(SignatureDoesNotMatch{}) 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{})
}
} }
} }

View File

@ -328,6 +328,20 @@ func (e SignatureDoesNotMatch) Error() string {
return "The request signature we calculated does not match the signature you provided" 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 // MissingDateHeader date header missing
type MissingDateHeader struct{} type MissingDateHeader struct{}

View File

@ -21,8 +21,10 @@ import (
"crypto/hmac" "crypto/hmac"
"encoding/hex" "encoding/hex"
"net/http" "net/http"
"net/url"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
"unicode/utf8" "unicode/utf8"
@ -35,7 +37,10 @@ import (
type Signature struct { type Signature struct {
AccessKeyID string AccessKeyID string
SecretAccessKey string SecretAccessKey string
AuthHeader string Presigned bool
PresignedPolicy bool
SignedHeaders []string
Signature string
Request *http.Request Request *http.Request
} }
@ -135,11 +140,9 @@ func (r *Signature) getSignedHeaders(signedHeaders map[string][]string) string {
} }
// extractSignedHeaders extract signed headers from Authorization header // extractSignedHeaders extract signed headers from Authorization header
func (r *Signature) extractSignedHeaders() map[string][]string { func (r Signature) extractSignedHeaders() map[string][]string {
authFields := strings.Split(strings.TrimSpace(r.AuthHeader), ",")
extractedHeaders := strings.Split(strings.Split(strings.TrimSpace(authFields[1]), "=")[1], ";")
extractedSignedHeadersMap := make(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)] val, ok := r.Request.Header[http.CanonicalHeaderKey(header)]
if !ok { if !ok {
// if not found continue, we will fail later // if not found continue, we will fail later
@ -161,6 +164,7 @@ func (r *Signature) extractSignedHeaders() map[string][]string {
// <HashedPayload> // <HashedPayload>
// //
func (r *Signature) getCanonicalRequest() 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) r.Request.URL.RawQuery = strings.Replace(r.Request.URL.Query().Encode(), "+", "%20", -1)
encodedPath, _ := urlEncodeName(r.Request.URL.Path) encodedPath, _ := urlEncodeName(r.Request.URL.Path)
// convert any space strings back to "+" // convert any space strings back to "+"
@ -171,7 +175,33 @@ func (r *Signature) getCanonicalRequest() string {
r.Request.URL.RawQuery, r.Request.URL.RawQuery,
r.getCanonicalHeaders(r.extractSignedHeaders()), r.getCanonicalHeaders(r.extractSignedHeaders()),
r.getSignedHeaders(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 =
// <HTTPMethod>\n
// <CanonicalURI>\n
// <CanonicalQueryString>\n
// <CanonicalHeaders>\n
// <SignedHeaders>\n
// <HashedPayload>
//
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") }, "\n")
return canonicalRequest return canonicalRequest
} }
@ -210,13 +240,62 @@ func (r *Signature) getSignature(signingKey []byte, stringToSign string) string
return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign))) 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 // DoesPolicySignatureMatch - Verify query headers with post policy
// returns true if matches, false other wise if error is not nil then it is always false // - 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) { func (r *Signature) DoesSignatureMatch(hashedPayload string) (bool, *probe.Error) {
// set new calulated payload // set new calulated payload
r.Request.Header.Set("X-Amz-Content-Sha256", hashedPayload) r.Request.Header.Set("X-Amz-Content-Sha256", hashedPayload)
// Add date if not present // Add date if not present throw error
var date string var date string
if date = r.Request.Header.Get(http.CanonicalHeaderKey("x-amz-date")); date == "" { if date = r.Request.Header.Get(http.CanonicalHeaderKey("x-amz-date")); date == "" {
if date = r.Request.Header.Get("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) signingKey := r.getSigningKey(t)
newSignature := r.getSignature(signingKey, stringToSign) newSignature := r.getSignature(signingKey, stringToSign)
authFields := strings.Split(strings.TrimSpace(r.AuthHeader), ",") if newSignature != r.Signature {
signature := strings.Split(strings.TrimSpace(authFields[2]), "=")[1]
if newSignature != signature {
return false, nil return false, nil
} }
return true, nil return true, nil

View File

@ -63,8 +63,24 @@ func (api MinioAPI) GetObjectHandler(w http.ResponseWriter, req *http.Request) {
writeErrorResponse(w, req, InternalError, acceptsContentType, req.URL.Path) writeErrorResponse(w, req, InternalError, acceptsContentType, req.URL.Path)
return 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) metadata, err := api.Donut.GetObjectMetadata(bucket, object, signature)
if err != nil { if err != nil {
errorIf(err.Trace(), "GetObject failed.", nil) errorIf(err.Trace(), "GetObject failed.", nil)

View File

@ -106,12 +106,16 @@ func initSignatureV4(req *http.Request) (*donut.Signature, *probe.Error) {
if err != nil { if err != nil {
return nil, err.Trace() 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 { for _, user := range authConfig.Users {
if user.AccessKeyID == accessKeyID { if user.AccessKeyID == accessKeyID {
signature := &donut.Signature{ signature := &donut.Signature{
AccessKeyID: user.AccessKeyID, AccessKeyID: user.AccessKeyID,
SecretAccessKey: user.SecretAccessKey, SecretAccessKey: user.SecretAccessKey,
AuthHeader: authHeaderValue, Signature: signature,
SignedHeaders: signedHeaders,
Request: req, Request: req,
} }
return signature, nil 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")) 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)
}