How can I generate a WM_SEC.AUTH_SIGNATURE in Elixir?

I’m trying to generate Walmart’s API WM_SEC.AUTH_SIGNATURE header.

I found a lot of examples in Ruby and Python, but no examples in Elixir. I’ve been trying to create one and try it out.

Here’s what I attempted.

# mix.exs
{:ex_crypto, git: "https://github.com/ntrepid8/ex_crypto.git", branch: "master"}

  def scrape_store_item(store_item) do
    consumer_id = "my-consumer-id"
    private_key_version = "1"
    private_key_password = "my-private-key-password"
    private_key =
      Application.app_dir(:my_app, "priv/keys/walmart/WM_IO_private_key.pem")
      |> ExPublicKey.load!(private_key_password)

    timestamp = DateTime.utc_now() |> DateTime.to_unix()

    url = "https://developer.api.walmart.com/api-proxy/service/affil/product/v2/taxonomy"
    message = "#{consumer_id}\n#{timestamp}\n#{private_key_version}\n"

    encoded_message = Base.encode64(message)

    {:ok, auth_signature} = ExPublicKey.sign(encoded_message, private_key)
    auth_signature = Base.encode64(auth_signature)

    middleware = [
      {Tesla.Middleware.BaseUrl,
        "https://developer.api.walmart.com/api-proxy/service/affil/product/v2"},
      {Tesla.Middleware.Headers,
        [
          {"WM_CONSUMER.ID", consumer_id},
          {"WM_CONSUMER.INTIMESTAMP", timestamp},
          {"WM_SEC.KEY_VERSION", private_key_version},
          {"WM_SEC.AUTH_SIGNATURE", auth_signature},
          {"WM_SHOW_REASON_CODES", "ALL"},
          {"Content-Type", "application/json"}
        ]}
    ]

    client = Tesla.client(middleware)

    {:ok, response} = Tesla.get(client, "/taxonomy") |> IO.inspect()
    IO.inspect(response.body)
  end

I get a 401 response with this code:

{:ok,
 %Tesla.Env{
   __client__: %Tesla.Client{
     adapter: nil,
     fun: nil,
     post: [],
     pre: [
       {Tesla.Middleware.BaseUrl, :call,
        ["https://developer.api.walmart.com/api-proxy/service/affil/product/v2"]},
       {Tesla.Middleware.Headers, :call,
        [
          [
            {"WM_CONSUMER.ID", "my-consumer-id"},
            {"WM_CONSUMER.INTIMESTAMP", 1654443660},
            {"WM_SEC.KEY_VERSION", "1"},
            {"WM_SEC.AUTH_SIGNATURE",
             "kdXG+e6R/n+8pH1ha1WKnzLrAHbUqmJsZfN9nOIyOzp6gsHAH7/VrX0K477cdzAq/v7YLpNJXZug3Yt6WTZoP17sZhz6Dig1BK1gg+EZqVqRaF3VJdRwBKlVgBO31s634xL7M8kPhXK11CsMxG8/9xjTGn2cDKEZ9aLeq15ECIfYa5tVtCdTcjNS4u6a7npByU9PIFp9a7n3h1KbW9C/9EA05kTuC1N0oS8nBlnKbA2+C0UW9EAvN4MaIkG0SqOqf/uEHn9BteAv8hI0Ayyny9RpJQmfZEpZ0G3htA7t1pWTzwxUsIJrF/5D1gV+IIYR7OiwHUg2RsIrnPohbznPQw=="}
          ]
        ]}
     ]
   },
   __module__: Tesla,
   body: <<31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 68, 204, 65, 138, 131, 64, 16,
     70, 225, 171, 20, 255, 122, 70, 156, 25, 199, 69, 175, 115, 132, 236, 67,
     97, 151, 177, 192, 110, 155, 174, 210, 4, 196, 187, 135, ...>>,
   headers: [
     {"content-encoding", "gzip"},
     {"content-type", "application/json;charset=utf-8"},
     {"last-modified", "Sun, 05 Jun 2022 15:41:00 GMT"},
     {"strict-transport-security", "max-age=86400"},
     {"wm_svc.env", "prod"},
     {"wm_svc.name", "affil-product"},
     {"wm_svc.version", "2.0.0"},
     {"x-lua-strict-transport-security", "max-age=86400"},
     {"x-tb", "1"},
     {"x-tb-optimization-total-bytes-saved", "0"},
     {"date", "Sun, 05 Jun 2022 15:41:00 GMT"},
     {"connection", "close"},
     {"set-cookie",
      "TS01a35e2a=01c5a4e2f95f0b472a3a7606aa7c7c33653874c13d636655443ecbca84d23369b19bc1de1973ac24c93ff1f24512e7af49264d46c6; Path=/; Secure"}
   ],
   method: :get,
   opts: [],
   query: [],
   status: 401,
   url: "https://developer.api.walmart.com/api-proxy/service/affil/product/v2/taxonomy"
 }}

