goamazon-s3google-cloud-platformgoogle-cloud-storage

GCP Cloud Storage - Golang aws sdk2 - Upload file with s3 INTEROPERABILITY Creds


I'm trying to implements download/upload a file from/to a bucket in cloud storage via the s3 go sdk aws-sdk-go-v2 using the Interoperability feature

The download is working as expected, but the upload isnt working, with this error message: SDK 2022/09/14 11:24:43 DEBUG request failed with unretryable error https response error StatusCode: 403, RequestID: , HostID: , api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.

As I use same access_key and secret_key for both download and upload, it does not seems to be a credentials problem. Plus, the service account behind the hmac keys has the Storage object Admin Role.

Here the code:

main.go

package main

import (
    "context"
    "fmt"
    "os"
    "strings"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/credentials"
    "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

var BUCKET_NAME = ""

func main() {

    //prepare gcp resolver
    gcpResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
        return aws.Endpoint{
            URL:               "https://storage.googleapis.com",
            SigningRegion:     "auto",
            Source:            aws.EndpointSourceCustom,
            HostnameImmutable: true,
        }, nil
    })
    //file with fornat : $accessKey:$secretKey
    file, _ := os.ReadFile("/home/bapt/creds/amz-keys-gcp-2")
    keys := strings.Split(string(file), ":")

    //init the config options
    optConfig := []func(*config.LoadOptions) error{
        config.WithRegion("auto"),
        config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(keys[0], strings.TrimRight(keys[1], "\n"), "")),
        config.WithClientLogMode(aws.LogRetries | aws.LogRequestWithBody | aws.LogResponseWithBody | aws.LogRequestEventMessage | aws.LogResponseEventMessage | aws.LogSigning),
        config.WithEndpointResolverWithOptions(gcpResolver),
    }

    //init config
    cfg, _ := config.LoadDefaultConfig(context.TODO(), optConfig...)

    //init service
    svc := s3.NewFromConfig(cfg)
    tempFile, _ := os.CreateTemp("/tmp", "test-gcp-*")
    defer tempFile.Close()
    downloader := manager.NewDownloader(svc)
    downloader.Download(context.TODO(),tempFile, &s3.GetObjectInput{
        Bucket: aws.String(BUCKET_NAME),
        Key:    aws.String("file-test.txt"),
    })
    //init uploader ( no multipart)
    uploader := manager.NewUploader(svc, func(u *manager.Uploader) {
        u.Concurrency = 1
        u.MaxUploadParts = 1
    })
    //upload
    _, err := uploader.Upload(context.TODO(), &s3.PutObjectInput{
        Bucket: aws.String(BUCKET_NAME),
        Key:    aws.String("file-test.txt"),
        Body:   strings.NewReader("HELLO"),
    })
    fmt.Println(err)
}

go.mod

module gcps3/v2

go 1.18

require (
    github.com/aws/aws-sdk-go-v2 v1.16.14
    github.com/aws/aws-sdk-go-v2/config v1.17.5
    github.com/aws/aws-sdk-go-v2/credentials v1.12.18
    github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.31
    github.com/aws/aws-sdk-go-v2/service/s3 v1.27.9
)

require (
    github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.7 // indirect
    github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.15 // indirect
    github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.21 // indirect
    github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.15 // indirect
    github.com/aws/aws-sdk-go-v2/internal/ini v1.3.22 // indirect
    github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.12 // indirect
    github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.8 // indirect
    github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.16 // indirect
    github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.15 // indirect
    github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.15 // indirect
    github.com/aws/aws-sdk-go-v2/service/sso v1.11.21 // indirect
    github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.3 // indirect
    github.com/aws/aws-sdk-go-v2/service/sts v1.16.17 // indirect
    github.com/aws/smithy-go v1.13.2 // indirect
    github.com/jmespath/go-jmespath v0.4.0 // indirect
)

