diff --git a/Tests/Aws.IoTCore.Devices.UnitTests.nfproj b/Tests/Aws.IoTCore.Devices.UnitTests.nfproj index 02556ed..ad7bba8 100644 --- a/Tests/Aws.IoTCore.Devices.UnitTests.nfproj +++ b/Tests/Aws.IoTCore.Devices.UnitTests.nfproj @@ -32,6 +32,8 @@ + + @@ -46,6 +48,9 @@ ..\packages\nanoFramework.Json.2.2.199\lib\nanoFramework.Json.dll + + ..\packages\nanoFramework.Runtime.Events.1.11.32\lib\nanoFramework.Runtime.Events.dll + ..\packages\nanoFramework.System.Collections.1.5.67\lib\nanoFramework.System.Collections.dll @@ -61,6 +66,18 @@ ..\packages\nanoFramework.System.IO.Streams.1.1.96\lib\System.IO.Streams.dll + + ..\packages\nanoFramework.System.Net.1.11.43\lib\System.Net.dll + + + ..\packages\nanoFramework.System.Net.Http.1.5.196\lib\System.Net.Http.dll + + + ..\packages\nanoFramework.System.Threading.1.1.52\lib\System.Threading.dll + + + + diff --git a/Tests/SignatureVersion4Tests.cs b/Tests/SignatureVersion4Tests.cs new file mode 100644 index 0000000..53fff8a --- /dev/null +++ b/Tests/SignatureVersion4Tests.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; +using System; +using System.Collections; + +namespace nanoFramework.Aws.SignatureVersion4.Tests +{ + // https://github.com/inspiration/uniface-aws-sigv4 + // https://github.com/FantasticFiasco/aws-signature-version-4 + // https://stackoverflow.com/questions/28966075/creating-a-hmac-signature-for-aws-rest-query-in-c-sharp + [TestClass] + public class SignatureVersion4Tests + { + + [TestMethod] + public void check_SignerForQueryParameters_ComputeSignature_Get_iotGateway_generated_correctly() + { + // See: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html + // https://github.com/aws/aws-sdk-java-v2/blob/master/core/auth/src/test/java/software/amazon/awssdk/auth/signer/Aws4SignerTest.java + // https://github.com/aws/aws-sdk-net/blob/master/sdk/test/Services/Signer/UnitTests/Generated/Endpoints/SignerEndpointProviderTests.cs + + var v4signer = new SignerForQueryParameterAuth + { + EndpointUri = new System.Uri("https://test.us-east-1.amazonaws.com"), + HttpMethod = "GET", + Service = "iotdevicegateway", + Region = "us-east-1" + }; + + Console.WriteLine(v4signer.ComputeSignature(new Hashtable(), "", "", "fakeAccessKey", "fakeSecret")); + + //http://demo.us-east-1.amazonaws.com + //?X-Amz-Algorithm=AWS4-HMAC-SHA256 + //&X-Amz-Credential=/20130721/us-east-1/iotdevicegateway/aws4_request + //&X-Amz-Date=20130721T201207Z + //&X-Amz-SignedHeaders=host + //&X-Amz-Signature= + + // replace '' with "fakeAccessKey" + // replace '' + // replace '/' with '%2F' in url + // ignore or inject datetime? + } + + [TestMethod] + public void check_SignerForQueryParameters_ComputeSignature_Post_S3_generated_correctly() + { + // See: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html + + + var v4signer = new SignerForQueryParameterAuth + { + EndpointUri = new System.Uri("https://test.us-east-1.amazonaws.com"), + HttpMethod = "POST", + Service = "s3", + Region = "us-east-1" + }; + + // FIXME: would fail! + var sigparams = new Hashtable + { + { "X-Amz-Expires", "86400" } + }; + + Console.WriteLine(v4signer.ComputeSignature(sigparams, "", "", "fakeAccessKey", "fakeSecret")); + + //http://demo.us-east-1.amazonaws.com + //?X-Amz-Algorithm=AWS4-HMAC-SHA256 + //&X-Amz-Credential=/20130721/us-east-1/s3/aws4_request + //&X-Amz-Date=20130721T201207Z + //&X-Amz-Expires=86400 + //&X-Amz-SignedHeaders=host + //&X-Amz-Signature= + + // replace '' with "fakeAccessKey" + // replace '' + // replace '/' with '%2F' in url + // ignore or inject datetime? + } + + } +} diff --git a/Tests/SortExtensionTests.cs b/Tests/SortExtensionTests.cs new file mode 100644 index 0000000..b24b76f --- /dev/null +++ b/Tests/SortExtensionTests.cs @@ -0,0 +1,97 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using nanoFramework.TestFramework; +using System; +using System.Collections; + +namespace nanoFramework.Aws.SignatureVersion4.Tests +{ + [TestClass] + public class SortExtensionTests + { + [TestMethod] + public void check_ArrayList_Sort_Ordinal_when_order_is_canonical_already() + { + var list = new ArrayList(); + list.Add("aTest"); + list.Add("Atest2"); + list.Add("btest"); + list.Add("ctest"); + + list.Sort(StringComparer.Ordinal); + + foreach (var item in list) + { + Console.WriteLine(item.ToString()); + } + + + + // Expect "atest;atest2;btest;ctest" + } + + [TestMethod] + public void check_check_ArrayList_Sort_Ordinal_when_order_is_not_sorted() + { + var list = new ArrayList(); + list.Add("aTest"); + list.Add("ctest"); + list.Add("btest"); + list.Add("Atest2"); + + list.Sort(StringComparer.Ordinal); + + foreach (var item in list) + { + Console.WriteLine(item.ToString()); + } + + // Expect "atest;atest2;btest;ctest" + } + + + [TestMethod] + public void check_ArrayList_Sort_OrdinalIgnoreCase_when_order_is_canonical_already() + { + var list = new ArrayList(); + list.Add("aTest"); + list.Add("Atest2"); + list.Add("btest"); + list.Add("ctest"); + + list.Sort(StringComparer.OrdinalIgnoreCase); + + foreach (var item in list) + { + Console.WriteLine(item.ToString()); + } + + + + // Expect "atest;atest2;btest;ctest" + } + + [TestMethod] + public void check_check_ArrayList_Sort_OrdinalIgnoreCase_when_order_is_not_sorted() + { + var list = new ArrayList(); + list.Add("aTest"); + list.Add("ctest"); + list.Add("btest"); + list.Add("Atest2"); + + list.Sort(StringComparer.OrdinalIgnoreCase); + + foreach (var item in list) + { + Console.WriteLine(item.ToString()); + } + + // Expect "atest;atest2;btest;ctest" + } + + } +} diff --git a/Tests/packages.config b/Tests/packages.config index b38dc7e..51ba884 100644 --- a/Tests/packages.config +++ b/Tests/packages.config @@ -2,8 +2,12 @@ + + + + \ No newline at end of file diff --git a/Tests/packages.lock.json b/Tests/packages.lock.json index 864bcd8..bb32c5c 100644 --- a/Tests/packages.lock.json +++ b/Tests/packages.lock.json @@ -14,6 +14,12 @@ "resolved": "2.2.199", "contentHash": "XBNKcI5hiUpn19NxhSYM4cxH0FXeefrohGD4tFrTlwhZw3hL1ie5UQJ0dPsaUBb/YkypkJZzQoxEvnwOj8DI5w==" }, + "nanoFramework.Runtime.Events": { + "type": "Direct", + "requested": "[1.11.32, 1.11.32]", + "resolved": "1.11.32", + "contentHash": "NyLUIwJDlpl5VKSd+ljmdDtO2WHHBvPvruo1ccaL+hd79z+6XMYze1AccOVXKGiZenLBCwDmFHwpgIQyHkM7GA==" + }, "nanoFramework.System.Collections": { "type": "Direct", "requested": "[1.5.67, 1.5.67]", @@ -26,12 +32,30 @@ "resolved": "1.1.96", "contentHash": "kJSy4EJwChO4Vq3vGWP9gNRPFDnTsDU5HxzeI7NDO+RjbDsx7B8EhKymoeTPLJCxQq8y/0P1KG2XCxGpggW+fw==" }, + "nanoFramework.System.Net": { + "type": "Direct", + "requested": "[1.11.43, 1.11.43]", + "resolved": "1.11.43", + "contentHash": "USwz59gxcNUzsiXfQohWSi8ANNwGDsp+qG4zBtHZU3rKMtvTsLI3rxdfMC77VehKqsCPn7aK3PU2oCRFo+1Rgg==" + }, + "nanoFramework.System.Net.Http": { + "type": "Direct", + "requested": "[1.5.196, 1.5.196]", + "resolved": "1.5.196", + "contentHash": "2qfUnvJa55Wx5C86HopeoUZVfXY+L6stufrlcKNHlalqIk4cc3Orv0Eqc0oroy3iB6aQGDw+tji7WYJS8LQNoA==" + }, "nanoFramework.System.Text": { "type": "Direct", "requested": "[1.3.42, 1.3.42]", "resolved": "1.3.42", "contentHash": "68HPjhersNpssbmEMUHdMw3073MHfGTfrkbRk9eILKbNPFfPFck7m4y9BlAi6DaguUJaeKxgyIojXF3SQrF8/A==" }, + "nanoFramework.System.Threading": { + "type": "Direct", + "requested": "[1.1.52, 1.1.52]", + "resolved": "1.1.52", + "contentHash": "kv+US/+7QKV1iT/snxBh032vwZ+3krJ4vujlSsvmS2nNj/nK64R3bq/ST3bCFquxHDD0mog8irtCBCsFazr4kA==" + }, "nanoFramework.TestFramework": { "type": "Direct", "requested": "[3.0.77, 3.0.77]", diff --git a/nanoFramework.Aws.IoTCore.Devices.nuspec b/nanoFramework.Aws.IoTCore.Devices.nuspec index fac97c1..b0784f8 100644 --- a/nanoFramework.Aws.IoTCore.Devices.nuspec +++ b/nanoFramework.Aws.IoTCore.Devices.nuspec @@ -22,6 +22,8 @@ This is an SDK for Aws IoTCore. + + diff --git a/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/HMACSHA256.cs b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/HMACSHA256.cs new file mode 100644 index 0000000..0797c78 --- /dev/null +++ b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/HMACSHA256.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +// +// Originally by Elze Kool see http://www.microframework.nl/2009/09/05/shahmac-digest-class/ +// Adjustments by Laurent Ellerbach laurelle@microsoft.com 2021/05/31 +// + +namespace System.Security.Cryptography +{ + using System; + + /// + /// Computes a Hash-based Message Authentication Code (HMAC) by using the System.Security.Cryptography.SHA256 + /// hash function. + /// + public class HMACSHA256 + { + /// + /// Gets or sets the key to use in the HMAC calculation. + /// + public byte[] Key { get; set; } + + /// + /// Initializes a new instance of the System.Security.Cryptography.HMACSHA256 class + /// with the specified key data. + /// + /// The secret key for System.Security.Cryptography.HMACSHA256 encryption. The key + /// can be any length. However, the recommended size is 64 bytes max. + public HMACSHA256(byte[] key) + { + Key = key; + } + + /// + /// Computes the hash value for the specified byte array. + /// + /// The input to compute the hash code for. + /// The computed hash code. + /// buffer is null. + public byte[] ComputeHash(byte[] buffer) + { + if (buffer == null) + { + throw new ArgumentNullException(); + } + + return ComputeHMACSHA256(Key, buffer); + } + + /// + /// Compute HMAC SHA-256 + /// + /// Secret + /// Password + /// 32 byte HMAC_SHA256 + private static byte[] ComputeHMACSHA256(byte[] secret, byte[] value) + { + // Create two arrays, bi and bo + var bi = new byte[64 + value.Length]; + var bo = new byte[64 + 32]; + + // Copy secret to both arrays + Array.Copy(secret, bi, secret.Length); + Array.Copy(secret, bo, secret.Length); + + for (var i = 0; i < 64; i++) + { + // Xor bi with 0x36 + bi[i] = (byte)(bi[i] ^ 0x36); + + // Xor bo with 0x5c + bo[i] = (byte)(bo[i] ^ 0x5c); + } + + // Append value to bi + Array.Copy(value, 0, bi, 64, value.Length); + + var sha256 = SHA256.Create(); + // Append SHA256(bi) to bo + var sha_bi = sha256.ComputeHash(bi); + Array.Copy(sha_bi, 0, bo, 64, 32); + + // Return SHA256(bo) + return sha256.ComputeHash(bo); + } + } +} \ No newline at end of file diff --git a/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/NanoArrayListExtensions.cs b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/NanoArrayListExtensions.cs new file mode 100644 index 0000000..5c85127 --- /dev/null +++ b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/NanoArrayListExtensions.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +namespace System.Collections +{ + /// + /// Extension methods used for canonicalization. + /// + public static class NanoArrayListExtensions + { + /// + /// Sort an ArrayList + /// + /// Items in the ArrayList + /// The Comparer + public static void Sort(this ArrayList items, IComparer comparer) + { + int i; + int j; + object tmpItem; + + for (i = 0; i <= items.Count -1; i++) + { + tmpItem = items[i]; + j = i; + + while ((j > 0) && (comparer.Compare(items[j - 1], tmpItem) > 0)) + { + items[j] = items[j - 1]; + j--; + } + + items[j] = tmpItem; + } + } + } + +} diff --git a/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/NanoOrdinalComparer.cs b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/NanoOrdinalComparer.cs new file mode 100644 index 0000000..b58aaa8 --- /dev/null +++ b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/NanoOrdinalComparer.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +namespace System.Collections +{ + + internal sealed class OrdinalComparer : StringComparer + { + private bool _ignoreCase; + + internal OrdinalComparer(bool ignoreCase) + { + _ignoreCase = ignoreCase; + } + + public override int Compare(string x, string y) + { + if (ReferenceEquals(x, y)) return 0; + if (x == null) return -1; + if (y == null) return 1; + + if (_ignoreCase) + { + return string.Compare(x.ToLower(), y.ToLower()); + } + + return string.Compare(x, y); + } + + } + +} diff --git a/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/NanoStringComparer.cs b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/NanoStringComparer.cs new file mode 100644 index 0000000..9b1a6f5 --- /dev/null +++ b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/NanoStringComparer.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +namespace System.Collections +{ + + /// + /// String Comparers + /// + public abstract class StringComparer : IComparer + { + private static readonly StringComparer _ordinal = new OrdinalComparer(false); + private static readonly StringComparer _ordinalIgnoreCase = new OrdinalComparer(true); + + /// + /// Ordinal Comparer + /// + public static StringComparer Ordinal + { + get + { + return _ordinal; + } + } + + /// + /// Ordinal Comparer (ignoring case) + /// + public static StringComparer OrdinalIgnoreCase + { + get + { + return _ordinalIgnoreCase; + } + } + + /// + /// Compares two objects + /// + /// Object x + /// Object y + /// The result + /// + public int Compare(object x, object y) + { + if (x == y) return 0; + if (x == null) return -1; + if (y == null) return 1; + + string sa = x as string; + if (sa != null) + { + string sb = y as string; + if (sb != null) + { + return Compare(sa, sb); + } + } + + IComparable ia = x as IComparable; + if (ia != null) + { + return ia.CompareTo(y); + } + + throw new ArgumentException("Argument_ImplementIComparable"); + } + + /// + /// Compares two strings + /// + /// String x + /// String y + /// The result. + public abstract int Compare(string x, string y); + } + +} diff --git a/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SHA256.cs b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SHA256.cs new file mode 100644 index 0000000..9416c58 --- /dev/null +++ b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SHA256.cs @@ -0,0 +1,189 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using System; + +namespace System.Security.Cryptography +{ + /// + /// Computes the SHA256 hash for the input data. + /// + public class SHA256 + { + // Number used in SHA256 hash function + private static readonly uint[] _sha256HashKeys = new uint[] + { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, + 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, + 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, + 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, + 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, + 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2 + }; + + /// + /// Creates an instance of the default implementation of System.Security.Cryptography.SHA256. + /// + /// + public static SHA256 Create() => new SHA256(); + + /// + /// Initializes a new instance of SHA256. + /// + protected SHA256() + { } + + /// + /// Compute SHA-256 digest + /// + /// Input array + /// The computed SHA256 + public byte[] ComputeHash(byte[] input) + { + // Initialize working parameters + uint a, b, c, d, e, f, g, h, i, s0, s1, t1, t2; + uint h0 = 0x6a09e667; + var h1 = 0xbb67ae85; + uint h2 = 0x3c6ef372; + var h3 = 0xa54ff53a; + uint h4 = 0x510e527f; + var h5 = 0x9b05688c; + uint h6 = 0x1f83d9ab; + uint h7 = 0x5be0cd19; + uint blockstart = 0; + + // Calculate how long the padded message should be + var newinputlength = input.Length + 1; + while ((newinputlength % 64) != 56) // length mod 512bits = 448bits + { + newinputlength++; + } + + // Create array for padded data + var processed = new byte[newinputlength + 8]; + Array.Copy(input, processed, input.Length); + + // Pad data with an 1 + processed[input.Length] = 0x80; + + // Pad data with big endian 64bit length of message + // We do only 32 bits becouse input.length is 32 bit + processed[processed.Length - 4] = (byte)(((input.Length * 8) & 0xFF000000) >> 24); + processed[processed.Length - 3] = (byte)(((input.Length * 8) & 0x00FF0000) >> 16); + processed[processed.Length - 2] = (byte)(((input.Length * 8) & 0x0000FF00) >> 8); + processed[processed.Length - 1] = (byte)((input.Length * 8) & 0x000000FF); + + // Block of 32 bits values used in calculations + var wordblock = new uint[64]; + + // Now process each 512 bit block + while (blockstart < processed.Length) + { + // break chunk into sixteen 32-bit big-endian words + for (i = 0; i < 16; i++) + wordblock[i] = BigEndianFromBytes(processed, blockstart + (i * 4)); + + // Extend the sixteen 32-bit words into sixty-four 32-bit words: + for (i = 16; i < 64; i++) + { + s0 = RotateRight(wordblock[i - 15], 7) ^ RotateRight(wordblock[i - 15], 18) ^ (wordblock[i - 15] >> 3); + s1 = RotateRight(wordblock[i - 2], 17) ^ RotateRight(wordblock[i - 2], 19) ^ (wordblock[i - 2] >> 10); + wordblock[i] = wordblock[i - 16] + s0 + wordblock[i - 7] + s1; + } + + // Initialize hash value for this chunk: + a = h0; + b = h1; + c = h2; + d = h3; + e = h4; + f = h5; + g = h6; + h = h7; + + // Main loop + for (i = 0; i < 64; i++) + { + t1 = h + (RotateRight(e, 6) ^ RotateRight(e, 11) ^ RotateRight(e, 25)) + Choice(e, f, g) + _sha256HashKeys[i] + wordblock[i]; + t2 = (RotateRight(a, 2) ^ RotateRight(a, 13) ^ RotateRight(a, 22)) + Majority(a, b, c); + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + + // Add this chunk's hash to result so far + h0 += a; + h1 += b; + h2 += c; + h3 += d; + h4 += e; + h5 += f; + h6 += g; + h7 += h; + + // Process next 512bit block + blockstart += 64; + } + + // Prepare output + var output = new byte[32]; + BytesFromBigEndian(h0, ref output, 0); + BytesFromBigEndian(h1, ref output, 4); + BytesFromBigEndian(h2, ref output, 8); + BytesFromBigEndian(h3, ref output, 12); + BytesFromBigEndian(h4, ref output, 16); + BytesFromBigEndian(h5, ref output, 20); + BytesFromBigEndian(h6, ref output, 24); + BytesFromBigEndian(h7, ref output, 28); + + return output; + } + + // Convert 4 bytes to big endian uint32 + internal static uint BigEndianFromBytes(byte[] input, uint start) + { + uint r = 0; + r |= (((uint)input[start]) << 24); + r |= (((uint)input[start + 1]) << 16); + r |= (((uint)input[start + 2]) << 8); + r |= ((uint)input[start + 3]); + return r; + } + + // Rotate bits right + private static uint RotateRight(uint x, int n) + { + return ((x >> n) | (x << (32 - n))); + } + + // SHA-224/SHA-256 choice function + private static uint Choice(uint x, uint y, uint z) + { + return ((x & y) ^ (~x & z)); + } + + // Convert big endian uint32 to bytes + internal static void BytesFromBigEndian(uint input, ref byte[] output, int start) + { + output[start] = (byte)((input & 0xFF000000) >> 24); + output[start + 1] = (byte)((input & 0x00FF0000) >> 16); + output[start + 2] = (byte)((input & 0x0000FF00) >> 8); + output[start + 3] = (byte)(input & 0x000000FF); + } + + // SHA-224/SHA-256 majority function + private static uint Majority(uint x, uint y, uint z) + { + return ((x & y) ^ (x & z) ^ (y & z)); + } + } +} \ No newline at end of file diff --git a/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SignerBase.cs b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SignerBase.cs new file mode 100644 index 0000000..5d0ed3d --- /dev/null +++ b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SignerBase.cs @@ -0,0 +1,234 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Web; + +namespace nanoFramework.Aws.SignatureVersion4 +{ + /// + /// Common methods and properties for all AWS Signature Version 4 signer variants + /// + public abstract class SignerBase + { + // SHA256 hash of an empty request body + internal const string EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + + internal const string SCHEME = "AWS4"; + internal const string ALGORITHM = "HMAC-SHA256"; + internal const string TERMINATOR = "aws4_request"; + + // format strings for the date/time and date stamps required during signing + internal const string ISO8601BasicFormat = "yyyyMMddTHHmmssZ"; + internal const string DateStringFormat = "yyyyMMdd"; + + // some common x-amz-* parameters + internal const string X_Amz_Algorithm = "X-Amz-Algorithm"; + internal const string X_Amz_Credential = "X-Amz-Credential"; + internal const string X_Amz_SignedHeaders = "X-Amz-SignedHeaders"; + internal const string X_Amz_Date = "X-Amz-Date"; + internal const string X_Amz_Signature = "X-Amz-Signature"; + internal const string X_Amz_Expires = "X-Amz-Expires"; + internal const string X_Amz_Content_SHA256 = "X-Amz-Content-SHA256"; + internal const string X_Amz_Decoded_Content_Length = "X-Amz-Decoded-Content-Length"; + internal const string X_Amz_Meta_UUID = "X-Amz-Meta-UUID"; + + // request canonicalization requires multiple whitespace compression + internal static readonly Regex CompressWhitespaceRegex = new Regex("\\s+"); + + // algorithm used to hash the canonical request that is supplied to + // the signature computation + internal static readonly SHA256 CanonicalRequestHashAlgorithm = SHA256.Create(); + + /// + /// The service endpoint, including the path to any resource. + /// + public Uri EndpointUri { get; set; } + + /// + /// The HTTP verb for the request, e.g. GET. + /// + public string HttpMethod { get; set; } + + /// + /// The signing name of the service, e.g. 's3'. + /// + public string Service { get; set; } + + /// + /// The system name of the AWS region associated with the endpoint, e.g. us-east-1. + /// + public string Region { get; set; } + + /// + /// Returns the canonical collection of header names that will be included in + /// the signature. For AWS Signature Version 4, all header names must be included in the process + /// in sorted canonicalized order. + /// + /// + /// The set of header names and values that will be sent with the request + /// + /// + /// The set of header names canonicalized to a flattened, ;-delimited string + /// + protected string CanonicalizeHeaderNames(IDictionary headers) + { + var headersToSign = new ArrayList(); + foreach (DictionaryEntry kvp in headers) + { + headersToSign.Add(kvp.Key); + } + headersToSign.Sort(StringComparer.OrdinalIgnoreCase); + + var sb = new StringBuilder(); + foreach (var header in headersToSign) + { + if (sb.Length > 0) + sb.Append(";"); + sb.Append(header.ToString().ToLower()); + } + return sb.ToString(); + } + + /// + /// Computes the canonical headers with values for the request. + /// For AWS Signature Version 4, all headers must be included in the signing process. + /// + /// The set of headers to be encoded + /// Canonicalized string of headers with values + protected virtual string CanonicalizeHeaders(IDictionary headers) + { + if (headers == null || headers.Count == 0) + return string.Empty; + + // step1: sort the headers using lower-case format; we create a new + // map to ensure we can do a subsequent key lookup using a lower-case + // key regardless of how 'headers' was created. + + var headerKeys = new ArrayList(); + foreach (DictionaryEntry kvp in headers) + { + headerKeys.Add(kvp.Key); + } + headerKeys.Sort(StringComparer.OrdinalIgnoreCase); + + // step2: form the canonical header:value entries in sorted order. + // Multiple white spaces in the values should be compressed to a single + // space. + var sb = new StringBuilder(); + foreach (var p in headerKeys) + { + sb.Append($"{p.ToString().ToLower()}={CompressWhitespaceRegex.Replace(headers[p].ToString().Trim(), " ")}\n"); + } + + return sb.ToString(); + } + + /// + /// Returns the canonical request string to go into the signer process; this + /// consists of several canonical sub-parts. + /// + /// + /// + /// + /// + /// The set of header names to be included in the signature, formatted as a flattened, ;-delimited string + /// + /// + /// + /// + /// Precomputed SHA256 hash of the request body content. For chunked encoding this + /// should be the fixed string ''. + /// + /// String representing the canonicalized request for signing + protected string CanonicalizeRequest(Uri endpointUri, + string httpMethod, + string queryParameters, + string canonicalizedHeaderNames, + string canonicalizedHeaders, + string bodyHash) + { + var canonicalRequest = new StringBuilder(); + + canonicalRequest.Append($"{httpMethod}\n"); + canonicalRequest.Append($"{CanonicalResourcePath(endpointUri)}\n"); + canonicalRequest.Append($"{queryParameters}\n"); + + canonicalRequest.Append($"{canonicalizedHeaders}\n"); + canonicalRequest.Append($"{canonicalizedHeaderNames}\n"); + + canonicalRequest.Append(bodyHash); + + return canonicalRequest.ToString(); + } + + /// + /// Returns the canonicalized resource path for the service endpoint + /// + /// Endpoint to the service/resource + /// Canonicalized resource path for the endpoint + protected string CanonicalResourcePath(Uri endpointUri) + { + if (string.IsNullOrEmpty(endpointUri.AbsolutePath)) + return "/"; + + // encode the path per RFC3986 + return HttpUtility.UrlEncode(endpointUri.AbsolutePath); // TODO: check if hash ('#') is allowed/supported as per original implementation. + } + + /// + /// Compute and return the multi-stage signing key for the request. + /// + /// The clear-text AWS secret key + /// The region in which the service request will be processed + /// Date of the request, in yyyyMMdd format + /// The name of the service being called by the request + /// Computed signing key + protected byte[] DeriveSigningKey(string awsSecretAccessKey, string region, string date, string service) + { + const string ksecretPrefix = SCHEME; + + string ksecret = (ksecretPrefix + awsSecretAccessKey); + + byte[] hashDate = ComputeKeyedHash(Encoding.UTF8.GetBytes(ksecret), Encoding.UTF8.GetBytes(date)); + byte[] hashRegion = ComputeKeyedHash(hashDate, Encoding.UTF8.GetBytes(region)); + byte[] hashService = ComputeKeyedHash(hashRegion, Encoding.UTF8.GetBytes(service)); + return ComputeKeyedHash(hashService, Encoding.UTF8.GetBytes(TERMINATOR)); + } + + /// + /// Compute and return the hash of a data blob using the specified algorithm + /// and key + /// + /// Hash key + /// Data blob + /// Hash of the data + protected byte[] ComputeKeyedHash(byte[] key, byte[] data) + { + var kha = new HMACSHA256(key); + return kha.ComputeHash(data); + } + + /// + /// Helper to format a byte array into string + /// + /// The data blob to process + /// If true, returns hex digits in lower case form + /// String version of the data + public static string ToHexString(byte[] data, bool lowercase) + { + var sb = new StringBuilder(); + for (var i = 0; i < data.Length; i++) + { + sb.Append(data[i].ToString(lowercase ? "x2" : "X2")); + } + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SignerForAuthorizationHeader.cs b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SignerForAuthorizationHeader.cs new file mode 100644 index 0000000..76ab8fc --- /dev/null +++ b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SignerForAuthorizationHeader.cs @@ -0,0 +1,152 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; + +namespace nanoFramework.Aws.SignatureVersion4 +{ + /// + /// AWS Signature Version 4 signer for signing requests + /// using an 'Authorization' header. + /// + public class SignerForAuthorizationHeader : SignerBase + { + /// + /// Computes an Version 4 signature for a request, ready for inclusion as an + /// 'Authorization' header. + /// + /// + /// The request headers; 'Host' and 'X-Amz-Date' will be added to this set. + /// + /// + /// Any query parameters that will be added to the endpoint. The parameters + /// should be specified in canonical format. + /// + /// + /// Precomputed SHA256 hash of the request body content; this value should also + /// be set as the header 'X-Amz-Content-SHA256' for non-streaming uploads. + /// + /// + /// The user's AWS Access Key. + /// + /// + /// The user's AWS Secret Key. + /// + /// + /// The computed authorization string for the request. This value needs to be set as the + /// header 'Authorization' on the subsequent HTTP request. + /// + public string ComputeSignature(IDictionary headers, + string queryParameters, + string bodyHash, + string awsAccessKey, + string awsSecretKey) + { + // first get the date and time for the subsequent request, and convert to ISO8601 format (without '-' and ':') + // for use in signature generation + var requestDateTime = DateTime.UtcNow; + var dateTimeStamp = requestDateTime.ToString(ISO8601BasicFormat); + + // update the headers with required 'x-amz-date' and 'host' values + headers.Add(X_Amz_Date, dateTimeStamp); + + var hostHeader = EndpointUri.Host; + hostHeader += ":" + EndpointUri.Port; // FIXME: should use //if (!EndpointUri.IsDefaultPort) + headers.Add("Host", hostHeader); + + // canonicalize the headers; we need the set of header names as well as the + // names and values to go into the signature process + var canonicalizedHeaderNames = CanonicalizeHeaderNames(headers); + var canonicalizedHeaders = CanonicalizeHeaders(headers); + + // if any query string parameters have been supplied, canonicalize them + // (note this sample assumes any required url encoding has been done already) + var canonicalizedQueryParameters = string.Empty; + if (!string.IsNullOrEmpty(queryParameters)) + { + var paramDictionary = new Hashtable(); + + var qparam = queryParameters.Split('&'); + foreach (string p in qparam) + { + var items = p.Split('='); + if (items.Length == 1) + { + paramDictionary.Add(items[0], null); + } + else + { + paramDictionary.Add(items[0], items[1]); + } + } + + var sb = new StringBuilder(); + var paramKeys = new ArrayList(); + + foreach (DictionaryEntry kvp in paramDictionary) + { + paramKeys.Add(kvp.Key); + } + + paramKeys.Sort(StringComparer.Ordinal); + foreach (var p in paramKeys) + { + if (sb.Length > 0) + sb.Append("&"); + sb.Append($"{p}={paramDictionary[p]}"); + } + + canonicalizedQueryParameters = sb.ToString(); + } + + // canonicalize the various components of the request + var canonicalRequest = CanonicalizeRequest(EndpointUri, + HttpMethod, + canonicalizedQueryParameters, + canonicalizedHeaderNames, + canonicalizedHeaders, + bodyHash); + Debug.WriteLine($"\nDEBUG-CanonicalRequest:\n{canonicalRequest}"); + + // generate a hash of the canonical request, to go into signature computation + var canonicalRequestHashBytes + = CanonicalRequestHashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(canonicalRequest)); + + // construct the string to be signed + var stringToSign = new StringBuilder(); + + var dateStamp = requestDateTime.ToString(DateStringFormat); + var scope = $"{dateStamp}/{Region}/{Service}/{TERMINATOR}"; + + stringToSign.Append($"{SCHEME}-{ALGORITHM}\n{dateTimeStamp}\n{scope}\n"); + stringToSign.Append(ToHexString(canonicalRequestHashBytes, true)); + + Debug.WriteLine($"\nDEBUG-StringToSign:\n{stringToSign}"); + + // compute the signing key + var kha = new HMACSHA256(DeriveSigningKey(awsSecretKey, Region, dateStamp, Service)); + + // compute the AWS4 signature and return it + var signature = kha.ComputeHash(Encoding.UTF8.GetBytes(stringToSign.ToString())); + var signatureString = ToHexString(signature, true); + Debug.WriteLine($"\nDEBUG-Signature:\n{signatureString}"); + + var authString = new StringBuilder(); + authString.Append($"{SCHEME}-{ALGORITHM} "); + authString.Append($"Credential={awsAccessKey}/{scope}, "); + authString.Append($"SignedHeaders={canonicalizedHeaderNames}, "); + authString.Append($"Signature={signatureString}"); + + var authorization = authString.ToString(); + Debug.WriteLine($"\nDEBUG-Authorization:\n{authorization}"); + + return authorization; + } + } +} diff --git a/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SignerForChunkedUpload.cs b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SignerForChunkedUpload.cs new file mode 100644 index 0000000..01b5939 --- /dev/null +++ b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SignerForChunkedUpload.cs @@ -0,0 +1,343 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; + + +namespace nanoFramework.Aws.SignatureVersion4 +{ + /// + /// AWS Signature Version 4 signer for signing 'chunked' uploads + /// using an Authorization header. + /// + public class SignerForChunkedUpload : SignerBase + { + // SHA256 substitute marker used in place of x-amz-content-sha256 when employing + // chunked uploads + internal const string STREAMING_BODY_SHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"; + + static readonly string CLRF = "\r\n"; + static readonly string CHUNK_STRING_TO_SIGN_PREFIX = "AWS4-HMAC-SHA256-PAYLOAD"; + static readonly string CHUNK_SIGNATURE_HEADER = ";chunk-signature="; + static readonly int SIGNATURE_LENGTH = 64; + static byte[] FINAL_CHUNK = new byte[0]; + + /// + /// Tracks the previously computed signature value; for chunk 0 this will + /// contain the signature included in the Authorization header. For subsequent + /// chunks it contains the computed signature of the prior chunk. + /// + public string LastComputedSignature { get; private set; } + + /// + /// Date and time of the original signing computation, in ISO 8601 basic format, + /// reused for each chunk + /// + public string DateTimeStamp { get; private set; } + + /// + /// The scope value of the original signing computation, reused for each chunk + /// + public string Scope { get; private set; } + + /// + /// The derived signing key used in the original signature computation and + /// re-used for each chunk + /// + byte[] SigningKey { get; set; } + + /// + /// Computes an AWS4 signature for a request, ready for inclusion as an + /// 'Authorization' header. + /// + /// + /// The request headers; 'Host' and 'X-Amz-Date' will be added to this set. + /// + /// + /// Any query parameters that will be added to the endpoint. The parameters + /// should be specified in canonical format. + /// + /// + /// Precomputed SHA256 hash of the request body content; this value should also + /// be set as the header 'X-Amz-Content-SHA256' for non-streaming uploads. + /// + /// + /// The user's AWS Access Key. + /// + /// + /// The user's AWS Secret Key. + /// + /// + /// The computed authorization string for the request. This value needs to be set as the + /// header 'Authorization' on the subsequent HTTP request. + /// + public string ComputeSignature(IDictionary headers, + string queryParameters, + string bodyHash, + string awsAccessKey, + string awsSecretKey) + { + // first get the date and time for the subsequent request, and convert to ISO8601 format (without '-' and ':') + // for use in signature generation + var requestDateTime = DateTime.UtcNow; + DateTimeStamp = requestDateTime.ToString(ISO8601BasicFormat); + + // update the headers with required 'x-amz-date' and 'host' values + headers.Add(X_Amz_Date, DateTimeStamp); + + var hostHeader = EndpointUri.Host; + hostHeader += ":" + EndpointUri.Port; // FIXME: should use //if (!EndpointUri.IsDefaultPort) + headers.Add("Host", hostHeader); + + // canonicalize the headers; we need the set of header names as well as the + // names and values to go into the signature process + var canonicalizedHeaderNames = CanonicalizeHeaderNames(headers); + var canonicalizedHeaders = CanonicalizeHeaders(headers); + + // if any query string parameters have been supplied, canonicalize them + // (note this sample assumes any required url encoding has been done already) + var canonicalizedQueryParameters = string.Empty; + if (!string.IsNullOrEmpty(queryParameters)) + { + var paramDictionary = new Hashtable(); + + var qparam = queryParameters.Split('&'); + foreach (string p in qparam) + { + var items = p.Split('='); + if (items.Length == 1) + { + paramDictionary.Add(items[0], null); + } + else + { + paramDictionary.Add(items[0], items[1]); + } + } + + var sb = new StringBuilder(); + var paramKeys = new ArrayList(); + + foreach (DictionaryEntry kvp in paramDictionary) + { + paramKeys.Add(kvp.Key); + } + + paramKeys.Sort(StringComparer.Ordinal); + foreach (var p in paramKeys) + { + if (sb.Length > 0) + sb.Append("&"); + sb.Append($"{p}={paramDictionary[p]}"); + } + + canonicalizedQueryParameters = sb.ToString(); + } + + // canonicalize the various components of the request + var canonicalRequest = CanonicalizeRequest(EndpointUri, + HttpMethod, + canonicalizedQueryParameters, + canonicalizedHeaderNames, + canonicalizedHeaders, + bodyHash); + Debug.WriteLine($"\nDEBUG-CanonicalRequest:\n{canonicalRequest}"); + + // generate a hash of the canonical request, to go into signature computation + var canonicalRequestHashBytes + = CanonicalRequestHashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(canonicalRequest)); + + // construct the string to be signed + var stringToSign = new StringBuilder(); + + var dateStamp = requestDateTime.ToString(DateStringFormat); + Scope = $"{dateStamp}/{Region}/{Service}/{TERMINATOR}"; + + stringToSign.Append($"{SCHEME}-{ALGORITHM}\n{DateTimeStamp}\n{Scope}\n"); + stringToSign.Append(ToHexString(canonicalRequestHashBytes, true)); + + Debug.WriteLine($"\nDEBUG-StringToSign:\n{stringToSign}"); + + // compute the signing key + SigningKey = DeriveSigningKey(awsSecretKey, Region, dateStamp, Service); + + var kha = new HMACSHA256(SigningKey); + + // compute the AWS4 signature and return it + var signature = kha.ComputeHash(Encoding.UTF8.GetBytes(stringToSign.ToString())); + var signatureString = ToHexString(signature, true); + Debug.WriteLine($"\nDEBUG-Signature:\n{signatureString}"); + + // cache the computed signature ready for chunk 0 upload + LastComputedSignature = signatureString; + + var authString = new StringBuilder(); + authString.Append($"{SCHEME}-{ALGORITHM} "); + authString.Append($"Credential={awsAccessKey}/{Scope}, "); + authString.Append($"SignedHeaders={canonicalizedHeaderNames}, "); + authString.Append($"Signature={signatureString}"); + + var authorization = authString.ToString(); + Debug.WriteLine($"\nDEBUG-Authorization:\n{authorization}"); + + return authorization; + } + + /// + /// Calculates the expanded payload size of our data when it is chunked + /// + /// + /// The true size of the data payload to be uploaded + /// + /// + /// The size of each chunk we intend to send; each chunk will be + /// prefixed with signed header data, expanding the overall size + /// by a determinable amount + /// + /// + /// The overall payload size to use as content-length on a chunked upload + /// + public long CalculateChunkedContentLength(long originalLength, long chunkSize) + { + if (originalLength <= 0) + throw new ArgumentOutOfRangeException("originalLength"); + if (chunkSize <= 0) + throw new ArgumentOutOfRangeException("chunkSize"); + + var maxSizeChunks = originalLength / chunkSize; + var remainingBytes = originalLength % chunkSize; + + var chunkedContentLength = maxSizeChunks * CalculateChunkHeaderLength(chunkSize) + + (remainingBytes > 0 ? CalculateChunkHeaderLength(remainingBytes) : 0) + + CalculateChunkHeaderLength(0); + + Debug.WriteLine($"\nDEBUG-Computed chunked content length for original length {originalLength} bytes, chunk size {chunkSize / 1024}KB is {chunkedContentLength} bytes"); + return chunkedContentLength; + } + + /// + /// Returns the size of a chunk header, which only varies depending + /// on the selected chunk size + /// + /// + /// The intended size of each chunk; this is placed into the chunk + /// header + /// + /// + /// The overall size of the header that will prefix the user data in + /// each chunk + /// + static long CalculateChunkHeaderLength(long chunkSize) + { + return chunkSize.ToString("X").Length + + CHUNK_SIGNATURE_HEADER.Length + + SIGNATURE_LENGTH + + CLRF.Length + + chunkSize + + CLRF.Length; + } + + /// + /// Returns a chunk for upload consisting of the signed 'header' or chunk + /// prefix plus the user data. The signature of the chunk incorporates the + /// signature of the previous chunk (or, if the first chunk, the signature + /// of the headers portion of the request). + /// + /// + /// The length of the user data contained in userData + /// + /// + /// Contains the user data to be sent in the upload chunk + /// + /// + /// A new buffer of data for upload containing the chunk header plus user data + /// + public byte[] ConstructSignedChunk(long userDataLen, byte[] userData) + { + // to keep our computation routine signatures simple, if the userData + // buffer contains less data than it could, shrink it. Note the special case + // to handle the requirement that we send an empty chunk to complete + // our chunked upload. + byte[] dataToChunk; + if (userDataLen == 0) + dataToChunk = FINAL_CHUNK; + else + { + if (userDataLen < userData.Length) + { + // shrink the chunkdata to fit + dataToChunk = new byte[userDataLen]; + Array.Copy(userData, 0, dataToChunk, 0, (int)userDataLen); + } + else + dataToChunk = userData; + } + + var chunkHeader = new StringBuilder(); + + // start with size of user data + chunkHeader.Append(dataToChunk.Length.ToString("X")); + + // nonsig-extension; we have none in these samples + const string nonsigExtension = ""; + + // if this is the first chunk, we package it with the signing result + // of the request headers, otherwise we use the cached signature + // of the previous chunk + + // sig-extension + var chunkStringToSign = + CHUNK_STRING_TO_SIGN_PREFIX + "\n" + + DateTimeStamp + "\n" + + Scope + "\n" + + LastComputedSignature + "\n" + + ToHexString(CanonicalRequestHashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(nonsigExtension)), true) + "\n" + + ToHexString(CanonicalRequestHashAlgorithm.ComputeHash(dataToChunk), true); + + Debug.WriteLine($"\nDEBUG-ChunkStringToSign:\n{chunkStringToSign}"); + + // compute the V4 signature for the chunk + var chunkSignature + = ToHexString(ComputeKeyedHash(SigningKey, + Encoding.UTF8.GetBytes(chunkStringToSign)), + true); + + Debug.WriteLine($"\nDEBUG-ChunkSignature:\n{chunkSignature}"); + + // cache the signature to include with the next chunk's signature computation + this.LastComputedSignature = chunkSignature; + + // construct the actual chunk, comprised of the non-signed extensions, the + // 'headers' we just signed and their signature, plus a newline then copy + // that plus the user's data to a payload to be written to the request stream + chunkHeader.Append(nonsigExtension + CHUNK_SIGNATURE_HEADER + chunkSignature); + chunkHeader.Append(CLRF); + + Debug.WriteLine($"\nDEBUG-ChunkHeader:\n{chunkHeader}"); + + try + { + var header = Encoding.UTF8.GetBytes(chunkHeader.ToString()); + var trailer = Encoding.UTF8.GetBytes(CLRF); + var signedChunk = new byte[header.Length + dataToChunk.Length + trailer.Length]; + + Array.Copy(header, 0, signedChunk, 0, header.Length); + Array.Copy(dataToChunk, 0, signedChunk, header.Length, dataToChunk.Length); + Array.Copy(trailer, 0, signedChunk, header.Length + dataToChunk.Length, trailer.Length); + + // this is the total data for the chunk that will be sent to the request stream + return signedChunk; + } + catch (Exception e) + { + throw new Exception("Unable to sign the chunked data. " + e.Message, e); + } + } + } +} diff --git a/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SignerForPOST.cs b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SignerForPOST.cs new file mode 100644 index 0000000..e1b3adc --- /dev/null +++ b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SignerForPOST.cs @@ -0,0 +1,167 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; + +namespace nanoFramework.Aws.SignatureVersion4 +{ + /// + /// AWS Signature Version 4 signer for signing POST requests + /// using a policy. + /// + public class SignerForPOST : SignerBase + { + internal string FormatCredentialStringForPolicy(DateTime dateTimeStamp) + { + return "AKIAIOSFODNN7EXAMPLE/20130806/cn-north-1/s3/aws4_request"; + } + + internal string FormatAlgorithmForPolicy + { + get { return "AWS4-HMAC-SHA256"; } + } + + internal string FormatDateTimeForPolicy(DateTime dateTimeStamp) + { + return dateTimeStamp.ToString(ISO8601BasicFormat); + } + + /// + /// Computes an AWS Signature Version 4 signature for a request, ready for inclusion as an + /// 'Authorization' header. + /// + /// + /// The request headers; 'Host' and 'X-Amz-Date' will be added to this set. + /// + /// + /// Any query parameters that will be added to the endpoint. The parameters + /// should be specified in canonical format. + /// + /// + /// Precomputed SHA256 hash of the request body content; this value should also + /// be set as the header 'X-Amz-Content-SHA256' for non-streaming uploads. + /// + /// + /// The user's AWS Access Key. + /// + /// + /// The user's AWS Secret Key. + /// + /// + /// The computed authorization string for the request. This value needs to be set as the + /// header 'Authorization' on the subsequent HTTP request. + /// + public string ComputeSignature(IDictionary headers, + string queryParameters, + string bodyHash, + string awsAccessKey, + string awsSecretKey) + { + // first get the date and time for the subsequent request, and convert to ISO8601 format (without '-' and ':') + // for use in signature generation + var requestDateTime = DateTime.UtcNow; + var dateTimeStamp = requestDateTime.ToString(ISO8601BasicFormat); + + // update the headers with required 'x-amz-date' and 'host' values + headers.Add(X_Amz_Date, dateTimeStamp); + + var hostHeader = EndpointUri.Host; + hostHeader += ":" + EndpointUri.Port; // FIXME: should use //if (!EndpointUri.IsDefaultPort) + headers.Add("Host", hostHeader); + + // canonicalize the headers; we need the set of header names as well as the + // names and values to go into the signature process + var canonicalizedHeaderNames = CanonicalizeHeaderNames(headers); + var canonicalizedHeaders = CanonicalizeHeaders(headers); + + // if any query string parameters have been supplied, canonicalize them + // (note this sample assumes any required url encoding has been done already) + var canonicalizedQueryParameters = string.Empty; + if (!string.IsNullOrEmpty(queryParameters)) + { + var paramDictionary = new Hashtable(); + + var qparam = queryParameters.Split('&'); + foreach (string p in qparam) + { + var items = p.Split('='); + if (items.Length == 1) + { + paramDictionary.Add(items[0], null); + } + else + { + paramDictionary.Add(items[0], items[1]); + } + } + + var sb = new StringBuilder(); + var paramKeys = new ArrayList(); + + foreach (DictionaryEntry kvp in paramDictionary) + { + paramKeys.Add(kvp.Key); + } + + paramKeys.Sort(StringComparer.Ordinal); + foreach (var p in paramKeys) + { + if (sb.Length > 0) + sb.Append("&"); + sb.Append($"{p}={paramDictionary[p]}"); + } + + canonicalizedQueryParameters = sb.ToString(); + } + + // canonicalize the various components of the request + var canonicalRequest = CanonicalizeRequest(EndpointUri, + HttpMethod, + canonicalizedQueryParameters, + canonicalizedHeaderNames, + canonicalizedHeaders, + bodyHash); + //Debug.WriteLine($"\nDEBUG-CanonicalRequest:\n{canonicalRequest}"); + + // generate a hash of the canonical request, to go into signature computation + var canonicalRequestHashBytes + = CanonicalRequestHashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(canonicalRequest)); + + // construct the string to be signed + var stringToSign = new StringBuilder(); + + var dateStamp = requestDateTime.ToString(DateStringFormat); + var scope = $"{dateStamp}/{Region}/{Service}/{TERMINATOR}"; + + stringToSign.Append($"{SCHEME}-{ALGORITHM}\n{dateTimeStamp}\n{scope}\n"); + stringToSign.Append(ToHexString(canonicalRequestHashBytes, true)); + + //Debug.WriteLine($"\nDEBUG-StringToSign:\n{stringToSign}"); + + // compute the signing key + var kha = new HMACSHA256(DeriveSigningKey(awsSecretKey, Region, dateStamp, Service)); + + // compute the AWS4 signature and return it + var signature = kha.ComputeHash(Encoding.UTF8.GetBytes(stringToSign.ToString())); + var signatureString = ToHexString(signature, true); + //Debug.WriteLine($"\nDEBUG-Signature:\n{signatureString}"); + + var authString = new StringBuilder(); + authString.Append($"{SCHEME}-{ALGORITHM} "); + authString.Append($"Credential={awsAccessKey}/{scope}, "); + authString.Append($"SignedHeaders={canonicalizedHeaderNames}, "); + authString.Append($"Signature={signatureString}"); + + var authorization = authString.ToString(); + //Debug.WriteLine($"\nDEBUG-Authorization:\n{authorization}"); + + return authorization; + } + } +} diff --git a/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SignerForQueryParameterAuth.cs b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SignerForQueryParameterAuth.cs new file mode 100644 index 0000000..a1c8a38 --- /dev/null +++ b/nanoFramework.Aws.IoTCore.Devices/AwsSignatureVersion4/SignerForQueryParameterAuth.cs @@ -0,0 +1,174 @@ + +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections; +using System.Security.Cryptography; +using System.Text; +using System.Web; + +namespace nanoFramework.Aws.SignatureVersion4 +{ + /// + /// AWS Signature Version 4 signer for signing requests + /// using query string parameters. + /// + public class SignerForQueryParameterAuth : SignerBase + { + /// + /// Computes an AWS Signature Version 4 authorization for a request, suitable for embedding + /// in query parameters. + /// + /// + /// The request headers; 'Host' and 'X-Amz-Date' will be added to this set. + /// + /// + /// Any query parameters that will be added to the endpoint. The parameters + /// should be specified in canonical format. + /// + /// + /// Precomputed SHA256 hash of the request body content; this value should also + /// be set as the header 'X-Amz-Content-SHA256' for non-streaming uploads. + /// + /// + /// The user's AWS Access Key. + /// + /// + /// The user's AWS Secret Key. + /// + /// + /// The string expressing the Signature V4 components to add to query parameters. + /// + public string ComputeSignature(IDictionary headers, + string queryParameters, + string bodyHash, + string awsAccessKey, + string awsSecretKey) + { + // first get the date and time for the subsequent request, and convert to ISO8601 format (without '-' and ':') + // for use in signature generation + var requestDateTime = DateTime.UtcNow; + var dateTimeStamp = requestDateTime.ToString(ISO8601BasicFormat); + + // extract the host portion of the endpoint to include in the signature calculation, + // unless already set + if (!headers.Contains("Host")) + { + var hostHeader = EndpointUri.Host; + hostHeader += ":" + EndpointUri.Port; // FIXME: should use //if (!EndpointUri.IsDefaultPort) + headers.Add("Host", hostHeader); + } + + var dateStamp = requestDateTime.ToString(DateStringFormat); + var scope = $"{dateStamp}/{Region}/{Service}/{TERMINATOR}"; + + // canonicalized headers need to be expressed in the query + // parameters processed in the signature + var canonicalizedHeaderNames = CanonicalizeHeaderNames(headers); + var canonicalizedHeaders = CanonicalizeHeaders(headers); + + // reform the query parameters to (a) add the parameters required for + // Signature V4 and (b) canonicalize the set before they go into the + // signature calculation. Note that this assumes parameter names and + // values added outside this routine are already url encoded + var paramDictionary = new Hashtable(); + if (!string.IsNullOrEmpty(queryParameters)) + { + var qparam = queryParameters.Split('&'); + foreach (string p in qparam) + { + var items = p.Split('='); + if (items.Length == 1) + { + paramDictionary.Add(items[0], null); + } + else + { + paramDictionary.Add(items[0], items[1]); + } + } + } + + // add the fixed authorization params required by Signature V4 + paramDictionary.Add(X_Amz_Algorithm, HttpUtility.UrlEncode($"{SCHEME}-{ALGORITHM}")); + paramDictionary.Add(X_Amz_Credential, HttpUtility.UrlEncode($"{awsAccessKey}/{scope}")); + paramDictionary.Add(X_Amz_SignedHeaders, HttpUtility.UrlEncode(canonicalizedHeaderNames)); + + // x-amz-date is now added as a query parameter, not a header, but still needs to be in ISO8601 basic form + paramDictionary.Add(X_Amz_Date, HttpUtility.UrlEncode(dateTimeStamp)); + + // build the expanded canonical query parameter string that will go into the + // signature computation + var sb = new StringBuilder(); + var paramKeys = new ArrayList(); + foreach (DictionaryEntry kvp in paramDictionary) + { + paramKeys.Add(kvp.Key); + } + paramKeys.Sort(StringComparer.Ordinal); + foreach (var p in paramKeys) + { + if (sb.Length > 0) + sb.Append("&"); + sb.Append($"{p}={paramDictionary[p]}"); + } + var canonicalizedQueryParameters = sb.ToString(); + + // express all the header and query parameter data as a canonical request string + var canonicalRequest = CanonicalizeRequest(EndpointUri, + HttpMethod, + canonicalizedQueryParameters, + canonicalizedHeaderNames, + canonicalizedHeaders, + bodyHash); + //Debug.WriteLine($"\nDEBUG-CanonicalRequest:\n{canonicalRequest}"); + + byte[] canonicalRequestHashBytes + = CanonicalRequestHashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(canonicalRequest)); + + // construct the string to be signed + var stringToSign = new StringBuilder(); + + stringToSign.Append($"{SCHEME}-{ALGORITHM}\n{dateTimeStamp}\n{scope}\n"); + stringToSign.Append(ToHexString(canonicalRequestHashBytes, true)); + + //Debug.WriteLine($"\nDEBUG-StringToSign:\n{stringToSign}"); + + // compute the multi-stage signing key + var kha = new HMACSHA256(DeriveSigningKey(awsSecretKey, Region, dateStamp, Service)); + + // compute the final signature for the request, place into the result and return to the + // user to be embedded in the request as needed + var signature = kha.ComputeHash(Encoding.UTF8.GetBytes(stringToSign.ToString())); + var signatureString = ToHexString(signature, true); + //Debug.WriteLine($"\nDEBUG-Signature:\n{signatureString}"); + + // form up the authorization parameters for the caller to place in the query string + var authString = new StringBuilder(); + var authParams = new string[] + { + X_Amz_Algorithm, + X_Amz_Credential, + X_Amz_Date, + X_Amz_SignedHeaders + }; + + foreach (var p in authParams) + { + if (authString.Length > 0) + authString.Append("&"); + authString.Append($"{p}={paramDictionary[p]}"); + } + + authString.Append($"&{X_Amz_Signature}={signatureString}"); + + var authorization = authString.ToString(); + //Debug.WriteLine($"\nDEBUG-Authorization:\n{authorization}"); + + return authorization; + } + } +} diff --git a/nanoFramework.Aws.IoTCore.Devices/WebsocketConnectionClient.cs b/nanoFramework.Aws.IoTCore.Devices/WebsocketConnectionClient.cs index d1f6692..82e743b 100644 --- a/nanoFramework.Aws.IoTCore.Devices/WebsocketConnectionClient.cs +++ b/nanoFramework.Aws.IoTCore.Devices/WebsocketConnectionClient.cs @@ -4,22 +4,73 @@ // using System; +using System.Net.WebSockets; +using nanoFramework.Aws.SignatureVersion4; -namespace nanoFramework.Aws.IoTCore.Devices.Devices.Client +namespace nanoFramework.Aws.IoTCore.Devices { /// - /// AWS IoT Core Websocket Connection Client for .NET nanoFramework + /// AWS IoT Core MQTT over Websocket Connection Client for .NET nanoFramework /// - public class WebsocketConnectionClient// : IDisposable + public class WebsocketConnectionClient : IDisposable { /// - /// Creates a new WebSocket Connection Client + /// The AWS IoT Core fully qualified domain name /// + public Uri EndpointUri { get; set; } + /// + /// The AWS IoT Core Region + /// + public string Region { get; set; } + /// + /// The AWS IoT Core Access Key + /// + public string AccessKey { get; set; } + /// + /// The AWS IoT Core Secret Key + /// + public string SecretKey { get; set; } + + const int _wssPort = 443; //Default WSS port. + + //private WebSocket _webSocket = null; + + /// + /// Creates a new MQTT over WebSocket Connection Client + /// + /// + /// Supports Signature Version 4 and Custom authentication over port 443 + /// public WebsocketConnectionClient() { - //TODO: implement! + // TODO: implement! look to the following: + // https://github.com/dotnet/MQTTnet/blob/master/Source/MQTTnet.Extensions.WebSocket4Net/WebSocket4NetMqttChannel.cs + + //wss://iot-endpoint/mqtt + + // sign with Signature Version 4 + var v4signer = new SignerForQueryParameterAuth + { + EndpointUri = EndpointUri, + HttpMethod = "GET", + Service = "iotdevicegateway", + Region = Region + }; + + // use mqtt client WithWebSocketServer + throw new NotImplementedException(); } + + /// + public void Dispose() + { + //if (_webSocket != null) + //{ + // GC.SuppressFinalize(_webSocket); + // _webSocket = null; + //} + } } } diff --git a/nanoFramework.Aws.IoTCore.Devices/nanoFramework.Aws.IoTCore.Devices.nfproj b/nanoFramework.Aws.IoTCore.Devices/nanoFramework.Aws.IoTCore.Devices.nfproj index 3581e57..9796821 100644 --- a/nanoFramework.Aws.IoTCore.Devices/nanoFramework.Aws.IoTCore.Devices.nfproj +++ b/nanoFramework.Aws.IoTCore.Devices/nanoFramework.Aws.IoTCore.Devices.nfproj @@ -31,6 +31,11 @@ + + + + + @@ -40,6 +45,11 @@ + + + + + @@ -77,6 +87,15 @@ ..\packages\nanoFramework.System.Net.1.11.43\lib\System.Net.dll + + ..\packages\nanoFramework.System.Net.Http.1.5.196\lib\System.Net.Http.dll + + + ..\packages\nanoFramework.System.Net.WebSockets.1.1.151\lib\System.Net.WebSockets.dll + + + ..\packages\nanoFramework.System.Text.RegularExpressions.1.1.128\lib\System.Text.RegularExpressions.dll + ..\packages\nanoFramework.System.Threading.1.1.52\lib\System.Threading.dll diff --git a/nanoFramework.Aws.IoTCore.Devices/packages.config b/nanoFramework.Aws.IoTCore.Devices/packages.config index cb0b1de..dda19fa 100644 --- a/nanoFramework.Aws.IoTCore.Devices/packages.config +++ b/nanoFramework.Aws.IoTCore.Devices/packages.config @@ -8,7 +8,10 @@ + + + \ No newline at end of file diff --git a/nanoFramework.Aws.IoTCore.Devices/packages.lock.json b/nanoFramework.Aws.IoTCore.Devices/packages.lock.json index d8ff835..f6d8754 100644 --- a/nanoFramework.Aws.IoTCore.Devices/packages.lock.json +++ b/nanoFramework.Aws.IoTCore.Devices/packages.lock.json @@ -50,12 +50,30 @@ "resolved": "1.11.43", "contentHash": "USwz59gxcNUzsiXfQohWSi8ANNwGDsp+qG4zBtHZU3rKMtvTsLI3rxdfMC77VehKqsCPn7aK3PU2oCRFo+1Rgg==" }, + "nanoFramework.System.Net.Http": { + "type": "Direct", + "requested": "[1.5.196, 1.5.196]", + "resolved": "1.5.196", + "contentHash": "2qfUnvJa55Wx5C86HopeoUZVfXY+L6stufrlcKNHlalqIk4cc3Orv0Eqc0oroy3iB6aQGDw+tji7WYJS8LQNoA==" + }, + "nanoFramework.System.Net.WebSockets": { + "type": "Direct", + "requested": "[1.1.151, 1.1.151]", + "resolved": "1.1.151", + "contentHash": "1xblkj0W+3yFq9OMUIgzUSna43ss2qtj5N6kS8n/DyTNmYplmFAIVhyGIXpAJghQuLFPoSND63KsU+GY37Uftg==" + }, "nanoFramework.System.Text": { "type": "Direct", "requested": "[1.3.42, 1.3.42]", "resolved": "1.3.42", "contentHash": "68HPjhersNpssbmEMUHdMw3073MHfGTfrkbRk9eILKbNPFfPFck7m4y9BlAi6DaguUJaeKxgyIojXF3SQrF8/A==" }, + "nanoFramework.System.Text.RegularExpressions": { + "type": "Direct", + "requested": "[1.1.128, 1.1.128]", + "resolved": "1.1.128", + "contentHash": "6INVYaaVjHFQ05BfIwm8hI9MFwVbuiE7zM87oADZm5XOBindOG9mOx7ooDaX+0Ni+JXm+ZvkdwBqLye1vZBI8Q==" + }, "nanoFramework.System.Threading": { "type": "Direct", "requested": "[1.1.52, 1.1.52]",