Global endpoint ambiguity

Hi @jmj, I generate the presigned url on my server with the following parameters:

AccessKey: tid_<>
SecretKey: tsec_<>
Endpoint: see below
Region: auto
Bucket: test.sessions.pinggg.cloud
Method: PUT
Expiration: 15

When I use fly.storage.tigris.dev as endpoint then everything works fine, but when I use t3.storage.dev then I get the message:

<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>
<Resource>/dFGR75OhGeGGRdNwyjIV3rOFMuxeBaNu8gus4mqSvGy/shares/1.png</Resource>
<RequestId>1750939451656676602</RequestId>
<Key>dFGR75OhGeGGRdNwyjIV3rOFMuxeBaNu8gus4mqSvGy/shares/1.png</Key>
<BucketName>test.sessions.pinggg.cloud</BucketName>
</Error>

Below you find the parts that generate the presigned url in Golang. It’s the method PresignObject(...) that is called to generate the signature:

//---------------------------------------------------------------------------

const (
	algorithm = "AWS4-HMAC-SHA256"
	service   = "s3"
	request   = "aws4_request"
)

//---------------------------------------------------------------------------

type Presigner struct {
    AccessKey  string
    SecretKey  string
    Region     string
    Endpoint   string
    Bucket     string
    Method     string
    Expiration time.Duration
}

//---------------------------------------------------------------------------

// Creates a presigned GET/PUT date and signature
func (p *Presigner) PresignObject(objectName string) (string, []byte) {

	dateOnly, dateTime := presigner_Stamp()

	signature := p.PresignObjectWithDate(objectName, dateOnly, dateTime)

	return dateTime, signature

}

//---------------------------------------------------------------------------

// Creates a presigned GET/PUT signature
func (p *Presigner) PresignObjectWithDate(objectName string, dateOnly string, dateTime string) []byte {

	mime := presigner_ExtractMimeType(objectName)

	// Build canonical URI
	var canonicalURI strings.Builder

	canonicalURI.WriteByte('/')
	canonicalURI.WriteString(p.Bucket)
	canonicalURI.WriteByte('/')
	canonicalURI.WriteString(objectName)

	// Build credential string
	var credential strings.Builder

	// These '/'s should be escaped
	credential.WriteString(p.AccessKey)
	credential.WriteString("%2F")
	credential.WriteString(dateOnly)
	credential.WriteString("%2F")
	credential.WriteString(p.Region)
	credential.WriteString("%2F")
	credential.WriteString(service)
	credential.WriteString("%2F")
	credential.WriteString(request)

	// Build query parameters
	var canonicalQueryString strings.Builder

	// Add parameters in the correct order
	canonicalQueryString.WriteString(fmt.Sprintf("X-Amz-Algorithm=%s", algorithm))
	canonicalQueryString.WriteString(fmt.Sprintf("&X-Amz-Credential=%s", credential.String()))
	canonicalQueryString.WriteString(fmt.Sprintf("&X-Amz-Date=%s", dateTime))
	canonicalQueryString.WriteString(fmt.Sprintf("&X-Amz-Expires=%d", int(p.Expiration.Seconds())))
	canonicalQueryString.WriteString("&X-Amz-SignedHeaders=host")

	// Build host and canonical headers
	var canonicalHeaders strings.Builder

	canonicalHeaders.WriteString(fmt.Sprintf("host:%s", p.Endpoint))
	canonicalHeaders.WriteByte('\n')

	// Build canonical request
	var canonicalRequest strings.Builder

	canonicalRequest.WriteString(p.Method)
	canonicalRequest.WriteByte('\n')
	canonicalRequest.WriteString(canonicalURI.String())
	canonicalRequest.WriteByte('\n')
	canonicalRequest.WriteString(canonicalQueryString.String())
	canonicalRequest.WriteByte('\n')
	canonicalRequest.WriteString(canonicalHeaders.String())
	canonicalRequest.WriteByte('\n')
	canonicalRequest.WriteString("host")
	canonicalRequest.WriteByte('\n')
	canonicalRequest.WriteString("UNSIGNED-PAYLOAD")

	// Build string to sign
	var scope strings.Builder

	scope.WriteString(dateOnly)
	scope.WriteByte('/')
	scope.WriteString(p.Region)
	scope.WriteByte('/')
	scope.WriteString(service)
	scope.WriteByte('/')
	scope.WriteString(request)

	var stringToSign strings.Builder

	stringToSign.WriteString(algorithm)
	stringToSign.WriteByte('\n')
	stringToSign.WriteString(dateTime)
	stringToSign.WriteByte('\n')
	stringToSign.WriteString(scope.String())
	stringToSign.WriteByte('\n')
	stringToSign.WriteString(hex.EncodeToString(presigner_Hash([]byte(canonicalRequest.String()))))

	// Calculate signature
	signingKey := presigner_SignKey(p.SecretKey, dateOnly, p.Region, service)
	signature := presigner_Hmac(signingKey, []byte(stringToSign.String()))

	return signature

}

//---------------------------------------------------------------------------

func presigner_ExtractMimeType(objectName string) string {

	ext := filepath.Ext(objectName)

	return mime.TypeByExtension(ext)

}

//---------------------------------------------------------------------------

// Creates a date only string and a date with time string
func presigner_Stamp() (string, string) {

	datetime := time.Now().UTC()

	return datetime.Format("20060102"), datetime.Format("20060102T150405Z")

}

//---------------------------------------------------------------------------

func presigner_Hash(data []byte) []byte {

	h := sha256.New()

	h.Write(data)

	return h.Sum(nil)
}

//---------------------------------------------------------------------------

func presigner_Hmac(key []byte, data []byte) []byte {

	h := hmac.New(sha256.New, key)

	h.Write(data)

	return h.Sum(nil)

}

//---------------------------------------------------------------------------

func presigner_SignKey(secretKey string, date string, region string, service string) []byte {

	kDate := presigner_Hmac([]byte("AWS4"+secretKey), []byte(date))
	kRegion := presigner_Hmac(kDate, []byte(region))
	kService := presigner_Hmac(kRegion, []byte(service))

	return presigner_Hmac(kService, []byte(request))

}

//---------------------------------------------------------------------------

The signature and date are send to the client and assembled into:

https://test.sessions.pinggg.cloud/dFGR75OhGeGGRdNwyjIV3rOFMuxeBaNu8gus4mqSvGy/shares/1.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=tid_mNpmYFuMLHfstgsbJEnjDXQLjRSgqXlznLCJdlY_ZIhereWHco%2F20250626%2Fauto%2Fs3%2Faws4_request&X-Amz-Date=20250626T120406Z&X-Amz-Expires=15&X-Amz-SignedHeaders=host&X-Amz-Signature=625a62854449727f57935b10caf1e9895c8ba0c7d424296c771b5e066f8b13bb

The client then uploads the file and gets the error listed above.

Could this be the result of me using a CNAME bucket mapped onto test.sessions.pinggg.cloud.fly.storage.tigris.dev?

Thanks in advance.