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 }