Here’s the example code someone shared for a working Ruby version.

version = 'YOUR VERSION'
consumer_id = "YOUR CONSUMER ID"
time_stamp = (Time.now.to_i * 1000).to_s
p_key = "YOUR PRIVATE KEY"
digest = OpenSSL::Digest.new('sha256')

data = consumer_id + "\n" + time_stamp + "\n" + version + "\n"

k = OpenSSL::PKey::RSA.new(p_key.to_s)
digest = OpenSSL::Digest::SHA256.new
signature = k.sign(digest,data)
signature = Base64.strict_encode64(signature)

headers = {
  "WM_SEC.KEY_VERSION": version,
  "WM_CONSUMER.ID": consumer_id,
  "WM_CONSUMER.INTIMESTAMP": time_stamp,
  "WM_SEC.AUTH_SIGNATURE": signature
}

puts HTTParty.get("https://developer.api.walmart.com/api-proxy/service/affil/product/v2/taxonomy", headers: headers).parsed_response

One difference I see is that the ruby version is doing * 1000 which means you want to_unix(:millisecond)

1 Like

Good catch, I made that change. Still getting a 401.

I think the issue is somewhere with the signing but I can’t figure it out. :frowning:

Also on Stackoverflow

The ruby code does Message → SHA256 → Sign → Base64.

Your Elixir code does Message → Base64 → Sign → Base64.

I think you’ll want to replace encoded_message = Base.encode64(message) with digest = :crypto.digest(:sha256, message) and sign that.

1 Like

Thanks for the help, that :crypto.digest/2 function doesn’t seem to exist in erlang 24.3.4.

** (UndefinedFunctionError) function :crypto.digest/2 is undefined or private

So then I tried this, but the API is still returning a 401 unauthorized.

digest =
:crypto.hash(:sha256, message)
|> IO.inspect()

{:ok, auth_signature} = ExPublicKey.sign(digest, private_key)
auth_signature = Base.encode64(auth_signature)

I’m going to try running that Ruby sample in the OP using my API keys.

Ok I can confirm that the Ruby example I posted above works.

Here’s the working Walmart API Ruby version with some missing require statement for would be Googlers.

require 'openssl'
require 'base64'
require 'httparty'

version = '1'
consumer_id = "my-consumer-id"
time_stamp = (Time.now.to_i * 1000).to_s

digest = OpenSSL::Digest::SHA256.new
p_key = OpenSSL::PKey::RSA.new(File.open("walmart/WM_IO_private_key.pem").read)
data = consumer_id + "\n" + time_stamp + "\n" + version + "\n"
signature = p_key.sign(digest, data)
signature = Base64.strict_encode64(signature)

headers = {
  "WM_SEC.KEY_VERSION": version,
  "WM_CONSUMER.ID": consumer_id,
  "WM_CONSUMER.INTIMESTAMP": time_stamp,
  "WM_SEC.AUTH_SIGNATURE": signature
}

puts HTTParty.get("https://developer.api.walmart.com/api-proxy/service/affil/product/v2/taxonomy", headers: headers).parsed_response

Would love some help for the Elixir part. I’m pretty stuck :sob:

I think he wanted to mention :crypto.hash/2 :slight_smile:

I figured it out with all of your help gents.

For Googlers here’s how you generate WM_SEC.AUTH_SIGNATURE in Elixir:

Make sure you have ex_crypto package installed from master branch since the latest version has necessary changes but is not published.

{:ex_crypto, git: "https://github.com/ntrepid8/ex_crypto.git", branch: "master"}

Then here’s the solution:

version = "1"
consumer_id = "my-consumer-id"
timestamp = DateTime.utc_now() |> DateTime.to_unix(:millisecond)

data = "#{consumer_id}\n#{timestamp}\n#{version}\n"

private_key =
  Application.app_dir(:my_app, "priv/keys/walmart/WM_IO_private_key.pem")
  |> ExPublicKey.load!()

{:ok, auth_signature} = ExPublicKey.sign(data, private_key)
auth_signature = Base.encode64(auth_signature)

middleware = [
  {Tesla.Middleware.BaseUrl,
    "https://developer.api.walmart.com/api-proxy/service/affil/product/v2"},
  {Tesla.Middleware.Headers,
    [
      {"WM_CONSUMER.ID", consumer_id},
      {"WM_CONSUMER.INTIMESTAMP", timestamp},
      {"WM_SEC.KEY_VERSION", version},
      {"WM_SEC.AUTH_SIGNATURE", auth_signature}
    ]}
]

client = Tesla.client(middleware)

Tesla.get(client, "/taxonomy")

I posted over on StackOverflow too, but here’s a version without ex_crypto

Mix.install([:tesla])

key_version = System.fetch_env!("WALLMART_KEY_VERSION")
consumer_id = System.fetch_env!("WALLMART_CONSUMER_ID")
private_key_pem = File.read!("WM_IO_private_key.pem")

