How to sign an Amazon web service request in .NET using SOAP and without WSE

The Amazon Product Advertising API (formerly Amazon Associates Web Service or Amazon AWS) has introduced a new rule that must be signed by all web service requests by August 15, 2009. They provided sample code on their website showing how to do this in C # using both REST and SOAP. The implementation I use is SOAP. You can find sample code here , I do not include it because there is a fair amount.

Problem Im has in his example code uses WSE 3 and our current code does not use WSE. Does anyone know how to implement this update simply by using the automatically generated code from WSDL? I like it when I don’t need to switch to WSE 3 stuff right now, if I don’t need it, because this update is rather a quick patch to hold us until we can fully implement this in the current version of dev (August 3, starting from August 3 drop 1 out of 5 requests in a live environment if they are not signed, which is bad news for our application).

Here is a snippet of the main part that does the actual signing of the SOAP request.

class ClientOutputFilter : SoapFilter { // to store the AWS Access Key ID and corresponding Secret Key. String akid; String secret; // Constructor public ClientOutputFilter(String awsAccessKeyId, String awsSecretKey) { this.akid = awsAccessKeyId; this.secret = awsSecretKey; } // Here the core logic: // 1. Concatenate operation name and timestamp to get StringToSign. // 2. Compute HMAC on StringToSign with Secret Key to get Signature. // 3. Add AWSAccessKeyId, Timestamp and Signature elements to the header. public override SoapFilterResult ProcessMessage(SoapEnvelope envelope) { var body = envelope.Body; var firstNode = body.ChildNodes.Item(0); String operation = firstNode.Name; DateTime currentTime = DateTime.UtcNow; String timestamp = currentTime.ToString("yyyy-MM-ddTHH:mm:ssZ"); String toSign = operation + timestamp; byte[] toSignBytes = Encoding.UTF8.GetBytes(toSign); byte[] secretBytes = Encoding.UTF8.GetBytes(secret); HMAC signer = new HMACSHA256(secretBytes); // important! has to be HMAC-SHA-256, SHA-1 will not work. byte[] sigBytes = signer.ComputeHash(toSignBytes); String signature = Convert.ToBase64String(sigBytes); // important! has to be Base64 encoded var header = envelope.Header; XmlDocument doc = header.OwnerDocument; // create the elements - Namespace and Prefix are critical! XmlElement akidElement = doc.CreateElement( AmazonHmacAssertion.AWS_PFX, "AWSAccessKeyId", AmazonHmacAssertion.AWS_NS); akidElement.AppendChild(doc.CreateTextNode(akid)); XmlElement tsElement = doc.CreateElement( AmazonHmacAssertion.AWS_PFX, "Timestamp", AmazonHmacAssertion.AWS_NS); tsElement.AppendChild(doc.CreateTextNode(timestamp)); XmlElement sigElement = doc.CreateElement( AmazonHmacAssertion.AWS_PFX, "Signature", AmazonHmacAssertion.AWS_NS); sigElement.AppendChild(doc.CreateTextNode(signature)); header.AppendChild(akidElement); header.AppendChild(tsElement); header.AppendChild(sigElement); // we're done return SoapFilterResult.Continue; } } 

And this call is called like that when you make the actual web service call

 // create an instance of the serivce var api = new AWSECommerceService(); // apply the security policy, which will add the require security elements to the // outgoing SOAP header var amazonHmacAssertion = new AmazonHmacAssertion(MY_AWS_ID, MY_AWS_SECRET); api.SetPolicy(amazonHmacAssertion.Policy()); 
+3
source share
4 answers

I have finished updating the code for using WCF since it is in the current version of dev I was working on. Then I used code that was posted on Amazon forums, but made it a little easier.

UPDATE: a new, easier to use code that allows you to use configuration settings for everything

In the previous code I posted, and what I saw elsewhere when the service object is created, one of the constructor overrides is used to tell it to use HTTPS, give it an HTTPS URL and manually attach a message inspector to do the signing. The error when using the default constructor is that you lose the ability to configure the service through the configuration file.

Since then, I redid this code so that you can continue to use the default constructor, without parameters, and configure the service through the configuration file. The good thing about this is that you don’t need to recompile your code to use it, or make changes after deployment, such as maxStringContentLength (which caused this change to happen, as well as detect problems associated with doing all this in the code), I also updated the signature part a bit so that you can specify which hash algorithm to use, as well as the regular expression to retrieve the Action.

These two changes are due to the fact that not all Amazon web services use the same hashing algorithm, and Action may need to be extracted differently. This means that you can reuse the same code for each type of service by simply changing the value in the configuration file.

