Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event Stream SigV4 Chunk Signinging #2996

Merged
merged 3 commits into from
Dec 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions aws/signer/v4/stream.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package v4

import (
"encoding/hex"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws/credentials"
)

type credentialValueProvider interface {
Get() (credentials.Value, error)
}

// StreamSigner implements signing of event stream encoded payloads
type StreamSigner struct {
region string
service string

credentials credentialValueProvider

prevSig []byte
}

// NewStreamSigner creates a SigV4 signer used to sign Event Stream encoded messages
func NewStreamSigner(region, service string, seedSignature []byte, credentials *credentials.Credentials) *StreamSigner {
return &StreamSigner{
region: region,
service: service,
credentials: credentials,
prevSig: seedSignature,
}
}

// GetSignature takes an event stream encoded headers and payload and returns a signature
func (s *StreamSigner) GetSignature(headers, payload []byte, date time.Time) ([]byte, error) {
credValue, err := s.credentials.Get()
if err != nil {
return nil, err
}

sigKey := deriveSigningKey(s.region, s.service, credValue.SecretAccessKey, date)

keyPath := buildSigningScope(s.region, s.service, date)

stringToSign := buildEventStreamStringToSign(headers, payload, s.prevSig, keyPath, date)

signature := hmacSHA256(sigKey, []byte(stringToSign))
s.prevSig = signature

return signature, nil
}

func buildEventStreamStringToSign(headers, payload, prevSig []byte, scope string, date time.Time) string {
return strings.Join([]string{
"AWS4-HMAC-SHA256-PAYLOAD",
formatTime(date),
scope,
hex.EncodeToString(prevSig),
hex.EncodeToString(hashSHA256(headers)),
hex.EncodeToString(hashSHA256(payload)),
}, "\n")
}
133 changes: 133 additions & 0 deletions aws/signer/v4/stream_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// +build go1.7

package v4

import (
"encoding/hex"
"fmt"
"strings"
"testing"
"time"

"github.com/aws/aws-sdk-go/aws/credentials"
)

type periodicBadCredentials struct {
call int
credentials *credentials.Credentials
}

func (p *periodicBadCredentials) Get() (credentials.Value, error) {
defer func() {
p.call++
}()

if p.call%2 == 0 {
return credentials.Value{}, fmt.Errorf("credentials error")
}

return p.credentials.Get()
}

type chunk struct {
headers, payload []byte
}

func mustDecodeHex(b []byte, err error) []byte {
if err != nil {
panic(err)
}

return b
}

