Closed nqbao closed 5 years ago
If I use this snippet, it works
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"fmt"
"net/url"
"strings"
"time"
)
func AwsIotWsUrl(accessKey string, secretKey string, sessionToken string, region string, endpoint string) string {
host := fmt.Sprintf("%s.iot.%s.amazonaws.com", endpoint, region)
// according to docs, time must be within 5min of actual time (or at least according to AWS servers)
now := time.Now().UTC()
dateLong := now.Format("20060102T150405Z")
dateShort := dateLong[:8]
serviceName := "iotdevicegateway"
scope := fmt.Sprintf("%s/%s/%s/aws4_request", dateShort, region, serviceName)
alg := "AWS4-HMAC-SHA256"
q := [][2]string{
{"X-Amz-Algorithm", alg},
{"X-Amz-Credential", accessKey + "/" + scope},
{"X-Amz-Date", dateLong},
{"X-Amz-SignedHeaders", "host"},
}
query := awsQueryParams(q)
signKey := awsSignKey(secretKey, dateShort, region, serviceName)
stringToSign := awsSignString(accessKey, secretKey, query, host, dateLong, alg, scope)
signature := fmt.Sprintf("%x", awsHmac(signKey, []byte(stringToSign)))
wsurl := fmt.Sprintf("wss://%s/mqtt?%s&X-Amz-Signature=%s", host, query, signature)
if sessionToken != "" {
wsurl = fmt.Sprintf("%s&X-Amz-Security-Token=%s", wsurl, url.QueryEscape(sessionToken))
}
return wsurl
}
func awsQueryParams(q [][2]string) string {
var buff bytes.Buffer
var i int
for _, param := range q {
if i != 0 {
buff.WriteRune('&')
}
i++
buff.WriteString(param[0])
buff.WriteRune('=')
buff.WriteString(url.QueryEscape(param[1]))
}
return buff.String()
}
func awsSignString(accessKey string, secretKey string, query string, host string, dateLongStr string, alg string, scopeStr string) string {
emptyStringHash := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
req := strings.Join([]string{
"GET",
"/mqtt",
query,
"host:" + host,
"", // separator
"host",
emptyStringHash,
}, "\n")
return strings.Join([]string{
alg,
dateLongStr,
scopeStr,
awsSha(req),
}, "\n")
}
func awsHmac(key []byte, data []byte) []byte {
h := hmac.New(sha256.New, key)
h.Write(data)
return h.Sum(nil)
}
func awsSignKey(secretKey string, dateShort string, region string, serviceName string) []byte {
h := awsHmac([]byte("AWS4"+secretKey), []byte(dateShort))
h = awsHmac(h, []byte(region))
h = awsHmac(h, []byte(serviceName))
h = awsHmac(h, []byte("aws4_request"))
return h
}
func awsSha(in string) string {
h := sha256.New()
fmt.Fprintf(h, "%s", in)
return fmt.Sprintf("%x", h.Sum(nil))
}
Hey @nqbao, thanks for reaching out to us. The SigV4 signing process for IoT Websocket URLs when using temporary session credentials requires appending the session token (X-Amz-Security-Token
) to the end of the URL after the canonical request is signed. By contrast, the standard SigV4 signing process implemented in the non-IoT AWS SDK's signers requires including the session token in the canonical request before the request is signed, so these signers won't work for Websocket URLs when using temporary session credentials. Instead you'll need to perform the SigV4 signing manually as shown in your second snippet or use one of the AWS IoT Device SDKs to pre-sign Websocket URLs using credentials that require a session token.
Alright, thank you. I will use the custom sign method.
I ran into this and found a simpler way to get things to work with the v4.Signer
. The key is to not provide the session token with your credentials, and add it to the URL after you've presigned your request.
To tweak your original code:
cred := credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.StaticProvider{
Value: credentials.Value{
AccessKeyID: *s.Credentials.AccessKeyId,
SecretAccessKey: *s.Credentials.SecretAccessKey,
// NOTE: we're not setting SessionToken here
},
},
// &credentials.EnvProvider{},
// &credentials.SharedCredentialsProvider{},
},
)
signer := v4.NewSigner(cred)
wsUrl, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
r := &http.Request{
Method: "GET",
URL: wsUrl,
}
m, err := signer.Presign(r, nil, "iotdevicegateway", region, 900*time.Second, time.Now())
if err != nil {
return nil, err
}
q := r.URL.Query()
q.Set("X-Amz-Security-Token", *s.Credentials.SessionToken)
r.URL.RawQuery = q.Encode()
fmt.Printf("%v\n%v\n", r.URL.String(), m)
return r.URL.String(), nil
Please fill out the sections below to help us address your issue.
Version of AWS SDK for Go?
v1.17.9
Version of Go (
go version
)?go version go1.11 darwin/amd64
What issue did you see?
I tried to use v4 to PreSign an AWS IoT Websocket URL with STS credentials. I always get bad status connection when I try to connect to the pre-signed URL.
Steps to reproduce
This code works with regular credential. I'm also 100% sure that the Assumed Role has the correct permission because I have tested it with https://github.com/aws/aws-iot-device-sdk-js before. It just does not work with Go.