Pergunta

A API de publicidade de produtos da Amazon (anteriormente Serviço da Web da Amazon Associates ou Amazon AWS) implementou uma nova regra que é até 15 de agosto de 2009, todas as solicitações de serviço da Web para eles devem ser assinadas. Eles forneceram código de amostra em seu site, mostrando como fazer isso em C# usando REST e SOAP. A implementação que estou usando é SOAP. Você pode encontrar o código de amostra aqui, Não estou incluindo porque há uma quantia razoável.

O problema que estou tendo é o código de amostra deles usa o WSE 3 e nosso código atual não usa WSE. Alguém sabe como implementar esta atualização apenas usando o código gerado automaticamente do WSDL? Eu gostaria de não precisar mudar para o WSE 3 agora, se não for necessário, pois esta atualização é mais um patch rápido para nos segurar até que possamos implementar completamente isso na versão atual do Dev (agosto 3º Eles estão começando a cair 1 em 5 solicitações, no ambiente ao vivo, se não forem assinadas, o que é uma má notícia para o nosso aplicativo).

Aqui está um trecho da parte principal que faz a assinatura real da solicitação de sabão.

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;
    }
}

E isso é chamado assim ao fazer a chamada de serviço da web real

// 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());
Foi útil?

Solução

Acabei atualizando o código para usar o WCF, já que é nisso que ele está na versão de desenvolvimento atual em que tenho trabalhado. Depois, usei algum código publicado nos fóruns da Amazon, mas tornou um pouco mais fácil de usar.

ATUALIZAR: novo código mais fácil de usar que permite que você ainda use as configurações de configuração para tudo

No código anterior que publiquei e o que vi em outro lugar, quando o objeto de serviço é criado, uma das substituições do construtor é usada para dizer para usar o HTTPS, dê o URL HTTPS e para anexar manualmente o inspetor de mensagem que fará a assinatura. A queda para não usar o construtor padrão é que você perde a capacidade de configurar o serviço através do arquivo de configuração.

Desde então, refiro esse código para que você possa continuar usando o construtor padrão, sem parametera, e configurar o serviço através do arquivo de configuração. O Benifit disso é que você não precisa recompilar seu código para usá -lo ou fazer alterações uma vez implantadas, como no MaxStringContentLength (que foi o que causou essa alteração, além de descobrir as quedas para fazer tudo em código) . Eu também atualizei um pouco a parte de assinatura para que você possa dizer qual algoritmo hash de hash para usar, bem como o Regex para extrair a ação.

Essas duas alterações são porque nem todos os serviços da Web da Amazon usam o mesmo algoritmo de hash e a ação pode precisar ser extraída de maneira diferente. Isso significa que você pode reutilizar o mesmo código para cada tipo de serviço apenas alterando o que está no arquivo de configuração.

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;
    }
}

Para usar isso, você não precisa fazer alterações no seu código existente, você pode até colocar o código de assinatura em outra montagem, se necessário. Você só precisa configurar a seção de configuração como assim (note: o número da versão é importante, sem que ele corresponda ao código não carregar ou executar)

<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>

Outras dicas

Ei Brian, estou lidando com o mesmo problema no meu aplicativo. Estou usando o código gerado pelo WSDL - na verdade, eu o gerei novamente hoje para garantir a versão mais recente. Descobri que a assinatura com um certificado X509 é o caminho mais direto. Com alguns minutos de teste, até agora parece funcionar bem. Essencialmente, você muda de:

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

Para:

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

Viper em bytesblocks.com postou mais detalhes, incluindo como obter o certificado X509 que a Amazon gera para você.

EDITAR: como a discussão aqui Indica, isso pode não assinar a solicitação. Vou postar enquanto eu aprendo mais.

EDITAR: Isso não parece assinar a solicitação. Em vez disso, parece exigir uma conexão HTTPS e usa o certificado para autenticação do cliente SSL. A autenticação do cliente SSL é um recurso usado com pouca frequência do SSL. Teria sido bom se a API de publicidade de produtos da Amazon a apoiasse como um mecanismo de autenticação! Infelizmente, isso não parece ser o caso. A evidência é dupla: (1) não é um dos esquemas de autenticação documentados, e (2) não importa qual certificado você especificar.

Alguma confusão é adicionada pela Amazon ainda não aplica a autenticação por solicitações, mesmo após proclamar o prazo de 15 de agosto de 2009. Isso faz com que as solicitações pareçam passar corretamente quando o certificado é adicionado, mesmo que não agregue nenhum valor.

Veja a resposta de Brian Surowiec para uma solução que funciona. Estou deixando esta resposta aqui para documentar a abordagem atraente, mas aparentemente falhou, pois ainda posso vê -la discutida em blogs e fóruns da Amazon.

Você pode fazer isso usando o ProtectionLevel atributos. Ver Entendendo o nível de proteção.

A implementação do SOAP da assinatura é meio desagradável. Eu fiz isso no PHP para uso em http://www.apisigning.com/. O truque que finalmente descobri foi que os parâmetros de assinatura, AwsaccessKey e timestamp precisam ir no cabeçalho do sabão. Além disso, a assinatura é apenas um hash da operação + timestamp e não precisa incluir nenhum parâmetros.

Não tenho certeza de como isso se encaixa em C#, mas pensei que poderia ser de algum uso

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top