func TestStreamingChunkSigner(t *testing.T) {
const (
region = "us-east-1"
service = "transcribe"
seedSignature = "9d9ab996c81f32c9d4e6fc166c92584f3741d1cb5ce325cd11a77d1f962c8de2"
)

staticCredentials := credentials.NewStaticCredentials("AKIDEXAMPLE", "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", "")
currentTime := time.Date(2019, 1, 27, 22, 37, 54, 0, time.UTC)

cases := map[string]struct {
credentials credentialValueProvider
chunks []chunk
expectedSignatures map[int]string
expectedErrors map[int]string
}{
"signature calculation": {
credentials: staticCredentials,
chunks: []chunk{
{headers: []byte("headers"), payload: []byte("payload")},
{headers: []byte("more headers"), payload: []byte("more payload")},
},
expectedSignatures: map[int]string{
0: "681a7eaa82891536f24af7ec7e9219ee251ccd9bac2f1b981eab7c5ec8579115",
1: "07633d9d4ab4d81634a2164934d1f648c7cbc6839a8cf0773d818127a267e4d6",
},
},
"signature calculation errors": {
credentials: &periodicBadCredentials{credentials: staticCredentials},
chunks: []chunk{
{headers: []byte("headers"), payload: []byte("payload")},
{headers: []byte("headers"), payload: []byte("payload")},
{headers: []byte("more headers"), payload: []byte("more payload")},
{headers: []byte("more headers"), payload: []byte("more payload")},
},
expectedSignatures: map[int]string{
1: "681a7eaa82891536f24af7ec7e9219ee251ccd9bac2f1b981eab7c5ec8579115",
3: "07633d9d4ab4d81634a2164934d1f648c7cbc6839a8cf0773d818127a267e4d6",
},
expectedErrors: map[int]string{
0: "credentials error",
2: "credentials error",
},
},
}

for name, tt := range cases {
t.Run(name, func(t *testing.T) {
chunkSigner := &StreamSigner{
region: region,
service: service,
credentials: tt.credentials,
prevSig: mustDecodeHex(hex.DecodeString(seedSignature)),
}

for i, chunk := range tt.chunks {
var expectedError string
if len(tt.expectedErrors) != 0 {
_, ok := tt.expectedErrors[i]
if ok {
expectedError = tt.expectedErrors[i]
}
}

signature, err := chunkSigner.GetSignature(chunk.headers, chunk.payload, currentTime)
if err == nil && len(expectedError) > 0 {
t.Errorf("expected error, but got nil")
continue
} else if err != nil && len(expectedError) == 0 {
t.Errorf("expected no error, but got %v", err)
continue
} else if err != nil && len(expectedError) > 0 && !strings.Contains(err.Error(), expectedError) {
t.Errorf("expected %v, but got %v", expectedError, err)
continue
} else if len(expectedError) > 0 {
continue
skmcgrail marked this conversation as resolved.
Show resolved Hide resolved
}

expectedSignature, ok := tt.expectedSignatures[i]
if !ok {
t.Fatalf("expected signature not provided for test case")
}

if e, a := expectedSignature, hex.EncodeToString(signature); e != a {
t.Errorf("expected %v, got %v", e, a)
}
}
})
}
}
64 changes: 38 additions & 26 deletions aws/signer/v4/v4.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const (
authHeaderPrefix = "AWS4-HMAC-SHA256"
timeFormat = "20060102T150405Z"
shortTimeFormat = "20060102"
awsV4Request = "aws4_request"

// emptyStringSHA256 is a SHA256 of an empty string
emptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
Expand Down Expand Up @@ -229,11 +230,9 @@ type signingCtx struct {

DisableURIPathEscaping bool

credValues credentials.Value
isPresign bool
formattedTime string
formattedShortTime string
unsignedPayload bool
credValues credentials.Value
isPresign bool
unsignedPayload bool

bodyDigest string
signedHeaders string
Expand Down Expand Up @@ -546,25 +545,17 @@ func (ctx *signingCtx) build(disableHeaderHoisting bool) error {
}

func (ctx *signingCtx) buildTime() {
ctx.formattedTime = ctx.Time.UTC().Format(timeFormat)
ctx.formattedShortTime = ctx.Time.UTC().Format(shortTimeFormat)

if ctx.isPresign {
duration := int64(ctx.ExpireTime / time.Second)
ctx.Query.Set("X-Amz-Date", ctx.formattedTime)
ctx.Query.Set("X-Amz-Date", formatTime(ctx.Time))
ctx.Query.Set("X-Amz-Expires", strconv.FormatInt(duration, 10))
} else {
ctx.Request.Header.Set("X-Amz-Date", ctx.formattedTime)
ctx.Request.Header.Set("X-Amz-Date", formatTime(ctx.Time))
}
}

func (ctx *signingCtx) buildCredentialString() {
ctx.credentialString = strings.Join([]string{
ctx.formattedShortTime,
ctx.Region,
ctx.ServiceName,
"aws4_request",
}, "/")
ctx.credentialString = buildSigningScope(ctx.Region, ctx.ServiceName, ctx.Time)

if ctx.isPresign {
ctx.Query.Set("X-Amz-Credential", ctx.credValues.AccessKeyID+"/"+ctx.credentialString)
Expand Down Expand Up @@ -653,19 +644,15 @@ func (ctx *signingCtx) buildCanonicalString() {
func (ctx *signingCtx) buildStringToSign() {
ctx.stringToSign = strings.Join([]string{
authHeaderPrefix,
ctx.formattedTime,
formatTime(ctx.Time),
ctx.credentialString,
hex.EncodeToString(makeSha256([]byte(ctx.canonicalString))),
hex.EncodeToString(hashSHA256([]byte(ctx.canonicalString))),
}, "\n")
}

func (ctx *signingCtx) buildSignature() {
secret := ctx.credValues.SecretAccessKey
date := makeHmac([]byte("AWS4"+secret), []byte(ctx.formattedShortTime))
region := makeHmac(date, []byte(ctx.Region))
service := makeHmac(region, []byte(ctx.ServiceName))
credentials := makeHmac(service, []byte("aws4_request"))
signature := makeHmac(credentials, []byte(ctx.stringToSign))
creds := deriveSigningKey(ctx.Region, ctx.ServiceName, ctx.credValues.SecretAccessKey, ctx.Time)
signature := hmacSHA256(creds, []byte(ctx.stringToSign))
ctx.signature = hex.EncodeToString(signature)
}

Expand Down Expand Up @@ -726,13 +713,13 @@ func (ctx *signingCtx) removePresign() {
ctx.Query.Del("X-Amz-SignedHeaders")
}

func makeHmac(key []byte, data []byte) []byte {
func hmacSHA256(key []byte, data []byte) []byte {
hash := hmac.New(sha256.New, key)
hash.Write(data)
return hash.Sum(nil)
}

func makeSha256(data []byte) []byte {
func hashSHA256(data []byte) []byte {
hash := sha256.New()
hash.Write(data)
return hash.Sum(nil)
Expand Down Expand Up @@ -804,3 +791,28 @@ func stripExcessSpaces(vals []string) {
vals[i] = string(buf[:m])
}
}

func buildSigningScope(region, service string, dt time.Time) string {
return strings.Join([]string{
formatShortTime(dt),
region,
service,
awsV4Request,
}, "/")
}

func deriveSigningKey(region, service, secretKey string, dt time.Time) []byte {
kDate := hmacSHA256([]byte("AWS4"+secretKey), []byte(formatShortTime(dt)))
kRegion := hmacSHA256(kDate, []byte(region))
kService := hmacSHA256(kRegion, []byte(service))
signingKey := hmacSHA256(kService, []byte(awsV4Request))
return signingKey
}

func formatShortTime(dt time.Time) string {
return dt.UTC().Format(shortTimeFormat)
}

func formatTime(dt time.Time) string {
return dt.UTC().Format(timeFormat)
}
6 changes: 3 additions & 3 deletions private/protocol/eventstream/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (e *Encoder) Encode(msg Message) (err error) {
}()
}

if err = encodeHeaders(e.headersBuf, msg.Headers); err != nil {
if err = EncodeHeaders(e.headersBuf, msg.Headers); err != nil {
return err
}

Expand Down Expand Up @@ -124,9 +124,9 @@ func encodePrelude(w io.Writer, crc hash.Hash32, headersLen, payloadLen uint32)
return nil
}

// encodeHeaders writes the header values to the writer encoded in the event
// EncodeHeaders writes the header values to the writer encoded in the event
// stream format. Returns an error if a header fails to encode.
func encodeHeaders(w io.Writer, headers Headers) error {
func EncodeHeaders(w io.Writer, headers Headers) error {
for _, h := range headers {
hn := headerName{
Len: uint8(len(h.Name)),
Expand Down
43 changes: 43 additions & 0 deletions private/protocol/eventstream/eventstreamapi/signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package eventstreamapi

import (
"bytes"
"time"

"github.com/aws/aws-sdk-go/private/protocol/eventstream"
)

const (
chunkSignatureHeader = ":chunk-signature"
chunkDateHeader = ":date"
)

// StreamSigner defines an interface for the implementation of signing of event stream payloads
type StreamSigner interface {
GetSignature(headers, payload []byte, date time.Time) ([]byte, error)
}

// MessageSigner encapsulates signing and attaching signatures to event stream messages
type MessageSigner struct {
Signer StreamSigner
}

// SignMessage takes the given event stream message generates and adds signature information
// to the event stream message.
func (s MessageSigner) SignMessage(msg *eventstream.Message, date time.Time) error {
msg.Headers.Set(chunkDateHeader, eventstream.TimestampValue(date))

var headers bytes.Buffer
if err := eventstream.EncodeHeaders(&headers, msg.Headers); err != nil {
return err
}

sig, err := s.Signer.GetSignature(headers.Bytes(), msg.Payload, date)
if err != nil {
return err
}

msg.Headers.Set(chunkSignatureHeader, eventstream.BytesValue(sig))

return nil
}
Loading