[pem_entry] = :public_key.pem_decode(private_key_pem)
private_key = :public_key.pem_entry_decode(pem_entry)
timestamp = System.os_time(:millisecond)

auth_signature =
  "#{consumer_id}\n#{timestamp}\n#{key_version}\n"
  |> :public_key.sign(:sha256, private_key)
  |> Base.encode64()

url = "https://developer.api.walmart.com/api-proxy/service/affil/product/v2/taxonomy"

headers = [
  {"WM_CONSUMER.ID", consumer_id},
  {"WM_CONSUMER.INTIMESTAMP", timestamp},
  {"WM_SEC.KEY_VERSION", key_version},
  {"WM_SEC.AUTH_SIGNATURE", auth_signature}
]

{:ok, %{body: body}} = Tesla.get(url, headers: headers)
IO.puts(body)
2 Likes

I was going to suggest that: the fact that ex_crypto implicitly hashes the input using SHA256, without even documenting that, may have contributed to the problem in the first place.

1 Like

Just wanted to come back to quote the Wallmart example and say how terrible this is as a their official instructions to generate their API headers. They would struggle to add any more layers of indirection. Building up an unsorted map of input key/values, converting values to a sorted newline-delimited string via a long-winded function called canonicalize, which builds up two new key and value strings using the unsorted map and builds up a sorted set as to index it by (assign each string from the generically typed map to an Object and convert it back to a String again for good measure). Assign the key and value strings to a variable called array and immediately discard the first value (the key string), just using the value string as input to the signature function. All of this just to concatenate three strings we already know with newlines, obfuscated by putting things in and out of maps and passing them around in variables call map, array[1], and canonicalize. Feels like this wasn’t reviewed at all. Also the docs say “Please review the various options you have to make from the documentation below.” - it’s not like there are any options, it’s do it correctly, or don’t use the api. :man_shrugging:

public class SignatureGenerator {

    public static void main(String[] args) {
        SignatureGenerator generator = new SignatureGenerator();

        String consumerId = "1de0df0a-9de1-47b5-bb45-2dc8f26c5ab2";
        String priviateKeyVersion = "1";
        String key = "YOUR FILE CONTENT HERE";

        long intimestamp = System.currentTimeMillis();

        System.out.println("consumerId: " + consumerId);
        System.out.println("intimestamp: " + intimestamp);

        Map<String, String> map = new HashMap<>();
        map.put("WM_CONSUMER.ID", consumerId);
        map.put("WM_CONSUMER.INTIMESTAMP", Long.toString(intimestamp));
        map.put("WM_SEC.KEY_VERSION", priviateKeyVersion);

        String[] array = canonicalize(map);

        String data = null;

        try {
            data = generator.generateSignature(key, array[1]);
        } catch(Exception e) { }
        System.out.println("Signature: " + data);
    }
    public String generateSignature(String key, String stringToSign) throws Exception {
        Signature signatureInstance = Signature.getInstance("SHA256WithRSA");

        ServiceKeyRep keyRep = new ServiceKeyRep(KeyRep.Type.PRIVATE, "RSA", "PKCS#8", Base64.decodeBase64(key));

        PrivateKey resolvedPrivateKey = (PrivateKey) keyRep.readResolve();

        signatureInstance.initSign(resolvedPrivateKey);

        byte[] bytesToSign = stringToSign.getBytes("UTF-8");
        signatureInstance.update(bytesToSign);
        byte[] signatureBytes = signatureInstance.sign();

        String signatureString = Base64.encodeBase64String(signatureBytes);

        return signatureString;
    }
    protected static String[] canonicalize(Map<String, String> headersToSign) {
        StringBuffer canonicalizedStrBuffer=new StringBuffer();
        StringBuffer parameterNamesBuffer=new StringBuffer();
        Set<String> keySet=headersToSign.keySet();

        // Create sorted key set to enforce order on the key names
        SortedSet<String> sortedKeySet=new TreeSet<String>(keySet);
        for (String key :sortedKeySet) {
            Object val=headersToSign.get(key);
            parameterNamesBuffer.append(key.trim()).append(";");
            canonicalizedStrBuffer.append(val.toString().trim()).append("\n");
        }
        return new String[] {parameterNamesBuffer.toString(), canonicalizedStrBuffer.toString()};
    }

    class ServiceKeyRep extends KeyRep  {
        private static final long serialVersionUID = -7213340660431987616L;
        public ServiceKeyRep(Type type, String algorithm, String format, byte[] encoded) {
            super(type, algorithm, format, encoded);
        }
        protected Object readResolve() throws ObjectStreamException {
            return super.readResolve();
        }
    }
}

The example code on their site isn’t even formatted I couldn’t believe it. This isn’t some rinky dink company and some obscure API endpoint that nobody would use. This is literally their official API authentication method!