I am trying to implement Ivona request signing based on this documnent
Everything works good and all the results match to the example value, except Signature result. So my result for the signature is cf1141e33a8fbba23913f8f36f29faa524a57db37690a1b819f43bbeaabf3b76 but in the document it is equal to 2cdfef28d5c5f6682280600a6141a8940c608cfefacb47f172329cbadb5864cc
Is it my mistake or a mistake in the Ivona document?
Below is the C# code I am using:
class Program
{
static void Main()
{
try
{
Console.WriteLine(SendIvonaRequest());
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private static string SendIvonaRequest()
{
var date = new DateTime(2013, 09, 13, 09, 20, 54, DateTimeKind.Utc);
const string algorithm = "AWS4-HMAC-SHA256";
const string regionName = "eu-west-1";
const string serviceName = "tts";
const string method = "POST";
const string canonicalUri = "/CreateSpeech";
const string canonicalQueryString = "";
const string contentType = "application/json";
const string accessKey = "MyAccessKey";
const string secretKey = "MySecretKey";
const string host = serviceName + "." + regionName + ".ivonacloud.com";
const string requestPayload = "{\"Input\":{\"Data\":\"Hello world\"}}";
var hashedRequestPayload = HexEncode(Hash(ToBytes(requestPayload)));
Debug.Assert(hashedRequestPayload.Equals("f43e25253839f2c3feae433c5e477d79f7dfafdc0e4af19a952adb44a60265ba"));
var dateStamp = date.ToString("yyyyMMdd");
var requestDate = date.ToString("yyyyMMddTHHmmss") + "Z";
var credentialScope = string.Format("{0}/{1}/{2}/aws4_request", dateStamp, regionName, serviceName);
var headers = new SortedDictionary<string, string>
{
{"content-type", "application/json"},
{"host", "tts.eu-west-1.ivonacloud.com"},
{"x-amz-content-sha256", hashedRequestPayload},
{"x-amz-date", requestDate}
};
string canonicalHeaders =
string.Join("\n", headers.Select(x => x.Key.ToLowerInvariant() + ":" + x.Value.Trim())) + "\n";
const string signedHeaders = "content-type;host;x-amz-content-sha256;x-amz-date";
// Task 1: Create a Canonical Request For Signature Version 4
var canonicalRequest = method + '\n' + canonicalUri + '\n' + canonicalQueryString +
'\n' + canonicalHeaders + '\n' + signedHeaders + '\n' + hashedRequestPayload;
var hashedCanonicalRequest = HexEncode(Hash(ToBytes(canonicalRequest)));
Debug.Assert(hashedCanonicalRequest.Equals("73ff17c0bf9da707afb02bbceb77d359ab945a460b5ac9fff7a0a61cfaab95e6"));
// Task 2: Create a String to Sign for Signature Version 4
// StringToSign = Algorithm + '\n' + RequestDate + '\n' + CredentialScope + '\n' + HashedCanonicalRequest
var stringToSign = string.Format("{0}\n{1}\n{2}\n{3}", algorithm, requestDate, credentialScope,
hashedCanonicalRequest);
Debug.Assert(stringToSign.Equals("AWS4-HMAC-SHA256" + "\n" +
"20130913T092054Z" + "\n" +
"20130913/eu-west-1/tts/aws4_request" + "\n" +
"73ff17c0bf9da707afb02bbceb77d359ab945a460b5ac9fff7a0a61cfaab95e6"));
// Task 3: Calculate the AWS Signature Version 4
// HMAC(HMAC(HMAC(HMAC("AWS4" + kSecret,"20130913"),"eu-west-1"),"tts"),"aws4_request")
byte[] signingKey = GetSignatureKey(secretKey, dateStamp, regionName, serviceName);
// signature = HexEncode(HMAC(derived-signing-key, string-to-sign))
var signature = HexEncode(HmacSha256(stringToSign, signingKey));
Debug.Assert(signature.Equals("2cdfef28d5c5f6682280600a6141a8940c608cfefacb47f172329cbadb5864cc"));
// Task 4: Prepare a signed request
// Authorization: algorithm Credential=access key ID/credential scope, SignedHeadaers=SignedHeaders, Signature=signature
var authorization =
string.Format("{0} Credential={1}/{2}/{3}/{4}/aws4_request, SignedHeaders={5}, Signature={6}",
algorithm, accessKey, dateStamp, regionName, serviceName, signedHeaders, signature);
// Send the request
var webRequest = WebRequest.Create("https://" + host + canonicalUri);
webRequest.Method = method;
webRequest.Timeout = 2000;
webRequest.ContentType = contentType;
webRequest.Headers.Add("X-Amz-date", requestDate);
webRequest.Headers.Add("Authorization", authorization);
webRequest.Headers.Add("x-amz-content-sha256", hashedRequestPayload);
webRequest.ContentLength = requestPayload.Length;
using (Stream newStream = webRequest.GetRequestStream())
{
newStream.Write(ToBytes(requestPayload), 0, requestPayload.Length);
}
var response = (HttpWebResponse) webRequest.GetResponse();
using (Stream responseStream = response.GetResponseStream())
{
if (responseStream != null)
{
using (var streamReader = new StreamReader(responseStream))
{
return streamReader.ReadToEnd();
}
}
}
return string.Empty;
}
private static byte[] GetSignatureKey(String key, String dateStamp, String regionName, String serviceName)
{
byte[] kDate = HmacSha256(dateStamp, ToBytes("AWS4" + key));
byte[] kRegion = HmacSha256(regionName, kDate);
byte[] kService = HmacSha256(serviceName, kRegion);
return HmacSha256("aws4_request", kService);
}
private static byte[] ToBytes(string str)
{
return Encoding.UTF8.GetBytes(str.ToCharArray());
}
private static string HexEncode(byte[] bytes)
{
return BitConverter.ToString(bytes).Replace("-", string.Empty).ToLowerInvariant();
}
private static byte[] Hash(byte[] bytes)
{
var sha256 = SHA256.Create();
return sha256.ComputeHash(bytes);
}
private static byte[] HmacSha256(String data, byte[] key)
{
return new HMACSHA256(key).ComputeHash(ToBytes(data));
}
}
UPD1:
I have also tried the examples from http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html and noticed that my code generate the same signature as in those examples. So I am assuming that there is an issue in Ivona document...
UPD2:
Everything works fine! I have implemented CreateSpeech method according to the description and uploaded a full example of the usage to GitHub https://github.com/MalyutinS/DotNetIvonaAPI
Solved! Actually there is an issue in documentation example. So the code works fine.
It's a bit old now, but it may be relevant to others using this code;
The code works fine as long as you only stick to ASCII-characters. For languages other than english, you must first convert the string to a UTF-8 byte array. That array may be longer than the number of characters in the string, consequently the length of the byte array sent through the WebRequest must reflect the length of the array itself, and not the character count.
Really a classic, but relevant to point out anyway..
This error results in the signature not being correct, hence an authentication error. The reason may not be obvious to everyone.
So the existing code;
Becomes:
Also; for a more proper and reusable way of doing AWS-4 signatures, see Amazons own: http://aws.amazon.com/code