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+ }
0 commit comments