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

loresoft / HashGate / 24360421502

13 Apr 2026 06:38PM UTC coverage: 72.756% (+0.2%) from 72.581%
24360421502

push

github

pwelter34
fix tests

50 of 92 branches covered (54.35%)

Branch coverage included in aggregate %.

177 of 220 relevant lines covered (80.45%)

5.64 hits per line

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

96.95
/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
/// Shared utilities and constants for the HMAC authentication implementation.
12
/// </summary>
13
/// <remarks>
14
/// Contains helpers for creating the canonical string-to-sign, computing HMAC-SHA256
15
/// signatures, building the <c>Authorization</c> header, and performing constant-time
16
/// string comparison.
17
/// </remarks>
18
public static class HmacAuthenticationShared
19
{
20
    /// <summary>
21
    /// Default authentication scheme name (<c>"HMAC"</c>).
22
    /// </summary>
23
    public const string DefaultSchemeName = "HMAC";
24

25
    /// <summary>
26
    /// Name of the <c>Authorization</c> HTTP header.
27
    /// </summary>
28
    public const string AuthorizationHeaderName = "Authorization";
29

30
    /// <summary>
31
    /// Name of the <c>Host</c> HTTP header.
32
    /// </summary>
33
    public const string HostHeaderName = "Host";
34

35
    /// <summary>
36
    /// Name of the <c>Content-Type</c> HTTP header.
37
    /// </summary>
38
    public const string ContentTypeHeaderName = "Content-Type";
39

40
    /// <summary>
41
    /// Name of the <c>Content-Length</c> HTTP header.
42
    /// </summary>
43
    public const string ContentLengthHeaderName = "Content-Length";
44

45
    /// <summary>
46
    /// Name of the <c>User-Agent</c> HTTP header.
47
    /// </summary>
48
    public const string UserAgentHeaderName = "User-Agent";
49

50
    /// <summary>
51
    /// Name of the <c>Date</c> HTTP header.
52
    /// </summary>
53
    public const string DateHeaderName = "Date";
54

55
    /// <summary>
56
    /// Name of the custom <c>x-date</c> header used to override the <c>Date</c> header.
57
    /// </summary>
58
    public const string DateOverrideHeaderName = "x-date";
59

60
    /// <summary>
61
    /// Name of the custom <c>x-timestamp</c> header used for request timestamping.
62
    /// </summary>
63
    public const string TimeStampHeaderName = "x-timestamp";
64

65
    /// <summary>
66
    /// Name of the custom <c>x-content-sha256</c> header containing the SHA-256 hash of the request body.
67
    /// </summary>
68
    public const string ContentHashHeaderName = "x-content-sha256";
69

70
    /// <summary>
71
    /// Name of the custom <c>x-nonce</c> header containing a unique per-request value.
72
    /// </summary>
73
    /// <remarks>
74
    /// Including this header in the signed headers makes every signature cryptographically
75
    /// unique, which is required for reliable replay protection when
76
    /// <c>EnableReplayProtection</c> is enabled.
77
    /// </remarks>
78
    public const string NonceHeaderName = "x-nonce";
79

80
    /// <summary>
81
    /// Base64-encoded SHA-256 hash of an empty string, used for requests with no body content.
82
    /// </summary>
83
    public const string EmptyContentHash = "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=";
84

85
    /// <summary>
86
    /// Default set of headers included in the signature calculation:
87
    /// <c>host</c>, <c>x-timestamp</c>, <c>x-content-sha256</c>, and <c>x-nonce</c>.
88
    /// </summary>
89
    public static readonly string[] DefaultSignedHeaders = ["host", TimeStampHeaderName, ContentHashHeaderName, NonceHeaderName];
1✔
90

91
    /// <summary>
92
    /// Creates a canonical string-to-sign from the HTTP method, path with query string,
93
    /// and signed header values.
94
    /// </summary>
95
    /// <param name="method">The HTTP method (e.g. <c>GET</c>, <c>POST</c>), converted to uppercase.</param>
96
    /// <param name="pathAndQuery">The request path including query string parameters.</param>
97
    /// <param name="headerValues">Ordered header values to include in the signature.</param>
98
    /// <returns>
99
    /// A canonical string in the format
100
    /// <c>METHOD\nPATH_AND_QUERY\nHEADER_VALUES</c> (values semicolon-separated).
101
    /// </returns>
102
    public static string CreateStringToSign(
103
        string method,
104
        string pathAndQuery,
105
        IReadOnlyList<string> headerValues)
106
    {
107
        // Measure lengths
108
        int methodLength = method.Length;
6✔
109
        int pathAndQueryLength = pathAndQuery.Length;
6✔
110
        int headerCount = 0;
6✔
111
        int headerValuesLength = 0;
6✔
112

113
        // Materialize headerValues to avoid multiple enumeration
114
        headerCount = headerValues.Count;
6✔
115
        for (int i = 0; i < headerValues.Count; i++)
42✔
116
            headerValuesLength += headerValues[i].Length;
15✔
117

118
        // Each header after the first gets a semicolon separator
119
        int separatorLength = headerCount > 1 ? headerCount - 1 : 0;
6✔
120

121
        // 2 for the two '\n' literals
122
        int totalLength = methodLength + pathAndQueryLength + headerValuesLength + separatorLength + 2;
6✔
123

124
#if NETSTANDARD2_0 || NETFRAMEWORK
125
        var stringBuilder = new StringBuilder(totalLength);
126

127
        stringBuilder
128
            .Append(method.ToUpperInvariant())
129
            .Append('\n')
130
            .Append(pathAndQuery)
131
            .Append('\n');
132

133
        // Write header values with semicolons
134
        for (int i = 0; i < headerValues.Count; i++)
135
        {
136
            if (i > 0)
137
                stringBuilder.Append(';');
138

139
            stringBuilder.Append(headerValues[i]);
140
        }
141

142
        return stringBuilder.ToString();
143
#else
144
        return string.Create(totalLength, (method, pathAndQuery, headerValues), (span, state) =>
6✔
145
        {
6✔
146
            int pos = 0;
6✔
147

6✔
148
            // Write method in uppercase
6✔
149
            state.method.AsSpan().ToUpperInvariant(span.Slice(pos, state.method.Length));
6✔
150
            pos += state.method.Length;
6✔
151

6✔
152
            // Write first newline
6✔
153
            span[pos++] = '\n';
6✔
154

6✔
155
            // Write pathAndQuery
6✔
156
            state.pathAndQuery.AsSpan().CopyTo(span.Slice(pos, state.pathAndQuery.Length));
6✔
157
            pos += state.pathAndQuery.Length;
6✔
158

6✔
159
            // Write second newline
6✔
160
            span[pos++] = '\n';
6✔
161

6✔
162
            // Write header values with semicolons
6✔
163
            for (int i = 0; i < state.headerValues.Count; i++)
42✔
164
            {
6✔
165
                if (i > 0)
15✔
166
                    span[pos++] = ';';
10✔
167

6✔
168
                var header = state.headerValues[i];
15✔
169
                header.AsSpan().CopyTo(span.Slice(pos, header.Length));
15✔
170
                pos += header.Length;
15✔
171
            }
6✔
172
        });
12✔
173
#endif
174
    }
175

176
    /// <summary>
177
    /// Computes an HMAC-SHA256 signature for the specified canonical string.
178
    /// </summary>
179
    /// <param name="stringToSign">The canonical string produced by <see cref="CreateStringToSign"/>.</param>
180
    /// <param name="secretKey">The secret key used for HMAC-SHA256 computation.</param>
181
    /// <returns>A Base64-encoded HMAC-SHA256 signature string.</returns>
182
    public static string GenerateSignature(
183
        string stringToSign,
184
        string secretKey)
185
    {
186
        // Convert secret and stringToSign to byte arrays
187
        var secretBytes = Encoding.UTF8.GetBytes(secretKey);
5✔
188
        var dataBytes = Encoding.UTF8.GetBytes(stringToSign);
5✔
189

190
#if NETSTANDARD2_0 || NETFRAMEWORK
191
        // Use traditional approach for .NET Standard 2.0 and .NET Framework
192
        using var hmac = new HMACSHA256(secretBytes);
193
        var hash = hmac.ComputeHash(dataBytes);
194
        return Convert.ToBase64String(hash);
195
#else
196
        // Compute HMACSHA256
197
        Span<byte> hash = stackalloc byte[32];
5✔
198
        using var hmac = new HMACSHA256(secretBytes);
5✔
199

200
        // Try to compute the hash using stackalloc for performance
201
        if (!hmac.TryComputeHash(dataBytes, hash, out _))
5!
202
        {
203
            // Fallback if stackalloc is not large enough (should not happen for SHA256)
204
            hash = hmac.ComputeHash(dataBytes);
×
205
        }
206

207
        // 32 bytes SHA256 -> 44 chars base64
208
        Span<char> base64 = stackalloc char[44];
5✔
209
        if (Convert.TryToBase64Chars(hash, base64, out int charsWritten))
5!
210
            return new string(base64[..charsWritten]);
5✔
211

212
        return Convert.ToBase64String(hash);
×
213
#endif
214
    }
5✔
215

216
    /// <summary>
217
    /// Builds a complete <c>Authorization</c> header value for the HMAC scheme.
218
    /// </summary>
219
    /// <param name="client">The client identifier.</param>
220
    /// <param name="signedHeaders">Header names that were included in the signature calculation.</param>
221
    /// <param name="signature">The Base64-encoded HMAC signature produced by <see cref="GenerateSignature"/>.</param>
222
    /// <returns>
223
    /// A header value in the format
224
    /// <c>HMAC Client={client}&amp;SignedHeaders={headers}&amp;Signature={signature}</c>.
225
    /// </returns>
226
    public static string GenerateAuthorizationHeader(
227
        string client,
228
        IReadOnlyList<string> signedHeaders,
229
        string signature)
230
    {
231
        const string scheme = DefaultSchemeName;
232
        const string clientPrefix = " Client=";
233
        const string signedHeadersPrefix = "&SignedHeaders=";
234
        const string signaturePrefix = "&Signature=";
235

236
#if NETSTANDARD2_0 || NETFRAMEWORK
237
        var stringBuilder = new StringBuilder();
238

239
        // Write scheme
240
        stringBuilder.Append(scheme);
241

242
        // Write client prefix and client
243
        stringBuilder.Append(clientPrefix);
244
        stringBuilder.Append(client);
245

246
        // Write signedHeaders prefix
247
        stringBuilder.Append(signedHeadersPrefix);
248

249
        // Write signedHeaders (semicolon separated)
250
        for (int i = 0; i < signedHeaders.Count; i++)
251
        {
252
            if (i > 0)
253
                stringBuilder.Append(';');
254

255
            stringBuilder.Append(signedHeaders[i]);
256
        }
257

258
        // Write signature prefix and signature
259
        stringBuilder.Append(signaturePrefix);
260
        stringBuilder.Append(signature);
261

262
        return stringBuilder.ToString();
263
#else
264
        // Calculate signedHeaders string and its length
265
        int signedHeadersCount = signedHeaders.Count;
5✔
266
        int signedHeadersLength = 0;
5✔
267

268
        for (int i = 0; i < signedHeadersCount; i++)
38✔
269
            signedHeadersLength += signedHeaders[i].Length;
14✔
270

271
        int signedHeadersSeparatorLength = signedHeadersCount > 1 ? signedHeadersCount - 1 : 0;
5✔
272

273
        int totalLength =
5✔
274
            scheme.Length +
5✔
275
            clientPrefix.Length +
5✔
276
            client.Length +
5✔
277
            signedHeadersPrefix.Length +
5✔
278
            signedHeadersLength +
5✔
279
            signedHeadersSeparatorLength +
5✔
280
            signaturePrefix.Length +
5✔
281
            signature.Length;
5✔
282

283
        return string.Create(totalLength, (client, signedHeaders, signature), (span, state) =>
5✔
284
        {
5✔
285
            int pos = 0;
5✔
286

5✔
287
            // Write scheme
5✔
288
            scheme.AsSpan().CopyTo(span.Slice(pos, scheme.Length));
5✔
289
            pos += scheme.Length;
5✔
290

5✔
291
            // Write client prefix
5✔
292
            clientPrefix.AsSpan().CopyTo(span.Slice(pos, clientPrefix.Length));
5✔
293
            pos += clientPrefix.Length;
5✔
294

5✔
295
            // Write client
5✔
296
            state.client.AsSpan().CopyTo(span.Slice(pos, state.client.Length));
5✔
297
            pos += state.client.Length;
5✔
298

5✔
299
            // Write signedHeaders prefix
5✔
300
            signedHeadersPrefix.AsSpan().CopyTo(span.Slice(pos, signedHeadersPrefix.Length));
5✔
301
            pos += signedHeadersPrefix.Length;
5✔
302

5✔
303
            // Write signedHeaders (semicolon separated)
5✔
304
            for (int i = 0; i < state.signedHeaders.Count; i++)
38✔
305
            {
5✔
306
                if (i > 0)
14✔
307
                    span[pos++] = ';';
10✔
308

5✔
309
                var header = state.signedHeaders[i];
14✔
310
                header.AsSpan().CopyTo(span.Slice(pos, header.Length));
14✔
311
                pos += header.Length;
14✔
312
            }
5✔
313

5✔
314
            // Write signature prefix
5✔
315
            signaturePrefix.AsSpan().CopyTo(span.Slice(pos, signaturePrefix.Length));
5✔
316
            pos += signaturePrefix.Length;
5✔
317

5✔
318
            // Write signature
5✔
319
            state.signature.AsSpan().CopyTo(span.Slice(pos, state.signature.Length));
5✔
320
            pos += state.signature.Length;
5✔
321
        });
10✔
322
#endif
323
    }
324

325
    /// <summary>
326
    /// Performs a constant-time comparison of two strings to prevent timing-based side-channel attacks.
327
    /// </summary>
328
    /// <remarks>
329
    /// Both strings are converted to UTF-8 byte arrays before comparison so the
330
    /// operation time does not vary with the position of the first differing character.
331
    /// </remarks>
332
    /// <param name="left">The first string to compare.</param>
333
    /// <param name="right">The second string to compare.</param>
334
    /// <returns><see langword="true"/> if the strings are equal; otherwise, <see langword="false"/>.</returns>
335
    public static bool FixedTimeEquals(
336
        string left,
337
        string right)
338
    {
339
        // Convert strings to byte arrays using UTF8 encoding
340
        var leftBytes = Encoding.UTF8.GetBytes(left);
8✔
341
        var rightBytes = Encoding.UTF8.GetBytes(right);
8✔
342

343
#if NETSTANDARD2_0 || NETFRAMEWORK
344
        // Constant-time comparison that does not leak length information.
345
        // XOR the lengths and accumulate into the result so a length mismatch
346
        // does not cause an early return (which would be a timing side-channel).
347
        int result = leftBytes.Length ^ rightBytes.Length;
348
        int minLength = Math.Min(leftBytes.Length, rightBytes.Length);
349

350
        for (int i = 0; i < minLength; i++)
351
            result |= leftBytes[i] ^ rightBytes[i];
352

353
        return result == 0;
354
#else
355
        // CryptographicOperations.FixedTimeEquals already handles length
356
        // differences in constant time by returning false without leaking
357
        // which bytes differ, but it does reveal differing lengths via timing.
358
        // For HMAC/SHA256 comparisons the outputs are always the same length,
359
        // so this is acceptable. Guard with a length check that folds into
360
        // the result to keep the public API safe for variable-length callers.
361
        if (leftBytes.Length != rightBytes.Length)
8✔
362
        {
363
            // Compare left against itself so we still spend time proportional
364
            // to the input length, then return false.
365
            CryptographicOperations.FixedTimeEquals(leftBytes, leftBytes);
4✔
366
            return false;
4✔
367
        }
368

369
        return CryptographicOperations.FixedTimeEquals(leftBytes, rightBytes);
4✔
370
#endif
371
    }
372
}
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