1 module vibe.aws.sigv4;
2 
3 import std.array;
4 import std.algorithm;
5 import std.digest.sha;
6 import std.range;
7 import std.stdio;
8 import std.string;
9 
10 static import vibe.textfilter.urlencode;
11 
12 
13 immutable algorithm = "AWS4-HMAC-SHA256";
14 immutable streaming_payload_hash = "STREAMING-" ~ algorithm ~ "-PAYLOAD";
15 
16 struct CanonicalRequest 
17 {
18     string method;
19     string uri;
20     string[string] queryParameters;
21     string[string] headers;
22     const(ubyte)[] payload;
23 }
24 
25 @trusted pure
26 string canonicalQueryString(in string[string] queryParameters)
27 {
28     alias encode = vibe.textfilter.urlencode.formEncode;
29 
30     string[string] encoded;
31     foreach (p; queryParameters.keys()) 
32     {
33         encoded[encode(p)] = encode(queryParameters[p]);
34     }
35     string[] keys = encoded.keys();
36     sort(keys);
37     return keys.map!(k => k ~ "=" ~ encoded[k]).join("&");
38 }
39 
40 @trusted pure
41 string canonicalHeaders(in string[string] headers)
42 {
43     string[string] trimmed;
44     foreach (h; headers.keys())
45     {
46         trimmed[h.toLower().strip()] = headers[h].strip();
47     }
48     string[] keys = trimmed.keys();
49     sort(keys);
50     return keys.map!(k => k ~ ":" ~ trimmed[k] ~ "\n").join("");
51 }
52 
53 @trusted pure
54 string signedHeaders(in string[string] headers)
55 {
56     string[] keys = headers.keys().map!(k => k.toLower()).array();
57     sort(keys);
58     return keys.join(";");
59 }
60 
61 @safe pure
62 string hash(in ubyte[] payload)
63 {
64     return sha256Of(payload)[].toHexString().toLower();
65 }
66 
67 @safe pure
68 private string requestStringBase(in CanonicalRequest r)
69 {
70     return 
71         r.method.toUpper() ~ "\n" ~
72         (r.uri.empty ? "/" : r.uri) ~ "\n" ~
73         canonicalQueryString(r.queryParameters) ~ "\n" ~
74         canonicalHeaders(r.headers) ~ "\n" ~
75         signedHeaders(r.headers);
76 }
77 
78 @safe pure
79 string requestString(in CanonicalRequest r)
80 {
81     return r.requestStringBase ~ "\n" ~
82         r.payload.hash;
83 }
84 
85 @safe pure
86 string streamingRequestString(in CanonicalRequest r)
87 {
88     return 
89         r.requestStringBase ~ "\n" ~
90         streaming_payload_hash;
91 }
92 
93 @safe pure
94 string makeCRSigV4(in CanonicalRequest r)
95 {
96     return r.requestString.representation.hash;
97 }
98 
99 @safe pure
100 string makeStreamingSigV4(in CanonicalRequest r)
101 {
102     return r.streamingRequestString.representation.hash;
103 }
104 
105 unittest {
106     string[string] empty;
107 
108     auto r = CanonicalRequest(
109             "POST",
110             "/",
111             empty,
112             ["content-type": "application/x-www-form-urlencoded; charset=utf-8",
113              "host": "iam.amazonaws.com",
114              "x-amz-date": "20110909T233600Z"],
115             cast(ubyte[])"Action=ListUsers&Version=2010-05-08");
116 
117     auto sig = makeCRSigV4(r);
118 
119     assert(sig == "3511de7e95d28ecd39e9513b642aee07e54f4941150d8df8bf94b328ef7e55e2");
120 }
121 
122 struct SignableRequest
123 {
124     string dateString;
125     string timeStringUTC;
126     string region;
127     string service;
128     CanonicalRequest canonicalRequest;
129 }
130 
131 private string signableStringBase(in SignableRequest r) @safe
132 {
133     return algorithm ~ "\n" ~
134         r.dateString ~ "T" ~ r.timeStringUTC ~ "Z\n" ~
135         r.dateString ~ "/" ~ r.region ~ "/" ~ r.service ~ "/aws4_request";
136 }
137 
138 string signableString(in SignableRequest r) @safe {
139     return r.signableStringBase ~ "\n" ~
140         r.canonicalRequest.makeCRSigV4;
141 }
142 
143 string signableStringForStream(in SignableRequest r) @safe {
144     return r.signableStringBase ~ "\n" ~
145         r.canonicalRequest.makeStreamingSigV4;
146 }
147 
148 unittest {
149     string[string] empty;
150 
151     SignableRequest r;
152     r.dateString = "20110909";
153     r.timeStringUTC = "233600";
154     r.region = "us-east-1";
155     r.service = "iam";
156     r.canonicalRequest = CanonicalRequest(
157             "POST",
158             "/",
159             empty,
160             ["content-type": "application/x-www-form-urlencoded; charset=utf-8",
161              "host": "iam.amazonaws.com",
162              "x-amz-date": "20110909T233600Z"],
163             cast(ubyte[])"Action=ListUsers&Version=2010-05-08");
164 
165     auto sampleString =
166         algorithm ~ "\n" ~
167         "20110909T233600Z\n" ~
168         "20110909/us-east-1/iam/aws4_request\n" ~ 
169         "3511de7e95d28ecd39e9513b642aee07e54f4941150d8df8bf94b328ef7e55e2";
170 
171     assert(sampleString == signableString(r));
172 }
173 
174 @safe pure nothrow @nogc
175 auto hmac_sha256(in ubyte[] key, in ubyte[] message)
176 in {
177     assert(key.length <= 64);
178 }
179 body {
180     assert(key.length <= 64);
181     SHA256 sha;
182     ubyte[64] pad = 0x36;
183     pad[0 .. key.length] ^= key[];
184     sha.put(pad);
185     sha.put(message);
186     auto hash = sha.finish;
187     sha.start;
188     pad[] = 0x5c;
189     pad[0 .. key.length] ^= key[];
190     sha.put(pad);
191     sha.put(hash);
192     hash = sha.finish;
193     return hash;
194 }
195 
196 unittest {
197     ubyte[] key = cast(ubyte[])"key";
198     ubyte[] message = cast(ubyte[])"The quick brown fox jumps over the lazy dog";
199 
200     string mac = hmac_sha256(key, message).toHexString().toLower();
201     assert(mac == "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8");
202 }
203 
204 auto signingKey(string secret, string dateString, string region, string service)
205 {
206     ubyte[] kSecret = cast(ubyte[])("AWS4" ~ secret);
207     auto kDate = hmac_sha256(kSecret, cast(ubyte[])dateString);
208     auto kRegion = hmac_sha256(kDate, cast(ubyte[])region);
209     auto kService = hmac_sha256(kRegion, cast(ubyte[])service);
210     return hmac_sha256(kService, cast(ubyte[])"aws4_request");
211 }
212 
213 unittest {
214     string secretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
215     auto signKey = signingKey(secretKey, "20110909", "us-east-1", "iam");
216     
217     ubyte[] expected = [152, 241, 216, 137, 254, 196, 244, 66, 26, 220, 82, 43, 171, 12, 225, 248, 46, 105, 41, 194, 98, 237, 21, 229, 169, 76, 144, 239, 209, 227, 176, 231 ];
218     assert(expected == signKey);
219 }
220 
221 alias sign = hmac_sha256;
222 
223 unittest {
224     auto sampleString =
225         "AWS4-HMAC-SHA256\n" ~
226         "20110909T233600Z\n" ~
227         "20110909/us-east-1/iam/aws4_request\n" ~ 
228         "3511de7e95d28ecd39e9513b642aee07e54f4941150d8df8bf94b328ef7e55e2";
229 
230     auto secretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
231     auto signKey = signingKey(secretKey, "20110909", "us-east-1", "iam");
232 
233     auto signature = sign(signKey, cast(ubyte[])sampleString).toHexString().toLower();
234     auto expected = "ced6826de92d2bdeed8f846f0bf508e8559e98e4b0199114b84c54174deb456c";
235 
236     assert(signature == expected);
237 }
238 
239 /**
240  * CredentialScope == date / region / service / aws4_request
241  */
242 string createSignatureHeader(string accessKeyID, string credentialScope, string[string] reqHeaders, ubyte[] signature)
243 {
244     return algorithm ~ " Credential=" ~ accessKeyID ~ "/" ~ credentialScope ~ "/aws4_request, SignedHeaders=" ~ signedHeaders(reqHeaders) ~ ", Signature=" ~ signature.toHexString().toLower();
245 }
246 
247 string dateFromISOString(string iso)
248 {
249     auto i = iso.indexOf('T');
250     if (i == -1) throw new Exception("ISO time in wrong format: " ~ iso);
251     return iso[0..i];
252 }
253 
254 string timeFromISOString(string iso)
255 {
256     auto t = iso.indexOf('T');
257     auto z = iso.indexOf('Z');
258     if (t == -1 || z == -1) throw new Exception("ISO time in wrong format: " ~ iso);
259     return iso[t+1..z];
260 }
261 
262 unittest {
263     assert(dateFromISOString("20110909T1203Z") == "20110909");
264 }
265 
266 struct SignableChunk
267 {
268     static immutable string emptyHash;
269 
270     static this()
271     {
272         emptyHash = hash([]);
273     }
274 
275     string dateString;
276     string timeStringUTC;
277     string region;
278     string service;
279 
280     string seedHash;
281     string payloadHash;
282 }
283 
284 string signableString(SignableChunk c) @safe {
285     return algorithm ~ "-PAYLOAD\n" ~
286         c.dateString ~ "T" ~ c.timeStringUTC ~ "Z\n" ~
287         c.dateString ~ "/" ~ c.region ~ "/" ~ c.service ~ "/aws4_request\n" ~
288         c.seedHash ~ "\n" ~
289         SignableChunk.emptyHash ~ "\n" ~
290         c.payloadHash;
291 }
292 
293 
294 unittest {
295     //Example taken from here: http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
296 
297     immutable string AWSAccessKeyId     = "AKIAIOSFODNN7EXAMPLE";
298     immutable string AWSSecretAccessKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
299 
300     immutable string isoDateTime = "20130524T000000Z";
301     immutable string date = dateFromISOString(isoDateTime);
302     immutable string time = timeFromISOString(isoDateTime);
303 
304     immutable string region  = "us-east-1";
305     immutable string service = "s3";
306     immutable string bucket = "examplebucket";
307 
308     /*  Request:
309       
310         PUT /examplebucket/chunkObject.txt HTTP/1.1
311         Host: s3.amazonaws.com
312         x-amz-date: 20130524T000000Z 
313         x-amz-storage-class: REDUCED_REDUNDANCY
314         Authorization: SignatureToBeCalculated
315         x-amz-content-sha256: STREAMING-AWS4-HMAC-SHA256-PAYLOAD
316         Content-Encoding: aws-chunked
317         x-amz-decoded-content-length: 66560
318         Content-Length: 66824
319         <Payload>
320      */
321 
322 
323     /*  Canonical Request:
324       
325         PUT
326         /examplebucket/chunkObject.txt
327 
328         content-encoding:aws-chunked
329         content-length:66824
330         host:s3.amazonaws.com
331         x-amz-content-sha256:STREAMING-AWS4-HMAC-SHA256-PAYLOAD
332         x-amz-date:20130524T000000Z
333         x-amz-decoded-content-length:66560
334         x-amz-storage-class:REDUCED_REDUNDANCY
335 
336         content-encoding;content-length;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-storage-class
337         STREAMING-AWS4-HMAC-SHA256-PAYLOAD
338      */
339 
340     auto canonicalRequest = CanonicalRequest(
341             "PUT",
342             "/examplebucket/chunkObject.txt",
343             null,
344             [
345                 "content-encoding":             "aws-chunked",
346                 "content-length":               "66824",
347                 "host":                         "s3.amazonaws.com",
348                 "x-amz-content-sha256":         streaming_payload_hash,
349                 "x-amz-date":                   isoDateTime,
350                 "x-amz-decoded-content-length": "66560",
351                 "x-amz-storage-class":          "REDUCED_REDUNDANCY",
352             ],
353             null
354         );
355 
356     auto canonicalRequestSignature = canonicalRequest.makeStreamingSigV4;
357     assert(canonicalRequestSignature == "cee3fed04b70f867d036f722359b0b1f2f0e5dc0efadbc082b76c4c60e316455");
358 
359     /* Signable String:
360        AWS4-HMAC-SHA256
361        20130524T000000Z
362        20130524/us-east-1/s3/aws4_request
363        cee3fed04b70f867d036f722359b0b1f2f0e5dc0efadbc082b76c4c60e316455
364      */
365 
366     auto signableRequest = SignableRequest(date, time, region, service, canonicalRequest);
367     auto signableString = signableRequest.signableStringForStream;
368     assert(signableString == "AWS4-HMAC-SHA256\n" ~
369                              "20130524T000000Z\n" ~ 
370                              "20130524/us-east-1/s3/aws4_request\n" ~
371                              "cee3fed04b70f867d036f722359b0b1f2f0e5dc0efadbc082b76c4c60e316455");
372 
373     auto key = signingKey(AWSSecretAccessKey, date, region, service);
374     auto binarySignature = key.sign(cast(ubyte[])signableString);
375     auto seedSignature = binarySignature.toHexString().toLower();
376     assert(seedSignature == "4f232c4386841ef735655705268965c44a0e4690baa4adea153f7db9fa80a0a9");
377 
378     auto credScope = date ~ "/" ~ region ~ "/" ~ service;
379     auto authHeader = createSignatureHeader(AWSAccessKeyId, credScope, canonicalRequest.headers, binarySignature);
380     assert(authHeader == "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request, " ~
381                          "SignedHeaders=content-encoding;content-length;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-storage-class, " ~
382                          "Signature=4f232c4386841ef735655705268965c44a0e4690baa4adea153f7db9fa80a0a9");
383 
384     auto payload1 = new ubyte[](0x10000);
385     payload1[] = 97;
386     auto chunk1 = SignableChunk(date,time,region,service,seedSignature,hash(payload1));
387     auto signableChunkString1 = chunk1.signableString;
388     assert(signableChunkString1 == "AWS4-HMAC-SHA256-PAYLOAD\n" ~ 
389                                    "20130524T000000Z\n" ~ 
390                                    "20130524/us-east-1/s3/aws4_request\n" ~ 
391                                    "4f232c4386841ef735655705268965c44a0e4690baa4adea153f7db9fa80a0a9\n" ~ 
392                                    "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n" ~ 
393                                    "bf718b6f653bebc184e1479f1935b8da974d701b893afcf49e701f3e2f9f9c5a");
394     auto chunkSignature1 = key.sign(cast(ubyte[])signableChunkString1).toHexString().toLower();
395     assert(chunkSignature1 == "ad80c730a21e5b8d04586a2213dd63b9a0e99e0e2307b0ade35a65485a288648");
396 
397     auto payload2 = new ubyte[](0x400);
398     payload2[] = 97;
399     auto chunk2 = SignableChunk(date,time,region,service,chunkSignature1,hash(payload2));
400     auto signableChunkString2 = chunk2.signableString;
401     assert(signableChunkString2 == "AWS4-HMAC-SHA256-PAYLOAD\n" ~
402                                    "20130524T000000Z\n" ~
403                                    "20130524/us-east-1/s3/aws4_request\n" ~
404                                    "ad80c730a21e5b8d04586a2213dd63b9a0e99e0e2307b0ade35a65485a288648\n" ~
405                                    "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n" ~
406                                    "2edc986847e209b4016e141a6dc8716d3207350f416969382d431539bf292e4a");
407     auto chunkSignature2 = key.sign(cast(ubyte[])signableChunkString2).toHexString().toLower();
408     assert(chunkSignature2 == "0055627c9e194cb4542bae2aa5492e3c1575bbb81b612b7d234b86a503ef5497");
409 
410     auto payload3 = new ubyte[](0);
411     auto chunk3 = SignableChunk(date,time,region,service,chunkSignature2,hash(payload3));
412     auto signableChunkString3 = chunk3.signableString;
413     assert(signableChunkString3 == "AWS4-HMAC-SHA256-PAYLOAD\n" ~
414                                    "20130524T000000Z\n" ~
415                                    "20130524/us-east-1/s3/aws4_request\n" ~
416                                    "0055627c9e194cb4542bae2aa5492e3c1575bbb81b612b7d234b86a503ef5497\n" ~
417                                    "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n" ~
418                                    "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
419     auto chunkSignature3 = key.sign(cast(ubyte[])signableChunkString3).toHexString().toLower();
420     assert(chunkSignature3 == "b6c6ea8a5354eaf15b3cb7646744f4275b71ea724fed81ceb9323e279d449df9");
421 }