Files
virtual-kubelet/vendor/github.com/hyperhq/hyper-api/signature/sign4.go
2017-12-05 17:53:58 -06:00

366 lines
9.2 KiB
Go

/*
Based on the AWS Signature Algorithm Sign4 http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
Based on the Implementation of https://github.com/smartystreets/go-aws-auth
- Both Sign and Check
- hostname of Hyper
- change header X-AMZ- to X-Hyper-
- changed normuri, treat // as /
*/
package signature
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
const (
headerPrefix = "X-Hyper-"
headerDate = "X-Hyper-Date"
headerContentHash = "X-Hyper-Content-Sha256"
headerAuthz = "Authorization"
metaAlgorithm = "HYPER-HMAC-SHA256"
keyPartsPrefix = "HYPER"
keyPartsRequest = "hyper_request"
timeFormatV4 = "20060102T150405Z"
reqExpiration = 5 * time.Minute
)
type AuthnHeader struct {
Algorithm string
AccessKey string
Scope string
SignedHeader string
Signature string
Date string
}
func Signiture4(secretKey string, req *http.Request, header *AuthnHeader, region string) (bool, error) {
meta := &metadata{
algorithm: header.Algorithm,
credentialScope: header.Scope,
signedHeaders: header.SignedHeader,
date: header.Date,
region: region,
service: "hyper",
}
hashedCanonReq, ok := canonicalRequestV4FromMeta(req, meta)
if !ok {
return false, errors.New("payload check error")
}
stringToSign := metaToSignV4(req, hashedCanonReq, meta)
signingKey := signingKeyV4(secretKey, meta.date, meta.region, meta.service)
signature := signatureV4(signingKey, stringToSign)
return signature == header.Signature, nil
}
func Sign4(accessKey, secretKey string, req *http.Request, region string) *http.Request {
prepareRequestV4(req)
meta := &metadata{}
// Task 1
hashedCanonReq := hashedCanonicalRequestV4(req, meta)
// Task 2
stringToSign := stringToSignV4(req, hashedCanonReq, meta, region)
// Task 3
signingKey := signingKeyV4(secretKey, meta.date, meta.region, meta.service)
signature := signatureV4(signingKey, stringToSign)
req.Header.Set(headerAuthz, buildAuthHeaderV4(accessKey, signature, meta))
return req
}
// Build Request Steps
func prepareRequestV4(request *http.Request) *http.Request {
necessaryDefaults := map[string]string{
"Content-Type": "application/json",
headerDate: timestampV4(),
}
for header, value := range necessaryDefaults {
if request.Header.Get(header) == "" {
request.Header.Set(header, value)
}
}
if request.URL.Path == "" {
request.URL.Path += "/"
}
return request
}
func hashedCanonicalRequestV4(request *http.Request, meta *metadata) string {
// TASK 1. http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
payload := readAndReplaceBody(request)
payloadHash := hashSHA256(payload)
request.Header.Set(headerContentHash, payloadHash)
// Set this in header values to make it appear in the range of headers to sign
request.Header.Set("Host", request.URL.Host)
var sortedHeaderKeys []string
for key := range request.Header {
switch key {
case "Content-Type", "Content-Md5", "Host":
default:
if !strings.HasPrefix(key, headerPrefix) {
continue
}
}
sortedHeaderKeys = append(sortedHeaderKeys, strings.ToLower(key))
}
sort.Strings(sortedHeaderKeys)
var headersToSign string
for _, key := range sortedHeaderKeys {
value := strings.TrimSpace(request.Header.Get(key))
if key == "host" {
//Hyper(AWS) does not include port in signing request.
if strings.Contains(value, ":") {
split := strings.Split(value, ":")
port := split[1]
if port == "80" || port == "443" {
value = split[0]
}
}
}
headersToSign += key + ":" + value + "\n"
}
meta.signedHeaders = concat(";", sortedHeaderKeys...)
canonicalRequest := concat("\n", request.Method, normuri(request.URL.Path), normquery(request.URL.Query()), headersToSign, meta.signedHeaders, payloadHash)
return hashSHA256([]byte(canonicalRequest))
}
func canonicalRequestV4FromMeta(request *http.Request, meta *metadata) (string, bool) {
payload := readPayload(request)
payloadHash := hashSHA256(payload)
if request.Header.Get(headerContentHash) != payloadHash {
return "", false
}
var headersToSign string
for _, hdr := range strings.Split(meta.signedHeaders, ";") {
value := strings.TrimSpace(request.Header.Get(hdr))
if hdr == "host" {
//Hyper(AWS) does not include port in signing request.
if strings.Contains(value, ":") {
split := strings.Split(value, ":")
port := split[1]
if port == "80" || port == "443" {
value = split[0]
}
}
}
headersToSign += hdr + ":" + value + "\n"
}
canonicalRequest := concat("\n", request.Method, normuri(request.URL.Path), normquery(request.URL.Query()), headersToSign, meta.signedHeaders, payloadHash)
return canonicalRequest, true
}
func stringToSignV4(request *http.Request, hashedCanonReq string, meta *metadata, region string) string {
// TASK 2. http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
requestTs := request.Header.Get(headerDate)
meta.algorithm = metaAlgorithm
meta.service, meta.region = serviceAndRegion(request.Host, region)
meta.date = tsDateV4(requestTs)
meta.credentialScope = concat("/", meta.date, meta.region, meta.service, keyPartsRequest)
return concat("\n", meta.algorithm, requestTs, meta.credentialScope, hashedCanonReq)
}
func metaToSignV4(request *http.Request, hashedCanonReq string, meta *metadata) string {
return concat("\n", meta.algorithm, request.Header.Get(headerDate), meta.credentialScope, hashedCanonReq)
}
func signingKeyV4(secretKey, date, region, service string) []byte {
kDate := hmacSHA256([]byte(keyPartsPrefix+secretKey), date)
kRegion := hmacSHA256(kDate, region)
kService := hmacSHA256(kRegion, service)
kSigning := hmacSHA256(kService, keyPartsRequest)
return kSigning
}
func signatureV4(signingKey []byte, stringToSign string) string {
// TASK 3. http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
return hex.EncodeToString(hmacSHA256(signingKey, stringToSign))
}
func buildAuthHeaderV4(accessKey, signature string, meta *metadata) string {
credential := accessKey + "/" + meta.credentialScope
return meta.algorithm +
" Credential=" + credential +
", SignedHeaders=" + meta.signedHeaders +
", Signature=" + signature
}
// Check Request Steps
func validateExpire(req *http.Request) bool {
dh := req.Header.Get(headerDate)
if dh == "" {
return false
}
date, err := time.ParseInLocation(timeFormatV4, dh, time.UTC)
if err != nil {
return false
}
if date.Add(reqExpiration).Before(time.Now().UTC()) {
return false
}
return true
}
// Details
type metadata struct {
algorithm string
credentialScope string
signedHeaders string
date string
region string
service string
}
func timestampV4() string {
return time.Now().UTC().Format(timeFormatV4)
}
func readAndReplaceBody(request *http.Request) []byte {
if request.Body == nil {
return []byte{}
}
payload, _ := ioutil.ReadAll(request.Body)
request.Body = ioutil.NopCloser(bytes.NewReader(payload))
return payload
}
func readPayload(req *http.Request) []byte {
if req.Body == nil {
return []byte{}
}
payload, _ := ioutil.ReadAll(req.Body)
return payload
}
func hmacSHA256(key []byte, content string) []byte {
mac := hmac.New(sha256.New, key)
mac.Write([]byte(content))
return mac.Sum(nil)
}
func hashSHA256(content []byte) string {
h := sha256.New()
h.Write(content)
return fmt.Sprintf("%x", h.Sum(nil))
}
func concat(delim string, str ...string) string {
return strings.Join(str, delim)
}
func normuri(uri string) string {
parts := []string{}
for _, s := range strings.Split(uri, "/") {
if s == "" {
//bypass empty path segments
continue
}
parts = append(parts, encodePathFrag(s))
}
return strings.Join(parts, "/")
}
func encodePathFrag(s string) string {
hexCount := 0
for i := 0; i < len(s); i++ {
c := s[i]
if shouldEscape(c) {
hexCount++
}
}
t := make([]byte, len(s)+2*hexCount)
j := 0
for i := 0; i < len(s); i++ {
c := s[i]
if shouldEscape(c) {
t[j] = '%'
t[j+1] = "0123456789ABCDEF"[c>>4]
t[j+2] = "0123456789ABCDEF"[c&15]
j += 3
} else {
t[j] = c
j++
}
}
return string(t)
}
func shouldEscape(c byte) bool {
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' {
return false
}
if '0' <= c && c <= '9' {
return false
}
if c == '-' || c == '_' || c == '.' || c == '~' {
return false
}
return true
}
func normquery(v url.Values) string {
queryString := v.Encode()
// Go encodes a space as '+' but Amazon requires '%20'. Luckily any '+' in the
// original query string has been percent escaped so all '+' chars that are left
// were originally spaces.
return strings.Replace(queryString, "+", "%20", -1)
}
// serviceAndRegion parsers a hostname to find out which ones it is.
func serviceAndRegion(host, r string) (service string, region string) {
// These are the defaults if the hostname doesn't suggest something else
region = r
service = "hyper"
// region.hyper.sh
if strings.HasSuffix(host, ".hyper.sh") {
parts := strings.SplitN(host, ".", 2)
if parts[1] == "hyper.sh" {
region = parts[0]
}
}
// no more service yet
return
}
func tsDateV4(timestamp string) string {
return timestamp[:8]
}