I'm trying to figure out how to pass a parameter in my .NET application. The URL request looks like:
http://webservices.amazon.com/onca/xml?
Service=AWSECommerceService
&Operation=ItemLookup
&ResponseGroup=Large
&SearchIndex=All
&IdType=UPC
&ItemId=635753490879
&AWSAccessKeyId=[Your_AWSAccessKeyID]
&AssociateTag=[Your_AssociateTag]
&Timestamp=[YYYY-MM-DDThh:mm:ssZ]
&Signature=[Request_Signature]
The part that I'm confused about are these:
&Timestamp=[YYYY-MM-DDThh:mm:ssZ]
&Signature=[Request_Signature]
I'm not sure whether I can Just simply do it something like this for timestamp part:
var TimeStamp = DateTime.Now; // without any special datetime formating?
So my question is how do I actually generate this signature URL in the request URL ?
I have all of these parameters above but I'm not sure how to generate this last one ?
Can someone help me out ?
AWS utilizes HMAC request-signing. Generally speaking, the way this works is that you create a "message", which is composed of things like your access key(s), request headers, request body and a timestamp. You then HMAC this "message" and that becomes your "signature" for the request. This prevents replay-attacks as each request must have a unique signature.
It looks like the timestamp simply needs to be in ISO format (YYYY-MM-DDThh:mm:ssZ), so, no you can't just use DateTime.Now. The default format utilized by ToString will not be ISO. Instead, you'd need to use something like:
DateTime.Now.ToString("yyyy-MM-ddThh:mm:sszzz");
Or it would actually probably be better to use UTC time and simply append a Z:
DateTime.UtcNow.ToString("yyyy-MM-ddThh:mm:ssZ");
As for creating the signature, see the AWS documentation, where they provide some sample code:
static byte[] HmacSHA256(String data, byte[] key)
{
String algorithm = "HmacSHA256";
KeyedHashAlgorithm kha = KeyedHashAlgorithm.Create(algorithm);
kha.Key = key;
return kha.ComputeHash(Encoding.UTF8.GetBytes(data));
}
static byte[] getSignatureKey(String key, String dateStamp, String regionName, String serviceName)
{
byte[] kSecret = Encoding.UTF8.GetBytes(("AWS4" + key).ToCharArray());
byte[] kDate = HmacSHA256(dateStamp, kSecret);
byte[] kRegion = HmacSHA256(regionName, kDate);
byte[] kService = HmacSHA256(serviceName, kRegion);
byte[] kSigning = HmacSHA256("aws4_request", kService);
return kSigning;
}
/*
DOCUMENTATION: https://docs.aws.amazon.com/AWSECommerceService/latest/DG/rest-signature.html#rest_detailedexample
*/
var itemID = "0679722769";
var accessKeyID = "AKIAIOSFODNN7EXAMPLE";
var timeStamp = DateTime.UtcNow.ToString("o");
var req = $"Service=AWSECommerceService&AWSAccessKeyId={accessKeyID}&Operation=ItemLookup&IdType=UPC&ItemId={itemID}&Version=2013-08-01&Timestamp={timeStamp}";
req = req.Replace(":", "%3A").Replace(",", "%2C"); //UrlDecode certain characters
var reqlist = req.Split('&').ToArray(); //we need to sort our key/value pairs
Array.Sort(reqlist);
req = String.Join("&", reqlist); //join everything back
var reqToSign = $#"GET
webservices.amazon.com
/onca/xml
{req}".Replace("\r", ""); //create the request for signing. We need to replace microsofts's crlf with just a lf; Make sure there are no leading spaces after the linefeeds.
var signage = getSignatureKey("1234567890",reqToSign);
req = $"http://webservices.amazon.com/onca/xml?{req}&Signature={signage}"; //create our request with the signature appended.
return req;
}
private static byte[] HmacSHA256(String data, byte[] key)
{
String algorithm = "HmacSHA256";
KeyedHashAlgorithm kha = KeyedHashAlgorithm.Create(algorithm);
kha.Key = key;
return kha.ComputeHash(Encoding.UTF8.GetBytes(data));
}
private static string getSignatureKey(string key, string stringToSign)
{
byte[] kSecret = Encoding.UTF8.GetBytes(key.ToCharArray());
byte[] kSigning = HmacSHA256(stringToSign, kSecret);
return WebUtility.UrlEncode(Convert.ToBase64String(kSigning));
}
Contrary to most of the answers found here and elsewhere, this is the only way that works. The entire request has to be hashed, not just particular parameters. I can't speak to other Amazon services, but the Commerce Service has to be done like this.
Quite a few answers here and elsewhere referenced this: https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
This is most certainly not correct. If you're not passing a region parameter, how can Amazon create the same signature since it doesn't have all the information.
Related
I want to use yubico OTP as a second factor in my application.
Yubico OTP documentation: https://developers.yubico.com/OTP/
The following is a c#(.net 6) example which reads the OTP via console (You need to press the button on the usbstick, then the otp is used as parameter for the rest service request). This sample is based on version 2.0 or the verify service (https://api.yubico.com/wsapi/2.0/verify)
using System.Security.Cryptography;
//Sample for validating OTP based on https://developers.yubico.com/OTP/OTPs_Explained.html
//Sample request: https://api.yubico.com/wsapi/2.0/verify?otp=vvvvvvcucrlcietctckflvnncdgckubflugerlnr&id=87&timeout=8&sl=50&nonce=askjdnkajsndjkasndkjsnad
// The yubico api clientid.
// You can open an api key here: https://upgrade.yubico.com/getapikey/
string yubicoCredentialClientId = "87";
// This is currently not required. Should be used to verify the response but its unclear whether this is possible or not.
// string yubicoCredentionPrivateKey = "";
string yubikeyValidationUrl = $"https://api.yubico.com/wsapi/2.0/verify?";
string nonce = "";
//Create a nonce
using (var random = RandomNumberGenerator.Create())
{
var tmpNonce = new byte[16];
random.GetBytes(tmpNonce);
nonce = BitConverter.ToString(tmpNonce).Replace("-", "");
}
//Get the OTP from yubikey
System.Console.WriteLine("Press yubikey button and then enter");
var otp = Console.ReadLine();
System.Console.WriteLine(otp);
string validationParameter = $"otp={otp}&id={yubicoCredentialClientId}&nonce={nonce}";
HttpClient client = new HttpClient();
var url = $"{yubikeyValidationUrl}{validationParameter}";
System.Console.WriteLine(url);
var result = client.GetAsync(url).Result;
System.Console.WriteLine(result.StatusCode);
string respnse = result.Content.ReadAsStringAsync().Result;
System.Console.WriteLine(respnse);
if (respnse.ToLower().Contains("status=ok"))
System.Console.WriteLine("OTP succsessful validated");
else
System.Console.WriteLine("OTP invalid");
This all works fine and even returns status=OK as part of the response when i use a valid OTP generated by the yubikey.
Question: Can i somehow validate the response using my yubico api private key? If not, it seems this authentication would be vulnerable to a man in the middle attack.
Side question: The request requires an api id and i even created one via https://upgrade.yubico.com/getapikey/ but i can just use any id and the request works all the same. Is this by design? If yes, what it the point of this id parameter in the first place?
There is actually documentation for this: https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html
A hmac-sha1 must be created for the parameters and then this signature must be added as an additional parameter.
//Create the signature based on https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html
//Prepare the parameters to be signed (Ordered alphabetically)
string signatureParameters = $"id={yubicoCredentialClientId}&nonce={nonce}&otp={otp}";
//Create the key based on the api key string
byte[] base64AsByte = Convert.FromBase64String(yubicoCredentionPrivateKey);
string signature = "";
using (var hmac = new HMACSHA1(base64AsByte))
{
//Create the hmacsha1
var signatureAsByte = hmac.ComputeHash(Encoding.UTF8.GetBytes(signatureParameters));
signature = Convert.ToBase64String(signatureAsByte);
}
//Add the signature
signatureParameters+=$"&h={signature}";
Such an url then looks like this(The signature is part of the h parameter):
https://api.yubico.com/wsapi/2.0/verify?id=42&nonce=5FB3D5377640BA3FB8955AF98D6B71EC&otp=foobar&h=XXVw+vqc3k//qFGG6+WbP96xXis=
Complete example
The following is a complete self-contained example howto use the Yubikey OTP in a .net application (Including validation of the signatures)
The followings steps are performed:
Create the parameters for the request
Create a nonce
Get OTP from yubikey
Sign the parameter using the API key
Call the verify service from yubico
Check otp
Check return status
Compare returned signature with built signature
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
//Sample for validating OTP based on https://developers.yubico.com/OTP/OTPs_Explained.html
//Sample request: "https://api.yubico.com/wsapi/2.0/verify?id=87&nonce=44D4185490BA8E77E58A38A98CF501E9&otp=cccccxxxvulhlletkijhrtifrintlerfbnbhtdnikl&h=f9Ht4a08iaFQYQBI5E0XUni3Pss="
//Sample response: h=TC/RXXcVqPWkFr4JPlf29nWEnig=\r\nt=2022-04-09T18:58:34Z0336\r\notp=ccxxxxxtbbvulhlletkijhrtifrintlerfbnbhtdnikl\r\nnonce=44D41854DDDA8E77E58A38A98CF501E9\r\nsl=100\r\nstatus=OK\r\n\r\n"
// The yubico api clientid. You can open an api key here: https://upgrade.yubico.com/getapikey/
string yubicoApiClientId = "REPLACEWITHCLIENTID";
// This is currently not required.
string yubicoApiPrivateKey = "REPLACEWITHAPIKEY";
string yubikeyValidationUrl = $"https://api.yubico.com/wsapi/2.0/verify?";
string nonce = "";
//Create the key based on the api key string
byte[] privateKey = Convert.FromBase64String(yubicoApiPrivateKey);
//Create a nonce
using (var random = RandomNumberGenerator.Create())
{
var tmpNonce = new byte[16];
random.GetBytes(tmpNonce);
nonce = BitConverter.ToString(tmpNonce).Replace("-", "");
}
//Get the OTP from yubikey (usb stick)
System.Console.WriteLine("Press yubikey button");
var otp = Console.ReadLine();
//Create the signature based on https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html
//Prepare the parameters to be signed (Ordered alphabetically)
string verifyParameters = $"id={yubicoApiClientId}&nonce={nonce}&otp={otp}";
string signature = "";
using (var hmac = new HMACSHA1(privateKey))
{
//Create the hmacsha1
var signatureAsByte = hmac.ComputeHash(Encoding.UTF8.GetBytes(verifyParameters));
signature = Convert.ToBase64String(signatureAsByte);
}
//Add the signature
verifyParameters += $"&h={signature}";
HttpClient client = new HttpClient();
var url = $"{yubikeyValidationUrl}{verifyParameters}";
System.Console.WriteLine(url);
var result = client.GetAsync(url).Result;
System.Console.WriteLine($"http statuscode: {result.StatusCode}");
string response = result.Content.ReadAsStringAsync().Result;
System.Console.WriteLine(response);
Match m = Regex.Match(response, "status=\\w*", RegexOptions.IgnoreCase);
if (m.Success)
Console.WriteLine($"OTP Status: {m.Value}");
//Verify signature based on https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html
//The response contains a signature (h parameter) which was signed with the same private key
//This means we can just calculate the hmacsha1 again (Without the h parameter and with ordering of the parameter)
//and then compare the returned signature with the created siganture
var lines = response.Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.None).ToList();
var returnedSignature = String.Empty;
string returnParameterToCheck = String.Empty;
foreach (var item in lines.OrderBy(x => x))
{
if (!string.IsNullOrEmpty(item) && !item.StartsWith("h="))
returnParameterToCheck += $"&{item}";
if (!string.IsNullOrEmpty(item) && item.StartsWith("h="))
returnedSignature = item.Replace("h=", "");
}
//Remove the first unnecessary '&' character
returnParameterToCheck = returnParameterToCheck.Remove(0, 1);
var signatureToCompare = String.Empty;
using (var hmac1 = new HMACSHA1(privateKey))
{
signatureToCompare = Convert.ToBase64String(hmac1.ComputeHash(Encoding.UTF8.GetBytes(returnParameterToCheck)));
}
if (returnedSignature == signatureToCompare)
System.Console.WriteLine("Signatures are equal");
else
System.Console.WriteLine("Signatures are not equal");
(I apparently don't have enough reputation, so I'm only allow to post 'answers')
#Manuel
I see this example all over the web in various languages, but none of them work correctly for me.
The return status is always status=OK regardless of what physical key I'm using.
I have access to a box of 50 yubikeys 5 nfc and if I use your example, status will be OK.
If I tamper with the ID, I will get responses like NO_SUCH_CLIENT or BAD_SIGNATURE etc.
So it is kind of important that certain parameters match, but the actual OTP isn't part of that.
I can register an ID and Secret at https://upgrade.yubico.com/getapikey
Do the verification in your code and it will be status=OK.
Then grab a brand new yubikey and try it and status will still be OK.
I tried verifying with my ID and Secret using a yubikey from a colleague and, you can guess it, status=OK.
So the only thing I'm really proving is that I possess 'a' yubikey.
For various reasons I need to be able to make Chef API requests in C#. I've followed the guides here (Header specification) and here (Bash example) but have reached a dead end. Every request I send comes back 401 Unauthorized with the content
{"error":["Invalid signature for user or client 'myuser'"]}
I have also configured Knife locally to use Fiddler as a HTTP proxy so I can inspect Knife HTTP requests and have copied the visible headers as accurately as possible, but naturally I cannot see the canonical header it generated nor the one the server expects.
However, I have confirmed in this way that the hashes I'm generating for the content (empty) and path are the same as Knife is generating.
Here's my code. Loading the RSA private key from PEM format is using an extension method taken from Christian Etter's blog - run out of links sorry.
const string path = "/cookbooks"
const string basePath = "https://chefserver.internal:443";
var timestamp = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ssZ");
var method = "GET";
var clientName = "myuser";
var hashedPath = ToBase64EncodedSha1String(path);
var hashedBody = ToBase64EncodedSha1String(String.Empty);
var canonicalHeader = String.Format("Method:{0}\nHashed Path:{1}\nX-Ops-Content-Hash:{2}\nX-Ops-Timestamp:{3}\nX-Ops-UserId:{4}",
method, hashedPath, hashedBody, timestamp, clientName);
var privateKey = File.ReadAllText("C:\\chef\\myuser.private.pem");
string signature;
byte[] rawData;
using (var rsa = new RSACryptoServiceProvider())
{
rsa.PersistKeyInCsp = false;
rsa.LoadPrivateKeyPEM(privateKey);
using (var sha1 = new SHA1CryptoServiceProvider())
{
rawData = rsa.SignData(Encoding.UTF8.GetBytes(canonicalHeader), sha1);
signature = Convert.ToBase64String(rawData);
}
}
var client = new HttpClient();
var message = new HttpRequestMessage();
message.Method = HttpMethod.Get;
message.RequestUri = new Uri(basePath + path);
message.Headers.Add("Accept", "application/json");
message.Headers.Add("Host", "chefserver.internal:443");
message.Headers.Add("X-Chef-Version", "11.12.4");
message.Headers.Add("X-Ops-Timestamp", timestamp);
message.Headers.Add("X-Ops-Sign", "version=1.0;");
message.Headers.Add("X-Ops-Userid", clientName);
message.Headers.Add("X-Ops-Content-Hash", hashedBody);
message.Headers.Add("User-Agent", "Chef Knife/11.4.0 (ruby-1.9.2-p320; ohai-6.16.0; x86_64-darwin11.3.0; +http://opscode.com)");
var currentItem = new StringBuilder();
var i = 1;
foreach (var l in signature)
{
currentItem.Append(l);
if (currentItem.Length == 60)
{
message.Headers.Add("X-Ops-Authorization-" + i, currentItem.ToString());
i++;
currentItem = new StringBuilder();
}
}
message.Headers.Add("X-Ops-Authorization-" + i, currentItem.ToString());
var response = await client.SendAsync(message);
var content = await response.Content.ReadAsStringAsync();
And the helper
private string ToBase64EncodedSha1String(string input)
{
return
Convert.ToBase64String(new SHA1CryptoServiceProvider().ComputeHash(Encoding.UTF8.GetBytes(input)));
}
I had a similar problem and found that the .NET crypto libraries did not support what was needed to make this work. I ended up using the BouncyCastle crypto libraries to accomplish this.
I made a simple C# class library that wraps this up. Take a look at that here: https://github.com/mattberther/dotnet-chef-api
I'm not super familiar with C# crypto, but the docs on SHA1CryptoServiceProvider seem to show that is is signing a SHA1 hash. Chef doesn't use signed hashes, you actually need to do an RSA signature on canonicalHeader itself.
SignData(Byte[], Object): Computes the hash value of the specified byte array using the specified hash algorithm, and signs the resulting hash value.
Is there a null or pass-through hash you can use?
So I am just trying to list the tables in the storage account to test the authorization using the Query Tables method. I tried using the SDK, but the SDK was trying to reference DLLs that aren't available in RT. Decided to try out the REST API. but am having trouble with the authentication from this spec http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx
public async Task ExecuteAsync()
{
try
{
HttpClient client = new HttpClient();
Dictionary<string, string> headers = GetHeaders("/Tables");
client.DefaultRequestHeaders.Date = DateTimeOffset.Parse(headers["Date"]);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("SharedKey", headers["Authorization"]);
const string url = "http://account-name.table.core.windows.net/Tables";
XmlReader reader = XmlReader.Create(await client.GetStreamAsync(url));
//
// Do some stuff with the reader here
//
}
catch (Exception e)
{
// handle exception
}
}
public Dictionary<string, string> GetHeaders(string resource)
{
Dictionary<string, string> headers = new Dictionary<string, string>();
headers["Date"] = DateTime.Now.ToString("R");
headers["Authorization"] = GetAuthorizationHeader(resource, headers["Date"]);
return headers;
}
public string GetAuthorizationHeader(string resource, string date)
{
const string key = PRIMARY_KEY;
const string accountName = ACCOUNT_NAME;
string signee = string.Join("\n", new List<string> { "GET", "", "", date, resource });
// make the signature
MacAlgorithmProvider hmac = MacAlgorithmProvider.OpenAlgorithm("HMAC_SHA256");
IBuffer keyMaterial = CryptographicBuffer.ConvertStringToBinary(key, BinaryStringEncoding.Utf8);
CryptographicKey hmacKey = hmac.CreateKey(keyMaterial);
IBuffer data = CryptographicBuffer.ConvertStringToBinary(signee, BinaryStringEncoding.Utf8);
IBuffer hash = CryptographicEngine.Sign(hmacKey, data);
string signature = CryptographicBuffer.EncodeToBase64String(hash);
return string.Format("{0}:{1}", accountName, signature);
}
Obviously I am missing something as I continue to get 403's. See any problems looking through this code?
A few comments:
There's a storage client library for Windows RT as well. Please take a look at my answer here: Working with Azure in Winrt with Rest API, trouble with signature.
Coming to your problem, can you try changing the following line of code:
headers["Date"] = DateTime.Now.ToString("R");
to
headers["Date"] = DateTime.UtcNow.ToString("R");
and see if that helps.
UPDATE
I also noticed that you're using CryptographicBuffer.ConvertStringToBinary to convert Base64 encoded key to bytes. Please try using CryptographicBuffer.DecodeFromBase64String (http://msdn.microsoft.com/en-us/library/windows/apps/windows.security.cryptography.cryptographicbuffer.decodefrombase64string.aspx) instead.
I'm trying to create a client for the new tent.io protocol that's being developed and they are using the HTTP MAC Oauth2 scheme described by https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-http-mac-01.
I've written a simple method in C# that creates the Authorization header, but when I submit my request I get a simple "Invalid MAC signature" error.
Since I don't have a reference implementation, I'm struggling to figure out what's wrong with my code. I'm posting it here in the hope that somebody can spot my mistake.
public string GetAuthorizationHeader(string macKeyIdentifier, string macKey, string macAlgorithm, string method, Uri uri)
{
TimeSpan t = (DateTime.UtcNow - new DateTime(1970, 1, 1));
string timestamp = ((int)t.TotalSeconds).ToString();
string nonce = new Random().Next().ToString();
string normalizedString = string.Format("{0}\n{1}\n{2}\n{3}\n{4}\n{5}\n\n",
timestamp,
nonce,
method,
uri.PathAndQuery,
uri.Host,
uri.Port);
HashAlgorithm hashGenerator = null;
if (macAlgorithm == "hmac-sha-256")
{
hashGenerator = new HMACSHA256(Encoding.ASCII.GetBytes(macKey));
}
else if (macAlgorithm == "hmac-sha-1")
{
hashGenerator = new HMACSHA1(Encoding.ASCII.GetBytes(macKey));
}
else
{
throw new InvalidOperationException("Unsupported MAC algorithm");
}
string hash = System.Convert.ToBase64String(hashGenerator.ComputeHash(Encoding.ASCII.GetBytes(normalizedString)));
StringBuilder authorizationHeader = new StringBuilder();
authorizationHeader.AppendFormat(#"id=""{0}"",ts=""{1}"",nonce=""{2}"",mac=""{3}""",
macKeyIdentifier, timestamp, nonce, hash);
return authorizationHeader.ToString();
}
I create the full header using the returned value and it looks something lke this
Authorization: MAC id="a:dfsdfa2",ts="1349277638",nonce="1469030797",mac="ibZ/HXaoz2VgBer3CK7K9vu0po3K+E36K+TQ9Sgcw6o="
I'm sure I'm missing something small, but I cannot see it.
Any help would be very much appreciated!
It turns out the code above is perfect, but I was passing the wrong HTTP method value into it!
Where I was getting the error, I was POST'ing JSON, but I had actually put "GET" into the GetAuthorizationMethod!
Once I'd corrected that, I got an access_token value from Tent.is.
Nicely executed tool at http://buchananweb.co.uk/security01.aspx showing HMAC using MD5 and SHA1, SHA256, SHA384, SHA512
Has anyone using .net actually worked out how to successfully sign a signature to use with CloudFront private content? After a couple of days of attempts all I can get is Access Denied.
I have been working with variations of the following code and also tried using OpenSSL.Net and AWSSDK but that does not have a sign method for RSA-SHA1 yet.
The signature (data) looks like this
{"Statement":[{"Resource":"http://xxxx.cloudfront.net/xxxx.jpg","Condition":{"DateLessThan":{"AWS:EpochTime":1266922799}}}]}
Update: Solved all of this by removing a single space in the above signature.
If only I had noticed it earlier!
This method attempts to sign the signature for use in the canned url. So of the variations have included chanding the padding used in the has and also reversing the byte[] before signing as apprently OpenSSL do it this way.
public string Sign(string data)
{
using (SHA1Managed SHA1 = new SHA1Managed())
{
RSACryptoServiceProvider provider = new RSACryptoServiceProvider();
RSACryptoServiceProvider.UseMachineKeyStore = false;
// Amazon PEM converted to XML using OpenSslKey
provider.FromXmlString("<RSAKeyValue><Modulus>.....");
byte[] plainbytes = System.Text.Encoding.UTF8.GetBytes(data);
byte[] hash = SHA1.ComputeHash(plainbytes);
//Array.Reverse(sig); // I have see some examples that reverse the hash
byte[] sig = provider.SignHash(hash, "SHA1");
return Convert.ToBase64String(sig);
}
}
It's useful to note that I have verified the content is setup correctly in S3 and CloudFront by generating a CloudFront canned policy url using my CloudBerry Explorer. How do they do it?
Here is the full code if anyone if interested:
internal class CloudFrontSecurityProvider
{
private readonly RSACryptoServiceProvider privateKey;
private readonly string privateKeyId;
private readonly SHA1Managed sha1 = new SHA1Managed();
public CloudFrontSecurityProvider(string privateKeyId, string privateKey)
{
this.privateKey = new RSACryptoServiceProvider();
RSACryptoServiceProvider.UseMachineKeyStore = false;
this.privateKey.FromXmlString( privateKey );
this.privateKeyId = privateKeyId;
}
private static int GetUnixTime(DateTime time)
{
DateTime referenceTime = new DateTime(1970, 1,1);
return (int) (time - referenceTime).TotalSeconds;
}
public string GetCannedUrl(string url, DateTime expiration)
{
string expirationEpoch = GetUnixTime( expiration ).ToString();
string policy =
#"{""Statement"":[{""Resource"":""<url>"",""Condition"":{""DateLessThan"":{""AWS:EpochTime"":<expiration>}}}]}".
Replace( "<url>", url ).
Replace( "<expiration>", expirationEpoch );
string signature = GetUrlSafeString( Sign( policy ) );
return url + string.Format("?Expires={0}&Signature={1}&Key-Pair-Id={2}", expirationEpoch, signature, this.privateKeyId);
}
private static string GetUrlSafeString(byte[] data)
{
return Convert.ToBase64String( data ).Replace( '+', '-' ).Replace( '=', '_' ).Replace( '/', '~' );
}
private byte[] Sign(string data)
{
byte[] plainbytes = Encoding.UTF8.GetBytes(data);
byte[] hash = sha1.ComputeHash(plainbytes);
return this.privateKey.SignHash(hash, "SHA1");
}
}
I've done up a blog post of what I had to do to get it working:
http://anthonyvscode.com/2011/01/11/using-amazon-s3-cloudfront-to-distribute-your-private-files/
might help others trying to work out the problems that I had getting it up and running
You can now do this using the SDK:
http://docs.aws.amazon.com/sdkfornet/v3/apidocs/Index.html
Navigate to Amazon.CloudFront > AmazonCloudFrontUrlSigner