Is this implementation of signed URLs reasonably secure? I'm trying to implement signed URLs for short lived access to static files.
The idea is:


generate an URL with an expiration timestamp (e.g. https://example.com/file.png?download=false&amp;expires=1586852158)
sign it with HMACSHA256 and a private key and append the signature at the end of URL (e.g. https://example.com/file.png?download=false&amp;expires=1586852158&amp;signature=6635ea14baeeaaffe71333cf6c7fa1f0af9f6cd1a17abb4e75ca275dec5906d1


When i receive the request on the server, I take out the signature parameter and verify that the rest of the URL signed with HMACSHA256 and the same private key results in the same signature.

The implementation is as follows:

    public static class URLSigner
    {
        private static string GetSignatureForUri(string uri, byte[] key)
        {
            var hmac = new HMACSHA256(key);
            var signature = hmac.ComputeHash(Encoding.UTF8.GetBytes(uri));
            var hexSignature = BitConverter.ToString(signature).Replace("-", string.Empty).ToLowerInvariant();
            return hexSignature;
        }

        public static string SignUri(string uri, byte[] key)
        {
            var hexSignature = GetSignatureForUri(uri, key);
            return QueryHelpers.AddQueryString(uri, new Dictionary&lt;string, string&gt; { { "signature", hexSignature }});
        }

        public static bool VerifyUri(string uri, byte[] key)
        {
            var signatureRegex = "[\\?&amp;]signature=([a-z0-9]+)$";
            var signatureMatch = Regex.Match(uri, signatureRegex);
            if (!signatureMatch.Success || signatureMatch.Groups.Count != 2)
                return false;
            var parsedSignature = signatureMatch.Groups[1].Value;

            var originalUri = Regex.Replace(uri, signatureRegex, "");
            var hexSignature = GetSignatureForUri(originalUri, key);
            return hexSignature == parsedSignature;
        }
    }


and it's used like so:

var signedUri = URLSigner.SignUri("https://example.com/file.png?download=false", secretKey);
var isVerified = URLSigner.VerifyUri(signedUri, secretKey);


The timestamp is verified separately on the request handler.

Is this implementation of signed URLs reasonably secure?
It looks like a rather straightforward use of HMAC. So I would presume it is safe from that perspective.

A note about terminology. Personally I would talk about an authentication tag or simply HMAC value because people often associate signatures with asymmetric algorithms such as RSA. The same goes for "private key"; I'd use secret key for symmetric keys as in your code.

Some remarks:


the regular expression is used twice, it seems more logical to match the entire GET request and make sure it ends with the group with the signature in it (even if that just means using .*);
to remove one way of attacks, I'd make sure that there aren't two authentication tags in the GET request;
to remove another, I'd include a sequence number or time stamp so that replay attacks are not possible;
hexadecimals are not very efficient, I'd use a base64url encoder, it's made for it;
the groups count check is unnecessary, either the regex matches or it doesn't.


One idea is to have a method split the URI and the signature, you don't need the authentication tag after verification after all - you could for instance use a tuple with the verification status and the URL without signature (as optional value), or use a yucky output parameter. Check your coding standards before doing either of these though.