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

loresoft / HashGate / 29701161024

19 Jul 2026 05:55PM UTC coverage: 65.982%. First build
29701161024

push

github

pwelter34
Add AddHmacClient and runtime options updates

235 of 472 branches covered (49.79%)

Branch coverage included in aggregate %.

146 of 173 new or added lines in 11 files covered. (84.39%)

696 of 939 relevant lines covered (74.12%)

26.54 hits per line

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

96.9
/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];
2✔
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;
81✔
109
        int pathAndQueryLength = pathAndQuery.Length;
81✔
110
        int headerCount = 0;
81✔
111
        int headerValuesLength = 0;
81✔
112

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

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

121
        // 2 for the two '\n' literals
122
        int totalLength = methodLength + pathAndQueryLength + headerValuesLength + separatorLength + 2;
81✔
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) =>
81✔
145
        {
81✔
146
            int pos = 0;
81✔
147

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

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

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

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

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

81✔
168
                var header = state.headerValues[i];
315✔
169
                header.AsSpan().CopyTo(span.Slice(pos, header.Length));
315✔
170
                pos += header.Length;
315✔
171
            }
81✔
172
        });
162✔
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);
80✔
188
        var dataBytes = Encoding.UTF8.GetBytes(stringToSign);
80✔
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 using the allocation-free one-shot static API
197
        Span<byte> hash = stackalloc byte[32];
80✔
198
        if (!HMACSHA256.TryHashData(secretBytes, dataBytes, hash, out _))
80!
199
        {
200
            // Fallback if the destination is not large enough (should not happen for SHA256)
NEW
201
            hash = HMACSHA256.HashData(secretBytes, dataBytes);
×
202
        }
203

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

209
        return Convert.ToBase64String(hash);
×
210
#endif
211
    }
212

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

233
#if NETSTANDARD2_0 || NETFRAMEWORK
234
        var stringBuilder = new StringBuilder();
235

236
        // Write scheme
237
        stringBuilder.Append(scheme);
238

239
        // Write client prefix and client
240
        stringBuilder.Append(clientPrefix);
241
        stringBuilder.Append(client);
242

243
        // Write signedHeaders prefix
244
        stringBuilder.Append(signedHeadersPrefix);
245

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

252
            stringBuilder.Append(signedHeaders[i]);
253
        }
254

255
        // Write signature prefix and signature
256
        stringBuilder.Append(signaturePrefix);
257
        stringBuilder.Append(signature);
258

259
        return stringBuilder.ToString();
260
#else
261
        // Calculate signedHeaders string and its length
262
        int signedHeadersCount = signedHeaders.Count;
54✔
263
        int signedHeadersLength = 0;
54✔
264

265
        for (int i = 0; i < signedHeadersCount; i++)
528✔
266
            signedHeadersLength += signedHeaders[i].Length;
210✔
267

268
        int signedHeadersSeparatorLength = signedHeadersCount > 1 ? signedHeadersCount - 1 : 0;
54✔
269

270
        int totalLength =
54✔
271
            scheme.Length +
54✔
272
            clientPrefix.Length +
54✔
273
            client.Length +
54✔
274
            signedHeadersPrefix.Length +
54✔
275
            signedHeadersLength +
54✔
276
            signedHeadersSeparatorLength +
54✔
277
            signaturePrefix.Length +
54✔
278
            signature.Length;
54✔
279

280
        return string.Create(totalLength, (client, signedHeaders, signature), (span, state) =>
54✔
281
        {
54✔
282
            int pos = 0;
54✔
283

54✔
284
            // Write scheme
54✔
285
            scheme.AsSpan().CopyTo(span.Slice(pos, scheme.Length));
54✔
286
            pos += scheme.Length;
54✔
287

54✔
288
            // Write client prefix
54✔
289
            clientPrefix.AsSpan().CopyTo(span.Slice(pos, clientPrefix.Length));
54✔
290
            pos += clientPrefix.Length;
54✔
291

54✔
292
            // Write client
54✔
293
            state.client.AsSpan().CopyTo(span.Slice(pos, state.client.Length));
54✔
294
            pos += state.client.Length;
54✔
295

54✔
296
            // Write signedHeaders prefix
54✔
297
            signedHeadersPrefix.AsSpan().CopyTo(span.Slice(pos, signedHeadersPrefix.Length));
54✔
298
            pos += signedHeadersPrefix.Length;
54✔
299

54✔
300
            // Write signedHeaders (semicolon separated)
54✔
301
            for (int i = 0; i < state.signedHeaders.Count; i++)
528✔
302
            {
54✔
303
                if (i > 0)
210✔
304
                    span[pos++] = ';';
157✔
305

54✔
306
                var header = state.signedHeaders[i];
210✔
307
                header.AsSpan().CopyTo(span.Slice(pos, header.Length));
210✔
308
                pos += header.Length;
210✔
309
            }
54✔
310

54✔
311
            // Write signature prefix
54✔
312
            signaturePrefix.AsSpan().CopyTo(span.Slice(pos, signaturePrefix.Length));
54✔
313
            pos += signaturePrefix.Length;
54✔
314

54✔
315
            // Write signature
54✔
316
            state.signature.AsSpan().CopyTo(span.Slice(pos, state.signature.Length));
54✔
317
            pos += state.signature.Length;
54✔
318
        });
108✔
319
#endif
320
    }
321

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

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

347
        for (int i = 0; i < minLength; i++)
348
            result |= leftBytes[i] ^ rightBytes[i];
349

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

366
        return CryptographicOperations.FixedTimeEquals(leftBytes, rightBytes);
58✔
367
#endif
368
    }
369
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc