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 }