Skip to content

Commit 98feb4b

Browse files
committed
Add sigv4 initial commit
Before we get real dirty.
1 parent fa506c3 commit 98feb4b

File tree

9 files changed

+1077
-5
lines changed

9 files changed

+1077
-5
lines changed
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
using System;
2+
using System.Collections;
3+
using System.Net.Http.Headers;
4+
using System.Security.Cryptography;
5+
using System.Text;
6+
using System.Text.RegularExpressions;
7+
8+
namespace nanoFramework.Aws.SignatureVersion4
9+
{
10+
/// <summary>
11+
/// Common methods and properties for all AWS Signature Version 4 signer variants
12+
/// </summary>
13+
public abstract class SignerBase
14+
{
15+
// SHA256 hash of an empty request body
16+
public const string EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
17+
18+
public const string SCHEME = "AWS4";
19+
public const string ALGORITHM = "HMAC-SHA256";
20+
public const string TERMINATOR = "aws4_request";
21+
22+
// format strings for the date/time and date stamps required during signing
23+
public const string ISO8601BasicFormat = "yyyyMMddTHHmmssZ";
24+
public const string DateStringFormat = "yyyyMMdd";
25+
26+
// some common x-amz-* parameters
27+
public const string X_Amz_Algorithm = "X-Amz-Algorithm";
28+
public const string X_Amz_Credential = "X-Amz-Credential";
29+
public const string X_Amz_SignedHeaders = "X-Amz-SignedHeaders";
30+
public const string X_Amz_Date = "X-Amz-Date";
31+
public const string X_Amz_Signature = "X-Amz-Signature";
32+
public const string X_Amz_Expires = "X-Amz-Expires";
33+
public const string X_Amz_Content_SHA256 = "X-Amz-Content-SHA256";
34+
public const string X_Amz_Decoded_Content_Length = "X-Amz-Decoded-Content-Length";
35+
public const string X_Amz_Meta_UUID = "X-Amz-Meta-UUID";
36+
37+
// the name of the keyed hash algorithm used in signing
38+
public const string HMACSHA256 = "HMACSHA256";
39+
40+
// request canonicalization requires multiple whitespace compression
41+
protected static readonly Regex CompressWhitespaceRegex = new Regex("\\s+");
42+
43+
// algorithm used to hash the canonical request that is supplied to
44+
// the signature computation
45+
public static HashAlgorithm CanonicalRequestHashAlgorithm = HashAlgorithm.Create("SHA-256");
46+
47+
/// <summary>
48+
/// The service endpoint, including the path to any resource.
49+
/// </summary>
50+
public Uri EndpointUri { get; set; }
51+
52+
/// <summary>
53+
/// The HTTP verb for the request, e.g. GET.
54+
/// </summary>
55+
public string HttpMethod { get; set; }
56+
57+
/// <summary>
58+
/// The signing name of the service, e.g. 's3'.
59+
/// </summary>
60+
public string Service { get; set; }
61+
62+
/// <summary>
63+
/// The system name of the AWS region associated with the endpoint, e.g. us-east-1.
64+
/// </summary>
65+
public string Region { get; set; }
66+
67+
/// <summary>
68+
/// Returns the canonical collection of header names that will be included in
69+
/// the signature. For AWS Signature Version 4, all header names must be included in the process
70+
/// in sorted canonicalized order.
71+
/// </summary>
72+
/// <param name="headers">
73+
/// The set of header names and values that will be sent with the request
74+
/// </param>
75+
/// <returns>
76+
/// The set of header names canonicalized to a flattened, ;-delimited string
77+
/// </returns>
78+
protected string CanonicalizeHeaderNames(IDictionary<string, string> headers)
79+
{
80+
var headersToSign = new List<string>(headers.Keys);
81+
headersToSign.Sort(StringComparer.OrdinalIgnoreCase);
82+
83+
var sb = new StringBuilder();
84+
foreach (var header in headersToSign)
85+
{
86+
if (sb.Length > 0)
87+
sb.Append(";");
88+
sb.Append(header.ToLower());
89+
}
90+
return sb.ToString();
91+
}
92+
93+
/// <summary>
94+
/// Computes the canonical headers with values for the request.
95+
/// For AWS Signature Version 4, all headers must be included in the signing process.
96+
/// </summary>
97+
/// <param name="headers">The set of headers to be encoded</param>
98+
/// <returns>Canonicalized string of headers with values</returns>
99+
protected virtual string CanonicalizeHeaders(IDictionary<string, string> headers)
100+
{
101+
if (headers == null || headers.Count == 0)
102+
return string.Empty;
103+
104+
// step1: sort the headers into lower-case format; we create a new
105+
// map to ensure we can do a subsequent key lookup using a lower-case
106+
// key regardless of how 'headers' was created.
107+
var sortedHeaderMap = new SortedDictionary<string, string>();
108+
foreach (var header in headers.Keys)
109+
{
110+
sortedHeaderMap.Add(header.ToLower(), headers[header]);
111+
}
112+
113+
// step2: form the canonical header:value entries in sorted order.
114+
// Multiple white spaces in the values should be compressed to a single
115+
// space.
116+
var sb = new StringBuilder();
117+
foreach (var header in sortedHeaderMap.Keys)
118+
{
119+
var headerValue = CompressWhitespaceRegex.Replace(sortedHeaderMap[header], " ");
120+
sb.AppendFormat("{0}:{1}\n", header, headerValue.Trim());
121+
}
122+
123+
return sb.ToString();
124+
}
125+
126+
/// <summary>
127+
/// Returns the canonical request string to go into the signer process; this
128+
/// consists of several canonical sub-parts.
129+
/// </summary>
130+
/// <param name="endpointUri"></param>
131+
/// <param name="httpMethod"></param>
132+
/// <param name="queryParameters"></param>
133+
/// <param name="canonicalizedHeaderNames">
134+
/// The set of header names to be included in the signature, formatted as a flattened, ;-delimited string
135+
/// </param>
136+
/// <param name="canonicalizedHeaders">
137+
/// </param>
138+
/// <param name="bodyHash">
139+
/// Precomputed SHA256 hash of the request body content. For chunked encoding this
140+
/// should be the fixed string ''.
141+
/// </param>
142+
/// <returns>String representing the canonicalized request for signing</returns>
143+
protected string CanonicalizeRequest(Uri endpointUri,
144+
string httpMethod,
145+
string queryParameters,
146+
string canonicalizedHeaderNames,
147+
string canonicalizedHeaders,
148+
string bodyHash)
149+
{
150+
var canonicalRequest = new StringBuilder();
151+
152+
canonicalRequest.Append(string.Format("{0}\n", httpMethod));
153+
canonicalRequest.Append(string.Format("{0}\n", CanonicalResourcePath(endpointUri)));
154+
canonicalRequest.Append(string.Format("{0}\n", queryParameters));
155+
156+
canonicalRequest.Append(string.Format("{0}\n", canonicalizedHeaders));
157+
canonicalRequest.Append(string.Format("{0}\n", canonicalizedHeaderNames));
158+
159+
canonicalRequest.Append(bodyHash);
160+
161+
return canonicalRequest.ToString();
162+
}
163+
164+
/// <summary>
165+
/// Returns the canonicalized resource path for the service endpoint
166+
/// </summary>
167+
/// <param name="endpointUri">Endpoint to the service/resource</param>
168+
/// <returns>Canonicalized resource path for the endpoint</returns>
169+
protected string CanonicalResourcePath(Uri endpointUri)
170+
{
171+
if (string.IsNullOrEmpty(endpointUri.AbsolutePath))
172+
return "/";
173+
174+
// encode the path per RFC3986
175+
return HttpHelpers.UrlEncode(endpointUri.AbsolutePath, true);
176+
}
177+
178+
/// <summary>
179+
/// Compute and return the multi-stage signing key for the request.
180+
/// </summary>
181+
/// <param name="algorithm">Hashing algorithm to use</param>
182+
/// <param name="awsSecretAccessKey">The clear-text AWS secret key</param>
183+
/// <param name="region">The region in which the service request will be processed</param>
184+
/// <param name="date">Date of the request, in yyyyMMdd format</param>
185+
/// <param name="service">The name of the service being called by the request</param>
186+
/// <returns>Computed signing key</returns>
187+
protected byte[] DeriveSigningKey(string algorithm, string awsSecretAccessKey, string region, string date, string service)
188+
{
189+
const string ksecretPrefix = SCHEME;
190+
char[] ksecret = null;
191+
192+
ksecret = (ksecretPrefix + awsSecretAccessKey).ToCharArray();
193+
194+
byte[] hashDate = ComputeKeyedHash(algorithm, Encoding.UTF8.GetBytes(ksecret), Encoding.UTF8.GetBytes(date));
195+
byte[] hashRegion = ComputeKeyedHash(algorithm, hashDate, Encoding.UTF8.GetBytes(region));
196+
byte[] hashService = ComputeKeyedHash(algorithm, hashRegion, Encoding.UTF8.GetBytes(service));
197+
return ComputeKeyedHash(algorithm, hashService, Encoding.UTF8.GetBytes(TERMINATOR));
198+
}
199+
200+
/// <summary>
201+
/// Compute and return the hash of a data blob using the specified algorithm
202+
/// and key
203+
/// </summary>
204+
/// <param name="algorithm">Algorithm to use for hashing</param>
205+
/// <param name="key">Hash key</param>
206+
/// <param name="data">Data blob</param>
207+
/// <returns>Hash of the data</returns>
208+
protected byte[] ComputeKeyedHash(string algorithm, byte[] key, byte[] data)
209+
{
210+
var kha = KeyedHashAlgorithm.Create(algorithm);
211+
kha.Key = key;
212+
return kha.ComputeHash(data);
213+
}
214+
215+
/// <summary>
216+
/// Helper to format a byte array into string
217+
/// </summary>
218+
/// <param name="data">The data blob to process</param>
219+
/// <param name="lowercase">If true, returns hex digits in lower case form</param>
220+
/// <returns>String version of the data</returns>
221+
public static string ToHexString(byte[] data, bool lowercase)
222+
{
223+
var sb = new StringBuilder();
224+
for (var i = 0; i < data.Length; i++)
225+
{
226+
sb.Append(data[i].ToString(lowercase ? "x2" : "X2"));
227+
}
228+
return sb.ToString();
229+
}
230+
}
231+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//using System;
2+
//using System.Collections;
3+
//using System.Globalization;
4+
//using System.Net.Http;
5+
//using System.Security.Cryptography;
6+
//using System.Text;
7+
8+
//namespace nanoFramework.Aws.SignatureVersion4
9+
//{
10+
// /// <summary>
11+
// /// AWS Signature Version 4 signer for signing requests
12+
// /// using an 'Authorization' header.
13+
// /// </summary>
14+
// public class SignerForAuthorizationHeader : SignerBase
15+
// {
16+
// /// <summary>
17+
// /// Computes an Version 4 signature for a request, ready for inclusion as an
18+
// /// 'Authorization' header.
19+
// /// </summary>
20+
// /// <param name="headers">
21+
// /// The request headers; 'Host' and 'X-Amz-Date' will be added to this set.
22+
// /// </param>
23+
// /// <param name="queryParameters">
24+
// /// Any query parameters that will be added to the endpoint. The parameters
25+
// /// should be specified in canonical format.
26+
// /// </param>
27+
// /// <param name="bodyHash">
28+
// /// Precomputed SHA256 hash of the request body content; this value should also
29+
// /// be set as the header 'X-Amz-Content-SHA256' for non-streaming uploads.
30+
// /// </param>
31+
// /// <param name="awsAccessKey">
32+
// /// The user's AWS Access Key.
33+
// /// </param>
34+
// /// <param name="awsSecretKey">
35+
// /// The user's AWS Secret Key.
36+
// /// </param>
37+
// /// <returns>
38+
// /// The computed authorization string for the request. This value needs to be set as the
39+
// /// header 'Authorization' on the subsequent HTTP request.
40+
// /// </returns>
41+
// public string ComputeSignature(IDictionary<string, string> headers,
42+
// string queryParameters,
43+
// string bodyHash,
44+
// string awsAccessKey,
45+
// string awsSecretKey)
46+
// {
47+
// // first get the date and time for the subsequent request, and convert to ISO 8601 format
48+
// // for use in signature generation
49+
// var requestDateTime = DateTime.UtcNow;
50+
// var dateTimeStamp = requestDateTime.ToString(ISO8601BasicFormat, CultureInfo.InvariantCulture);
51+
52+
// // update the headers with required 'x-amz-date' and 'host' values
53+
// headers.Add(X_Amz_Date, dateTimeStamp);
54+
55+
// var hostHeader = EndpointUri.Host;
56+
// if (!EndpointUri.IsDefaultPort)
57+
// hostHeader += ":" + EndpointUri.Port;
58+
// headers.Add("Host", hostHeader);
59+
60+
// // canonicalize the headers; we need the set of header names as well as the
61+
// // names and values to go into the signature process
62+
// var canonicalizedHeaderNames = CanonicalizeHeaderNames(headers);
63+
// var canonicalizedHeaders = CanonicalizeHeaders(headers);
64+
65+
// // if any query string parameters have been supplied, canonicalize them
66+
// // (note this sample assumes any required url encoding has been done already)
67+
// var canonicalizedQueryParameters = string.Empty;
68+
// if (!string.IsNullOrEmpty(queryParameters))
69+
// {
70+
// var paramDictionary = queryParameters.Split('&').Select(p => p.Split('='))
71+
// .ToDictionary(nameval => nameval[0],
72+
// nameval => nameval.Length > 1
73+
// ? nameval[1] : "");
74+
75+
// var sb = new StringBuilder();
76+
// var paramKeys = new List<string>(paramDictionary.Keys);
77+
// paramKeys.Sort(StringComparer.Ordinal);
78+
// foreach (var p in paramKeys)
79+
// {
80+
// if (sb.Length > 0)
81+
// sb.Append("&");
82+
// sb.AppendFormat("{0}={1}", p, paramDictionary[p]);
83+
// }
84+
85+
// canonicalizedQueryParameters = sb.ToString();
86+
// }
87+
88+
// // canonicalize the various components of the request
89+
// var canonicalRequest = CanonicalizeRequest(EndpointUri,
90+
// HttpMethod,
91+
// canonicalizedQueryParameters,
92+
// canonicalizedHeaderNames,
93+
// canonicalizedHeaders,
94+
// bodyHash);
95+
// Logger.LogDebug($"\nCanonicalRequest:\n{canonicalRequest}");
96+
97+
// // generate a hash of the canonical request, to go into signature computation
98+
// var canonicalRequestHashBytes
99+
// = CanonicalRequestHashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(canonicalRequest));
100+
101+
// // construct the string to be signed
102+
// var stringToSign = new StringBuilder();
103+
104+
// var dateStamp = requestDateTime.ToString(DateStringFormat, CultureInfo.InvariantCulture);
105+
// var scope = string.Format("{0}/{1}/{2}/{3}",
106+
// dateStamp,
107+
// Region,
108+
// Service,
109+
// TERMINATOR);
110+
111+
// stringToSign.AppendFormat("{0}-{1}\n{2}\n{3}\n", SCHEME, ALGORITHM, dateTimeStamp, scope);
112+
// stringToSign.Append(ToHexString(canonicalRequestHashBytes, true));
113+
114+
// Logger.LogDebug($"\nStringToSign:\n{stringToSign}");
115+
116+
// // compute the signing key
117+
// var kha = KeyedHashAlgorithm.Create(HMACSHA256);
118+
// kha.Key = DeriveSigningKey(HMACSHA256, awsSecretKey, Region, dateStamp, Service);
119+
120+
// // compute the AWS4 signature and return it
121+
// var signature = kha.ComputeHash(Encoding.UTF8.GetBytes(stringToSign.ToString()));
122+
// var signatureString = ToHexString(signature, true);
123+
// Logger.LogDebug($"\nSignature:\n{signatureString}");
124+
125+
// var authString = new StringBuilder();
126+
// authString.AppendFormat("{0}-{1} ", SCHEME, ALGORITHM);
127+
// authString.AppendFormat("Credential={0}/{1}, ", awsAccessKey, scope);
128+
// authString.AppendFormat("SignedHeaders={0}, ", canonicalizedHeaderNames);
129+
// authString.AppendFormat("Signature={0}", signatureString);
130+
131+
// var authorization = authString.ToString();
132+
// Logger.LogDebug($"\nAuthorization:\n{authorization}");
133+
134+
// return authorization;
135+
// }
136+
// }
137+
//}

0 commit comments

Comments
 (0)