└── sign4.go /sign4.go: -------------------------------------------------------------------------------- 1 | package awsauth 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/md5" 7 | "crypto/sha256" 8 | "encoding/base64" 9 | "encoding/hex" 10 | "fmt" 11 | "io/ioutil" 12 | "net/http" 13 | "net/url" 14 | "sort" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | // Credentials stores the information necessary to authorize with AWS and it 20 | // is from this information that requests are signed. 21 | type Credentials struct { 22 | AccessKeyID string 23 | SecretAccessKey string 24 | SecurityToken string `json:"Token"` 25 | Expiration time.Time 26 | } 27 | 28 | type metadata struct { 29 | algorithm string 30 | credentialScope string 31 | signedHeaders string 32 | date string 33 | region string 34 | service string 35 | } 36 | 37 | func hmacSHA256(key []byte, content string) []byte { 38 | mac := hmac.New(sha256.New, key) 39 | mac.Write([]byte(content)) 40 | return mac.Sum(nil) 41 | } 42 | 43 | func hashSHA256(content []byte) string { 44 | h := sha256.New() 45 | h.Write(content) 46 | return fmt.Sprintf("%x", h.Sum(nil)) 47 | } 48 | 49 | func hashMD5(content []byte) string { 50 | h := md5.New() 51 | h.Write(content) 52 | return base64.StdEncoding.EncodeToString(h.Sum(nil)) 53 | } 54 | 55 | func readAndReplaceBody(request *http.Request) []byte { 56 | if request.Body == nil { 57 | return []byte{} 58 | } 59 | payload, _ := ioutil.ReadAll(request.Body) 60 | request.Body = ioutil.NopCloser(bytes.NewReader(payload)) 61 | return payload 62 | } 63 | 64 | func concat(delim string, str ...string) string { 65 | return strings.Join(str, delim) 66 | } 67 | 68 | var now = func() time.Time { 69 | return time.Now().UTC() 70 | } 71 | 72 | func normuri(uri string) string { 73 | parts := strings.Split(uri, "/") 74 | for i := range parts { 75 | parts[i] = encodePathFrag(parts[i]) 76 | } 77 | return strings.Join(parts, "/") 78 | } 79 | 80 | func encodePathFrag(s string) string { 81 | hexCount := 0 82 | for i := 0; i < len(s); i++ { 83 | c := s[i] 84 | if shouldEscape(c) { 85 | hexCount++ 86 | } 87 | } 88 | t := make([]byte, len(s)+2*hexCount) 89 | j := 0 90 | for i := 0; i < len(s); i++ { 91 | c := s[i] 92 | if shouldEscape(c) { 93 | t[j] = '%' 94 | t[j+1] = "0123456789ABCDEF"[c>>4] 95 | t[j+2] = "0123456789ABCDEF"[c&15] 96 | j += 3 97 | } else { 98 | t[j] = c 99 | j++ 100 | } 101 | } 102 | return string(t) 103 | } 104 | 105 | func shouldEscape(c byte) bool { 106 | if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' { 107 | return false 108 | } 109 | if '0' <= c && c <= '9' { 110 | return false 111 | } 112 | if c == '-' || c == '_' || c == '.' || c == '~' { 113 | return false 114 | } 115 | return true 116 | } 117 | 118 | func normquery(v url.Values) string { 119 | queryString := v.Encode() 120 | 121 | // Go encodes a space as '+' but Amazon requires '%20'. Luckily any '+' in the 122 | // original query string has been percent escaped so all '+' chars that are left 123 | // were originally spaces. 124 | 125 | return strings.Replace(queryString, "+", "%20", -1) 126 | } 127 | 128 | // Sign4 signs a request with Signed Signature Version 4. 129 | func Sign4(request *http.Request, keys Credentials, region, service string) *http.Request { 130 | 131 | // Add the X-Amz-Security-Token header when using STS 132 | if keys.SecurityToken != "" { 133 | request.Header.Set("X-Amz-Security-Token", keys.SecurityToken) 134 | } 135 | 136 | prepareRequestV4(request) 137 | meta := new(metadata) 138 | 139 | // Task 1 140 | hashedCanonReq := hashedCanonicalRequestV4(request, meta) 141 | 142 | // Task 2 143 | meta.service = service 144 | meta.region = region 145 | stringToSign := stringToSignV4(request, hashedCanonReq, meta) 146 | 147 | // Task 3 148 | signingKey := signingKeyV4(keys.SecretAccessKey, meta.date, meta.region, meta.service) 149 | signature := signatureV4(signingKey, stringToSign) 150 | 151 | request.Header.Set("Authorization", buildAuthHeaderV4(signature, meta, keys)) 152 | 153 | return request 154 | } 155 | 156 | func hashedCanonicalRequestV4(request *http.Request, meta *metadata) string { 157 | // TASK 1. http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html 158 | 159 | payload := readAndReplaceBody(request) 160 | payloadHash := hashSHA256(payload) 161 | request.Header.Set("X-Amz-Content-Sha256", payloadHash) 162 | 163 | // Set this in header values to make it appear in the range of headers to sign 164 | request.Header.Set("Host", request.Host) 165 | 166 | var sortedHeaderKeys []string 167 | for key, _ := range request.Header { 168 | switch key { 169 | case "Content-Type", "Content-Md5", "Host": 170 | default: 171 | if !strings.HasPrefix(key, "X-Amz-") { 172 | continue 173 | } 174 | } 175 | sortedHeaderKeys = append(sortedHeaderKeys, strings.ToLower(key)) 176 | } 177 | sort.Strings(sortedHeaderKeys) 178 | 179 | var headersToSign string 180 | for _, key := range sortedHeaderKeys { 181 | value := strings.TrimSpace(request.Header.Get(key)) 182 | if key == "host" { 183 | //AWS does not include port in signing request. 184 | if strings.Contains(value, ":") { 185 | split := strings.Split(value, ":") 186 | port := split[1] 187 | if port == "80" || port == "443" { 188 | value = split[0] 189 | } 190 | } 191 | } 192 | headersToSign += key + ":" + value + "\n" 193 | } 194 | meta.signedHeaders = concat(";", sortedHeaderKeys...) 195 | canonicalRequest := concat("\n", request.Method, normuri(request.URL.Path), normquery(request.URL.Query()), headersToSign, meta.signedHeaders, payloadHash) 196 | 197 | return hashSHA256([]byte(canonicalRequest)) 198 | } 199 | 200 | func stringToSignV4(request *http.Request, hashedCanonReq string, meta *metadata) string { 201 | // TASK 2. http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html 202 | 203 | requestTs := request.Header.Get("X-Amz-Date") 204 | 205 | meta.algorithm = "AWS4-HMAC-SHA256" 206 | meta.date = tsDateV4(requestTs) 207 | meta.credentialScope = concat("/", meta.date, meta.region, meta.service, "aws4_request") 208 | 209 | return concat("\n", meta.algorithm, requestTs, meta.credentialScope, hashedCanonReq) 210 | } 211 | 212 | func signatureV4(signingKey []byte, stringToSign string) string { 213 | // TASK 3. http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html 214 | 215 | return hex.EncodeToString(hmacSHA256(signingKey, stringToSign)) 216 | } 217 | 218 | func prepareRequestV4(request *http.Request) *http.Request { 219 | necessaryDefaults := map[string]string{ 220 | "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", 221 | "X-Amz-Date": timestampV4(), 222 | } 223 | 224 | for header, value := range necessaryDefaults { 225 | if request.Header.Get(header) == "" { 226 | request.Header.Set(header, value) 227 | } 228 | } 229 | 230 | if request.URL.Path == "" { 231 | request.URL.Path += "/" 232 | } 233 | 234 | return request 235 | } 236 | 237 | func signingKeyV4(secretKey, date, region, service string) []byte { 238 | kDate := hmacSHA256([]byte("AWS4"+secretKey), date) 239 | kRegion := hmacSHA256(kDate, region) 240 | kService := hmacSHA256(kRegion, service) 241 | kSigning := hmacSHA256(kService, "aws4_request") 242 | return kSigning 243 | } 244 | 245 | func buildAuthHeaderV4(signature string, meta *metadata, keys Credentials) string { 246 | credential := keys.AccessKeyID + "/" + meta.credentialScope 247 | 248 | return meta.algorithm + 249 | " Credential=" + credential + 250 | ", SignedHeaders=" + meta.signedHeaders + 251 | ", Signature=" + signature 252 | } 253 | 254 | func timestampV4() string { 255 | return now().Format(timeFormatV4) 256 | } 257 | 258 | func tsDateV4(timestamp string) string { 259 | return timestamp[:8] 260 | } 261 | 262 | const timeFormatV4 = "20060102T150405Z" 263 | --------------------------------------------------------------------------------