mirror of
https://github.com/minio/minio.git
synced 2024-12-24 22:25:54 -05:00
PresignedPost: Support for Signature V2 presigned POST Policy. (#3043)
fixes #2993
This commit is contained in:
parent
4b5b363c6c
commit
e51be73ac7
@ -33,8 +33,8 @@ const (
|
|||||||
iso8601DateFormat = "20060102T150405Z"
|
iso8601DateFormat = "20060102T150405Z"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newPostPolicyBytes - creates a bare bones postpolicy string with key and bucket matches.
|
// newPostPolicyBytesV4 - creates a bare bones postpolicy string with key and bucket matches.
|
||||||
func newPostPolicyBytes(credential, bucketName, objectKey string, expiration time.Time) []byte {
|
func newPostPolicyBytesV4(credential, bucketName, objectKey string, expiration time.Time) []byte {
|
||||||
t := time.Now().UTC()
|
t := time.Now().UTC()
|
||||||
// Add the expiration date.
|
// Add the expiration date.
|
||||||
expirationStr := fmt.Sprintf(`"expiration": "%s"`, expiration.Format(expirationDateFormat))
|
expirationStr := fmt.Sprintf(`"expiration": "%s"`, expiration.Format(expirationDateFormat))
|
||||||
@ -59,6 +59,25 @@ func newPostPolicyBytes(credential, bucketName, objectKey string, expiration tim
|
|||||||
return []byte(retStr)
|
return []byte(retStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newPostPolicyBytesV2 - creates a bare bones postpolicy string with key and bucket matches.
|
||||||
|
func newPostPolicyBytesV2(bucketName, objectKey string, expiration time.Time) []byte {
|
||||||
|
// Add the expiration date.
|
||||||
|
expirationStr := fmt.Sprintf(`"expiration": "%s"`, expiration.Format(expirationDateFormat))
|
||||||
|
// Add the bucket condition, only accept buckets equal to the one passed.
|
||||||
|
bucketConditionStr := fmt.Sprintf(`["eq", "$bucket", "%s"]`, bucketName)
|
||||||
|
// Add the key condition, only accept keys equal to the one passed.
|
||||||
|
keyConditionStr := fmt.Sprintf(`["eq", "$key", "%s"]`, objectKey)
|
||||||
|
|
||||||
|
// Combine all conditions into one string.
|
||||||
|
conditionStr := fmt.Sprintf(`"conditions":[%s, %s]`, bucketConditionStr, keyConditionStr)
|
||||||
|
retStr := "{"
|
||||||
|
retStr = retStr + expirationStr + ","
|
||||||
|
retStr = retStr + conditionStr
|
||||||
|
retStr = retStr + "}"
|
||||||
|
|
||||||
|
return []byte(retStr)
|
||||||
|
}
|
||||||
|
|
||||||
// Wrapper for calling TestPostPolicyHandlerHandler tests for both XL multiple disks and single node setup.
|
// Wrapper for calling TestPostPolicyHandlerHandler tests for both XL multiple disks and single node setup.
|
||||||
func TestPostPolicyHandler(t *testing.T) {
|
func TestPostPolicyHandler(t *testing.T) {
|
||||||
ExecObjectLayerTest(t, testPostPolicyHandler)
|
ExecObjectLayerTest(t, testPostPolicyHandler)
|
||||||
@ -105,9 +124,34 @@ func testPostPolicyHandler(obj ObjectLayer, instanceType string, t TestErrHandle
|
|||||||
t.Fatalf("%s : %s", instanceType, err.Error())
|
t.Fatalf("%s : %s", instanceType, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collection of non-exhaustive ListMultipartUploads test cases, valid errors
|
// Test cases for signature-V2.
|
||||||
// and success responses.
|
testCasesV2 := []struct {
|
||||||
testCases := []struct {
|
expectedStatus int
|
||||||
|
accessKey string
|
||||||
|
secretKey string
|
||||||
|
}{
|
||||||
|
{http.StatusForbidden, "invalidaccesskey", credentials.SecretAccessKey},
|
||||||
|
{http.StatusForbidden, credentials.AccessKeyID, "invalidsecretkey"},
|
||||||
|
{http.StatusNoContent, credentials.AccessKeyID, credentials.SecretAccessKey},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range testCasesV2 {
|
||||||
|
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req, perr := newPostRequestV2("", bucketName, "testobject", test.accessKey, test.secretKey)
|
||||||
|
if perr != nil {
|
||||||
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v", i+1, instanceType, perr)
|
||||||
|
}
|
||||||
|
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler.
|
||||||
|
// Call the ServeHTTP to execute the handler.
|
||||||
|
apiRouter.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != test.expectedStatus {
|
||||||
|
t.Fatalf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, test.expectedStatus, rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test cases for signature-V4.
|
||||||
|
testCasesV4 := []struct {
|
||||||
objectName string
|
objectName string
|
||||||
data []byte
|
data []byte
|
||||||
expectedRespStatus int
|
expectedRespStatus int
|
||||||
@ -144,10 +188,10 @@ func testPostPolicyHandler(obj ObjectLayer, instanceType string, t TestErrHandle
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCasesV4 {
|
||||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
req, perr := newPostRequest("", bucketName, testCase.objectName, testCase.data, testCase.accessKey, testCase.secretKey)
|
req, perr := newPostRequestV4("", bucketName, testCase.objectName, testCase.data, testCase.accessKey, testCase.secretKey)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v", i+1, instanceType, perr)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v", i+1, instanceType, perr)
|
||||||
}
|
}
|
||||||
@ -173,7 +217,57 @@ func postPresignSignatureV4(policyBase64 string, t time.Time, secretAccessKey, l
|
|||||||
return signature
|
return signature
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPostRequest(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string) (*http.Request, error) {
|
func newPostRequestV2(endPoint, bucketName, objectName string, accessKey, secretKey string) (*http.Request, error) {
|
||||||
|
// Expire the request five minutes from now.
|
||||||
|
expirationTime := time.Now().UTC().Add(time.Minute * 5)
|
||||||
|
// Create a new post policy.
|
||||||
|
policy := newPostPolicyBytesV2(bucketName, objectName, expirationTime)
|
||||||
|
// Only need the encoding.
|
||||||
|
encodedPolicy := base64.StdEncoding.EncodeToString(policy)
|
||||||
|
|
||||||
|
// Presign with V4 signature based on the policy.
|
||||||
|
signature := calculateSignatureV2(encodedPolicy, secretKey)
|
||||||
|
|
||||||
|
formData := map[string]string{
|
||||||
|
"AWSAccessKeyId": accessKey,
|
||||||
|
"bucket": bucketName,
|
||||||
|
"key": objectName,
|
||||||
|
"policy": encodedPolicy,
|
||||||
|
"signature": signature,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the multipart form.
|
||||||
|
var buf bytes.Buffer
|
||||||
|
w := multipart.NewWriter(&buf)
|
||||||
|
|
||||||
|
// Set the normal formData
|
||||||
|
for k, v := range formData {
|
||||||
|
w.WriteField(k, v)
|
||||||
|
}
|
||||||
|
// Set the File formData
|
||||||
|
writer, err := w.CreateFormFile("file", "s3verify/post/object")
|
||||||
|
if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
writer.Write([]byte("hello world"))
|
||||||
|
// Close before creating the new request.
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
// Set the body equal to the created policy.
|
||||||
|
reader := bytes.NewReader(buf.Bytes())
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", makeTestTargetURL(endPoint, bucketName, objectName, nil), reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set form content-type.
|
||||||
|
req.Header.Set("Content-Type", w.FormDataContentType())
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPostRequestV4(endPoint, bucketName, objectName string, objData []byte, accessKey, secretKey string) (*http.Request, error) {
|
||||||
// Keep time.
|
// Keep time.
|
||||||
t := time.Now().UTC()
|
t := time.Now().UTC()
|
||||||
// Expire the request five minutes from now.
|
// Expire the request five minutes from now.
|
||||||
@ -181,7 +275,7 @@ func newPostRequest(endPoint, bucketName, objectName string, objData []byte, acc
|
|||||||
// Get the user credential.
|
// Get the user credential.
|
||||||
credStr := getCredential(accessKey, serverConfig.GetRegion(), t)
|
credStr := getCredential(accessKey, serverConfig.GetRegion(), t)
|
||||||
// Create a new post policy.
|
// Create a new post policy.
|
||||||
policy := newPostPolicyBytes(credStr, bucketName, objectName, expirationTime)
|
policy := newPostPolicyBytesV4(credStr, bucketName, objectName, expirationTime)
|
||||||
// Only need the encoding.
|
// Only need the encoding.
|
||||||
encodedPolicy := base64.StdEncoding.EncodeToString(policy)
|
encodedPolicy := base64.StdEncoding.EncodeToString(policy)
|
||||||
|
|
||||||
|
@ -56,7 +56,19 @@ var resourceList = []string{
|
|||||||
"website",
|
"website",
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add post policy signature.
|
func doesPolicySignatureV2Match(formValues map[string]string) APIErrorCode {
|
||||||
|
cred := serverConfig.GetCredential()
|
||||||
|
accessKey := formValues["Awsaccesskeyid"]
|
||||||
|
if accessKey != cred.AccessKeyID {
|
||||||
|
return ErrInvalidAccessKeyID
|
||||||
|
}
|
||||||
|
signature := formValues["Signature"]
|
||||||
|
policy := formValues["Policy"]
|
||||||
|
if signature != calculateSignatureV2(policy, cred.SecretAccessKey) {
|
||||||
|
return ErrSignatureDoesNotMatch
|
||||||
|
}
|
||||||
|
return ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@ -198,26 +210,24 @@ func doesSignV2Match(r *http.Request) APIErrorCode {
|
|||||||
return ErrNone
|
return ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func calculateSignatureV2(stringToSign string, secret string) string {
|
||||||
|
hm := hmac.New(sha1.New, []byte(secret))
|
||||||
|
hm.Write([]byte(stringToSign))
|
||||||
|
return base64.StdEncoding.EncodeToString(hm.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
// 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 := presignV2STS(method, encodedResource, encodedQuery, headers, expires)
|
||||||
hm := hmac.New(sha1.New, []byte(cred.SecretAccessKey))
|
return calculateSignatureV2(stringToSign, cred.SecretAccessKey)
|
||||||
hm.Write([]byte(stringToSign))
|
|
||||||
signature := base64.StdEncoding.EncodeToString(hm.Sum(nil))
|
|
||||||
return signature
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 := signV2STS(method, encodedResource, encodedQuery, headers)
|
||||||
|
signature := calculateSignatureV2(stringToSign, cred.SecretAccessKey)
|
||||||
hm := hmac.New(sha1.New, []byte(cred.SecretAccessKey))
|
|
||||||
hm.Write([]byte(stringToSign))
|
|
||||||
signature := base64.StdEncoding.EncodeToString(hm.Sum(nil))
|
|
||||||
return fmt.Sprintf("%s %s:%s", signV2Algorithm, cred.AccessKeyID, signature)
|
return fmt.Sprintf("%s %s:%s", signV2Algorithm, cred.AccessKeyID, signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,3 +180,35 @@ func TestValidateV2AuthHeader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDoesPolicySignatureV2Match(t *testing.T) {
|
||||||
|
if err := initConfig(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := serverConfig.Save(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
creds := serverConfig.GetCredential()
|
||||||
|
policy := "policy"
|
||||||
|
testCases := []struct {
|
||||||
|
accessKey string
|
||||||
|
policy string
|
||||||
|
signature string
|
||||||
|
errCode APIErrorCode
|
||||||
|
}{
|
||||||
|
{"invalidAccessKey", policy, calculateSignatureV2(policy, creds.SecretAccessKey), ErrInvalidAccessKeyID},
|
||||||
|
{creds.AccessKeyID, policy, calculateSignatureV2("random", creds.SecretAccessKey), ErrSignatureDoesNotMatch},
|
||||||
|
{creds.AccessKeyID, policy, calculateSignatureV2(policy, creds.SecretAccessKey), ErrNone},
|
||||||
|
}
|
||||||
|
for i, test := range testCases {
|
||||||
|
formValues := make(map[string]string)
|
||||||
|
formValues["Awsaccesskeyid"] = test.accessKey
|
||||||
|
formValues["Signature"] = test.signature
|
||||||
|
formValues["Policy"] = test.policy
|
||||||
|
errCode := doesPolicySignatureV2Match(formValues)
|
||||||
|
if errCode != test.errCode {
|
||||||
|
t.Fatalf("(%d) expected to get %s, instead got %s", i+1, niceError(test.errCode), niceError(errCode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -153,9 +153,6 @@ func parsePostPolicyFormV4(policy string) (PostPolicyForm, error) {
|
|||||||
|
|
||||||
// checkPostPolicy - apply policy conditions and validate input values.
|
// checkPostPolicy - apply policy conditions and validate input values.
|
||||||
func checkPostPolicy(formValues map[string]string) APIErrorCode {
|
func checkPostPolicy(formValues map[string]string) APIErrorCode {
|
||||||
if formValues["X-Amz-Algorithm"] != signV4Algorithm {
|
|
||||||
return ErrSignatureVersionNotSupported
|
|
||||||
}
|
|
||||||
/// Decoding policy
|
/// Decoding policy
|
||||||
policyBytes, err := base64.StdEncoding.DecodeString(formValues["Policy"])
|
policyBytes, err := base64.StdEncoding.DecodeString(formValues["Policy"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -145,10 +145,19 @@ func getSignature(signingKey []byte, stringToSign string) string {
|
|||||||
return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign)))
|
return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check to see if Policy is signed correctly.
|
||||||
|
func doesPolicySignatureMatch(formValues map[string]string) APIErrorCode {
|
||||||
|
// For SignV2 - Signature field will be valid
|
||||||
|
if formValues["Signature"] != "" {
|
||||||
|
return doesPolicySignatureV2Match(formValues)
|
||||||
|
}
|
||||||
|
return doesPolicySignatureV4Match(formValues)
|
||||||
|
}
|
||||||
|
|
||||||
// doesPolicySignatureMatch - Verify query headers with post policy
|
// doesPolicySignatureMatch - Verify query headers with post policy
|
||||||
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
|
// - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
|
||||||
// returns ErrNone if the signature matches.
|
// returns ErrNone if the signature matches.
|
||||||
func doesPolicySignatureMatch(formValues map[string]string) APIErrorCode {
|
func doesPolicySignatureV4Match(formValues map[string]string) APIErrorCode {
|
||||||
// Access credentials.
|
// Access credentials.
|
||||||
cred := serverConfig.GetCredential()
|
cred := serverConfig.GetCredential()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user