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 import vibe.textfilter.urlencode;
11 
12 
13 const algorithm = "AWS4-HMAC-SHA256";
14 
15 struct CanonicalRequest 
16 {
17     string method;
18     string uri;
19     string[string] queryParameters;
20     string[string] headers;
21     ubyte[] payload;
22 }
23 
24 string canonicalQueryString(string[string] queryParameters)
25 {
26     alias encode = vibe.textfilter.urlencode.formEncode;
27 
28     string[string] encoded;
29     foreach (p; queryParameters.keys()) 
30     {
31         encoded[encode(p)] = encode(queryParameters[p]);
32     }
33     string[] keys = encoded.keys();
34     sort(keys);
35     return keys.map!(k => k ~ "=" ~ encoded[k]).join("&");
36 }
37 
38 string canonicalHeaders(string[string] headers)
39 {
40     string[string] trimmed;
41     foreach (h; headers.keys())
42     {
43         trimmed[h.toLower().strip()] = headers[h].strip();
44     }
45     string[] keys = trimmed.keys();
46     sort(keys);
47     return keys.map!(k => k ~ ":" ~ trimmed[k] ~ "\n").join("");
48 }
49 
50 string signedHeaders(string[string] headers)
51 {
52     string[] keys = headers.keys().map!(k => k.toLower()).array();
53     sort(keys);
54     return keys.join(";");
55 }
56 
57 string hash(T)(T payload)
58 {
59     auto hash = sha256Of(payload);
60     return hash.toHexString().toLower();
61 }
62 
63 string makeCRSigV4(CanonicalRequest r)
64 {
65     auto cr = 
66         r.method.toUpper() ~ "\n" ~
67         (r.uri.empty ? "/" : r.uri) ~ "\n" ~
68         canonicalQueryString(r.queryParameters) ~ "\n" ~
69         canonicalHeaders(r.headers) ~ "\n" ~
70         signedHeaders(r.headers) ~ "\n" ~
71         hash(r.payload);
72 
73     return hash(cr);
74 }
75 
76 unittest {
77     string[string] empty;
78 
79     auto r = CanonicalRequest(
80             "POST",
81             "/",
82             empty,
83             ["content-type": "application/x-www-form-urlencoded; charset=utf-8",
84              "host": "iam.amazonaws.com",
85              "x-amz-date": "20110909T233600Z"],
86             cast(ubyte[])"Action=ListUsers&Version=2010-05-08");
87 
88     auto sig = makeCRSigV4(r);
89 
90     assert(sig == "3511de7e95d28ecd39e9513b642aee07e54f4941150d8df8bf94b328ef7e55e2");
91 }
92 
93 struct SignableRequest
94 {
95     string dateString;
96     string timeStringUTC;
97     string region;
98     string service;
99     CanonicalRequest canonicalRequest;
100 }
101 
102 string signableString(SignableRequest r) {
103     return algorithm ~ "\n" ~
104         r.dateString ~ "T" ~ r.timeStringUTC ~ "Z\n" ~
105         r.dateString ~ "/" ~ r.region ~ "/" ~ r.service ~ "/aws4_request\n" ~
106         makeCRSigV4(r.canonicalRequest);
107 }
108 
109 unittest {
110     string[string] empty;
111 
112     SignableRequest r;
113     r.dateString = "20110909";
114     r.timeStringUTC = "233600";
115     r.region = "us-east-1";
116     r.service = "iam";
117     r.canonicalRequest = CanonicalRequest(
118             "POST",
119             "/",
120             empty,
121             ["content-type": "application/x-www-form-urlencoded; charset=utf-8",
122              "host": "iam.amazonaws.com",
123              "x-amz-date": "20110909T233600Z"],
124             cast(ubyte[])"Action=ListUsers&Version=2010-05-08");
125 
126     auto sampleString =
127         algorithm ~ "\n" ~
128         "20110909T233600Z\n" ~
129         "20110909/us-east-1/iam/aws4_request\n" ~ 
130         "3511de7e95d28ecd39e9513b642aee07e54f4941150d8df8bf94b328ef7e55e2";
131 
132     assert(sampleString == signableString(r));
133 }
134 
135 ubyte[] array_xor(ubyte[] b1, ubyte[] b2)
136 {
137     assert(b1.length == b2.length);
138     ubyte[] ret;
139     for (uint i = 0; i < b1.length; i++)
140         ret ~= b1[i] ^ b2[i];
141     return ret;
142 }
143 
144 auto hmac_sha256(R)(ubyte[] key, R message)
145 {
146     ubyte[] paddedKey = key[0..$];
147     while (paddedKey.length < 64) paddedKey ~= 0; // Pad to input block size of sha256
148     ubyte[] opad = (cast(ubyte)0x5c).repeat().take(64).array();
149     ubyte[] ipad = (cast(ubyte)0x36).repeat().take(64).array();
150 
151     return sha256Of(array_xor(paddedKey, opad).chain(cast(ubyte[])sha256Of(array_xor(paddedKey, ipad).chain(message))));
152 }
153 
154 unittest {
155     ubyte[] key = cast(ubyte[])"key";
156     ubyte[] message = cast(ubyte[])"The quick brown fox jumps over the lazy dog";
157 
158     string mac = hmac_sha256(key, message).toHexString().toLower();
159     assert(mac == "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8");
160 }
161 
162 auto signingKey(string secret, string dateString, string region, string service)
163 {
164     ubyte[] kSecret = cast(ubyte[])("AWS4" ~ secret);
165     auto kDate = hmac_sha256(kSecret, cast(ubyte[])dateString);
166     auto kRegion = hmac_sha256(kDate, cast(ubyte[])region);
167     auto kService = hmac_sha256(kRegion, cast(ubyte[])service);
168     return hmac_sha256(kService, cast(ubyte[])"aws4_request");
169 }
170 
171 unittest {
172     string secretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
173     auto signKey = signingKey(secretKey, "20110909", "us-east-1", "iam");
174     
175     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 ];
176     assert(expected == signKey);
177 }
178 
179 alias sign = hmac_sha256;
180 
181 unittest {
182     auto sampleString =
183         "AWS4-HMAC-SHA256\n" ~
184         "20110909T233600Z\n" ~
185         "20110909/us-east-1/iam/aws4_request\n" ~ 
186         "3511de7e95d28ecd39e9513b642aee07e54f4941150d8df8bf94b328ef7e55e2";
187 
188     auto secretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
189     auto signKey = signingKey(secretKey, "20110909", "us-east-1", "iam");
190 
191     auto signature = sign(signKey, cast(ubyte[])sampleString).toHexString().toLower();
192     auto expected = "ced6826de92d2bdeed8f846f0bf508e8559e98e4b0199114b84c54174deb456c";
193 
194     assert(signature == expected);
195 }
196 
197 /**
198  * CredentialScope == date / region / service / aws4_request
199  */
200 string createSignatureHeader(string accessKeyID, string credentialScope, string[string] reqHeaders, ubyte[] signature)
201 {
202     return algorithm ~ " Credential=" ~ accessKeyID ~ "/" ~ credentialScope ~ "/aws4_request, SignedHeaders=" ~ signedHeaders(reqHeaders) ~ ", Signature=" ~ signature.toHexString().toLower();
203 }
204 
205 string dateFromISOString(string iso)
206 {
207     auto i = iso.indexOf('T');
208     if (i == -1) throw new Exception("ISO time in wrong format: " ~ iso);
209     return iso[0..i];
210 }
211 
212 string timeFromISOString(string iso)
213 {
214     auto t = iso.indexOf('T');
215     auto z = iso.indexOf('Z');
216     if (t == -1 || z == -1) throw new Exception("ISO time in wrong format: " ~ iso);
217     return iso[t+1..z];
218 }
219 
220 unittest {
221     assert(dateFromISOString("20110909T1203Z") == "20110909");
222 }