SOAP を使用し、WSE を使用せずに .NET で Amazon ウェブ サービス リクエストに署名する方法
-
05-07-2019 - |
質問
Amazon Product Advertising API (以前の Amazon Associates Web Service または Amazon AWS) は、2009 年 8 月 15 日までに、Amazon Product Advertising API (以前の Amazon Associates Web Service または Amazon AWS) に対するすべての Web サービス リクエストに署名する必要があるという新しいルールを実装しました。彼らは、REST と SOAP の両方を使用して C# でこれを行う方法を示すサンプル コードをサイトに提供しています。私が使用している実装は SOAP です。サンプルコードを見つけることができます ここ, かなりの量があるので省きます。
私が抱えている問題は、サンプル コードでは WSE 3 が使用されており、現在のコードでは WSE が使用されていないことです。WSDL から自動生成されたコードを使用するだけでこの更新を実装する方法を知っている人はいますか?このアップデートは、現在の開発バージョン (8 月) で完全に実装できるまで保留するためのクイック パッチのようなものであるため、必要がない場合は、今すぐ WSE 3 に切り替える必要はないと思います。 3 番目に、ライブ環境では、署名されていないリクエストの 5 件に 1 件がドロップされ始めており、これはアプリケーションにとって悪いニュースです)。
SOAP リクエストの実際の署名を行う主要部分のスニペットを次に示します。
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's 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;
}
}
そして、それは実際のWebサービス呼び出しを行うときに次のように呼び出されます
// 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());
解決
WCFを使用するようにコードを更新することになりました。これは、現在取り組んでいる開発バージョンのWCFにあるからです。次に、Amazonフォーラムに投稿されたコードを使用しましたが、少し使いやすくしました。
更新:新しい使いやすいコードにより、すべての設定を引き続き使用できます
私が投稿した以前のコードおよび他の場所で見たものでは、サービスオブジェクトが作成されると、コンストラクタオーバーライドの1つを使用して、HTTPSを使用するように指示し、HTTPS URLを与え、メッセージインスペクターを手動で添付しますそれは署名を行います。デフォルトのコンストラクターを使用しないことの欠点は、構成ファイルを介してサービスを構成する機能が失われることです。
このコードをやり直したので、引き続きデフォルトのパラメータなしのコンストラクタを使用し、構成ファイルを介してサービスを構成できます。これの利点は、これを使用するためにコードを再コンパイルする必要がないことです。または、maxStringContentLengthなどの展開後に変更を加える必要がありません(これにより、この変更が行われ、コードですべてを行うことの失敗を発見できます)。 。また、アクションを抽出するための正規表現だけでなく、使用するハッシュアルゴリズムを伝えることができるように、署名部分も少し更新しました。
これら2つの変更は、AmazonのすべてのWebサービスが同じハッシュアルゴリズムを使用しているわけではなく、アクションを異なる方法で抽出する必要があるためです。つまり、構成ファイルの内容を変更するだけで、各サービスタイプに同じコードを再利用できます。
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;
}
}
これを使用するには、既存のコードを変更する必要はありません。必要に応じて、署名コードを他のアセンブリ全体に配置することもできます。 configセクションをセットアップするだけです(注:バージョン番号は重要です。一致しないと、コードはロードまたは実行されません)
<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:\/\/.+/(?<action>.+)" />
</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>
他のヒント
こんにちは、ブライアン、私は自分のアプリで同じ問題に取り組んでいます。私は WSDL で生成されたコードを使用しています。実際、最新バージョンを確保するために今日再度生成しました。X509 証明書を使用して署名するのが最も簡単な方法であることがわかりました。数分間テストを行ったところ、今のところ問題なく動作しているようです。基本的に次のように変更します。
AWSECommerceService service = new AWSECommerceService();
// ...then invoke some AWS call
に:
AWSECommerceService service = new AWSECommerceService();
service.ClientCertificates.Add(X509Certificate.CreateFromCertFile(@"path/to/cert.pem"));
// ...then invoke some AWS call
bytesblocks.com に Viper が投稿されました さらに詳しく, には、Amazon が生成する X509 証明書の取得方法が含まれます。
編集:議論として ここ これは、実際にはリクエストに署名していない可能性があることを示しています。さらに学び次第投稿します。
編集:これはリクエストにまったく署名していないようです。代わりに、https 接続が必要なようで、SSL クライアント認証に証明書を使用します。SSL クライアント認証は、あまり使用されない SSL の機能です。Amazon 製品広告 API が認証メカニズムとしてサポートしていれば良かったのにと思います。残念ながらそうではないようです。証拠は 2 つあります。(1) それは次のどれでもない 文書化された認証スキーム, 、(2) どの証明書を指定するかは関係ありません。
Amazon が宣言した 2009 年 8 月 15 日の期限を過ぎても、依然としてリクエストに対する認証を強制していないため、多少の混乱が生じています。これにより、値が追加されない場合でも、証明書を追加するとリクエストが正しく通過したように見えます。
有効な解決策については、Brian Surowiec の回答を参照してください。ブログや Amazon フォーラムでまだ議論されているのを目にすることができるため、魅力的ではあるが明らかに失敗したアプローチを文書化するために、この回答をここに残しておきます。
ProtectionLevel
属性を使用してこれを行うことができます。 保護レベルについてを参照してください。
署名のSOAP実装は、やや厄介です。 http://www.apisigning.com/ で使用するためにPHPで作成しました。最後にわかったトリックは、Signature、AWSAccessKey、およびTimestampパラメーターをSOAPヘッダーに含める必要があることです。また、署名は操作+タイムスタンプの単なるハッシュであり、パラメーターを含める必要はありません。
それがC#にどのように適合するかはわかりませんが、それは何らかの有用性があると考えました