mirror of https://github.com/minio/minio.git
Merge pull request #879 from harshavardhana/presigned-signature-v4
Implement presigned signature v4 support
This commit is contained in:
commit
62e31e7eb0
|
@ -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
|
||||||
|
|
|
@ -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{})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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{}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue