mirror of
https://github.com/minio/minio.git
synced 2024-12-26 23:25:54 -05:00
17d145df3a
Signature struct should be immutable, this fixes an issue with AWS cli not being able to do multipart put operations.
392 lines
13 KiB
Go
392 lines
13 KiB
Go
/*
|
|
* Minio Cloud Storage, (C) 2015 Minio, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
// Package signature4 implements helper functions to validate AWS
|
|
// Signature Version '4' authorization header.
|
|
//
|
|
// This package provides comprehensive helpers for following signature
|
|
// types.
|
|
// - Based on Authorization header.
|
|
// - Based on Query parameters.
|
|
// - Based on Form POST policy.
|
|
package signature4
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/minio/minio/pkg/crypto/sha256"
|
|
"github.com/minio/minio/pkg/probe"
|
|
)
|
|
|
|
// Sign - local variables
|
|
type Sign struct {
|
|
accessKeyID string
|
|
secretAccessKey string
|
|
region string
|
|
httpRequest *http.Request
|
|
extractedSignedHeaders http.Header
|
|
}
|
|
|
|
// AWS Signature Version '4' constants.
|
|
const (
|
|
signV4Algorithm = "AWS4-HMAC-SHA256"
|
|
iso8601Format = "20060102T150405Z"
|
|
yyyymmdd = "20060102"
|
|
)
|
|
|
|
// New - initialize a new authorization checkes.
|
|
func New(accessKeyID, secretAccessKey, region string) (*Sign, *probe.Error) {
|
|
if !isValidAccessKey.MatchString(accessKeyID) {
|
|
return nil, ErrInvalidAccessKeyID("Invalid access key id.", accessKeyID).Trace(accessKeyID)
|
|
}
|
|
if !isValidSecretKey.MatchString(secretAccessKey) {
|
|
return nil, ErrInvalidAccessKeyID("Invalid secret key.", secretAccessKey).Trace(secretAccessKey)
|
|
}
|
|
if region == "" {
|
|
return nil, ErrRegionISEmpty("Region is empty.").Trace()
|
|
}
|
|
signature := &Sign{
|
|
accessKeyID: accessKeyID,
|
|
secretAccessKey: secretAccessKey,
|
|
region: region,
|
|
}
|
|
return signature, nil
|
|
}
|
|
|
|
// SetHTTPRequestToVerify - sets the http request which needs to be verified.
|
|
func (s *Sign) SetHTTPRequestToVerify(r *http.Request) Sign {
|
|
// Do not set http request if its 'nil'.
|
|
if r == nil {
|
|
return *s
|
|
}
|
|
s.httpRequest = r
|
|
return *s
|
|
}
|
|
|
|
// getCanonicalHeaders generate a list of request headers with their values
|
|
func (s Sign) getCanonicalHeaders(signedHeaders http.Header) string {
|
|
var headers []string
|
|
vals := make(http.Header)
|
|
for k, vv := range signedHeaders {
|
|
headers = append(headers, strings.ToLower(k))
|
|
vals[strings.ToLower(k)] = vv
|
|
}
|
|
headers = append(headers, "host")
|
|
sort.Strings(headers)
|
|
|
|
var buf bytes.Buffer
|
|
for _, k := range headers {
|
|
buf.WriteString(k)
|
|
buf.WriteByte(':')
|
|
switch {
|
|
case k == "host":
|
|
buf.WriteString(s.httpRequest.Host)
|
|
fallthrough
|
|
default:
|
|
for idx, v := range vals[k] {
|
|
if idx > 0 {
|
|
buf.WriteByte(',')
|
|
}
|
|
buf.WriteString(v)
|
|
}
|
|
buf.WriteByte('\n')
|
|
}
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// getSignedHeaders generate a string i.e alphabetically sorted, semicolon-separated list of lowercase request header names
|
|
func (s Sign) getSignedHeaders(signedHeaders http.Header) string {
|
|
var headers []string
|
|
for k := range signedHeaders {
|
|
headers = append(headers, strings.ToLower(k))
|
|
}
|
|
headers = append(headers, "host")
|
|
sort.Strings(headers)
|
|
return strings.Join(headers, ";")
|
|
}
|
|
|
|
// getCanonicalRequest generate a canonical request of style
|
|
//
|
|
// canonicalRequest =
|
|
// <HTTPMethod>\n
|
|
// <CanonicalURI>\n
|
|
// <CanonicalQueryString>\n
|
|
// <CanonicalHeaders>\n
|
|
// <SignedHeaders>\n
|
|
// <HashedPayload>
|
|
//
|
|
func (s *Sign) getCanonicalRequest() string {
|
|
payload := s.httpRequest.Header.Get(http.CanonicalHeaderKey("x-amz-content-sha256"))
|
|
s.httpRequest.URL.RawQuery = strings.Replace(s.httpRequest.URL.Query().Encode(), "+", "%20", -1)
|
|
encodedPath := getURLEncodedName(s.httpRequest.URL.Path)
|
|
// Convert any space strings back to "+".
|
|
encodedPath = strings.Replace(encodedPath, "+", "%20", -1)
|
|
canonicalRequest := strings.Join([]string{
|
|
s.httpRequest.Method,
|
|
encodedPath,
|
|
s.httpRequest.URL.RawQuery,
|
|
s.getCanonicalHeaders(s.extractedSignedHeaders),
|
|
s.getSignedHeaders(s.extractedSignedHeaders),
|
|
payload,
|
|
}, "\n")
|
|
return canonicalRequest
|
|
}
|
|
|
|
// getCanonicalRequest generate a canonical request of style
|
|
//
|
|
// canonicalRequest =
|
|
// <HTTPMethod>\n
|
|
// <CanonicalURI>\n
|
|
// <CanonicalQueryString>\n
|
|
// <CanonicalHeaders>\n
|
|
// <SignedHeaders>\n
|
|
// <HashedPayload>
|
|
//
|
|
func (s Sign) getPresignedCanonicalRequest(presignedQuery string) string {
|
|
rawQuery := strings.Replace(presignedQuery, "+", "%20", -1)
|
|
encodedPath := getURLEncodedName(s.httpRequest.URL.Path)
|
|
// Convert any space strings back to "+".
|
|
encodedPath = strings.Replace(encodedPath, "+", "%20", -1)
|
|
canonicalRequest := strings.Join([]string{
|
|
s.httpRequest.Method,
|
|
encodedPath,
|
|
rawQuery,
|
|
s.getCanonicalHeaders(s.extractedSignedHeaders),
|
|
s.getSignedHeaders(s.extractedSignedHeaders),
|
|
"UNSIGNED-PAYLOAD",
|
|
}, "\n")
|
|
return canonicalRequest
|
|
}
|
|
|
|
// getScope generate a string of a specific date, an AWS region, and a service.
|
|
func (s Sign) getScope(t time.Time) string {
|
|
scope := strings.Join([]string{
|
|
t.Format(yyyymmdd),
|
|
s.region,
|
|
"s3",
|
|
"aws4_request",
|
|
}, "/")
|
|
return scope
|
|
}
|
|
|
|
// getStringToSign a string based on selected query values.
|
|
func (s Sign) getStringToSign(canonicalRequest string, t time.Time) string {
|
|
stringToSign := signV4Algorithm + "\n" + t.Format(iso8601Format) + "\n"
|
|
stringToSign = stringToSign + s.getScope(t) + "\n"
|
|
canonicalRequestBytes := sha256.Sum256([]byte(canonicalRequest))
|
|
stringToSign = stringToSign + hex.EncodeToString(canonicalRequestBytes[:])
|
|
return stringToSign
|
|
}
|
|
|
|
// getSigningKey hmac seed to calculate final signature.
|
|
func (s Sign) getSigningKey(t time.Time) []byte {
|
|
secret := s.secretAccessKey
|
|
date := sumHMAC([]byte("AWS4"+secret), []byte(t.Format(yyyymmdd)))
|
|
region := sumHMAC(date, []byte(s.region))
|
|
service := sumHMAC(region, []byte("s3"))
|
|
signingKey := sumHMAC(service, []byte("aws4_request"))
|
|
return signingKey
|
|
}
|
|
|
|
// getSignature final signature in hexadecimal form.
|
|
func (s Sign) getSignature(signingKey []byte, stringToSign string) string {
|
|
return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign)))
|
|
}
|
|
|
|
// 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 (s *Sign) DoesPolicySignatureMatch(formValues map[string]string) (bool, *probe.Error) {
|
|
// Parse credential tag.
|
|
credential, err := parseCredential("Credential=" + formValues["X-Amz-Credential"])
|
|
if err != nil {
|
|
return false, err.Trace(formValues["X-Amz-Credential"])
|
|
}
|
|
|
|
// Verify if the access key id matches.
|
|
if credential.accessKeyID != s.accessKeyID {
|
|
return false, ErrInvalidAccessKeyID("Access key id does not match with our records.", credential.accessKeyID).Trace(credential.accessKeyID)
|
|
}
|
|
|
|
// Verify if the region is valid.
|
|
reqRegion := credential.scope.region
|
|
if !isValidRegion(reqRegion, s.region) {
|
|
return false, ErrInvalidRegion("Requested region is not recognized.", reqRegion).Trace(reqRegion)
|
|
}
|
|
|
|
// Save region.
|
|
s.region = reqRegion
|
|
|
|
// Parse date string.
|
|
t, e := time.Parse(iso8601Format, formValues["X-Amz-Date"])
|
|
if e != nil {
|
|
return false, probe.NewError(e)
|
|
}
|
|
signingKey := s.getSigningKey(t)
|
|
newSignature := s.getSignature(signingKey, formValues["Policy"])
|
|
if newSignature != formValues["X-Amz-Signature"] {
|
|
return false, nil
|
|
}
|
|
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 (s *Sign) DoesPresignedSignatureMatch() (bool, *probe.Error) {
|
|
// Parse request query string.
|
|
preSignValues, err := parsePreSignV4(s.httpRequest.URL.Query())
|
|
if err != nil {
|
|
return false, err.Trace(s.httpRequest.URL.String())
|
|
}
|
|
|
|
// Verify if the access key id matches.
|
|
if preSignValues.Credential.accessKeyID != s.accessKeyID {
|
|
return false, ErrInvalidAccessKeyID("Access key id does not match with our records.", preSignValues.Credential.accessKeyID).Trace(preSignValues.Credential.accessKeyID)
|
|
}
|
|
|
|
// Verify if region is valid.
|
|
reqRegion := preSignValues.Credential.scope.region
|
|
if !isValidRegion(reqRegion, s.region) {
|
|
return false, ErrInvalidRegion("Requested region is not recognized.", reqRegion).Trace(reqRegion)
|
|
}
|
|
|
|
// Save region.
|
|
s.region = reqRegion
|
|
|
|
// Extract all the signed headers along with its values.
|
|
s.extractedSignedHeaders = extractSignedHeaders(preSignValues.SignedHeaders, s.httpRequest.Header)
|
|
|
|
// Construct new query.
|
|
query := make(url.Values)
|
|
query.Set("X-Amz-Algorithm", signV4Algorithm)
|
|
|
|
if time.Now().UTC().Sub(preSignValues.Date) > time.Duration(preSignValues.Expires) {
|
|
return false, ErrExpiredPresignRequest("Presigned request already expired, please initiate a new request.")
|
|
}
|
|
|
|
// Save the date and expires.
|
|
t := preSignValues.Date
|
|
expireSeconds := int(time.Duration(preSignValues.Expires) / time.Second)
|
|
|
|
// Construct the query.
|
|
query.Set("X-Amz-Date", t.Format(iso8601Format))
|
|
query.Set("X-Amz-Expires", strconv.Itoa(expireSeconds))
|
|
query.Set("X-Amz-SignedHeaders", s.getSignedHeaders(s.extractedSignedHeaders))
|
|
query.Set("X-Amz-Credential", s.accessKeyID+"/"+s.getScope(t))
|
|
|
|
// Save other headers available in the request parameters.
|
|
for k, v := range s.httpRequest.URL.Query() {
|
|
if strings.HasPrefix(strings.ToLower(k), "x-amz") {
|
|
continue
|
|
}
|
|
query[k] = v
|
|
}
|
|
|
|
// Get the encoded query.
|
|
encodedQuery := query.Encode()
|
|
|
|
// Verify if date query is same.
|
|
if s.httpRequest.URL.Query().Get("X-Amz-Date") != query.Get("X-Amz-Date") {
|
|
return false, nil
|
|
}
|
|
// Verify if expires query is same.
|
|
if s.httpRequest.URL.Query().Get("X-Amz-Expires") != query.Get("X-Amz-Expires") {
|
|
return false, nil
|
|
}
|
|
// Verify if signed headers query is same.
|
|
if s.httpRequest.URL.Query().Get("X-Amz-SignedHeaders") != query.Get("X-Amz-SignedHeaders") {
|
|
return false, nil
|
|
}
|
|
// Verify if credential query is same.
|
|
if s.httpRequest.URL.Query().Get("X-Amz-Credential") != query.Get("X-Amz-Credential") {
|
|
return false, nil
|
|
}
|
|
// Verify finally if signature is same.
|
|
newSignature := s.getSignature(s.getSigningKey(t), s.getStringToSign(s.getPresignedCanonicalRequest(encodedQuery), t))
|
|
if s.httpRequest.URL.Query().Get("X-Amz-Signature") != newSignature {
|
|
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 (s *Sign) DoesSignatureMatch(hashedPayload string) (bool, *probe.Error) {
|
|
// Save authorization header.
|
|
v4Auth := s.httpRequest.Header.Get("Authorization")
|
|
|
|
// Parse signature version '4' header.
|
|
signV4Values, err := parseSignV4(v4Auth)
|
|
if err != nil {
|
|
return false, err.Trace(v4Auth)
|
|
}
|
|
|
|
// Extract all the signed headers along with its values.
|
|
s.extractedSignedHeaders = extractSignedHeaders(signV4Values.SignedHeaders, s.httpRequest.Header)
|
|
|
|
// Verify if the access key id matches.
|
|
if signV4Values.Credential.accessKeyID != s.accessKeyID {
|
|
return false, ErrInvalidAccessKeyID("Access key id does not match with our records.", signV4Values.Credential.accessKeyID).Trace(signV4Values.Credential.accessKeyID)
|
|
}
|
|
|
|
// Verify if region is valid.
|
|
reqRegion := signV4Values.Credential.scope.region
|
|
if !isValidRegion(reqRegion, s.region) {
|
|
return false, ErrInvalidRegion("Requested region is not recognized.", reqRegion).Trace(reqRegion)
|
|
}
|
|
|
|
// Save region.
|
|
s.region = reqRegion
|
|
|
|
// Set input payload.
|
|
s.httpRequest.Header.Set("X-Amz-Content-Sha256", hashedPayload)
|
|
|
|
// Extract date, if not present throw error.
|
|
var date string
|
|
if date = s.httpRequest.Header.Get(http.CanonicalHeaderKey("x-amz-date")); date == "" {
|
|
if date = s.httpRequest.Header.Get("Date"); date == "" {
|
|
return false, ErrMissingDateHeader("Date header is missing from the request.").Trace()
|
|
}
|
|
}
|
|
// Parse date header.
|
|
t, e := time.Parse(iso8601Format, date)
|
|
if e != nil {
|
|
return false, probe.NewError(e)
|
|
}
|
|
|
|
// Signature version '4'.
|
|
canonicalRequest := s.getCanonicalRequest()
|
|
stringToSign := s.getStringToSign(canonicalRequest, t)
|
|
signingKey := s.getSigningKey(t)
|
|
newSignature := s.getSignature(signingKey, stringToSign)
|
|
|
|
// Verify if signature match.
|
|
if newSignature != signV4Values.Signature {
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|