Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Signed Url Generator for .Net with Timestamp and HMACSHA512

Jealous of AWS style signed-URL links? Wish you could do that too?

Generate your own signed links with two simple classes.

<a href='/SignatureRequired/GetSecretThings?@Html.QuerystringFragmentTimestampAndSignature())>
    Get Secret Things Here
</a>
  • My use-case was to restrict the link to the original IP Address:
<a href='/SignatureRequired/GetSecretThings?@Html.QuerystringFragmentTimestampAndSignature(Request.UserAddress))>
    Get Secret Things Here
</a>

When /SignatureRequired/GetSecretThingsWhen?.... is called, verify the hmac server-side with

public async Task<JsonResult> Validate(string PhoneNumber, DateTime timestamp, string hmac)
{
    if (hmac != HMacSigner.TimestampedSignatureBase64(timestamp)) throw ...

  • Consider what your timestamp means: is it signedWhen or validUntil? I use signedWhen, and apply an expiry check:
 if (Rule_IsSignatureExpired(timestamp)) throw ...
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
public static class HMacSigner
{
/// <summary>HtmlHelper method for MVC 4 &amp; 5 to
/// create a querystring fragment that can be used directly in markup for a link.</summary>
/// <param name="htmlHelper"></param>
/// <param name="message">Defaults to "timestamponly". Use the default if you have no message, but only want a signed timestamp</param>
/// <param name="timeStamp">Defaults to <see cref="DateTime.UtcNow"/></param>
/// <returns>A UrlEncoded string of format "timestamp={0}&amp;hmac={1}", where
/// - the hmac will be the output of <see cref="TimestampedSignatureBase64"/>
/// - the timestamp is ISO8601 formatted.
/// If '+' appears in the timestamp, it will be encoded as '%2B'
/// </returns>
///
public static IHtmlString QuerystringFragmentTimestampAndSignature(this HtmlHelper htmlHelper,
string message = "timestamponly", DateTime timeStamp = default(DateTime))
{
if(timeStamp==default(DateTime))timeStamp=DateTime.UtcNow;
return htmlHelper.Raw(QuerystringFragmentTimestampAndSignature(message, timeStamp));
}
/// <summary>If you are not using Mvc, replace this with your preferred UrlEncode utility.</summary>
public static readonly Func<string, string> UrlEncode = System.Web.HttpUtility.UrlEncode;
static readonly byte[] Key = Encoding.ASCII
.GetBytes("===>>> replace this with a generate-once-fixed-for-lifetime-of-your-application secret random byte array <<<====").Take(64)
.ToArray();
/// <summary>Return a timestamped signature suitable for immediate use in an url query string</summary>
/// <param name="message">Defaults to "timestamponly". Use the default if you have no message, but only want a signed timestamp</param>
/// <param name="timeStamp">Defaults to <see cref="DateTime.UtcNow"/></param>
/// <returns>A UrlEncoded string of format "timestamp={0}&amp;hmac={1}", where
/// - the hmac will be the output of <see cref="TimestampedSignatureBase64"/>
/// - the timestamp is ISO8601 formatted.
/// If '+' appears in the timestamp, it will be encoded as '%2B'
/// </returns>
public static string QuerystringFragmentTimestampAndSignature(
string message = "timestamponly", DateTime timeStamp=default(DateTime))
{
if(timeStamp==default(DateTime))timeStamp=DateTime.UtcNow;
var hmac = TimestampedSignatureBase64(timeStamp, message);
return String.Format("timestamp={0}&hmac={1}", timeStamp.ToString("O").Replace("+", "%2B"),UrlEncode(hmac));
}
public static string TimestampedSignatureBase64(DateTime timeStamp, string message)
{
return SignatureBase64( message + timeStamp.ToString("O") );
}
public static string SignatureBase64(string inString)
{
using(var outStream= new MemoryStream())
{
return Convert.ToBase64String(GetHmac(inString, outStream).ToArray());
}
}
/// <summary>Fill <paramref name="outputStream"/> with an Hmac for <paramref name="message"/>, using our private <see cref="Key"/></summary>
/// <param name="message">Your message</param>
/// <param name="outputStream">The stream to write to.</param>
/// <returns><paramref name="outputStream"/></returns>
public static TStream GetHmac<TStream>(string message, TStream outputStream) where TStream:Stream
{
using (var inStream = new MemoryStream(Encoding.UTF8.GetBytes(message)))
using (HMACSHA512 hmacker = new HMACSHA512(Key))
{
byte[] hmac = hmacker.ComputeHash(inStream);
inStream.Position = 0;
outputStream.Write(hmac, 0, hmac.Length);
int bytesRead;
byte[] buffer = new byte[1024];
do
{
bytesRead = inStream.Read(buffer, 0, 1024);
outputStream.Write(buffer, 0, bytesRead);
} while (bytesRead > 0);
outputStream.Position = 0;
return outputStream;
}
}
/// <param name="base64EncodedHMac"></param>
/// <returns>True if <paramref name="base64EncodedHMac"/> is an HMac generated by our private <see cref="Key"/></returns>
public static bool IsHmacSignedWithMyKey(string base64EncodedHMac)
{
using(var inStream= new MemoryStream(Convert.FromBase64String(base64EncodedHMac)))
using (HMACSHA512 hmac = new HMACSHA512(Key))
{
byte[] storedHash = new byte[hmac.HashSize / 8];
inStream.Position = 0;
inStream.Read(storedHash, 0, storedHash.Length);
byte[] computedHash = hmac.ComputeHash(inStream);
for (int i = 0; i < storedHash.Length; i++)
{
if (computedHash[i] != storedHash[i])
{
return false;
}
}
}
return true;
}
}
using System;
using System.Web;
using System.Web.Mvc;
public class SignatureRequiredController : Controller
{
readonly ISecretService secretService; public interface ISecretService { object GetSecretThings(string inputs); }
public SignatureRequiredController(ISecretService secretService) { this.secretService = secretService; }
/// <returns>"timestamp={DateTime.Now as ISO8601}&amp;hmac={hmac}"</returns>
/// <remarks><strong>The embedded values are UrlEncoded</strong></remarks>
public string GetSignedWhenAndHmacForTest(string test, string message)
{
if (Request.IsLocal)return HMacSigner.QuerystringFragmentTimestampAndSignature(message,DateTime.Now);
//
LoggingConfig.Logger.Error("Call to test method GetSignedWhenAndHmacForTest from a non-local host {Request}", Request.ToJson());
throw new HttpException(404, "Not Found");
}
[System.Web.Mvc.Authorize]
public ActionResult AUrlWhichReturnsASignedLinkToSecretThings()
{
return Content(
@"
<p>The View containing this <strong>link</strong> can require authorization,
but the method GetSecretThings is public, and relies instead on
<ul>
<li>The signed url including a timestamp
<li>The timestamp can't be modified without invalidating the hmac
<li>GetSecretThings checks both the Hmac and your expiry rule before revealing its secret things
</p>
<p>Actually, the code as given also restricts by IP address, so it's very not public at all. To generate a link you can pass on, remove the IpAddress check.
&lt;a href='/SignatureRequired/GetSecretThings?@Html.QuerystringFragmentTimestampAndSignature(Request.UserAddress))'&gt;Get Secret Things Here&lt;/a&gt;
");
}
public JsonResult GetSecretThings(string inputs, DateTime timestamp, string hmac)
{
if (!HMacSigner.IsHmacSignedWithMyKey(hmac) || HMacSigner.TimestampedSignatureBase64(timestamp, Request.UserHostAddress) != hmac)
{
var e = new HttpException(403, "Invalid Key");
LoggingConfig.Logger.Error(e, "GetSecretThings({inputs}) by {Request}", inputs, Request.ToJson());
throw e;
}
if (Rule_IsSignatureExpired(timestamp))
{
var e = new HttpException(403, "Key has expired");
LoggingConfig.Logger.Error(e, "GetSecretThings({inputs}) by {Request}", inputs, Request.ToJson());
throw e;
}
return Json(secretService.GetSecretThings(inputs));
}
static bool Rule_IsSignatureExpired(DateTime timestamp){ return timestamp + TimeSpan.FromHours(2) < DateTime.Now; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.