430 lines
13 KiB
Go
430 lines
13 KiB
Go
|
package v4a
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"encoding/hex"
|
||
|
"fmt"
|
||
|
"math/big"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"reflect"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/crypto"
|
||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||
|
"github.com/aws/smithy-go/logging"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
accessKey = "AKISORANDOMAASORANDOM"
|
||
|
secretKey = "q+jcrXGc+0zWN6uzclKVhvMmUsIfRPa4rlRandom"
|
||
|
)
|
||
|
|
||
|
func TestDeriveECDSAKeyPairFromSecret(t *testing.T) {
|
||
|
privateKey, err := deriveKeyFromAccessKeyPair(accessKey, secretKey)
|
||
|
if err != nil {
|
||
|
t.Fatalf("expected no error, got %v", err)
|
||
|
}
|
||
|
|
||
|
expectedX := func() *big.Int {
|
||
|
t.Helper()
|
||
|
b, ok := new(big.Int).SetString("15D242CEEBF8D8169FD6A8B5A746C41140414C3B07579038DA06AF89190FFFCB", 16)
|
||
|
if !ok {
|
||
|
t.Fatalf("failed to parse big integer")
|
||
|
}
|
||
|
return b
|
||
|
}()
|
||
|
expectedY := func() *big.Int {
|
||
|
t.Helper()
|
||
|
b, ok := new(big.Int).SetString("515242CEDD82E94799482E4C0514B505AFCCF2C0C98D6A553BF539F424C5EC0", 16)
|
||
|
if !ok {
|
||
|
t.Fatalf("failed to parse big integer")
|
||
|
}
|
||
|
return b
|
||
|
}()
|
||
|
|
||
|
if privateKey.X.Cmp(expectedX) != 0 {
|
||
|
t.Errorf("expected % X, got % X", expectedX, privateKey.X)
|
||
|
}
|
||
|
if privateKey.Y.Cmp(expectedY) != 0 {
|
||
|
t.Errorf("expected % X, got % X", expectedY, privateKey.Y)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestSignHTTP(t *testing.T) {
|
||
|
req := buildRequest("dynamodb", "us-east-1")
|
||
|
|
||
|
signer, credProvider := buildSigner(t, true)
|
||
|
|
||
|
key, err := credProvider.RetrievePrivateKey(context.Background())
|
||
|
if err != nil {
|
||
|
t.Fatalf("expect no error, got %v", err)
|
||
|
}
|
||
|
|
||
|
err = signer.SignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0))
|
||
|
if err != nil {
|
||
|
t.Fatalf("expected no error, got %v", err)
|
||
|
}
|
||
|
|
||
|
expectedDate := "19700101T000000Z"
|
||
|
expectedAlg := "AWS4-ECDSA-P256-SHA256"
|
||
|
expectedCredential := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request"
|
||
|
expectedSignedHeaders := "content-length;content-type;host;x-amz-date;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore;x-amz-region-set;x-amz-security-token;x-amz-target"
|
||
|
expectedStrToSignHash := "4ba7d0482cf4d5450cefdc067a00de1a4a715e444856fa3e1d85c35fb34d9730"
|
||
|
|
||
|
q := req.Header
|
||
|
|
||
|
validateAuthorization(t, q.Get("Authorization"), expectedAlg, expectedCredential, expectedSignedHeaders, expectedStrToSignHash)
|
||
|
|
||
|
if e, a := expectedDate, q.Get("X-Amz-Date"); e != a {
|
||
|
t.Errorf("expect %v, got %v", e, a)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestSignHTTP_NoSessionToken(t *testing.T) {
|
||
|
req := buildRequest("dynamodb", "us-east-1")
|
||
|
|
||
|
signer, credProvider := buildSigner(t, false)
|
||
|
|
||
|
key, err := credProvider.RetrievePrivateKey(context.Background())
|
||
|
if err != nil {
|
||
|
t.Fatalf("expect no error, got %v", err)
|
||
|
}
|
||
|
|
||
|
err = signer.SignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0))
|
||
|
if err != nil {
|
||
|
t.Fatalf("expected no error, got %v", err)
|
||
|
}
|
||
|
|
||
|
expectedAlg := "AWS4-ECDSA-P256-SHA256"
|
||
|
expectedCredential := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request"
|
||
|
expectedSignedHeaders := "content-length;content-type;host;x-amz-date;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore;x-amz-region-set;x-amz-target"
|
||
|
expectedStrToSignHash := "1aeefb422ae6aa0de7aec829da813e55cff35553cac212dffd5f9474c71e47ee"
|
||
|
|
||
|
q := req.Header
|
||
|
|
||
|
validateAuthorization(t, q.Get("Authorization"), expectedAlg, expectedCredential, expectedSignedHeaders, expectedStrToSignHash)
|
||
|
}
|
||
|
|
||
|
func TestPresignHTTP(t *testing.T) {
|
||
|
req := buildRequest("dynamodb", "us-east-1")
|
||
|
|
||
|
signer, credProvider := buildSigner(t, false)
|
||
|
|
||
|
key, err := credProvider.RetrievePrivateKey(context.Background())
|
||
|
if err != nil {
|
||
|
t.Fatalf("expect no error, got %v", err)
|
||
|
}
|
||
|
|
||
|
query := req.URL.Query()
|
||
|
query.Set("X-Amz-Expires", "18000")
|
||
|
req.URL.RawQuery = query.Encode()
|
||
|
|
||
|
signedURL, _, err := signer.PresignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0))
|
||
|
if err != nil {
|
||
|
t.Fatalf("expected no error, got %v", err)
|
||
|
}
|
||
|
|
||
|
expectedDate := "19700101T000000Z"
|
||
|
expectedAlg := "AWS4-ECDSA-P256-SHA256"
|
||
|
expectedHeaders := "content-length;content-type;host;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore"
|
||
|
expectedCredential := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request"
|
||
|
expectedStrToSignHash := "d7ffbd2fab644384c056957e6ac38de4ae68246764b5f5df171b3824153b6397"
|
||
|
expectedTarget := "prefix.Operation"
|
||
|
|
||
|
signedReq, err := url.Parse(signedURL)
|
||
|
if err != nil {
|
||
|
t.Fatalf("expected no error, got %v", err)
|
||
|
}
|
||
|
|
||
|
q := signedReq.Query()
|
||
|
|
||
|
validateSignature(t, expectedStrToSignHash, q.Get("X-Amz-Signature"))
|
||
|
|
||
|
if e, a := expectedAlg, q.Get("X-Amz-Algorithm"); e != a {
|
||
|
t.Errorf("expect %v, got %v", e, a)
|
||
|
}
|
||
|
if e, a := expectedCredential, q.Get("X-Amz-Credential"); e != a {
|
||
|
t.Errorf("expect %v, got %v", e, a)
|
||
|
}
|
||
|
if e, a := expectedHeaders, q.Get("X-Amz-SignedHeaders"); e != a {
|
||
|
t.Errorf("expect %v, got %v", e, a)
|
||
|
}
|
||
|
if e, a := expectedDate, q.Get("X-Amz-Date"); e != a {
|
||
|
t.Errorf("expect %v, got %v", e, a)
|
||
|
}
|
||
|
if a := q.Get("X-Amz-Meta-Other-Header"); len(a) != 0 {
|
||
|
t.Errorf("expect %v to be empty", a)
|
||
|
}
|
||
|
if e, a := expectedTarget, q.Get("X-Amz-Target"); e != a {
|
||
|
t.Errorf("expect %v, got %v", e, a)
|
||
|
}
|
||
|
if e, a := "us-east-1", q.Get("X-Amz-Region-Set"); e != a {
|
||
|
t.Errorf("expect %v, got %v", e, a)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestPresignHTTP_BodyWithArrayRequest(t *testing.T) {
|
||
|
req := buildRequest("dynamodb", "us-east-1")
|
||
|
req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a"
|
||
|
|
||
|
signer, credProvider := buildSigner(t, true)
|
||
|
|
||
|
key, err := credProvider.RetrievePrivateKey(context.Background())
|
||
|
if err != nil {
|
||
|
t.Fatalf("expect no error, got %v", err)
|
||
|
}
|
||
|
|
||
|
query := req.URL.Query()
|
||
|
query.Set("X-Amz-Expires", "300")
|
||
|
req.URL.RawQuery = query.Encode()
|
||
|
|
||
|
signedURI, _, err := signer.PresignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0))
|
||
|
if err != nil {
|
||
|
t.Fatalf("expected no error, got %v", err)
|
||
|
}
|
||
|
|
||
|
signedReq, err := url.Parse(signedURI)
|
||
|
if err != nil {
|
||
|
t.Fatalf("expected no error, got %v", err)
|
||
|
}
|
||
|
|
||
|
expectedAlg := "AWS4-ECDSA-P256-SHA256"
|
||
|
expectedDate := "19700101T000000Z"
|
||
|
expectedHeaders := "content-length;content-type;host;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore"
|
||
|
expectedStrToSignHash := "acff64fd3689be96259d4112c3742ff79f4da0d813bc58a285dc1c4449760bec"
|
||
|
expectedCred := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request"
|
||
|
expectedTarget := "prefix.Operation"
|
||
|
|
||
|
q := signedReq.Query()
|
||
|
|
||
|
validateSignature(t, expectedStrToSignHash, q.Get("X-Amz-Signature"))
|
||
|
|
||
|
if e, a := expectedAlg, q.Get("X-Amz-Algorithm"); e != a {
|
||
|
t.Errorf("expect %v, got %v", e, a)
|
||
|
}
|
||
|
if e, a := expectedCred, q.Get("X-Amz-Credential"); e != a {
|
||
|
t.Errorf("expect %v, got %v", e, a)
|
||
|
}
|
||
|
if e, a := expectedHeaders, q.Get("X-Amz-SignedHeaders"); e != a {
|
||
|
t.Errorf("expect %v, got %v", e, a)
|
||
|
}
|
||
|
if e, a := expectedDate, q.Get("X-Amz-Date"); e != a {
|
||
|
t.Errorf("expect %v, got %v", e, a)
|
||
|
}
|
||
|
if a := q.Get("X-Amz-Meta-Other-Header"); len(a) != 0 {
|
||
|
t.Errorf("expect %v to be empty, was not", a)
|
||
|
}
|
||
|
if e, a := expectedTarget, q.Get("X-Amz-Target"); e != a {
|
||
|
t.Errorf("expect %v, got %v", e, a)
|
||
|
}
|
||
|
if e, a := "us-east-1", q.Get("X-Amz-Region-Set"); e != a {
|
||
|
t.Errorf("expect %v, got %v", e, a)
|
||
|
}
|
||
|
}
|
||
|
func TestSign_buildCanonicalHeaders(t *testing.T) {
|
||
|
serviceName := "mockAPI"
|
||
|
region := "mock-region"
|
||
|
endpoint := "https://" + serviceName + "." + region + ".amazonaws.com"
|
||
|
|
||
|
req, err := http.NewRequest("POST", endpoint, nil)
|
||
|
if err != nil {
|
||
|
t.Fatalf("failed to create request, %v", err)
|
||
|
}
|
||
|
|
||
|
req.Header.Set("FooInnerSpace", " inner space ")
|
||
|
req.Header.Set("FooLeadingSpace", " leading-space")
|
||
|
req.Header.Add("FooMultipleSpace", "no-space")
|
||
|
req.Header.Add("FooMultipleSpace", "\ttab-space")
|
||
|
req.Header.Add("FooMultipleSpace", "trailing-space ")
|
||
|
req.Header.Set("FooNoSpace", "no-space")
|
||
|
req.Header.Set("FooTabSpace", "\ttab-space\t")
|
||
|
req.Header.Set("FooTrailingSpace", "trailing-space ")
|
||
|
req.Header.Set("FooWrappedSpace", " wrapped-space ")
|
||
|
|
||
|
credProvider := &SymmetricCredentialAdaptor{
|
||
|
SymmetricProvider: staticCredentialsProvider{
|
||
|
Value: aws.Credentials{
|
||
|
AccessKeyID: accessKey,
|
||
|
SecretAccessKey: secretKey,
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
key, err := credProvider.RetrievePrivateKey(context.Background())
|
||
|
if err != nil {
|
||
|
t.Fatalf("expect no error, got %v", err)
|
||
|
}
|
||
|
|
||
|
ctx := &httpSigner{
|
||
|
Request: req,
|
||
|
ServiceName: serviceName,
|
||
|
RegionSet: []string{region},
|
||
|
Credentials: key,
|
||
|
Time: time.Date(2021, 10, 20, 12, 42, 0, 0, time.UTC),
|
||
|
}
|
||
|
|
||
|
build, err := ctx.Build()
|
||
|
if err != nil {
|
||
|
t.Fatalf("expected no error, got %v", err)
|
||
|
}
|
||
|
|
||
|
expectCanonicalString := strings.Join([]string{
|
||
|
`POST`,
|
||
|
`/`,
|
||
|
``,
|
||
|
`fooinnerspace:inner space`,
|
||
|
`fooleadingspace:leading-space`,
|
||
|
`foomultiplespace:no-space,tab-space,trailing-space`,
|
||
|
`foonospace:no-space`,
|
||
|
`footabspace:tab-space`,
|
||
|
`footrailingspace:trailing-space`,
|
||
|
`foowrappedspace:wrapped-space`,
|
||
|
`host:mockAPI.mock-region.amazonaws.com`,
|
||
|
`x-amz-date:20211020T124200Z`,
|
||
|
`x-amz-region-set:mock-region`,
|
||
|
``,
|
||
|
`fooinnerspace;fooleadingspace;foomultiplespace;foonospace;footabspace;footrailingspace;foowrappedspace;host;x-amz-date;x-amz-region-set`,
|
||
|
``,
|
||
|
}, "\n")
|
||
|
if diff := cmpDiff(expectCanonicalString, build.CanonicalString); diff != "" {
|
||
|
t.Errorf("expect match, got\n%s", diff)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func validateAuthorization(t *testing.T, authorization, expectedAlg, expectedCredential, expectedSignedHeaders, expectedStrToSignHash string) {
|
||
|
t.Helper()
|
||
|
split := strings.SplitN(authorization, " ", 2)
|
||
|
|
||
|
if len(split) != 2 {
|
||
|
t.Fatal("unexpected authorization header format")
|
||
|
}
|
||
|
|
||
|
if e, a := split[0], expectedAlg; e != a {
|
||
|
t.Errorf("expected %v, got %v", e, a)
|
||
|
}
|
||
|
|
||
|
keyValues := strings.Split(split[1], ", ")
|
||
|
seen := make(map[string]string)
|
||
|
|
||
|
for _, kv := range keyValues {
|
||
|
idx := strings.Index(kv, "=")
|
||
|
if idx == -1 {
|
||
|
continue
|
||
|
}
|
||
|
key, value := kv[:idx], kv[idx+1:]
|
||
|
seen[key] = value
|
||
|
}
|
||
|
|
||
|
if a, ok := seen["Credential"]; ok {
|
||
|
if expectedCredential != a {
|
||
|
t.Errorf("expected credential %v, got %v", expectedCredential, a)
|
||
|
}
|
||
|
} else {
|
||
|
t.Errorf("Credential not found in authorization string")
|
||
|
}
|
||
|
|
||
|
if a, ok := seen["SignedHeaders"]; ok {
|
||
|
if expectedSignedHeaders != a {
|
||
|
t.Errorf("expected signed headers %v, got %v", expectedSignedHeaders, a)
|
||
|
}
|
||
|
} else {
|
||
|
t.Errorf("SignedHeaders not found in authorization string")
|
||
|
}
|
||
|
|
||
|
if a, ok := seen["Signature"]; ok {
|
||
|
validateSignature(t, expectedStrToSignHash, a)
|
||
|
} else {
|
||
|
t.Errorf("signature not found in authorization string")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func validateSignature(t *testing.T, expectedHash, signature string) {
|
||
|
t.Helper()
|
||
|
pair, err := deriveKeyFromAccessKeyPair(accessKey, secretKey)
|
||
|
if err != nil {
|
||
|
t.Fatalf("expected no error, got %v", err)
|
||
|
}
|
||
|
|
||
|
hash, _ := hex.DecodeString(expectedHash)
|
||
|
sig, _ := hex.DecodeString(signature)
|
||
|
|
||
|
ok, err := crypto.VerifySignature(&pair.PublicKey, hash, sig)
|
||
|
if err != nil {
|
||
|
t.Fatalf("expected no error, got %v", err)
|
||
|
}
|
||
|
if !ok {
|
||
|
t.Errorf("failed to verify signing singature")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func buildRequest(serviceName, region string) *http.Request {
|
||
|
endpoint := "https://" + serviceName + "." + region + ".amazonaws.com"
|
||
|
req, _ := http.NewRequest("POST", endpoint, nil)
|
||
|
req.URL.Opaque = "//example.org/bucket/key-._~,!@%23$%25^&*()"
|
||
|
req.Header.Set("X-Amz-Target", "prefix.Operation")
|
||
|
req.Header.Set("Content-Type", "application/x-amz-json-1.0")
|
||
|
|
||
|
req.Header.Set("Content-Length", strconv.Itoa(1024))
|
||
|
|
||
|
req.Header.Set("X-Amz-Meta-Other-Header", "some-value=!@#$%^&* (+)")
|
||
|
req.Header.Add("X-Amz-Meta-Other-Header_With_Underscore", "some-value=!@#$%^&* (+)")
|
||
|
req.Header.Add("X-amz-Meta-Other-Header_With_Underscore", "some-value=!@#$%^&* (+)")
|
||
|
return req
|
||
|
}
|
||
|
|
||
|
func buildSigner(t *testing.T, withToken bool) (*Signer, CredentialsProvider) {
|
||
|
creds := aws.Credentials{
|
||
|
AccessKeyID: accessKey,
|
||
|
SecretAccessKey: secretKey,
|
||
|
}
|
||
|
|
||
|
if withToken {
|
||
|
creds.SessionToken = "TOKEN"
|
||
|
}
|
||
|
|
||
|
return NewSigner(func(options *SignerOptions) {
|
||
|
options.Logger = loggerFunc(func(format string, v ...interface{}) {
|
||
|
t.Logf(format, v...)
|
||
|
})
|
||
|
}), &SymmetricCredentialAdaptor{
|
||
|
SymmetricProvider: staticCredentialsProvider{
|
||
|
Value: creds,
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type loggerFunc func(format string, v ...interface{})
|
||
|
|
||
|
func (l loggerFunc) Logf(_ logging.Classification, format string, v ...interface{}) {
|
||
|
l(format, v...)
|
||
|
}
|
||
|
|
||
|
type staticCredentialsProvider struct {
|
||
|
Value aws.Credentials
|
||
|
}
|
||
|
|
||
|
func (s staticCredentialsProvider) Retrieve(_ context.Context) (aws.Credentials, error) {
|
||
|
v := s.Value
|
||
|
if v.AccessKeyID == "" || v.SecretAccessKey == "" {
|
||
|
return aws.Credentials{
|
||
|
Source: "Source Name",
|
||
|
}, fmt.Errorf("static credentials are empty")
|
||
|
}
|
||
|
|
||
|
if len(v.Source) == 0 {
|
||
|
v.Source = "Source Name"
|
||
|
}
|
||
|
|
||
|
return v, nil
|
||
|
}
|
||
|
|
||
|
func cmpDiff(e, a interface{}) string {
|
||
|
if !reflect.DeepEqual(e, a) {
|
||
|
return fmt.Sprintf("%v != %v", e, a)
|
||
|
}
|
||
|
return ""
|
||
|
}
|