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 }