Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,224 @@
|
||||
// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module s3
|
||||
|
||||
import time
|
||||
|
||||
// Test vectors come from the AWS-published Signature V4 reference test
|
||||
// suite ("Examples of the complete version 4 signing process").
|
||||
// These secret/key pairs are the documented example values, not real
|
||||
// credentials.
|
||||
const aws_secret = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
|
||||
const aws_key_id = 'AKIAIOSFODNN7EXAMPLE'
|
||||
|
||||
// SigV4 vector for `s3-get-object` (examplebucket / test.txt with a Range
|
||||
// header). Source: AWS docs (Signature V4 examples).
|
||||
//
|
||||
// GET /test.txt HTTP/1.1
|
||||
// Host: examplebucket.s3.amazonaws.com
|
||||
// Range: bytes=0-9
|
||||
// X-Amz-Date: 20130524T000000Z
|
||||
// X-Amz-Content-SHA256: <empty-sha>
|
||||
fn test_aws_sigv4_get_object_vector() {
|
||||
t := time.parse_iso8601('2013-05-24T00:00:00.000Z') or { panic(err) }
|
||||
creds := Credentials{
|
||||
access_key_id: aws_key_id
|
||||
secret_access_key: aws_secret
|
||||
region: 'us-east-1'
|
||||
endpoint: 'examplebucket.s3.amazonaws.com'
|
||||
virtual_hosted_style: false // host already carries the bucket
|
||||
}
|
||||
req := SignRequest{
|
||||
method: 'GET'
|
||||
path: '/test.txt'
|
||||
query: ''
|
||||
payload_hash: empty_sha256
|
||||
extra_headers: {
|
||||
'range': 'bytes=0-9'
|
||||
}
|
||||
sign_time: t
|
||||
}
|
||||
signed := sign_request(creds, req) or { panic(err) }
|
||||
expected := 'AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, SignedHeaders=host;range;x-amz-content-sha256;x-amz-date, Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41'
|
||||
assert signed.authorization == expected, 'Got:\n${signed.authorization}\nExpected:\n${expected}'
|
||||
}
|
||||
|
||||
// SigV4 vector for `s3-put-object` (examplebucket / test$file.text with body
|
||||
// "Welcome to Amazon S3.").
|
||||
//
|
||||
// PUT /test%24file.text HTTP/1.1
|
||||
// Host: examplebucket.s3.amazonaws.com
|
||||
// Date: Fri, 24 May 2013 00:00:00 GMT
|
||||
// x-amz-date: 20130524T000000Z
|
||||
// x-amz-storage-class: REDUCED_REDUNDANCY
|
||||
fn test_aws_sigv4_put_object_vector() {
|
||||
t := time.parse_iso8601('2013-05-24T00:00:00.000Z') or { panic(err) }
|
||||
creds := Credentials{
|
||||
access_key_id: aws_key_id
|
||||
secret_access_key: aws_secret
|
||||
region: 'us-east-1'
|
||||
endpoint: 'examplebucket.s3.amazonaws.com'
|
||||
virtual_hosted_style: false
|
||||
}
|
||||
body := 'Welcome to Amazon S3.'
|
||||
body_hash := sha256_hex(body.bytes())
|
||||
req := SignRequest{
|
||||
method: 'PUT'
|
||||
path: '/test%24file.text'
|
||||
query: ''
|
||||
payload_hash: body_hash
|
||||
extra_headers: {
|
||||
'date': 'Fri, 24 May 2013 00:00:00 GMT'
|
||||
'x-amz-storage-class': 'REDUCED_REDUNDANCY'
|
||||
}
|
||||
sign_time: t
|
||||
}
|
||||
signed := sign_request(creds, req) or { panic(err) }
|
||||
expected := 'AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, SignedHeaders=date;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class, Signature=98ad721746da40c64f1a55b78f14c238d841ea1380cd77a1b5971af0ece108bd'
|
||||
assert signed.authorization == expected, 'Got:\n${signed.authorization}\nExpected:\n${expected}'
|
||||
}
|
||||
|
||||
// Asserts the canonical request string for a ListObjectsV2 GET. The
|
||||
// canonical request is the most stable interop point — Authorization line
|
||||
// formatting has wobbled across AWS doc revisions, but the canonical hash
|
||||
// is always the same.
|
||||
fn test_aws_sigv4_list_objects_canonical_request() {
|
||||
headers := {
|
||||
'host': 'examplebucket.s3.amazonaws.com'
|
||||
'x-amz-content-sha256': empty_sha256
|
||||
'x-amz-date': '20130524T000000Z'
|
||||
}
|
||||
canonical := build_canonical_request('GET', '/', 'list-type=2&prefix=foo', headers,
|
||||
'host;x-amz-content-sha256;x-amz-date', empty_sha256)
|
||||
expected := 'GET\n/\nlist-type=2&prefix=foo\nhost:examplebucket.s3.amazonaws.com\nx-amz-content-sha256:${empty_sha256}\nx-amz-date:20130524T000000Z\n\nhost;x-amz-content-sha256;x-amz-date\n${empty_sha256}'
|
||||
assert canonical == expected, 'canonical mismatch:\n${canonical}'
|
||||
}
|
||||
|
||||
// Cross-checked manually with openssl on the AWS docs example
|
||||
// (date 20120215, region us-east-1, service iam):
|
||||
//
|
||||
// echo -n 'aws4_request' | openssl dgst -sha256 -mac HMAC -macopt hexkey:<kService>
|
||||
fn test_signing_key_chain() {
|
||||
key := derive_signing_key(aws_secret, '20120215', 'us-east-1', 'iam')
|
||||
assert to_hex_lower(key) == '004aa806e13dae88b9032d9261bcb04c67d023afadd221e6b0d206e1760e0b5e', 'Got: ${to_hex_lower(key)}'
|
||||
}
|
||||
|
||||
fn test_normalize_header_value_collapses_whitespace() {
|
||||
assert normalize_header_value(' hello world ') == 'hello world'
|
||||
assert normalize_header_value('a\tb\tc') == 'a b c'
|
||||
assert normalize_header_value('') == ''
|
||||
}
|
||||
|
||||
fn test_canonical_query_string_sorted_and_encoded() {
|
||||
q := canonical_query_string({
|
||||
'b': '2'
|
||||
'a': '1'
|
||||
'c': 'hello world'
|
||||
})
|
||||
assert q == 'a=1&b=2&c=hello%20world'
|
||||
}
|
||||
|
||||
fn test_format_amz_date() {
|
||||
t := time.parse_iso8601('2024-01-15T12:34:56.000Z') or { panic(err) }
|
||||
assert format_amz_date(t) == '20240115T123456Z'
|
||||
}
|
||||
|
||||
fn test_signer_rejects_unsupported_method() {
|
||||
creds := Credentials{
|
||||
access_key_id: 'KEY'
|
||||
secret_access_key: 'secret'
|
||||
region: 'us-east-1'
|
||||
endpoint: 'examplebucket.s3.amazonaws.com'
|
||||
}
|
||||
if _ := sign_request(creds, SignRequest{ method: 'PATCH', path: '/x', payload_hash: empty_sha256 }) {
|
||||
assert false
|
||||
}
|
||||
}
|
||||
|
||||
fn test_signer_rejects_crlf_in_extra_headers() {
|
||||
creds := Credentials{
|
||||
access_key_id: 'KEY'
|
||||
secret_access_key: 'secret'
|
||||
region: 'us-east-1'
|
||||
endpoint: 'examplebucket.s3.amazonaws.com'
|
||||
}
|
||||
bad := SignRequest{
|
||||
method: 'GET'
|
||||
path: '/foo'
|
||||
payload_hash: empty_sha256
|
||||
extra_headers: {
|
||||
'x-evil': 'value\r\nInjected: yes'
|
||||
}
|
||||
}
|
||||
if _ := sign_request(creds, bad) {
|
||||
assert false, 'expected CRLF rejection'
|
||||
} else {
|
||||
if err is S3Error {
|
||||
assert err.code == 'InvalidHeader' || err.code == 'InvalidCredentials', 'unexpected code: ${err.code}'
|
||||
} else {
|
||||
assert false, 'wrong error type'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_presign_rejects_crlf_in_extra_query() {
|
||||
creds := Credentials{
|
||||
access_key_id: 'KEY'
|
||||
secret_access_key: 'secret'
|
||||
region: 'us-east-1'
|
||||
endpoint: 'examplebucket.s3.amazonaws.com'
|
||||
}
|
||||
bad := PresignRequest{
|
||||
method: 'GET'
|
||||
path: '/foo'
|
||||
extra_query: {
|
||||
'X-Evil': 'value\r\nInjected: yes'
|
||||
}
|
||||
}
|
||||
if _ := presign_url(creds, bad) {
|
||||
assert false, 'expected CRLF rejection'
|
||||
} else {
|
||||
assert err is S3Error
|
||||
}
|
||||
}
|
||||
|
||||
fn test_presign_rejects_invalid_expiry() {
|
||||
creds := Credentials{
|
||||
access_key_id: 'KEY'
|
||||
secret_access_key: 'secret'
|
||||
region: 'us-east-1'
|
||||
endpoint: 'examplebucket.s3.amazonaws.com'
|
||||
}
|
||||
if _ := presign_url(creds, PresignRequest{ method: 'GET', path: '/x', expires_in: 0 }) {
|
||||
assert false, 'expires_in=0 must be rejected'
|
||||
}
|
||||
if _ := presign_url(creds, PresignRequest{
|
||||
method: 'GET'
|
||||
path: '/x'
|
||||
expires_in: 700_000
|
||||
})
|
||||
{
|
||||
assert false, 'expires_in > 7 days must be rejected'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_presign_url_contains_required_query_params() {
|
||||
creds := Credentials{
|
||||
access_key_id: 'KEY'
|
||||
secret_access_key: 'secret'
|
||||
region: 'us-east-1'
|
||||
endpoint: 'examplebucket.s3.amazonaws.com'
|
||||
}
|
||||
url := presign_url(creds, PresignRequest{
|
||||
method: 'GET'
|
||||
path: '/foo.txt'
|
||||
expires_in: 300
|
||||
}) or { panic(err) }
|
||||
assert url.contains('X-Amz-Algorithm=AWS4-HMAC-SHA256')
|
||||
assert url.contains('X-Amz-Credential=')
|
||||
assert url.contains('X-Amz-Date=')
|
||||
assert url.contains('X-Amz-Expires=300')
|
||||
assert url.contains('X-Amz-SignedHeaders=host')
|
||||
assert url.contains('X-Amz-Signature=')
|
||||
}
|
||||
Reference in new issue
Block a user