Compare commits

...
1 Commits
Author SHA1 Message Date
Nick Craig-Wood d07036b92a s3: fix Alibaba OSS "Only support authorization for OSS4-HMAC-SHA256" errors
Some Alibaba Cloud OSS endpoints (dedicated regions in particular)
only accept OSS native V4 signatures (OSS4-HMAC-SHA256) and reject
the AWS style signatures rclone sends. Alibaba also require OSS V4
signatures for accounts created after March 2025 and buckets created
after September 2025.

This implements OSS V4 signing as a custom signer plugged into the
AWS SDK and enables it by default for the Alibaba provider. It can
be controlled with the new --s3-use-oss-v4-auth flag.

The signer translates the x-amz-* request headers into their x-oss-*
equivalents (including the storage class names) so that they take
part in the signature, and translates the x-oss-* response headers
back so the SDK can parse them.

See: https://forum.rclone.org/t/rclone-v1-74-4-cannot-access-alibaba-oss-endpoint-with-oss4-hmac-sha256-only/54090
2026-08-11 20:48:06 +01:00
5 changed files with 629 additions and 0 deletions

No files matched your search

+384
View File
@@ -0,0 +1,384 @@
// OSS V4 signing (OSS4-HMAC-SHA256) for Alibaba Cloud OSS
//
// Implemented from
// https://www.alibabacloud.com/help/en/oss/developer-reference/recommend-to-use-signature-version-4
package s3
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net"
"net/http"
"net/textproto"
"net/url"
"sort"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
v4signer "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
)
const (
ossAlgorithm = "OSS4-HMAC-SHA256"
ossUnsignedPayload = "UNSIGNED-PAYLOAD"
ossAmzPrefix = "x-amz-"
ossPrefix = "x-oss-"
)
// ossV4Signer signs requests with OSS native V4 signatures
// (OSS4-HMAC-SHA256) as required by Alibaba Cloud OSS endpoints which
// don't accept AWS style signatures.
//
// Implements the HTTPSignerV4 interface.
type ossV4Signer struct {
region string // signing region for the credential scope
product string // signing product - "oss" or "oss-cloudbox"
endpointHost string // host of the endpoint to detect virtual hosted style requests
}
// newOSSV4Signer creates an OSS V4 signer from the options.
//
// The signing region is derived from the endpoint if possible,
// otherwise the region option is used.
func newOSSV4Signer(opt *Options) *ossV4Signer {
host := ossEndpointHost(opt.Endpoint)
region, product := ossRegionProduct(host)
if region == "" {
region = opt.Region
}
return &ossV4Signer{
region: region,
product: product,
endpointHost: host,
}
}
// ossEndpointHost returns the lower case host part of an endpoint
// which may or may not have a scheme, or "" if it can't be parsed.
func ossEndpointHost(endpoint string) string {
if endpoint == "" {
return ""
}
if !strings.Contains(endpoint, "://") {
endpoint = "https://" + endpoint
}
u, err := url.Parse(endpoint)
if err != nil {
return ""
}
return strings.ToLower(u.Hostname())
}
// ossRegionProduct derives the signing region and product from an OSS
// endpoint host.
//
// Standard endpoints look like oss-<region>[-internal].aliyuncs.com
// and CloudBox endpoints look like
// <cb-id>.<region>.oss-cloudbox[-internal].aliyuncs.com where the
// signing region is the CloudBox ID.
//
// It returns region == "" if the region could not be derived, eg for
// accelerate endpoints or custom domains.
func ossRegionProduct(host string) (region, product string) {
product = "oss"
labels := strings.Split(host, ".")
for _, label := range labels[1:] {
if label == "oss-cloudbox" || label == "oss-cloudbox-internal" {
return labels[0], "oss-cloudbox"
}
}
if r, ok := strings.CutPrefix(labels[0], "oss-"); ok {
r = strings.TrimSuffix(r, "-internal")
if r != "" && !strings.HasPrefix(r, "accelerate") {
region = r
}
}
return region, product
}
// ossStorageClassToOSS maps AWS style storage class names (and upper
// cased native names as produced by SetTier) to the case sensitive
// native OSS names. Native names pass through unchanged.
var ossStorageClassToOSS = map[string]string{
"STANDARD": "Standard",
"STANDARD_IA": "IA",
"GLACIER": "Archive",
"DEEP_ARCHIVE": "ColdArchive",
"ARCHIVE": "Archive",
"COLDARCHIVE": "ColdArchive",
"DEEPCOLDARCHIVE": "DeepColdArchive",
}
// ossStorageClassFromOSS maps native OSS storage class names back to
// the AWS style names the S3 compatible interface uses.
var ossStorageClassFromOSS = map[string]string{
"Standard": "STANDARD",
"IA": "STANDARD_IA",
"Archive": "GLACIER",
"ColdArchive": "DEEP_ARCHIVE",
}
// ossHeaderRename returns the x-oss-* header equivalent to the
// x-amz-* header with the given suffix (the part after "x-amz-",
// lower case). hasKey should be true if the request refers to an
// object rather than a bucket.
//
// It returns "" for headers which should be dropped because the
// signer sets the x-oss-* equivalent itself.
func ossHeaderRename(amzSuffix string, hasKey bool) string {
switch amzSuffix {
case "date", "content-sha256", "security-token":
return ""
case "acl":
// OSS uses different headers for object and bucket ACLs
if hasKey {
return "x-oss-object-acl"
}
return "x-oss-acl"
case "server-side-encryption-aws-kms-key-id":
return "x-oss-server-side-encryption-key-id"
}
return ossPrefix + amzSuffix
}
// ossTranslateRequest renames the x-amz-* headers the SDK generates
// into the x-oss-* equivalents that OSS uses so that they take part
// in the signature. OSS requires all x-oss-* headers sent to be
// signed.
func ossTranslateRequest(req *http.Request, hasKey bool) {
var keys []string
for k := range req.Header {
if strings.HasPrefix(strings.ToLower(k), ossAmzPrefix) {
keys = append(keys, k)
}
}
for _, k := range keys {
values := req.Header.Values(k)
req.Header.Del(k)
newKey := ossHeaderRename(strings.ToLower(k)[len(ossAmzPrefix):], hasKey)
if newKey == "" {
continue
}
switch newKey {
case "x-oss-copy-source":
// OSS wants the copy source to start with a "/"
for i, v := range values {
if !strings.HasPrefix(v, "/") {
values[i] = "/" + v
}
}
case "x-oss-storage-class":
// OSS uses different case sensitive storage class names
for i, v := range values {
if native, ok := ossStorageClassToOSS[v]; ok {
values[i] = native
}
}
}
for _, v := range values {
req.Header.Add(newKey, v)
}
}
}
// ossCanonicalURI returns the canonical URI for the request,
// expanding a virtual hosted style bucket in the host back to
// /bucket/key form.
func (s *ossV4Signer) ossCanonicalURI(u *url.URL) string {
path := u.EscapedPath()
if path == "" {
path = "/"
}
host := strings.ToLower(u.Host)
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
if s.endpointHost != "" && strings.HasSuffix(host, "."+s.endpointHost) {
bucket := strings.TrimSuffix(host, "."+s.endpointHost)
path = "/" + bucket + path
}
return path
}
// ossCanonicalQuery returns the canonical query string for the
// request. This signs the query parameters exactly as they are sent
// on the wire except that "+" is re-encoded as %20 and the
// parameters are sorted. Parameters with an empty value are signed
// as a bare name without "=".
func ossCanonicalQuery(rawQuery string) string {
query := strings.ReplaceAll(rawQuery, "+", "%20")
type param struct{ name, value string }
var params []param
for _, part := range strings.Split(query, "&") {
if part == "" {
continue
}
name, value, _ := strings.Cut(part, "=")
params = append(params, param{name, value})
}
sort.Slice(params, func(i, j int) bool {
if params[i].name != params[j].name {
return params[i].name < params[j].name
}
return params[i].value < params[j].value
})
var b strings.Builder
for i, p := range params {
if i > 0 {
b.WriteByte('&')
}
b.WriteString(p.name)
if p.value != "" {
b.WriteByte('=')
b.WriteString(p.value)
}
}
return b.String()
}
// ossCanonicalHeaders returns the canonical headers for the request.
// OSS signs all x-oss-* headers plus content-type and content-md5.
func ossCanonicalHeaders(header http.Header) string {
var names []string
values := make(map[string]string)
for k, vs := range header {
name := strings.ToLower(k)
if name != "content-type" && name != "content-md5" && !strings.HasPrefix(name, ossPrefix) {
continue
}
trimmed := make([]string, len(vs))
for i, v := range vs {
trimmed[i] = strings.TrimSpace(v)
}
names = append(names, name)
values[name] = strings.Join(trimmed, ",")
}
sort.Strings(names)
var b strings.Builder
for _, name := range names {
b.WriteString(name)
b.WriteByte(':')
b.WriteString(values[name])
b.WriteByte('\n')
}
return b.String()
}
// ossHMAC returns the HMAC-SHA256 of value with the given key
func ossHMAC(key []byte, value string) []byte {
h := hmac.New(sha256.New, key)
_, _ = h.Write([]byte(value))
return h.Sum(nil)
}
// SignHTTP signs the request with an OSS V4 signature.
//
// It rewrites the AWS style x-amz-* headers into their x-oss-*
// equivalents before signing.
//
// Implements the HTTPSignerV4 interface.
func (s *ossV4Signer) SignHTTP(ctx context.Context, credentials aws.Credentials, req *http.Request, payloadHash string, service string, region string, signingTime time.Time, optFns ...func(*v4signer.SignerOptions)) error {
uri := s.ossCanonicalURI(req.URL)
// an object request has a path after the /bucket/ prefix
hasKey := strings.Contains(strings.Trim(uri, "/"), "/")
ossTranslateRequest(req, hasKey)
utcTime := signingTime.UTC()
datetime := utcTime.Format("20060102T150405Z")
date := utcTime.Format("20060102")
req.Header.Set("Date", utcTime.Format(http.TimeFormat))
req.Header.Set("x-oss-date", datetime)
// OSS only supports unsigned payloads with V4 signatures
req.Header.Set("x-oss-content-sha256", ossUnsignedPayload)
if credentials.SessionToken != "" {
req.Header.Set("x-oss-security-token", credentials.SessionToken)
}
canonicalRequest := strings.Join([]string{
req.Method,
uri,
ossCanonicalQuery(req.URL.RawQuery),
ossCanonicalHeaders(req.Header),
"", // no additional signed headers
ossUnsignedPayload,
}, "\n")
hash := sha256.Sum256([]byte(canonicalRequest))
scope := date + "/" + s.region + "/" + s.product + "/aliyun_v4_request"
stringToSign := ossAlgorithm + "\n" + datetime + "\n" + scope + "\n" + hex.EncodeToString(hash[:])
key := ossHMAC([]byte("aliyun_v4"+credentials.SecretAccessKey), date)
key = ossHMAC(key, s.region)
key = ossHMAC(key, s.product)
key = ossHMAC(key, "aliyun_v4_request")
signature := hex.EncodeToString(ossHMAC(key, stringToSign))
req.Header.Set("Authorization", ossAlgorithm+" Credential="+credentials.AccessKeyID+"/"+scope+",Signature="+signature)
return nil
}
// ossTranslateResponse copies x-oss-* response headers to their
// x-amz-* equivalents so the SDK can parse them, eg for user
// metadata, version IDs and storage class.
func ossTranslateResponse(header http.Header) {
additions := make(map[string][]string)
for k, vs := range header {
suffix, ok := strings.CutPrefix(strings.ToLower(k), ossPrefix)
if !ok {
continue
}
amzKey := ossAmzPrefix + suffix
switch suffix {
case "server-side-encryption-key-id":
amzKey = "x-amz-server-side-encryption-aws-kms-key-id"
case "storage-class":
// OSS uses different case sensitive storage class names
mapped := make([]string, len(vs))
for i, v := range vs {
if aws, ok := ossStorageClassFromOSS[v]; ok {
mapped[i] = aws
} else {
mapped[i] = v
}
}
vs = mapped
}
canonical := textproto.CanonicalMIMEHeaderKey(amzKey)
if _, exists := header[canonical]; !exists {
additions[canonical] = vs
}
}
for k, vs := range additions {
header[k] = vs
}
}
// ossTransport is an http.RoundTripper which makes OSS responses
// look like S3 responses to the SDK.
type ossTransport struct {
base http.RoundTripper
}
// RoundTrip implements http.RoundTripper
func (t *ossTransport) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := t.base.RoundTrip(req)
if resp != nil {
ossTranslateResponse(resp.Header)
}
return resp, err
}
// ossFixupResponses returns a copy of client whose responses have
// their x-oss-* headers copied to the x-amz-* equivalents.
func ossFixupResponses(client *http.Client) *http.Client {
newClient := *client
if newClient.Transport == nil {
newClient.Transport = http.DefaultTransport
}
newClient.Transport = &ossTransport{base: newClient.Transport}
return &newClient
}
+211
View File
@@ -0,0 +1,211 @@
package s3
import (
"context"
"net/http"
"net/url"
"strings"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ossV4TestRequest makes the test request used by the signing test
// vectors from the official OSS Go SDK:
// https://github.com/aliyun/alibabacloud-oss-go-sdk-v2/blob/master/oss/signer/signer_test.go
func ossV4TestRequest(t *testing.T, host string) *http.Request {
req, err := http.NewRequest("PUT", "http://"+host+"/1234%2B-/123/1.txt", nil)
require.NoError(t, err)
req.Header.Add("x-oss-head1", "value")
req.Header.Add("abc", "value")
req.Header.Add("ZAbc", "value")
req.Header.Add("XYZ", "value")
req.Header.Add("content-type", "text/plain")
values := url.Values{}
values.Add("param1", "value1")
values.Add("+param1", "value3")
values.Add("|param1", "value4")
values.Add("+param2", "")
values.Add("|param2", "")
values.Add("param2", "")
req.URL.RawQuery = values.Encode()
return req
}
func TestOSSV4SignHTTP(t *testing.T) {
signer := &ossV4Signer{
region: "cn-hangzhou",
product: "oss",
endpointHost: "oss-cn-hangzhou.aliyuncs.com",
}
req := ossV4TestRequest(t, "bucket.oss-cn-hangzhou.aliyuncs.com")
creds := aws.Credentials{AccessKeyID: "ak", SecretAccessKey: "sk"}
err := signer.SignHTTP(context.Background(), creds, req, "", "s3", "us-east-1", time.Unix(1702743657, 0))
require.NoError(t, err)
assert.Equal(t, "OSS4-HMAC-SHA256 Credential=ak/20231216/cn-hangzhou/oss/aliyun_v4_request,Signature=e21d18daa82167720f9b1047ae7e7f1ce7cb77a31e8203a7d5f4624fa0284afe", req.Header.Get("Authorization"))
assert.Equal(t, time.Unix(1702743657, 0).UTC().Format("20060102T150405Z"), req.Header.Get("x-oss-date"))
assert.Equal(t, "UNSIGNED-PAYLOAD", req.Header.Get("x-oss-content-sha256"))
}
func TestOSSV4SignHTTPToken(t *testing.T) {
signer := &ossV4Signer{
region: "cn-hangzhou",
product: "oss",
endpointHost: "oss-cn-hangzhou.aliyuncs.com",
}
req := ossV4TestRequest(t, "bucket.oss-cn-hangzhou.aliyuncs.com")
creds := aws.Credentials{AccessKeyID: "ak", SecretAccessKey: "sk", SessionToken: "token"}
err := signer.SignHTTP(context.Background(), creds, req, "", "s3", "us-east-1", time.Unix(1702784856, 0))
require.NoError(t, err)
assert.Equal(t, "OSS4-HMAC-SHA256 Credential=ak/20231217/cn-hangzhou/oss/aliyun_v4_request,Signature=b94a3f999cf85bcdc00d332fbd3734ba03e48382c36fa4d5af5df817395bd9ea", req.Header.Get("Authorization"))
assert.Equal(t, "token", req.Header.Get("x-oss-security-token"))
}
func TestOSSV4SignHTTPCloudBox(t *testing.T) {
signer := &ossV4Signer{
region: "cb-123",
product: "oss-cloudbox",
endpointHost: "cb-123.cn-hangzhou.oss-cloudbox.aliyuncs.com",
}
req := ossV4TestRequest(t, "bucket.cb-123.cn-hangzhou.oss-cloudbox.aliyuncs.com")
creds := aws.Credentials{AccessKeyID: "ak", SecretAccessKey: "sk"}
err := signer.SignHTTP(context.Background(), creds, req, "", "s3", "us-east-1", time.Unix(1702743657, 0))
require.NoError(t, err)
assert.Equal(t, "OSS4-HMAC-SHA256 Credential=ak/20231216/cb-123/oss-cloudbox/aliyun_v4_request,Signature=94ce1f12c17d148ea681030275a94449d3357f5b5b21133996eec80af3e08a43", req.Header.Get("Authorization"))
}
func TestOSSRegionProduct(t *testing.T) {
for _, test := range []struct {
host string
region string
product string
}{
{"oss-cn-hangzhou.aliyuncs.com", "cn-hangzhou", "oss"},
{"oss-cn-hangzhou-internal.aliyuncs.com", "cn-hangzhou", "oss"},
{"oss-cn-jinan-acdr-ut-1-internal.aliyuncs.com", "cn-jinan-acdr-ut-1", "oss"},
{"oss-us-east-1.aliyuncs.com", "us-east-1", "oss"},
{"oss-accelerate.aliyuncs.com", "", "oss"},
{"oss-accelerate-overseas.aliyuncs.com", "", "oss"},
{"cb-123.cn-hangzhou.oss-cloudbox.aliyuncs.com", "cb-123", "oss-cloudbox"},
{"cb-123.cn-hangzhou.oss-cloudbox-internal.aliyuncs.com", "cb-123", "oss-cloudbox"},
{"my.custom.domain.example.com", "", "oss"},
{"", "", "oss"},
} {
region, product := ossRegionProduct(test.host)
assert.Equal(t, test.region, region, test.host)
assert.Equal(t, test.product, product, test.host)
}
}
func TestOSSEndpointHost(t *testing.T) {
assert.Equal(t, "oss-cn-hangzhou.aliyuncs.com", ossEndpointHost("oss-cn-hangzhou.aliyuncs.com"))
assert.Equal(t, "oss-cn-hangzhou.aliyuncs.com", ossEndpointHost("https://oss-cn-hangzhou.aliyuncs.com"))
assert.Equal(t, "oss-cn-hangzhou.aliyuncs.com", ossEndpointHost("http://OSS-cn-hangzhou.aliyuncs.com:8080"))
assert.Equal(t, "", ossEndpointHost(""))
}
func TestOSSCanonicalURI(t *testing.T) {
signer := &ossV4Signer{endpointHost: "oss-cn-hangzhou.aliyuncs.com"}
for _, test := range []struct {
url string
want string
hasKey bool
}{
// virtual hosted style
{"https://bucket.oss-cn-hangzhou.aliyuncs.com/", "/bucket/", false},
{"https://bucket.oss-cn-hangzhou.aliyuncs.com/key", "/bucket/key", true},
{"https://bucket.oss-cn-hangzhou.aliyuncs.com/dir/key", "/bucket/dir/key", true},
{"https://bucket.oss-cn-hangzhou.aliyuncs.com", "/bucket/", false},
// path style
{"https://oss-cn-hangzhou.aliyuncs.com/", "/", false},
{"https://oss-cn-hangzhou.aliyuncs.com/bucket", "/bucket", false},
{"https://oss-cn-hangzhou.aliyuncs.com/bucket/key", "/bucket/key", true},
} {
u, err := url.Parse(test.url)
require.NoError(t, err)
uri := signer.ossCanonicalURI(u)
assert.Equal(t, test.want, uri, test.url)
hasKey := strings.Contains(strings.Trim(uri, "/"), "/")
assert.Equal(t, test.hasKey, hasKey, test.url)
}
}
func TestOSSTranslateRequest(t *testing.T) {
req, err := http.NewRequest("PUT", "https://bucket.oss-cn-hangzhou.aliyuncs.com/key", nil)
require.NoError(t, err)
req.Header.Set("X-Amz-Meta-Mtime", "1400000000.000000")
req.Header.Set("X-Amz-Copy-Source", "bucket/path/file")
req.Header.Set("X-Amz-Acl", "private")
req.Header.Set("X-Amz-Storage-Class", "GLACIER")
req.Header.Set("X-Amz-Date", "junk")
req.Header.Set("X-Amz-Content-Sha256", "junk")
req.Header.Set("X-Amz-Security-Token", "junk")
req.Header.Set("X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id", "keyid")
req.Header.Set("Content-Type", "text/plain")
ossTranslateRequest(req, true)
assert.Equal(t, "1400000000.000000", req.Header.Get("x-oss-meta-mtime"))
assert.Equal(t, "/bucket/path/file", req.Header.Get("x-oss-copy-source"))
assert.Equal(t, "private", req.Header.Get("x-oss-object-acl"))
assert.Equal(t, "Archive", req.Header.Get("x-oss-storage-class"))
assert.Equal(t, "keyid", req.Header.Get("x-oss-server-side-encryption-key-id"))
assert.Equal(t, "text/plain", req.Header.Get("Content-Type"))
for k := range req.Header {
assert.False(t, strings.HasPrefix(strings.ToLower(k), "x-amz-"), k)
}
// a copy source which already has a leading / is left alone
req.Header.Set("X-Amz-Copy-Source", "/bucket/path/file")
ossTranslateRequest(req, true)
assert.Equal(t, "/bucket/path/file", req.Header.Get("x-oss-copy-source"))
// storage class names are mapped to the native OSS names
for _, test := range []struct{ in, want string }{
{"STANDARD", "Standard"},
{"STANDARD_IA", "IA"},
{"GLACIER", "Archive"},
{"DEEP_ARCHIVE", "ColdArchive"},
{"DEEPCOLDARCHIVE", "DeepColdArchive"},
{"Standard", "Standard"},
{"IA", "IA"},
{"unknown", "unknown"},
} {
req.Header.Del("x-oss-storage-class")
req.Header.Set("X-Amz-Storage-Class", test.in)
ossTranslateRequest(req, true)
assert.Equal(t, test.want, req.Header.Get("x-oss-storage-class"), test.in)
}
// bucket level requests use x-oss-acl
req, err = http.NewRequest("PUT", "https://bucket.oss-cn-hangzhou.aliyuncs.com/", nil)
require.NoError(t, err)
req.Header.Set("X-Amz-Acl", "private")
ossTranslateRequest(req, false)
assert.Equal(t, "private", req.Header.Get("x-oss-acl"))
}
func TestOSSTranslateResponse(t *testing.T) {
header := http.Header{}
header.Set("X-Oss-Meta-Mtime", "1400000000.000000")
header.Set("X-Oss-Request-Id", "1234")
header.Set("X-Oss-Version-Id", "abcd")
header.Set("X-Oss-Storage-Class", "Standard")
header.Set("X-Oss-Server-Side-Encryption-Key-Id", "keyid")
header.Set("Etag", `"abc"`)
ossTranslateResponse(header)
assert.Equal(t, "1400000000.000000", header.Get("x-amz-meta-mtime"))
assert.Equal(t, "1234", header.Get("x-amz-request-id"))
assert.Equal(t, "abcd", header.Get("x-amz-version-id"))
assert.Equal(t, "STANDARD", header.Get("x-amz-storage-class"))
assert.Equal(t, "keyid", header.Get("x-amz-server-side-encryption-aws-kms-key-id"))
// the x-oss- headers are left in place
assert.Equal(t, "1234", header.Get("x-oss-request-id"))
// an existing x-amz- header is not overwritten
header = http.Header{}
header.Set("X-Oss-Meta-Mtime", "1")
header.Set("X-Amz-Meta-Mtime", "2")
ossTranslateResponse(header)
assert.Equal(t, "2", header.Get("x-amz-meta-mtime"))
}
+1
View File
@@ -35,3 +35,4 @@ storage_class:
bucket_acl: true
quirks:
use_multipart_etag: false # multipar ETags differ from AWS
use_oss_v4_auth: true # some endpoints only accept OSS V4 signatures
+1
View File
@@ -30,6 +30,7 @@ type Quirks struct {
MightGzip *bool `yaml:"might_gzip,omitempty"`
UseMultipartUploads *bool `yaml:"use_multipart_uploads,omitempty"`
UseUnsignedPayload *bool `yaml:"use_unsigned_payload,omitempty"`
UseOSSV4Auth *bool `yaml:"use_oss_v4_auth,omitempty"`
ListVersionsOldestFirst *bool `yaml:"list_versions_oldest_first,omitempty"`
UseXID *bool `yaml:"use_x_id,omitempty"`
SignAcceptEncoding *bool `yaml:"sign_accept_encoding,omitempty"`
+32
View File
@@ -393,6 +393,25 @@ If it is set then rclone will use v2 authentication.
Use this only if v4 signatures don't work, e.g. pre Jewel/v10 CEPH.`,
Default: false,
Advanced: true,
}, {
Name: "use_oss_v4_auth",
Help: `If true use Alibaba Cloud OSS native V4 authentication (OSS4-HMAC-SHA256).
Some Alibaba Cloud OSS endpoints (for example dedicated regions) only
accept OSS V4 signatures and reject AWS style signatures with the
error "Only support authorization for OSS4-HMAC-SHA256". Alibaba
require OSS V4 signatures for accounts created after March 2025 and
buckets created after September 2025.
This is enabled by default for the Alibaba provider.
The region for the signature is read from the endpoint if possible,
otherwise the region option is used.
Note that presigned requests (rclone link and use_presigned_request)
are still signed with AWS style signatures.`,
Default: fs.Tristate{},
Advanced: true,
}, {
Name: "use_dual_stack",
Help: `If true use AWS S3 dual-stack endpoint (IPv6 support).
@@ -1109,6 +1128,7 @@ type Options struct {
UploadConcurrency int `config:"upload_concurrency"`
ForcePathStyle bool `config:"force_path_style"`
V2Auth bool `config:"v2_auth"`
UseOSSV4Auth fs.Tristate `config:"use_oss_v4_auth"`
UseAccelerateEndpoint bool `config:"use_accelerate_endpoint"`
UseARNRegion bool `config:"use_arn_region"`
LeavePartsOnError bool `config:"leave_parts_on_error"`
@@ -1586,6 +1606,11 @@ func s3Connection(ctx context.Context, opt *Options, client *http.Client) (s3Cli
}
setQuirks(opt, provider)
useOSSV4Auth := opt.UseOSSV4Auth.Value && !opt.V2Auth && opt.Region != "other-v2-signature"
if useOSSV4Auth {
// Make OSS responses look like S3 responses
client = ossFixupResponses(client)
}
awsConfig.RetryMaxAttempts = ci.LowLevelRetries
awsConfig.HTTPClient = client
@@ -1636,6 +1661,12 @@ func s3Connection(ctx context.Context, opt *Options, client *http.Client) (s3Cli
s3Opt.HTTPSignerV4 = &v2Signer{opt: opt}
})
}
} else if useOSSV4Auth {
signer := newOSSV4Signer(opt)
fs.Debugf(nil, "Using OSS V4 auth with signing region %q", signer.region)
options = append(options, func(s3Opt *s3.Options) {
s3Opt.HTTPSignerV4 = signer
})
}
// Fixup the request if needed
@@ -1809,6 +1840,7 @@ func setQuirks(opt *Options, provider *Provider) {
opt.UploadCutoff = math.MaxInt64
}
set(&opt.UseUnsignedPayload, true, provider.Quirks.UseUnsignedPayload)
set(&opt.UseOSSV4Auth, false, provider.Quirks.UseOSSV4Auth)
set(&opt.UseXID, true, provider.Quirks.UseXID)
set(&opt.SignAcceptEncoding, true, provider.Quirks.SignAcceptEncoding)
set(&opt.ObjectLockSupported, true, provider.Quirks.ObjectLockSupported)