• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loresoft / HashGate / 18761910243

23 Oct 2025 09:02PM UTC coverage: 79.529%. Remained the same
18761910243

push

github

pwelter34
Merge branch 'main' of https://github.com/loresoft/HashGate

118 of 182 branches covered (64.84%)

Branch coverage included in aggregate %.

321 of 370 relevant lines covered (86.76%)

12.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.92
/src/HashGate.AspNetCore/HmacAuthenticationShared.cs
1
using System.Security.Cryptography;
2
using System.Text;
3

4
#if HTTP_CLIENT
5
namespace HashGate.HttpClient;
6
#else
7
namespace HashGate.AspNetCore;
8
#endif
9

10
/// <summary>
11
/// Provides shared utilities and constants for HMAC authentication implementation.
12
/// Contains methods for creating string-to-sign, generating signatures, and creating authorization headers.
13
/// </summary>
14
public static class HmacAuthenticationShared
15
{
16
    /// <summary>
17
    /// The default authentication scheme name used for HMAC authentication.
18
    /// </summary>
19
    public const string DefaultSchemeName = "HMAC";
20

21
    /// <summary>
22
    /// The name of the Authorization HTTP header.
23
    /// </summary>
24
    public const string AuthorizationHeaderName = "Authorization";
25

26
    /// <summary>
27
    /// The name of the Host HTTP header.
28
    /// </summary>
29
    public const string HostHeaderName = "Host";
30

31
    /// <summary>
32
    /// The name of the Content-Type HTTP header.
33
    /// </summary>
34
    public const string ContentTypeHeaderName = "Content-Type";
35

36
    /// <summary>
37
    /// The name of the Content-Length HTTP header.
38
    /// </summary>
39
    public const string ContentLengthHeaderName = "Content-Length";
40

41
    /// <summary>
42
    /// The name of the User-Agent HTTP header.
43
    /// </summary>
44
    public const string UserAgentHeaderName = "User-Agent";
45

46
    /// <summary>
47
    /// The name of the Date HTTP header.
48
    /// </summary>
49
    public const string DateHeaderName = "Date";
50

51
    /// <summary>
52
    /// The name of the custom x-date header used to override the Date header.
53
    /// </summary>
54
    public const string DateOverrideHeaderName = "x-date";
55

56
    /// <summary>
57
    /// The name of the custom x-timestamp header used for request timestamping.
58
    /// </summary>
59
    public const string TimeStampHeaderName = "x-timestamp";
60

61
    /// <summary>
62
    /// The name of the custom x-content-sha256 header containing the SHA256 hash of the request body.
63
    /// </summary>
64
    public const string ContentHashHeaderName = "x-content-sha256";
65

66
    /// <summary>
67
    /// Base64-encoded SHA256 hash of an empty string, used for requests with no body content.
68
    /// </summary>
69
    public const string EmptyContentHash = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=";
70

71
    /// <summary>
72
    /// The default set of headers that are included in the signature calculation.
73
    /// Includes host, x-timestamp, and x-content-sha256 headers.
74
    /// </summary>
75
    public static readonly string[] DefaultSignedHeaders = ["host", TimeStampHeaderName, ContentHashHeaderName];
2✔
76

77
    /// <summary>
78
    /// Creates a canonical string representation for signing based on the HTTP method, path with query string, and header values.
79
    /// The format is: METHOD\nPATH_AND_QUERY\nHEADER_VALUES (semicolon-separated).
80
    /// </summary>
81
    /// <param name="method">The HTTP method (GET, POST, etc.) that will be converted to uppercase.</param>
82
    /// <param name="pathAndQuery">The request path including query string parameters.</param>
83
    /// <param name="headerValues">The collection of header values to include in the signature, in the order they should appear.</param>
84
    /// <returns>A canonical string representation suitable for HMAC signature calculation.</returns>
85
    public static string CreateStringToSign(
86
        string method,
87
        string pathAndQuery,
88
        IReadOnlyList<string> headerValues)
89
    {
90
        // Measure lengths
91
        int methodLength = method.Length;
23✔
92
        int pathAndQueryLength = pathAndQuery.Length;
23✔
93
        int headerCount = 0;
23✔
94
        int headerValuesLength = 0;
23✔
95

96
        // Materialize headerValues to avoid multiple enumeration
97
        headerCount = headerValues.Count;
23✔
98
        for (int i = 0; i < headerValues.Count; i++)
160✔
99
            headerValuesLength += headerValues[i].Length;
57✔
100

101
        // Each header after the first gets a semicolon separator
102
        int separatorLength = headerCount > 1 ? headerCount - 1 : 0;
23✔
103

104
        // 2 for the two '\n' literals
105
        int totalLength = methodLength + pathAndQueryLength + headerValuesLength + separatorLength + 2;
23✔
106

107
#if NETSTANDARD2_0
108
        var stringBuilder = new StringBuilder(totalLength);
109

110
        stringBuilder
111
            .Append(method.ToUpperInvariant())
112
            .Append('\n')
113
            .Append(pathAndQuery)
114
            .Append('\n');
115

116
        // Write header values with semicolons
117
        for (int i = 0; i < headerValues.Count; i++)
118
        {
119
            if (i > 0)
120
                stringBuilder.Append(';');
121

122
            stringBuilder.Append(headerValues[i]);
123
        }
124

125
        return stringBuilder.ToString();
126
#else
127
        return string.Create(totalLength, (method, pathAndQuery, headerValues), (span, state) =>
23✔
128
        {
23✔
129
            int pos = 0;
23✔
130

23✔
131
            // Write method in uppercase
23✔
132
            state.method.AsSpan().ToUpperInvariant(span.Slice(pos, state.method.Length));
23✔
133
            pos += state.method.Length;
23✔
134

23✔
135
            // Write first newline
23✔
136
            span[pos++] = '\n';
23✔
137

23✔
138
            // Write pathAndQuery
23✔
139
            state.pathAndQuery.AsSpan().CopyTo(span.Slice(pos, state.pathAndQuery.Length));
23✔
140
            pos += state.pathAndQuery.Length;
23✔
141

23✔
142
            // Write second newline
23✔
143
            span[pos++] = '\n';
23✔
144

23✔
145
            // Write header values with semicolons
23✔
146
            for (int i = 0; i < state.headerValues.Count; i++)
160✔
147
            {
23✔
148
                if (i > 0)
57✔
149
                    span[pos++] = ';';
36✔
150

23✔
151
                var header = state.headerValues[i];
57✔
152
                header.AsSpan().CopyTo(span.Slice(pos, header.Length));
57✔
153
                pos += header.Length;
57✔
154
            }
23✔
155
        });
46✔
156
#endif
157
    }
158

159
    /// <summary>
160
    /// Generates an HMAC-SHA256 signature for the provided string using the specified secret key.
161
    /// The signature is returned as a Base64-encoded string.
162
    /// </summary>
163
    /// <param name="stringToSign">The canonical string representation to be signed.</param>
164
    /// <param name="secretKey">The secret key used for HMAC-SHA256 calculation.</param>
165
    /// <returns>A Base64-encoded HMAC-SHA256 signature.</returns>
166
    public static string GenerateSignature(
167
        string stringToSign,
168
        string secretKey)
169
    {
170
        // Convert secret and stringToSign to byte arrays
171
        var secretBytes = Encoding.UTF8.GetBytes(secretKey);
21✔
172
        var dataBytes = Encoding.UTF8.GetBytes(stringToSign);
21✔
173

174
#if NETSTANDARD2_0
175
        // Use traditional approach for .NET Standard 2.0
176
        using var hmac = new HMACSHA256(secretBytes);
177
        var hash = hmac.ComputeHash(dataBytes);
178
        return Convert.ToBase64String(hash);
179
#else
180
        // Compute HMACSHA256
181
        Span<byte> hash = stackalloc byte[32];
21✔
182
        using var hmac = new HMACSHA256(secretBytes);
21✔
183

184
        // Try to compute the hash using stackalloc for performance
185
        if (!hmac.TryComputeHash(dataBytes, hash, out _))
21!
186
        {
187
            // Fallback if stackalloc is not large enough (should not happen for SHA256)
188
            hash = hmac.ComputeHash(dataBytes);
×
189
        }
190

191
        // 32 bytes SHA256 -> 44 chars base64
192
        Span<char> base64 = stackalloc char[44];
21✔
193
        if (Convert.TryToBase64Chars(hash, base64, out int charsWritten))
21!
194
            return new string(base64[..charsWritten]);
21✔
195

196
        return Convert.ToBase64String(hash);
×
197
#endif
198
    }
21✔
199

200
    /// <summary>
201
    /// Generates a complete Authorization header value in the format:
202
    /// "HMAC Client={client}&amp;SignedHeaders={headers}&amp;Signature={signature}".
203
    /// </summary>
204
    /// <param name="client">The client identifier used in the authorization header.</param>
205
    /// <param name="signedHeaders">The collection of header names that were included in the signature calculation.</param>
206
    /// <param name="signature">The Base64-encoded HMAC signature.</param>
207
    /// <returns>A complete Authorization header value ready for use in HTTP requests.</returns>
208
    public static string GenerateAuthorizationHeader(
209
        string client,
210
        IReadOnlyList<string> signedHeaders,
211
        string signature)
212
    {
213
        const string scheme = DefaultSchemeName;
214
        const string clientPrefix = " Client=";
215
        const string signedHeadersPrefix = "&SignedHeaders=";
216
        const string signaturePrefix = "&Signature=";
217

218
#if NETSTANDARD2_0
219
        var stringBuilder = new StringBuilder();
220

221
        // Write scheme
222
        stringBuilder.Append(scheme);
223

224
        // Write client prefix and client
225
        stringBuilder.Append(clientPrefix);
226
        stringBuilder.Append(client);
227

228
        // Write signedHeaders prefix
229
        stringBuilder.Append(signedHeadersPrefix);
230

231
        // Write signedHeaders (semicolon separated)
232
        for (int i = 0; i < signedHeaders.Count; i++)
233
        {
234
            if (i > 0)
235
                stringBuilder.Append(';');
236

237
            stringBuilder.Append(signedHeaders[i]);
238
        }
239

240
        // Write signature prefix and signature
241
        stringBuilder.Append(signaturePrefix);
242
        stringBuilder.Append(signature);
243

244
        return stringBuilder.ToString();
245
#else
246
        // Calculate signedHeaders string and its length
247
        int signedHeadersCount = signedHeaders.Count;
17✔
248
        int signedHeadersLength = 0;
17✔
249

250
        for (int i = 0; i < signedHeadersCount; i++)
120✔
251
            signedHeadersLength += signedHeaders[i].Length;
43✔
252

253
        int signedHeadersSeparatorLength = signedHeadersCount > 1 ? signedHeadersCount - 1 : 0;
17✔
254

255
        int totalLength =
17✔
256
            scheme.Length +
17✔
257
            clientPrefix.Length +
17✔
258
            client.Length +
17✔
259
            signedHeadersPrefix.Length +
17✔
260
            signedHeadersLength +
17✔
261
            signedHeadersSeparatorLength +
17✔
262
            signaturePrefix.Length +
17✔
263
            signature.Length;
17✔
264

265
        return string.Create(totalLength, (client, signedHeaders, signature), (span, state) =>
17✔
266
        {
17✔
267
            int pos = 0;
17✔
268

17✔
269
            // Write scheme
17✔
270
            scheme.AsSpan().CopyTo(span.Slice(pos, scheme.Length));
17✔
271
            pos += scheme.Length;
17✔
272

17✔
273
            // Write client prefix
17✔
274
            clientPrefix.AsSpan().CopyTo(span.Slice(pos, clientPrefix.Length));
17✔
275
            pos += clientPrefix.Length;
17✔
276

17✔
277
            // Write client
17✔
278
            state.client.AsSpan().CopyTo(span.Slice(pos, state.client.Length));
17✔
279
            pos += state.client.Length;
17✔
280

17✔
281
            // Write signedHeaders prefix
17✔
282
            signedHeadersPrefix.AsSpan().CopyTo(span.Slice(pos, signedHeadersPrefix.Length));
17✔
283
            pos += signedHeadersPrefix.Length;
17✔
284

17✔
285
            // Write signedHeaders (semicolon separated)
17✔
286
            for (int i = 0; i < state.signedHeaders.Count; i++)
120✔
287
            {
17✔
288
                if (i > 0)
43✔
289
                    span[pos++] = ';';
28✔
290

17✔
291
                var header = state.signedHeaders[i];
43✔
292
                header.AsSpan().CopyTo(span.Slice(pos, header.Length));
43✔
293
                pos += header.Length;
43✔
294
            }
17✔
295

17✔
296
            // Write signature prefix
17✔
297
            signaturePrefix.AsSpan().CopyTo(span.Slice(pos, signaturePrefix.Length));
17✔
298
            pos += signaturePrefix.Length;
17✔
299

17✔
300
            // Write signature
17✔
301
            state.signature.AsSpan().CopyTo(span.Slice(pos, state.signature.Length));
17✔
302
            pos += state.signature.Length;
17✔
303
        });
34✔
304
#endif
305
    }
306

307
    /// <summary>
308
    /// Performs a constant-time comparison of two strings to prevent timing attacks.
309
    /// Both strings are converted to UTF-8 byte arrays before comparison.
310
    /// </summary>
311
    /// <param name="left">The first string to compare.</param>
312
    /// <param name="right">The second string to compare.</param>
313
    /// <returns><c>true</c> if the strings are equal; otherwise, <c>false</c>.</returns>
314
    public static bool FixedTimeEquals(
315
        string left,
316
        string right)
317
    {
318
        // Convert strings to byte arrays using UTF8 encoding
319
        var leftBytes = Encoding.UTF8.GetBytes(left);
25✔
320
        var rightBytes = Encoding.UTF8.GetBytes(right);
25✔
321

322
        // If lengths differ, return false immediately
323
        if (leftBytes.Length != rightBytes.Length)
25✔
324
            return false;
9✔
325

326
#if NETSTANDARD2_0
327
        // Manual constant-time comparison for .NET Standard 2.0
328
        int result = 0;
329
        for (int i = 0; i < leftBytes.Length; i++)
330
            result |= leftBytes[i] ^ rightBytes[i];
331

332
        return result == 0;
333
#else
334
        // Use FixedTimeEquals for constant-time comparison
335
        return CryptographicOperations.FixedTimeEquals(leftBytes, rightBytes);
16✔
336
#endif
337
    }
338
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc