2015-07-08 19:54:32 -04:00
/ *
2016-03-12 19:08:15 -05:00
* Minio Cloud Storage , ( C ) 2015 , 2016 Minio , Inc .
2015-07-08 19:54:32 -04:00
*
* 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 .
* /
2016-03-12 19:08:15 -05:00
// This file implements helper functions to validate AWS
2016-02-21 20:57:05 -05:00
// 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.
2016-03-12 19:08:15 -05:00
package main
2015-07-08 19:54:32 -04:00
import (
"bytes"
"encoding/hex"
"net/http"
2015-10-01 01:59:52 -04:00
"net/url"
2015-07-08 19:54:32 -04:00
"sort"
2015-10-01 01:59:52 -04:00
"strconv"
2015-07-08 19:54:32 -04:00
"strings"
"time"
2016-02-10 19:40:09 -05:00
"github.com/minio/minio/pkg/crypto/sha256"
"github.com/minio/minio/pkg/probe"
2015-07-08 19:54:32 -04:00
)
2016-02-21 20:57:05 -05:00
// AWS Signature Version '4' constants.
2015-07-08 19:54:32 -04:00
const (
2016-02-15 20:42:39 -05:00
signV4Algorithm = "AWS4-HMAC-SHA256"
iso8601Format = "20060102T150405Z"
yyyymmdd = "20060102"
2015-07-08 19:54:32 -04:00
)
// getCanonicalHeaders generate a list of request headers with their values
2016-03-12 19:08:15 -05:00
func getCanonicalHeaders ( signedHeaders http . Header , host string ) string {
2015-07-08 19:54:32 -04:00
var headers [ ] string
2016-02-15 20:42:39 -05:00
vals := make ( http . Header )
2015-07-10 20:21:53 -04:00
for k , vv := range signedHeaders {
2015-07-08 19:54:32 -04:00
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" :
2016-03-12 19:08:15 -05:00
buf . WriteString ( host )
2015-07-08 19:54:32 -04:00
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
2016-03-12 19:08:15 -05:00
func getSignedHeaders ( signedHeaders http . Header ) string {
2015-07-08 19:54:32 -04:00
var headers [ ] string
2015-07-10 20:21:53 -04:00
for k := range signedHeaders {
2015-07-08 19:54:32 -04:00
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>
//
2016-03-12 19:08:15 -05:00
func getCanonicalRequest ( extractedSignedHeaders http . Header , payload , queryStr , urlPath , method , host string ) string {
rawQuery := strings . Replace ( queryStr , "+" , "%20" , - 1 )
encodedPath := getURLEncodedName ( urlPath )
2016-02-15 20:42:39 -05:00
// Convert any space strings back to "+".
2015-07-08 19:54:32 -04:00
encodedPath = strings . Replace ( encodedPath , "+" , "%20" , - 1 )
canonicalRequest := strings . Join ( [ ] string {
2016-03-12 19:08:15 -05:00
method ,
2015-07-08 19:54:32 -04:00
encodedPath ,
2016-03-12 19:08:15 -05:00
rawQuery ,
getCanonicalHeaders ( extractedSignedHeaders , host ) ,
getSignedHeaders ( extractedSignedHeaders ) ,
2015-10-01 01:59:52 -04:00
payload ,
} , "\n" )
return canonicalRequest
}
// getCanonicalRequest generate a canonical request of style
//
// canonicalRequest =
// <HTTPMethod>\n
// <CanonicalURI>\n
// <CanonicalQueryString>\n
// <CanonicalHeaders>\n
// <SignedHeaders>\n
// <HashedPayload>
//
2016-03-12 19:08:15 -05:00
func getPresignCanonicalRequest ( extractedSignedHeaders http . Header , presignedQuery , urlPath , method , host string ) string {
2015-10-01 01:59:52 -04:00
rawQuery := strings . Replace ( presignedQuery , "+" , "%20" , - 1 )
2016-03-12 19:08:15 -05:00
encodedPath := getURLEncodedName ( urlPath )
2016-02-15 20:42:39 -05:00
// Convert any space strings back to "+".
2015-10-01 01:59:52 -04:00
encodedPath = strings . Replace ( encodedPath , "+" , "%20" , - 1 )
canonicalRequest := strings . Join ( [ ] string {
2016-03-12 19:08:15 -05:00
method ,
2015-10-01 01:59:52 -04:00
encodedPath ,
rawQuery ,
2016-03-12 19:08:15 -05:00
getCanonicalHeaders ( extractedSignedHeaders , host ) ,
getSignedHeaders ( extractedSignedHeaders ) ,
2015-10-01 01:59:52 -04:00
"UNSIGNED-PAYLOAD" ,
2015-07-08 19:54:32 -04:00
} , "\n" )
return canonicalRequest
}
2016-02-15 20:42:39 -05:00
// getScope generate a string of a specific date, an AWS region, and a service.
2016-03-12 19:08:15 -05:00
func getScope ( t time . Time , region string ) string {
2015-07-08 19:54:32 -04:00
scope := strings . Join ( [ ] string {
t . Format ( yyyymmdd ) ,
2016-03-12 19:08:15 -05:00
region ,
2015-07-08 19:54:32 -04:00
"s3" ,
"aws4_request" ,
} , "/" )
return scope
}
2016-02-15 20:42:39 -05:00
// getStringToSign a string based on selected query values.
2016-03-12 19:08:15 -05:00
func getStringToSign ( canonicalRequest string , t time . Time , region string ) string {
2016-02-15 20:42:39 -05:00
stringToSign := signV4Algorithm + "\n" + t . Format ( iso8601Format ) + "\n"
2016-03-12 19:08:15 -05:00
stringToSign = stringToSign + getScope ( t , region ) + "\n"
2016-02-10 19:40:09 -05:00
canonicalRequestBytes := sha256 . Sum256 ( [ ] byte ( canonicalRequest ) )
stringToSign = stringToSign + hex . EncodeToString ( canonicalRequestBytes [ : ] )
2015-07-08 19:54:32 -04:00
return stringToSign
}
2016-02-15 20:42:39 -05:00
// getSigningKey hmac seed to calculate final signature.
2016-03-12 19:08:15 -05:00
func getSigningKey ( secretKey string , t time . Time , region string ) [ ] byte {
date := sumHMAC ( [ ] byte ( "AWS4" + secretKey ) , [ ] byte ( t . Format ( yyyymmdd ) ) )
regionBytes := sumHMAC ( date , [ ] byte ( region ) )
service := sumHMAC ( regionBytes , [ ] byte ( "s3" ) )
2015-07-08 19:54:32 -04:00
signingKey := sumHMAC ( service , [ ] byte ( "aws4_request" ) )
return signingKey
}
2016-02-15 20:42:39 -05:00
// getSignature final signature in hexadecimal form.
2016-03-12 19:08:15 -05:00
func getSignature ( signingKey [ ] byte , stringToSign string ) string {
2015-07-08 19:54:32 -04:00
return hex . EncodeToString ( sumHMAC ( signingKey , [ ] byte ( stringToSign ) ) )
}
2016-03-12 19:08:15 -05:00
// doesPolicySignatureMatch - Verify query headers with post policy
2015-10-01 01:59:52 -04:00
// - 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
2016-03-12 19:08:15 -05:00
func doesPolicySignatureMatch ( formValues map [ string ] string ) ( bool , * probe . Error ) {
// Access credentials.
cred := serverConfig . GetCredential ( )
// Server region.
region := serverConfig . GetRegion ( )
2016-02-15 20:42:39 -05:00
// Parse credential tag.
2016-03-12 19:08:15 -05:00
credHeader , err := parseCredentialHeader ( "Credential=" + formValues [ "X-Amz-Credential" ] )
2015-10-02 02:51:17 -04:00
if err != nil {
2016-02-15 20:42:39 -05:00
return false , err . Trace ( formValues [ "X-Amz-Credential" ] )
}
// Verify if the access key id matches.
2016-03-12 19:08:15 -05:00
if credHeader . accessKey != cred . AccessKeyID {
return false , ErrInvalidAccessKey ( "Access key id does not match with our records." , credHeader . accessKey ) . Trace ( credHeader . accessKey )
2016-02-15 20:42:39 -05:00
}
// Verify if the region is valid.
2016-03-12 19:08:15 -05:00
if ! isValidRegion ( credHeader . scope . region , region ) {
return false , ErrInvalidRegion ( "Requested region is not recognized." , credHeader . scope . region ) . Trace ( credHeader . scope . region )
2016-02-15 20:42:39 -05:00
}
// Parse date string.
t , e := time . Parse ( iso8601Format , formValues [ "X-Amz-Date" ] )
if e != nil {
return false , probe . NewError ( e )
2015-10-02 02:51:17 -04:00
}
2016-03-12 19:08:15 -05:00
// Get signing key.
signingKey := getSigningKey ( cred . SecretAccessKey , t , region )
// Get signature.
newSignature := getSignature ( signingKey , formValues [ "Policy" ] )
// Verify signature.
2016-02-15 20:42:39 -05:00
if newSignature != formValues [ "X-Amz-Signature" ] {
2015-10-02 02:51:17 -04:00
return false , nil
}
2015-10-01 01:59:52 -04:00
return true , nil
}
2016-03-12 19:08:15 -05:00
// doesPresignedSignatureMatch - Verify query headers with presigned signature
2015-10-01 01:59:52 -04:00
// - 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
2016-03-12 19:08:15 -05:00
func doesPresignedSignatureMatch ( r * http . Request ) ( bool , * probe . Error ) {
// Access credentials.
cred := serverConfig . GetCredential ( )
// Server region.
region := serverConfig . GetRegion ( )
// Copy request
req := * r
2016-02-15 20:42:39 -05:00
// Parse request query string.
2016-03-12 19:08:15 -05:00
preSignValues , err := parsePreSignV4 ( req . URL . Query ( ) )
2015-10-01 01:59:52 -04:00
if err != nil {
2016-03-12 19:08:15 -05:00
return false , err . Trace ( req . URL . String ( ) )
2015-10-01 01:59:52 -04:00
}
2016-02-15 20:42:39 -05:00
// Verify if the access key id matches.
2016-03-12 19:08:15 -05:00
if preSignValues . Credential . accessKey != cred . AccessKeyID {
return false , ErrInvalidAccessKey ( "Access key id does not match with our records." , preSignValues . Credential . accessKey ) . Trace ( preSignValues . Credential . accessKey )
2015-10-01 01:59:52 -04:00
}
2016-02-15 20:42:39 -05:00
// Verify if region is valid.
2016-03-12 19:08:15 -05:00
sRegion := preSignValues . Credential . scope . region
if ! isValidRegion ( sRegion , region ) {
return false , ErrInvalidRegion ( "Requested region is not recognized." , sRegion ) . Trace ( sRegion )
2015-10-01 01:59:52 -04:00
}
2016-02-15 20:42:39 -05:00
// Extract all the signed headers along with its values.
2016-03-12 19:08:15 -05:00
extractedSignedHeaders := extractSignedHeaders ( preSignValues . SignedHeaders , req . Header )
2016-02-15 20:42:39 -05:00
// Construct new query.
query := make ( url . Values )
query . Set ( "X-Amz-Algorithm" , signV4Algorithm )
2016-02-21 20:57:05 -05:00
if time . Now ( ) . UTC ( ) . Sub ( preSignValues . Date ) > time . Duration ( preSignValues . Expires ) {
2016-02-15 20:42:39 -05:00
return false , ErrExpiredPresignRequest ( "Presigned request already expired, please initiate a new request." )
2015-10-01 01:59:52 -04:00
}
2016-02-15 20:42:39 -05:00
// Save the date and expires.
2016-02-21 20:57:05 -05:00
t := preSignValues . Date
expireSeconds := int ( time . Duration ( preSignValues . Expires ) / time . Second )
2016-02-15 20:42:39 -05:00
2016-02-18 05:13:52 -05:00
// Construct the query.
2015-10-01 01:59:52 -04:00
query . Set ( "X-Amz-Date" , t . Format ( iso8601Format ) )
query . Set ( "X-Amz-Expires" , strconv . Itoa ( expireSeconds ) )
2016-03-12 19:08:15 -05:00
query . Set ( "X-Amz-SignedHeaders" , getSignedHeaders ( extractedSignedHeaders ) )
query . Set ( "X-Amz-Credential" , cred . AccessKeyID + "/" + getScope ( t , region ) )
2016-02-07 06:37:54 -05:00
// Save other headers available in the request parameters.
2016-03-12 19:08:15 -05:00
for k , v := range req . URL . Query ( ) {
2016-02-07 06:37:54 -05:00
if strings . HasPrefix ( strings . ToLower ( k ) , "x-amz" ) {
continue
}
query [ k ] = v
}
2016-02-18 05:13:52 -05:00
// Get the encoded query.
2015-10-01 01:59:52 -04:00
encodedQuery := query . Encode ( )
2016-01-28 14:55:00 -05:00
// Verify if date query is same.
2016-03-12 19:08:15 -05:00
if req . URL . Query ( ) . Get ( "X-Amz-Date" ) != query . Get ( "X-Amz-Date" ) {
2016-01-28 14:55:00 -05:00
return false , nil
}
// Verify if expires query is same.
2016-03-12 19:08:15 -05:00
if req . URL . Query ( ) . Get ( "X-Amz-Expires" ) != query . Get ( "X-Amz-Expires" ) {
2016-01-28 14:55:00 -05:00
return false , nil
}
// Verify if signed headers query is same.
2016-03-12 19:08:15 -05:00
if req . URL . Query ( ) . Get ( "X-Amz-SignedHeaders" ) != query . Get ( "X-Amz-SignedHeaders" ) {
2016-01-28 14:55:00 -05:00
return false , nil
}
// Verify if credential query is same.
2016-03-12 19:08:15 -05:00
if req . URL . Query ( ) . Get ( "X-Amz-Credential" ) != query . Get ( "X-Amz-Credential" ) {
2016-01-28 14:55:00 -05:00
return false , nil
}
2016-03-12 19:08:15 -05:00
/// Verify finally if signature is same.
// Get canonical request.
presignedCanonicalReq := getPresignCanonicalRequest ( extractedSignedHeaders , encodedQuery , req . URL . Path , req . Method , req . Host )
// Get string to sign from canonical request.
presignedStringToSign := getStringToSign ( presignedCanonicalReq , t , region )
// Get hmac presigned signing key.
presignedSigningKey := getSigningKey ( cred . SecretAccessKey , t , region )
// Get new signature.
newSignature := getSignature ( presignedSigningKey , presignedStringToSign )
// Verify signature.
if req . URL . Query ( ) . Get ( "X-Amz-Signature" ) != newSignature {
2015-10-01 01:59:52 -04:00
return false , nil
}
return true , nil
}
2016-03-12 19:08:15 -05:00
// doesSignatureMatch - Verify authorization header with calculated header in accordance with
2015-10-01 01:59:52 -04:00
// - 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
2016-03-12 19:08:15 -05:00
func doesSignatureMatch ( hashedPayload string , r * http . Request ) ( bool , * probe . Error ) {
// Access credentials.
cred := serverConfig . GetCredential ( )
// Server region.
region := serverConfig . GetRegion ( )
// Copy request.
req := * r
2016-02-15 20:42:39 -05:00
// Save authorization header.
2016-03-12 19:08:15 -05:00
v4Auth := req . Header . Get ( "Authorization" )
2016-02-15 20:42:39 -05:00
// 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.
2016-03-12 19:08:15 -05:00
extractedSignedHeaders := extractSignedHeaders ( signV4Values . SignedHeaders , req . Header )
2016-02-15 20:42:39 -05:00
// Verify if the access key id matches.
2016-03-12 19:08:15 -05:00
if signV4Values . Credential . accessKey != cred . AccessKeyID {
return false , ErrInvalidAccessKey ( "Access key id does not match with our records." , signV4Values . Credential . accessKey ) . Trace ( signV4Values . Credential . accessKey )
2016-02-15 20:42:39 -05:00
}
2015-07-09 17:42:04 -04:00
2016-02-15 20:42:39 -05:00
// Verify if region is valid.
2016-03-12 19:08:15 -05:00
sRegion := signV4Values . Credential . scope . region
if ! isValidRegion ( sRegion , region ) {
return false , ErrInvalidRegion ( "Requested region is not recognized." , sRegion ) . Trace ( sRegion )
2016-02-15 20:42:39 -05:00
}
// Extract date, if not present throw error.
2015-07-08 19:54:32 -04:00
var date string
2016-03-12 19:08:15 -05:00
if date = req . Header . Get ( http . CanonicalHeaderKey ( "x-amz-date" ) ) ; date == "" {
if date = r . Header . Get ( "Date" ) ; date == "" {
2016-02-15 20:42:39 -05:00
return false , ErrMissingDateHeader ( "Date header is missing from the request." ) . Trace ( )
2015-07-08 19:54:32 -04:00
}
}
2016-02-15 20:42:39 -05:00
// Parse date header.
t , e := time . Parse ( iso8601Format , date )
if e != nil {
return false , probe . NewError ( e )
2015-07-08 19:54:32 -04:00
}
2016-03-12 19:08:15 -05:00
// Query string.
queryStr := req . URL . Query ( ) . Encode ( )
// Get canonical request.
canonicalRequest := getCanonicalRequest ( extractedSignedHeaders , hashedPayload , queryStr , req . URL . Path , req . Method , req . Host )
// Get string to sign from canonical request.
stringToSign := getStringToSign ( canonicalRequest , t , region )
// Get hmac signing key.
signingKey := getSigningKey ( cred . SecretAccessKey , t , region )
// Calculate signature.
newSignature := getSignature ( signingKey , stringToSign )
2016-02-15 20:42:39 -05:00
// Verify if signature match.
if newSignature != signV4Values . Signature {
2015-07-09 17:42:04 -04:00
return false , nil
}
return true , nil
2015-07-08 19:54:32 -04:00
}