-
Notifications
You must be signed in to change notification settings - Fork 3
/
Aws4HmacAuthorizationHeaderBuilder.cs
143 lines (131 loc) · 9.17 KB
/
Aws4HmacAuthorizationHeaderBuilder.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
using System;
using System.Linq;
using System.Text;
using Cuemon.AspNetCore.Authentication;
using Cuemon.AspNetCore.Authentication.Hmac;
using Cuemon.Collections.Generic;
using Cuemon.Net;
using Cuemon.Security.Cryptography;
using Microsoft.AspNetCore.Http;
namespace Cuemon.Extensions.AspNetCore.Authentication.AwsSignature4
{
/// <summary>
/// Provides a way to fluently represent a HTTP AWS4-HMAC-SHA256 Authentication header.
/// </summary>
/// <seealso cref="HmacAuthorizationHeaderBuilder{TAuthorizationHeaderBuilder}"/>
/// <remarks>https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html</remarks>
public class Aws4HmacAuthorizationHeaderBuilder : HmacAuthorizationHeaderBuilder<Aws4HmacAuthorizationHeaderBuilder>
{
/// <summary>
/// Initializes a new instance of the <see cref="Aws4HmacAuthorizationHeaderBuilder"/> class.
/// </summary>
public Aws4HmacAuthorizationHeaderBuilder() : base(Aws4HmacFields.Scheme)
{
MapRelation(nameof(AddFromRequest), HmacFields.HttpMethod, HmacFields.UriPath, HmacFields.UriQuery, HmacFields.HttpHeaders, HmacFields.Payload);
MapRelation(nameof(AddCredentialScope), HmacFields.CredentialScope, Aws4HmacFields.DateStamp, Aws4HmacFields.DateTimeStamp, Aws4HmacFields.Region, Aws4HmacFields.Service);
}
/// <summary>
/// Adds the credential scope that defines the remote resource.
/// </summary>
/// <param name="timestamp">The <see cref="DateTime"/> value part of the credential scope.</param>
/// <param name="region">The AWS region part of the credential scope. Default is <c>eu-west-1</c>.</param>
/// <param name="service">The service name part of the credential scope. Default is <c>s3</c> (Simple Storage Service).</param>
/// <param name="termination">The termination string part of the credential scope. Default is <c>aws4_request</c>.</param>
/// <returns>A reference to this instance so that additional calls can be chained.</returns>
/// <remarks>The following string represents the scope part of the Credential parameter for a S3 request in the eu-west-1 Region: <c>20220710/eu-west-1/s3/aws4_request</c></remarks>
public Aws4HmacAuthorizationHeaderBuilder AddCredentialScope(DateTime timestamp, string region = "eu-west-1", string service = "s3", string termination = Aws4HmacFields.Aws4Request)
{
Validator.ThrowIfNullOrWhitespace(region);
return AddOrUpdate(Aws4HmacFields.DateTimeStamp, timestamp.ToAwsDateTimeString())
.AddOrUpdate(Aws4HmacFields.DateStamp, timestamp.ToAwsDateString())
.AddOrUpdate(Aws4HmacFields.Region, region)
.AddOrUpdate(Aws4HmacFields.Service, service)
.AddOrUpdate(HmacFields.CredentialScope, $"{timestamp.ToAwsDateString()}/{region}/{service}/{termination}");
}
/// <summary>
/// Adds the necessary fields that is part of an HTTP request.
/// </summary>
/// <param name="request">An instance of the <see cref="T:Microsoft.AspNetCore.Http.HttpRequest" /> object.</param>
/// <returns>A reference to this instance so that additional calls can be chained.</returns>
public override Aws4HmacAuthorizationHeaderBuilder AddFromRequest(HttpRequest request)
{
Validator.ThrowIfNull(request);
return AddOrUpdate(HmacFields.HttpMethod, request.Method)
.AddOrUpdate(HmacFields.UriPath, request.Path.ToUriComponent())
.AddOrUpdate(HmacFields.UriQuery, string.Concat(request.Query.OrderBy(pair => pair.Key).Select(pair => $"{Decorator.Enclose(pair.Key).UrlEncode()}={Decorator.Enclose(pair.Value.ToString()).UrlEncode()}")))
.AddOrUpdate(HmacFields.HttpHeaders, request.Headers.Count == 0 ? null : string.Concat(request.Headers.OrderBy(pair => pair.Key).Select(pair => $"{pair.Key.ToLowerInvariant()}:{DelimitedString.Create(pair.Value, o => o.StringConverter = s => $"{s.Trim()}{Alphanumeric.Linefeed}")}")))
.AddOrUpdate(HmacFields.Payload, UnkeyedHashFactory.CreateCryptoSha256().ComputeHash(request.Body).ToHexadecimalString());
}
/// <summary>
/// Converts the request to a standardized (canonical) format and computes a message digest.
/// </summary>
/// <returns>A <see cref="T:System.String" /> representation, in hexadecimal, of the computed canonical request.</returns>
/// <remarks>https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html</remarks>
public override string ComputeCanonicalRequest()
{
ValidateData(HmacFields.UriPath, HmacFields.UriQuery, HmacFields.HttpHeaders, HmacFields.Payload);
EnsureSignedHeaders(out var signedHeaders);
var signedHeadersLookup = signedHeaders.Split(HmacFields.SignedHeadersDelimiter).ToList();
var headersToSign = DelimitedString.Create(Data[HmacFields.HttpHeaders].Split(Alphanumeric.Linefeed.ToCharArray()).Where(header =>
{
var kvp = header.Split(HmacFields.HttpHeadersDelimiter);
return signedHeadersLookup.Contains(kvp[0]);
}), o => o.Delimiter = Alphanumeric.Linefeed) + Alphanumeric.Linefeed;
var canonicalRequest = new StringBuilder(Data[HmacFields.HttpMethod])
.Append(Alphanumeric.LinefeedChar)
.Append(Data[HmacFields.UriPath])
.Append(Alphanumeric.LinefeedChar)
.Append(Data[HmacFields.UriQuery])
.Append(Alphanumeric.LinefeedChar)
.Append(headersToSign)
.Append(Alphanumeric.LinefeedChar)
.Append(signedHeaders)
.Append(Alphanumeric.LinefeedChar)
.Append(Data[HmacFields.Payload]).ToString();
AddOrUpdate(HmacFields.CanonicalRequest, canonicalRequest);
return UnkeyedHashFactory.CreateCryptoSha256().ComputeHash(canonicalRequest).ToHexadecimalString();
}
/// <summary>
/// Computes the signature of this instance using a series of hash-based message authentication codes (HMACs).
/// </summary>
/// <returns>A <see cref="T:System.String" /> representation, in hexadecimal, of the computed signature of this instance.</returns>
/// <remarks>https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html, https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html</remarks>
public override string ComputeSignature()
{
ValidateData(Aws4HmacFields.DateTimeStamp, HmacFields.ClientSecret, Aws4HmacFields.DateStamp, Aws4HmacFields.Region, Aws4HmacFields.Service);
var stringToSign = string.Concat(AuthenticationScheme,
Alphanumeric.Linefeed,
Data[Aws4HmacFields.DateTimeStamp],
Alphanumeric.Linefeed,
Decorator.Enclose(Data).GetValueOrDefault(HmacFields.CredentialScope),
Alphanumeric.Linefeed,
ComputeCanonicalRequest());
var secret = Decorator.Enclose($"AWS4{Data[HmacFields.ClientSecret]}").ToByteArray();
var dateSecret = KeyedHashFactory.CreateHmacCryptoSha256(secret).ComputeHash(Data[Aws4HmacFields.DateStamp]).GetBytes();
var dateRegionSecret = KeyedHashFactory.CreateHmacCryptoSha256(dateSecret).ComputeHash(Data[Aws4HmacFields.Region]).GetBytes();
var dateRegionServiceSecret = KeyedHashFactory.CreateHmacCryptoSha256(dateRegionSecret).ComputeHash(Data[Aws4HmacFields.Service]).GetBytes();
var signingSecret = KeyedHashFactory.CreateHmacCryptoSha256(dateRegionServiceSecret).ComputeHash(Aws4HmacFields.Aws4Request).GetBytes();
AddOrUpdate(HmacFields.StringToSign, stringToSign);
return KeyedHashFactory.CreateHmacCryptoSha256(signingSecret).ComputeHash(stringToSign).ToHexadecimalString();
}
/// <summary>
/// Builds an instance of <see cref="HmacAuthorizationHeader"/> that implements <see cref="AuthorizationHeader" />.
/// </summary>
/// <returns>An instance of <see cref="HmacAuthorizationHeader"/>.</returns>
/// <remarks>https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html</remarks>
public override HmacAuthorizationHeader Build()
{
ValidateData(HmacFields.UriPath, HmacFields.UriQuery, HmacFields.HttpHeaders, HmacFields.Payload, Aws4HmacFields.DateTimeStamp, HmacFields.ClientSecret, Aws4HmacFields.DateStamp, Aws4HmacFields.Region, Aws4HmacFields.Service);
EnsureSignedHeaders(out var signedHeaders);
return new Aws4HmacAuthorizationHeader(Data[HmacFields.ClientId], Decorator.Enclose(Data).GetValueOrDefault(HmacFields.CredentialScope), signedHeaders, ComputeSignature());
}
private void EnsureSignedHeaders(out string signedHeaders)
{
if (!Data.TryGetValue(HmacFields.SignedHeaders, out signedHeaders))
{
signedHeaders = "host;x-amz-content-sha256;x-amz-date";
AddSignedHeaders(signedHeaders.Split(HmacFields.SignedHeadersDelimiter));
}
}
}
}