And here the debug trace of the PUT ( my bucket name is replaced by BUCKET, and access_key with GOOG1ID:

SDK 2022/09/14 14:52:37 DEBUG Request Signature:
---[ CANONICAL STRING  ]-----------------------------
PUT
/BUCKET/file-test.txt
x-id=PutObject
accept-encoding:identity
amz-sdk-invocation-id:d6776820-e336-4bdf-afa3-0ca3b6d5b0a0
amz-sdk-request:attempt=1; max=3
content-length:5
content-type:application/octet-stream
host:storage.googleapis.com
x-amz-content-sha256:UNSIGNED-PAYLOAD
x-amz-date:20220914T125237Z

accept-encoding;amz-sdk-invocation-id;amz-sdk-request;content-length;content-type;host;x-amz-content-sha256;x-amz-date
UNSIGNED-PAYLOAD
---[ STRING TO SIGN ]--------------------------------
AWS4-HMAC-SHA256
20220914T125237Z
20220914/auto/s3/aws4_request
bc09.....daf520
-----------------------------------------------------
SDK 2022/09/14 14:52:37 DEBUG Request
PUT /BUCKET/file-test.txt?x-id=PutObject HTTP/1.1
Host: storage.googleapis.com
User-Agent: aws-sdk-go-v2/1.16.14 os/linux lang/go/1.18.1 md/GOOS/linux md/GOARCH/amd64 api/s3/1.27.9 ft/s3-transfer
Content-Length: 5
Accept-Encoding: identity
Amz-Sdk-Invocation-Id: d6776820-e336-4bdf-afa3-0ca3b6d5b0a0
Amz-Sdk-Request: attempt=1; max=3
Authorization: AWS4-HMAC-SHA256 Credential=GOOG1ID/20220914/auto/s3/aws4_request, SignedHeaders=accept-encoding;amz-sdk-invocation-id;amz-sdk-request;content-length;content-type;host;x-amz-content-sha256;x-amz-date, Signature=c994....37d0
Content-Type: application/octet-stream
X-Amz-Content-Sha256: UNSIGNED-PAYLOAD
X-Amz-Date: 20220914T125237Z

HELLO
SDK 2022/09/14 14:52:37 DEBUG Response
HTTP/2.0 403 Forbidden
Content-Length: 883
Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
Content-Type: application/xml; charset=UTF-8
Date: Wed, 14 Sep 2022 12:52:38 GMT
Server: UploadServer
X-Guploader-Uploadid: ADPycdt0aCu6BTmzWQl2Ehc4q2sP8rtexDb4Keyn6cQL_GigREvc8T1CzX0HH-ZXgw_6XWLJPPYXufwRCr0Sl7uSsiIi0Q

<?xml version='1.0' encoding='UTF-8'?><Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.</Message><StringToSign>AWS4-HMAC-SHA256
20220914T125237Z
20220914/auto/s3/aws4_request
0a10....d63e</StringToSign><CanonicalRequest>PUT
/BUCKET/file-test.txt
x-id=PutObject
accept-encoding:identity,gzip(gfe)
amz-sdk-invocation-id:d6776820-e336-4bdf-afa3-0ca3b6d5b0a0
amz-sdk-request:attempt=1; max=3
content-length:5
content-type:application/octet-stream
host:storage.googleapis.com
x-amz-content-sha256:UNSIGNED-PAYLOAD
x-amz-date:20220914T125237Z

accept-encoding;amz-sdk-invocation-id;amz-sdk-request;content-length;content-type;host;x-amz-content-sha256;x-amz-date
UNSIGNED-PAYLOAD</CanonicalRequest></Error>
SDK 2022/09/14 14:52:37 DEBUG request failed with unretryable error https response error StatusCode: 403, RequestID: , HostID: , api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.
operation error S3: PutObject, https response error StatusCode: 403, RequestID: , HostID: , api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.

I tested the upload, with this python script, and it works (same creds)

boto3.set_stream_logger('', logging.DEBUG)
GCP_BUCKET = True
FILE_TO_BE_UPLOADED = '/tmp/toto'

if GCP_BUCKET:
    ACCESS_KEY = "ACESS"
    SECRET_KEY = "SECRET"
    bucket_name = "BUCKET"    
    region_name="auto"
    endpoint_url="https://storage.googleapis.com"
    
def makeS3Client():
    s3 = boto3.client("s3", 
                region_name=region_name,
                endpoint_url=endpoint_url,
                aws_access_key_id=ACCESS_KEY,
                aws_secret_access_key=SECRET_KEY,
                )

    return s3


def upload_file(s3,bucket_name,fname):
    """
    Uploads file to S3 bucket using S3 client object
    :return: None
    """
    object_name = os.path.basename(fname)
    file_name = os.path.abspath(fname)
    #file_name = os.path.join(pathlib.Path(__file__).parent.resolve(), fname)

    response = s3.upload_file(file_name, bucket_name, object_name)
    #print(response)  # prints None

The only different between go and python i saw in the https requests made is the signedHeaders used.

But the go code with an s3 aws bucket is working fine...

Am I missing an option ?

Thanks for your help.


Solution

  • The issue that @h3yduck mentioned, it seems that accept-encoding needs to be excluded from the signature signing process in the v2 library. Since the signature is dependent on the headers mentioned in SignedHeaders, we will have to recalculate the signature ourselves.

    The AWS Configuration object exposes a variable for a custom http client to be used for all services. We can use this by defining our own RoundTripper that modifies the request's signature to one that doesn't account for accept-encoding.

    Example:

    package main
    
    import (
        "context"
        "fmt"
        "net/http"
        "net/http/httputil"
        "strings"
        "time"
    
        "github.com/aws/aws-sdk-go-v2/aws"
        v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
        "github.com/aws/aws-sdk-go-v2/config"
        "github.com/aws/aws-sdk-go-v2/credentials"
        "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
        "github.com/aws/aws-sdk-go-v2/service/s3"
    )
    
    var BUCKET_NAME = "test"
    
    type RecalculateV4Signature struct {
        next   http.RoundTripper
        signer *v4.Signer
        cfg    aws.Config
    }
    
    func (lt *RecalculateV4Signature) RoundTrip(req *http.Request) (*http.Response, error) {
        // store for later use
        val := req.Header.Get("Accept-Encoding")
    
        // delete the header so the header doesn't account for in the signature
        req.Header.Del("Accept-Encoding")
    
        // sign with the same date
        timeString := req.Header.Get("X-Amz-Date")
        timeDate, _ := time.Parse("20060102T150405Z", timeString)
    
        creds, _ := lt.cfg.Credentials.Retrieve(req.Context())
        err := lt.signer.SignHTTP(req.Context(), creds, req, v4.GetPayloadHash(req.Context()), "s3", lt.cfg.Region, timeDate)
        if err != nil {
            return nil, err
        }
        // Reset Accept-Encoding if desired
        req.Header.Set("Accept-Encoding", val)
    
        fmt.Println("AfterAdjustment")
        rrr, _ := httputil.DumpRequest(req, false)
        fmt.Println(string(rrr))
    
        // follows up the original round tripper
        return lt.next.RoundTrip(req)
    }
    
    func main() {
    
        //prepare gcp resolver
        gcpResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
            return aws.Endpoint{
                URL:               "https://storage.googleapis.com",
                SigningRegion:     "auto",
                Source:            aws.EndpointSourceCustom,
                HostnameImmutable: true,
            }, nil
        })
        //file with format : $accessKey:$secretKey
    
        //init the config options
        optConfig := []func(*config.LoadOptions) error{
            config.WithRegion("auto"),
            config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("test", "test", "session")),
            config.WithClientLogMode(aws.LogRetries | aws.LogRequestWithBody | aws.LogResponseWithBody | aws.LogRequestEventMessage | aws.LogResponseEventMessage | aws.LogSigning),
            config.WithEndpointResolverWithOptions(gcpResolver),
        }
    
        //init config
        cfg, _ := config.LoadDefaultConfig(context.TODO(), optConfig...)
        // Assign custom client with our own transport
        cfg.HTTPClient = &http.Client{Transport: &RecalculateV4Signature{http.DefaultTransport, v4.NewSigner(), cfg}}
        //init service
        svc := s3.NewFromConfig(cfg)
        uploader := manager.NewUploader(svc, func(u *manager.Uploader) {
            u.Concurrency = 1
            u.MaxUploadParts = 1
        })
        //upload
        _, err := uploader.Upload(context.TODO(), &s3.PutObjectInput{
            Bucket: aws.String(BUCKET_NAME),
            Key:    aws.String("file-test.txt"),
            Body:   strings.NewReader("HELLO"),
        })
        fmt.Println(err)
    }
    
    

    P.S. the client should ideally be built on BuildableClient from aws as they provide sensible defaults for their services, I will leave that up to the reader