v2 / vlib / net / s3 / signer_test.v
224 lines · 210 sloc · 7.44 KB · 4142432483c4e8de44ab7b0d6ac944f3251e03c8
Raw
1// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module s3
5
6import time
7
8// Test vectors come from the AWS-published Signature V4 reference test
9// suite ("Examples of the complete version 4 signing process").
10// These secret/key pairs are the documented example values, not real
11// credentials.
12const aws_secret = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
13const aws_key_id = 'AKIAIOSFODNN7EXAMPLE'
14
15// SigV4 vector for `s3-get-object` (examplebucket / test.txt with a Range
16// header). Source: AWS docs (Signature V4 examples).
17//
18// GET /test.txt HTTP/1.1
19// Host: examplebucket.s3.amazonaws.com
20// Range: bytes=0-9
21// X-Amz-Date: 20130524T000000Z
22// X-Amz-Content-SHA256: <empty-sha>
23fn test_aws_sigv4_get_object_vector() {
24 t := time.parse_iso8601('2013-05-24T00:00:00.000Z') or { panic(err) }
25 creds := Credentials{
26 access_key_id: aws_key_id
27 secret_access_key: aws_secret
28 region: 'us-east-1'
29 endpoint: 'examplebucket.s3.amazonaws.com'
30 virtual_hosted_style: false // host already carries the bucket
31 }
32 req := SignRequest{
33 method: 'GET'
34 path: '/test.txt'
35 query: ''
36 payload_hash: empty_sha256
37 extra_headers: {
38 'range': 'bytes=0-9'
39 }
40 sign_time: t
41 }
42 signed := sign_request(creds, req) or { panic(err) }
43 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'
44 assert signed.authorization == expected, 'Got:\n${signed.authorization}\nExpected:\n${expected}'
45}
46
47// SigV4 vector for `s3-put-object` (examplebucket / test$file.text with body
48// "Welcome to Amazon S3.").
49//
50// PUT /test%24file.text HTTP/1.1
51// Host: examplebucket.s3.amazonaws.com
52// Date: Fri, 24 May 2013 00:00:00 GMT
53// x-amz-date: 20130524T000000Z
54// x-amz-storage-class: REDUCED_REDUNDANCY
55fn test_aws_sigv4_put_object_vector() {
56 t := time.parse_iso8601('2013-05-24T00:00:00.000Z') or { panic(err) }
57 creds := Credentials{
58 access_key_id: aws_key_id
59 secret_access_key: aws_secret
60 region: 'us-east-1'
61 endpoint: 'examplebucket.s3.amazonaws.com'
62 virtual_hosted_style: false
63 }
64 body := 'Welcome to Amazon S3.'
65 body_hash := sha256_hex(body.bytes())
66 req := SignRequest{
67 method: 'PUT'
68 path: '/test%24file.text'
69 query: ''
70 payload_hash: body_hash
71 extra_headers: {
72 'date': 'Fri, 24 May 2013 00:00:00 GMT'
73 'x-amz-storage-class': 'REDUCED_REDUNDANCY'
74 }
75 sign_time: t
76 }
77 signed := sign_request(creds, req) or { panic(err) }
78 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'
79 assert signed.authorization == expected, 'Got:\n${signed.authorization}\nExpected:\n${expected}'
80}
81
82// Asserts the canonical request string for a ListObjectsV2 GET. The
83// canonical request is the most stable interop point — Authorization line
84// formatting has wobbled across AWS doc revisions, but the canonical hash
85// is always the same.
86fn test_aws_sigv4_list_objects_canonical_request() {
87 headers := {
88 'host': 'examplebucket.s3.amazonaws.com'
89 'x-amz-content-sha256': empty_sha256
90 'x-amz-date': '20130524T000000Z'
91 }
92 canonical := build_canonical_request('GET', '/', 'list-type=2&prefix=foo', headers,
93 'host;x-amz-content-sha256;x-amz-date', empty_sha256)
94 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}'
95 assert canonical == expected, 'canonical mismatch:\n${canonical}'
96}
97
98// Cross-checked manually with openssl on the AWS docs example
99// (date 20120215, region us-east-1, service iam):
100//
101// echo -n 'aws4_request' | openssl dgst -sha256 -mac HMAC -macopt hexkey:<kService>
102fn test_signing_key_chain() {
103 key := derive_signing_key(aws_secret, '20120215', 'us-east-1', 'iam')
104 assert to_hex_lower(key) == '004aa806e13dae88b9032d9261bcb04c67d023afadd221e6b0d206e1760e0b5e', 'Got: ${to_hex_lower(key)}'
105}
106
107fn test_normalize_header_value_collapses_whitespace() {
108 assert normalize_header_value(' hello world ') == 'hello world'
109 assert normalize_header_value('a\tb\tc') == 'a b c'
110 assert normalize_header_value('') == ''
111}
112
113fn test_canonical_query_string_sorted_and_encoded() {
114 q := canonical_query_string({
115 'b': '2'
116 'a': '1'
117 'c': 'hello world'
118 })
119 assert q == 'a=1&b=2&c=hello%20world'
120}
121
122fn test_format_amz_date() {
123 t := time.parse_iso8601('2024-01-15T12:34:56.000Z') or { panic(err) }
124 assert format_amz_date(t) == '20240115T123456Z'
125}
126
127fn test_signer_rejects_unsupported_method() {
128 creds := Credentials{
129 access_key_id: 'KEY'
130 secret_access_key: 'secret'
131 region: 'us-east-1'
132 endpoint: 'examplebucket.s3.amazonaws.com'
133 }
134 if _ := sign_request(creds, SignRequest{ method: 'PATCH', path: '/x', payload_hash: empty_sha256 }) {
135 assert false
136 }
137}
138
139fn test_signer_rejects_crlf_in_extra_headers() {
140 creds := Credentials{
141 access_key_id: 'KEY'
142 secret_access_key: 'secret'
143 region: 'us-east-1'
144 endpoint: 'examplebucket.s3.amazonaws.com'
145 }
146 bad := SignRequest{
147 method: 'GET'
148 path: '/foo'
149 payload_hash: empty_sha256
150 extra_headers: {
151 'x-evil': 'value\r\nInjected: yes'
152 }
153 }
154 if _ := sign_request(creds, bad) {
155 assert false, 'expected CRLF rejection'
156 } else {
157 if err is S3Error {
158 assert err.code == 'InvalidHeader' || err.code == 'InvalidCredentials', 'unexpected code: ${err.code}'
159 } else {
160 assert false, 'wrong error type'
161 }
162 }
163}
164
165fn test_presign_rejects_crlf_in_extra_query() {
166 creds := Credentials{
167 access_key_id: 'KEY'
168 secret_access_key: 'secret'
169 region: 'us-east-1'
170 endpoint: 'examplebucket.s3.amazonaws.com'
171 }
172 bad := PresignRequest{
173 method: 'GET'
174 path: '/foo'
175 extra_query: {
176 'X-Evil': 'value\r\nInjected: yes'
177 }
178 }
179 if _ := presign_url(creds, bad) {
180 assert false, 'expected CRLF rejection'
181 } else {
182 assert err is S3Error
183 }
184}
185
186fn test_presign_rejects_invalid_expiry() {
187 creds := Credentials{
188 access_key_id: 'KEY'
189 secret_access_key: 'secret'
190 region: 'us-east-1'
191 endpoint: 'examplebucket.s3.amazonaws.com'
192 }
193 if _ := presign_url(creds, PresignRequest{ method: 'GET', path: '/x', expires_in: 0 }) {
194 assert false, 'expires_in=0 must be rejected'
195 }
196 if _ := presign_url(creds, PresignRequest{
197 method: 'GET'
198 path: '/x'
199 expires_in: 700_000
200 })
201 {
202 assert false, 'expires_in > 7 days must be rejected'
203 }
204}
205
206fn test_presign_url_contains_required_query_params() {
207 creds := Credentials{
208 access_key_id: 'KEY'
209 secret_access_key: 'secret'
210 region: 'us-east-1'
211 endpoint: 'examplebucket.s3.amazonaws.com'
212 }
213 url := presign_url(creds, PresignRequest{
214 method: 'GET'
215 path: '/foo.txt'
216 expires_in: 300
217 }) or { panic(err) }
218 assert url.contains('X-Amz-Algorithm=AWS4-HMAC-SHA256')
219 assert url.contains('X-Amz-Credential=')
220 assert url.contains('X-Amz-Date=')
221 assert url.contains('X-Amz-Expires=300')
222 assert url.contains('X-Amz-SignedHeaders=host')
223 assert url.contains('X-Amz-Signature=')
224}
225