Pregunta

La API de publicidad de productos de Amazon (anteriormente Servicio web de Amazon Associates o Amazon AWS) ha implementado una nueva regla que es que antes del 15 de agosto de 2009 todas las solicitudes de servicios web deben estar firmadas. Han proporcionado código de muestra en su sitio que muestra cómo hacer esto en C # usando REST y SOAP. La implementación que estoy usando es SOAP. Puede encontrar el código de ejemplo aquí , I & # 8217; No lo incluyo porque hay una cantidad justa.

El problema que tengo es que su código de muestra usa WSE 3 y nuestro código actual no usa WSE. ¿Alguien sabe cómo implementar esta actualización con solo usar el código generado automáticamente desde el WSDL? Me gustaría no tener que cambiar a WSE 3 ahora mismo si no tengo que hacerlo ya que esta actualización es más que un parche rápido que nos retiene hasta que podamos implementar esto completamente en la versión actual. versión dev (el 3 de agosto están empezando a eliminar 1 de cada 5 solicitudes en el entorno real, si no están firmadas, lo que es una mala noticia para nuestra aplicación).

Aquí & # 8217; s un fragmento de la parte principal que hace la firma real de la solicitud 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;
    }
}

Y eso se llama así cuando se realiza la llamada al servicio 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());
¿Fue útil?

Solución

Terminé de actualizar el código para usar WCF, ya que es lo que está en la versión de desarrollo actual en la que he estado trabajando. Luego utilicé un código que se publicó en los foros de Amazon, pero lo hice un poco más fácil de usar.

ACTUALIZACIÓN: un código más fácil de usar que te permite seguir utilizando la configuración para todo

En el código anterior que publiqué, y lo que he visto en otra parte, cuando se crea el objeto de servicio, se usa una de las anulaciones del constructor para decirle que use HTTPS, asígnele la URL de HTTPS y adjunte manualmente el inspector de mensajes Eso hará la firma. La desventaja de no usar el constructor predeterminado es que se pierde la capacidad de configurar el servicio a través del archivo de configuración.

Desde entonces he rehecho este código para que pueda continuar usando el constructor predeterminado, sin parámetros, y configurar el servicio a través del archivo de configuración. La ventaja de esto es que no tiene que volver a compilar su código para usarlo, o realizar cambios una vez implementados, como maxStringContentLength (que es lo que causó este cambio, así como descubrir las desventajas para hacerlo todo en código) . También actualicé un poco la parte de la firma para que pueda decirle qué algoritmo de hash usar y la expresión regular para extraer la Acción.

Estos dos cambios se deben a que no todos los servicios web de Amazon utilizan el mismo algoritmo de hash y es posible que la Acción deba extraerse de manera diferente. Esto significa que puede reutilizar el mismo código para cada tipo de servicio simplemente cambiando lo que & # 8217; s en el archivo de configuración.

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 esto, no necesita realizar ningún cambio en su código existente, incluso puede colocar el código de firma en otro conjunto si es necesario. Solo necesita configurar la sección de configuración como tal (nota: el número de versión es importante, sin que coincida con el código no se cargará o ejecutará)

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

Otros consejos

Hola, Brian, estoy lidiando con el mismo problema en mi aplicación. Estoy utilizando el código generado por WSDL; de hecho, lo volví a generar hoy para garantizar la última versión. Encontré que firmar con un certificado X509 es la ruta más sencilla. Con unos minutos de pruebas en mi haber, hasta ahora parece funcionar bien. Esencialmente cambias 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 en bytesblocks.com publicó más detalles , incluido cómo obtener el certificado X509 que Amazon genera para usted.

EDIT : como la discusión aquí indica, esto podría no firmar la solicitud. Publicaré a medida que aprenda más.

EDIT : esto no parece firmar la solicitud en absoluto. En su lugar, parece que requiere una conexión https y utiliza el certificado para la autenticación de cliente SSL. La autenticación de cliente SSL es una característica de SSL que se usa con poca frecuencia. ¡Hubiera sido bueno si la API de publicidad de productos de Amazon lo admitiera como un mecanismo de autenticación! Lamentablemente ese no parece ser el caso. La evidencia es doble: (1) no es uno de los documentado esquemas de autenticación , y (2) no importa qué certificado especifique.

Amazon aún agrega cierta confusión al no aplicar la autenticación en las solicitudes, incluso después de su fecha límite del 15 de agosto de 2009. Esto hace que las solicitudes parezcan aprobarse correctamente cuando se agrega el certificado, aunque es posible que no agregue ningún valor.

Observe la respuesta de Brian Surowiec para encontrar una solución que funcione. Dejo esta respuesta aquí para documentar el enfoque atractivo pero aparentemente fallido, ya que aún puedo verlo discutido en los blogs y foros de Amazon.

Puede hacer esto usando los atributos ProtectionLevel . Consulte Descripción del nivel de protección .

La implementación en jabón de la firma es algo desagradable. Lo hice en PHP para usar en http://www.apisigning.com/ . El truco que finalmente descubrí fue que los parámetros Signature, AWSAccessKey y Timestamp deben ir en el encabezado SOAP. Además, la firma es solo un hash de la marca de tiempo de Operation +, y no necesita incluir ningún parámetro.

No estoy seguro de cómo eso encaja en C #, pero pensé que podría ser de alguna utilidad

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top