 public class SigningExtension : BehaviorExtensionElement { public override Type BehaviorType { get { return typeof(SigningBehavior); } } [ConfigurationProperty("actionPattern", IsRequired = true)] public string ActionPattern { get { return this["actionPattern"] as string; } set { this["actionPattern"] = value; } } [ConfigurationProperty("algorithm", IsRequired = true)] public string Algorithm { get { return this["algorithm"] as string; } set { this["algorithm"] = value; } } [ConfigurationProperty("algorithmKey", IsRequired = true)] public string AlgorithmKey { get { return this["algorithmKey"] as string; } set { this["algorithmKey"] = value; } } protected override object CreateBehavior() { var hmac = HMAC.Create(Algorithm); if (hmac == null) { throw new ArgumentException(string.Format("Algorithm of type ({0}) is not supported.", Algorithm)); } if (string.IsNullOrEmpty(AlgorithmKey)) { throw new ArgumentException("AlgorithmKey cannot be null or empty."); } hmac.Key = Encoding.UTF8.GetBytes(AlgorithmKey); return new SigningBehavior(hmac, ActionPattern); } } public class SigningBehavior : IEndpointBehavior { private HMAC algorithm; private string actionPattern; public SigningBehavior(HMAC algorithm, string actionPattern) { this.algorithm = algorithm; this.actionPattern = actionPattern; } public void Validate(ServiceEndpoint endpoint) { } public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { } public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { clientRuntime.MessageInspectors.Add(new SigningMessageInspector(algorithm, actionPattern)); } } public class SigningMessageInspector : IClientMessageInspector { private readonly HMAC Signer; private readonly Regex ActionRegex; public SigningMessageInspector(HMAC algorithm, string actionPattern) { Signer = algorithm; ActionRegex = new Regex(actionPattern); } public void AfterReceiveReply(ref Message reply, object correlationState) { } public object BeforeSendRequest(ref Message request, IClientChannel channel) { var operation = GetOperation(request.Headers.Action); var timeStamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"); var toSignBytes = Encoding.UTF8.GetBytes(operation + timeStamp); var sigBytes = Signer.ComputeHash(toSignBytes); var signature = Convert.ToBase64String(sigBytes); request.Headers.Add(MessageHeader.CreateHeader("AWSAccessKeyId", Helpers.NameSpace, Helpers.AWSAccessKeyId)); request.Headers.Add(MessageHeader.CreateHeader("Timestamp", Helpers.NameSpace, timeStamp)); request.Headers.Add(MessageHeader.CreateHeader("Signature", Helpers.NameSpace, signature)); return null; } private string GetOperation(string request) { var match = ActionRegex.Match(request); var val = match.Groups["action"]; return val.Value; } } 

To use this, you do not need to make any changes to the existing code, you can even put the signature code as a whole, if necessary. You just need to configure the configuration section like this (note: the version number is important, it does not load or start without code matching)

 <system.serviceModel> <extensions> <behaviorExtensions> <add name="signer" type="WebServices.Amazon.SigningExtension, AmazonExtensions, Version=1.3.11.7, Culture=neutral, PublicKeyToken=null" /> </behaviorExtensions> </extensions> <behaviors> <endpointBehaviors> <behavior name="AWSECommerceBehaviors"> <signer algorithm="HMACSHA256" algorithmKey="..." actionPattern="\w:\/\/.+/(?&lt;action&gt;.+)" /> </behavior> </endpointBehaviors> </behaviors> <bindings> <basicHttpBinding> <binding name="AWSECommerceServiceBinding" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard" messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered" useDefaultWebProxy="true" maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536"> <readerQuotas maxDepth="32" maxStringContentLength="16384" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384" /> <security mode="Transport"> <transport clientCredentialType="None" proxyCredentialType="None" realm="" /> <message clientCredentialType="UserName" algorithmSuite="Default" /> </security> </binding> </basicHttpBinding> </bindings> <client> <endpoint address="https://ecs.amazonaws.com/onca/soap?Service=AWSECommerceService" behaviorConfiguration="AWSECommerceBehaviors" binding="basicHttpBinding" bindingConfiguration="AWSECommerceServiceBinding" contract="WebServices.Amazon.AWSECommerceServicePortType" name="AWSECommerceServicePort" /> </client> </system.serviceModel> 
+7
source

Hey Brian, I am dealing with the same issue in my application. I use the code generated by WSDL - in fact, I created it again today to provide the latest version. I found that signing up with an X509 certificate is the easiest way. With a few minutes of testing under my belt, so far everything is working fine. Essentially you are changing:

 AWSECommerceService service = new AWSECommerceService(); // ...then invoke some AWS call 

To:

 AWSECommerceService service = new AWSECommerceService(); service.ClientCertificates.Add(X509Certificate.CreateFromCertFile(@"path/to/cert.pem")); // ...then invoke some AWS call 

Viper at bytesblocks.com sent details , including how to get the X509 certificate that Amazon generates for you.

EDIT : as the discussion here indicates that it cannot actually sign the request. Delivers when I learn more.

EDIT : this does not sign the request at all. Instead, an https connection is apparently required and a certificate is used to authenticate the SSL client. SSL authentication SSL is a rarely used SSL feature. It would be nice if the Amazon product advertising API supported it as an authentication mechanism! Unfortunately, this is not the case. The evidence is twofold: (1) this is not one of the documented authentication schemes , and (2) it does not matter which certificate you specify.

Some confusion is being added by Amazon, still not applying authentication for requests, even after they announced the deadline on August 15, 2009. This causes the requests to display correctly when the certificate is added, even if it cannot add any value.

Look at Brian Surovich for an answer to a solution that works. I leave this answer here to document an attractive, but clearly unsuccessful approach, as I still see it on Amazon blogs and forums.

+1
source

You can do this using the ProtectionLevel attributes. See Understanding Security Levels .

0
source

The soapy implementation of the signature is disgusting. I did this in PHP for use at http://www.apisigning.com/ . The trick I finally understood was that the Signature, AWSAccessKey, and Timestamp parameters should go in the SOAP header. In addition, Signature is just a hash of the Operation + timestamp, and it does not need to include any parameters.

I'm not sure how this fits into C #, but thought it might be useful

0
source

Source: https://habr.com/ru/post/912156/


All Articles