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]",