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