minio/cmd/signature-v2.go
Harshavardhana 8f2a3efa85
disallow sub-credentials based on root credentials to gain priviledges (#12947)
This happens because of a change added where any sub-credential
with parentUser == rootCredential i.e (MINIO_ROOT_USER) will
always be an owner, you cannot generate credentials with lower
session policy to restrict their access.

This doesn't affect user service accounts created with regular
users, LDAP or OpenID
2021-08-12 18:07:08 -07:00

412 lines
12 KiB
Go

// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"crypto/hmac"
"crypto/sha1"
"crypto/subtle"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/auth"
)
// Whitelist resource list that will be used in query string for signature-V2 calculation.
//
// This list should be kept alphabetically sorted, do not hastily edit.
var resourceList = []string{
"acl",
"cors",
"delete",
"encryption",
"legal-hold",
"lifecycle",
"location",
"logging",
"notification",
"partNumber",
"policy",
"requestPayment",
"response-cache-control",
"response-content-disposition",
"response-content-encoding",
"response-content-language",
"response-content-type",
"response-expires",
"retention",
"select",
"select-type",
"tagging",
"torrent",
"uploadId",
"uploads",
"versionId",
"versioning",
"versions",
"website",
}
// Signature and API related constants.
const (
signV2Algorithm = "AWS"
)
// AWS S3 Signature V2 calculation rule is give here:
// http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationStringToSign
func doesPolicySignatureV2Match(formValues http.Header) (auth.Credentials, APIErrorCode) {
accessKey := formValues.Get(xhttp.AmzAccessKeyID)
r := &http.Request{Header: formValues}
cred, _, s3Err := checkKeyValid(r, accessKey)
if s3Err != ErrNone {
return cred, s3Err
}
policy := formValues.Get("Policy")
signature := formValues.Get(xhttp.AmzSignatureV2)
if !compareSignatureV2(signature, calculateSignatureV2(policy, cred.SecretKey)) {
return cred, ErrSignatureDoesNotMatch
}
return cred, ErrNone
}
// Escape encodedQuery string into unescaped list of query params, returns error
// if any while unescaping the values.
func unescapeQueries(encodedQuery string) (unescapedQueries []string, err error) {
for _, query := range strings.Split(encodedQuery, "&") {
var unescapedQuery string
unescapedQuery, err = url.QueryUnescape(query)
if err != nil {
return nil, err
}
unescapedQueries = append(unescapedQueries, unescapedQuery)
}
return unescapedQueries, nil
}
// doesPresignV2SignatureMatch - Verify query headers with presigned signature
// - http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth
// returns ErrNone if matches. S3 errors otherwise.
func doesPresignV2SignatureMatch(r *http.Request) APIErrorCode {
// r.RequestURI will have raw encoded URI as sent by the client.
tokens := strings.SplitN(r.RequestURI, "?", 2)
encodedResource := tokens[0]
encodedQuery := ""
if len(tokens) == 2 {
encodedQuery = tokens[1]
}
var (
filteredQueries []string
gotSignature string
expires string
accessKey string
err error
)
var unescapedQueries []string
unescapedQueries, err = unescapeQueries(encodedQuery)
if err != nil {
return ErrInvalidQueryParams
}
// Extract the necessary values from presigned query, construct a list of new filtered queries.
for _, query := range unescapedQueries {
keyval := strings.SplitN(query, "=", 2)
if len(keyval) != 2 {
return ErrInvalidQueryParams
}
switch keyval[0] {
case xhttp.AmzAccessKeyID:
accessKey = keyval[1]
case xhttp.AmzSignatureV2:
gotSignature = keyval[1]
case xhttp.Expires:
expires = keyval[1]
default:
filteredQueries = append(filteredQueries, query)
}
}
// Invalid values returns error.
if accessKey == "" || gotSignature == "" || expires == "" {
return ErrInvalidQueryParams
}
cred, _, s3Err := checkKeyValid(r, accessKey)
if s3Err != ErrNone {
return s3Err
}
// Make sure the request has not expired.
expiresInt, err := strconv.ParseInt(expires, 10, 64)
if err != nil {
return ErrMalformedExpires
}
// Check if the presigned URL has expired.
if expiresInt < UTCNow().Unix() {
return ErrExpiredPresignRequest
}
encodedResource, err = getResource(encodedResource, r.Host, globalDomainNames)
if err != nil {
return ErrInvalidRequest
}
expectedSignature := preSignatureV2(cred, r.Method, encodedResource, strings.Join(filteredQueries, "&"), r.Header, expires)
if !compareSignatureV2(gotSignature, expectedSignature) {
return ErrSignatureDoesNotMatch
}
return ErrNone
}
func getReqAccessKeyV2(r *http.Request) (auth.Credentials, bool, APIErrorCode) {
if accessKey := r.Form.Get(xhttp.AmzAccessKeyID); accessKey != "" {
return checkKeyValid(r, accessKey)
}
// below is V2 Signed Auth header format, splitting on `space` (after the `AWS` string).
// Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature
authFields := strings.Split(r.Header.Get(xhttp.Authorization), " ")
if len(authFields) != 2 {
return auth.Credentials{}, false, ErrMissingFields
}
// Then will be splitting on ":", this will seprate `AWSAccessKeyId` and `Signature` string.
keySignFields := strings.Split(strings.TrimSpace(authFields[1]), ":")
if len(keySignFields) != 2 {
return auth.Credentials{}, false, ErrMissingFields
}
return checkKeyValid(r, keySignFields[0])
}
// Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature;
// Signature = Base64( HMAC-SHA1( YourSecretKey, UTF-8-Encoding-Of( StringToSign ) ) );
//
// StringToSign = HTTP-Verb + "\n" +
// Content-Md5 + "\n" +
// Content-Type + "\n" +
// Date + "\n" +
// CanonicalizedProtocolHeaders +
// CanonicalizedResource;
//
// CanonicalizedResource = [ SlashSeparator + Bucket ] +
// <HTTP-Request-URI, from the protocol name up to the query string> +
// [ subresource, if present. For example "?acl", "?location", "?logging", or "?torrent"];
//
// CanonicalizedProtocolHeaders = <described below>
// doesSignV2Match - Verify authorization header with calculated header in accordance with
// - http://docs.aws.amazon.com/AmazonS3/latest/dev/auth-request-sig-v2.html
// returns true if matches, false otherwise. if error is not nil then it is always false
func validateV2AuthHeader(r *http.Request) (auth.Credentials, APIErrorCode) {
var cred auth.Credentials
v2Auth := r.Header.Get(xhttp.Authorization)
if v2Auth == "" {
return cred, ErrAuthHeaderEmpty
}
// Verify if the header algorithm is supported or not.
if !strings.HasPrefix(v2Auth, signV2Algorithm) {
return cred, ErrSignatureVersionNotSupported
}
cred, _, apiErr := getReqAccessKeyV2(r)
if apiErr != ErrNone {
return cred, apiErr
}
return cred, ErrNone
}
func doesSignV2Match(r *http.Request) APIErrorCode {
v2Auth := r.Header.Get(xhttp.Authorization)
cred, apiError := validateV2AuthHeader(r)
if apiError != ErrNone {
return apiError
}
// r.RequestURI will have raw encoded URI as sent by the client.
tokens := strings.SplitN(r.RequestURI, "?", 2)
encodedResource := tokens[0]
encodedQuery := ""
if len(tokens) == 2 {
encodedQuery = tokens[1]
}
unescapedQueries, err := unescapeQueries(encodedQuery)
if err != nil {
return ErrInvalidQueryParams
}
encodedResource, err = getResource(encodedResource, r.Host, globalDomainNames)
if err != nil {
return ErrInvalidRequest
}
prefix := fmt.Sprintf("%s %s:", signV2Algorithm, cred.AccessKey)
if !strings.HasPrefix(v2Auth, prefix) {
return ErrSignatureDoesNotMatch
}
v2Auth = v2Auth[len(prefix):]
expectedAuth := signatureV2(cred, r.Method, encodedResource, strings.Join(unescapedQueries, "&"), r.Header)
if !compareSignatureV2(v2Auth, expectedAuth) {
return ErrSignatureDoesNotMatch
}
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.
func preSignatureV2(cred auth.Credentials, method string, encodedResource string, encodedQuery string, headers http.Header, expires string) string {
stringToSign := getStringToSignV2(method, encodedResource, encodedQuery, headers, expires)
return calculateSignatureV2(stringToSign, cred.SecretKey)
}
// Return the signature v2 of a given request.
func signatureV2(cred auth.Credentials, method string, encodedResource string, encodedQuery string, headers http.Header) string {
stringToSign := getStringToSignV2(method, encodedResource, encodedQuery, headers, "")
signature := calculateSignatureV2(stringToSign, cred.SecretKey)
return signature
}
// compareSignatureV2 returns true if and only if both signatures
// are equal. The signatures are expected to be base64 encoded strings
// according to the AWS S3 signature V2 spec.
func compareSignatureV2(sig1, sig2 string) bool {
// Decode signature string to binary byte-sequence representation is required
// as Base64 encoding of a value is not unique:
// For example "aGVsbG8=" and "aGVsbG8=\r" will result in the same byte slice.
signature1, err := base64.StdEncoding.DecodeString(sig1)
if err != nil {
return false
}
signature2, err := base64.StdEncoding.DecodeString(sig2)
if err != nil {
return false
}
return subtle.ConstantTimeCompare(signature1, signature2) == 1
}
// Return canonical headers.
func canonicalizedAmzHeadersV2(headers http.Header) string {
var keys []string
keyval := make(map[string]string, len(headers))
for key := range headers {
lkey := strings.ToLower(key)
if !strings.HasPrefix(lkey, "x-amz-") {
continue
}
keys = append(keys, lkey)
keyval[lkey] = strings.Join(headers[key], ",")
}
sort.Strings(keys)
var canonicalHeaders []string
for _, key := range keys {
canonicalHeaders = append(canonicalHeaders, key+":"+keyval[key])
}
return strings.Join(canonicalHeaders, "\n")
}
// Return canonical resource string.
func canonicalizedResourceV2(encodedResource, encodedQuery string) string {
queries := strings.Split(encodedQuery, "&")
keyval := make(map[string]string)
for _, query := range queries {
key := query
val := ""
index := strings.Index(query, "=")
if index != -1 {
key = query[:index]
val = query[index+1:]
}
keyval[key] = val
}
var canonicalQueries []string
for _, key := range resourceList {
val, ok := keyval[key]
if !ok {
continue
}
if val == "" {
canonicalQueries = append(canonicalQueries, key)
continue
}
canonicalQueries = append(canonicalQueries, key+"="+val)
}
// The queries will be already sorted as resourceList is sorted, if canonicalQueries
// is empty strings.Join returns empty.
canonicalQuery := strings.Join(canonicalQueries, "&")
if canonicalQuery != "" {
return encodedResource + "?" + canonicalQuery
}
return encodedResource
}
// Return string to sign under two different conditions.
// - if expires string is set then string to sign includes date instead of the Date header.
// - if expires string is empty then string to sign includes date header instead.
func getStringToSignV2(method string, encodedResource, encodedQuery string, headers http.Header, expires string) string {
canonicalHeaders := canonicalizedAmzHeadersV2(headers)
if len(canonicalHeaders) > 0 {
canonicalHeaders += "\n"
}
date := expires // Date is set to expires date for presign operations.
if date == "" {
// If expires date is empty then request header Date is used.
date = headers.Get(xhttp.Date)
}
// From the Amazon docs:
//
// StringToSign = HTTP-Verb + "\n" +
// Content-Md5 + "\n" +
// Content-Type + "\n" +
// Date/Expires + "\n" +
// CanonicalizedProtocolHeaders +
// CanonicalizedResource;
stringToSign := strings.Join([]string{
method,
headers.Get(xhttp.ContentMD5),
headers.Get(xhttp.ContentType),
date,
canonicalHeaders,
}, "\n")
return stringToSign + canonicalizedResourceV2(encodedResource, encodedQuery)
}