eBay / digital-signature-verification-ebay-api

Verification of digital signatures for use by developers sending HTTP requests to eBay's APIs
Apache License 2.0
8 stars 7 forks source link

Signature verification in C# #4

Closed jsaxdev closed 1 year ago

jsaxdev commented 2 years ago

Hello,

I'd like to know how to sign and verify signature in C# ? I tried to reuse the same logic as in this repo source code. This is what I came with but it is still showing that signature invalid:

using Newtonsoft.Json;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Security;
using RestSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace EbayHttpSignature
{
    internal class Program
    {
        private static string[] signatureParameters = new string[] { "content-digest", "x-ebay-signature-key", "@method", "@path", "@authority" };
        private static string signatureInput = string.Empty;

        static void Main(string[] args)
        {
            var client = new RestClient("http://localhost:8080");
            var request = new RestRequest("verifysignature", Method.Post);
            var uri = client.BuildUri(request);
            var message = JsonConvert.SerializeObject("{\"hello\": \"world\"}");
            var contentHash = ComputeContentHash(message);
            request.AddStringBody(message, DataFormat.Json);
            request.AddHeader("x-ebay-signature-key", GetJWE());
            request.AddHeader("Content-Digest", $"sha-256=:{contentHash}:");
            request.AddHeader("Signature", $"sig1=:{SignSignature(request, uri)}:");
            request.AddHeader("Signature-Input", $"sig1={signatureInput}");
            var result = client.Execute(request);
        }

        static string GetJWE()
        {
            try
            {
                var now = DateTimeOffset.Now;
                var payload = new Dictionary<string, object>();
                payload.Add("appid", "app1");
                payload.Add("pkey", "MCowBQYDK2VwAyEAJrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=");
                payload.Add("iat", now.ToUnixTimeSeconds());
                payload.Add("nbf", now.ToUnixTimeSeconds());
                payload.Add("exp", new DateTimeOffset(new DateTime(now.Ticks + 1000L * 60 * 60 * 24 * 365 * 10)).ToUnixTimeSeconds());
                payload.Add("jti", Guid.NewGuid());
                payload.Add("alg", "EdDSA");
                payload.Add("kid", "abc");
                var jwe = Jose.JWE.Encrypt(JsonConvert.SerializeObject(payload),
                    new[] { new Jose.JweRecipient(Jose.JweAlgorithm.A256GCMKW, Convert.FromBase64String("uSA8lSm4fWRrDs8JvjS/Y6D5Ohp3mIKC4AhN/nBaGxs=")) },
                    Jose.JweEncryption.A256GCM,
                    compression: Jose.JweCompression.DEF,
                    mode: Jose.SerializationMode.Compact);

                return jwe;
            }
            catch (Exception ex)
            {
                throw new SignatureException("Error creating JWE: " + ex.Message, ex);
            }
        }

        static bool VerifySignature(string signature, string signatureBase)
        {
            var keyParameter = ReadAsymmetricKeyParameter("C:\\ebay\\publickey.pem");
            var signer = new Ed25519Signer();
            signer.Init(false, keyParameter);
            var signatureBytes = Convert.FromBase64String(signature);
            var signatureBaseBytes = Encoding.UTF8.GetBytes(signatureBase);
            signer.BlockUpdate(signatureBaseBytes, 0, signatureBaseBytes.Length);
            return signer.VerifySignature(signatureBytes);
        }

        static string SignSignature(RestRequest request, Uri uri)
        {
            var signature = GetSignature(request, uri);
            var signatureBase = Encoding.UTF8.GetBytes(signature);
            var key = PrivateKeyFactory.CreateKey(Convert.FromBase64String("MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF"));
            var signer = new Ed25519Signer();
            signer.Init(true, key);
            signer.BlockUpdate(signatureBase, 0, signatureBase.Length);
            var sig = Convert.ToBase64String(signer.GenerateSignature(), Base64FormattingOptions.None);
            //var isVerified = VerifySignature(sig, signature);
            return sig;
        }

        static Org.BouncyCastle.Crypto.AsymmetricKeyParameter ReadAsymmetricKeyParameter(string pemFilename)
        {
            var fileStream = System.IO.File.OpenText(pemFilename);
            var pemReader = new Org.BouncyCastle.OpenSsl.PemReader(fileStream);
            var KeyParameter = (Org.BouncyCastle.Crypto.AsymmetricKeyParameter)pemReader.ReadObject();
            return KeyParameter;
        }

        static string GetSignature(RestRequest request, Uri uri)
        {
            var sb = new StringBuilder();
            var requestHeaders = request.Parameters.Where(x => x.Type == ParameterType.HttpHeader);
            foreach (var param in signatureParameters)
            {
                sb.Append($"\"{param.ToLower()}\": ");
                if (param.StartsWith("@"))
                {
                    switch (param.ToLower())
                    {
                        case "@method":
                            sb.Append(request.Method.ToString().ToUpper());
                            break;
                        case "@path":
                            sb.Append(uri.AbsolutePath);
                            break;
                        case "@authority":
                            sb.Append(uri.Authority);
                            break;
                    }
                }
                else
                {
                    var value = requestHeaders.FirstOrDefault(x => x.Name.ToLower() == param.ToLower());
                    if (value is null)
                        throw new Exception("Header " + param + " not included in message");
                    sb.Append(value.Value);
                }
                sb.AppendLine();
            }

            sb.Append("\"@signature-params\": ");
            signatureInput = GetSignatureInput();
            sb.Append(signatureInput);

            return sb.ToString();
        }

        static string GetSignatureInput()
        {
            var sb = new StringBuilder($"(");
            foreach (var param in signatureParameters)
            {
                if (sb.ToString().EndsWith("("))
                    sb.Append($"\"{param}\"");
                else
                    sb.Append($" \"{param}\"");
            }
            sb.Append($");created={DateTimeOffset.Now.ToUnixTimeSeconds()};keyid=\"abc\";alg=\"EdDSA\"");

            return sb.ToString();
        }

        static string ComputeContentHash(string content)
        {
            using (var sha256 = SHA256.Create())
            {
                byte[] hashedBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(content));
                return Convert.ToBase64String(hashedBytes);
            }
        }
    }
}
uherberg commented 2 years ago

@jsaxdev Try it again once https://github.com/eBay/digital-signature-verification-ebay-api/pull/6 is merged. There was some issue with the code. I added a "cipher" claim into the JWE (just like it will be in production), which is either "RSA" or "Ed25519".

uherberg commented 2 years ago

@jsaxdev And cool that you wrote this in C#. We would love to be able to provide your code as sample code sniplet on the eBay developer web page if you agree to it.

uherberg commented 2 years ago

@jsaxdev I merged the PR. Please try it again and let me know if it works. If not, I will take a closer look at your code

UkeHa commented 1 year ago

@uherberg, sadly it doesn't work. I changed the payload "payload.Add("alg", "EdDSA");" to payload.Add("cipher", "Ed25519"); and it says the JWE isn't correct.

I get this error "Signature invalid" even if i force iat and nbf to be 1658440308

public static string GetJWE()
        {
            var expirationTime = (long)DateTime.UtcNow.AddYears(10).Subtract(DateTime.UnixEpoch).TotalSeconds;
            try
            {
                var now = DateTimeOffset.Now;
                var payload = new Dictionary<string, object>
                {
                    { "cipher", "Ed25519" },
                    { "nbf", 1658440308 },// now.ToUnixTimeSeconds());
                    { "appid", "app1" },
                    { "pkey", "MCowBQYDK2VwAyEAJrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=" },
                    { "exp", 1985780626 }, //expirationTime);
                    { "iat", 1658440308 },// now.ToUnixTimeSeconds());
                    { "jti", "1c687eb7-dee2-40c8-8db1-bb549650c06c" }, //Guid.NewGuid());
                    { "kid", "abc" }
                };
                var jwe = JWE.Encrypt(JsonConvert.SerializeObject(payload),
                    new[] { new JweRecipient(JweAlgorithm.A256GCMKW, Convert.FromBase64String("uSA8lSm4fWRrDs8JvjS/Y6D5Ohp3mIKC4AhN/nBaGxs=")) },
                    JweEncryption.A256GCM,
                    compression: JweCompression.DEF,
                    mode: SerializationMode.Compact);                 
                return jwe;
            }
            catch (Exception ex)
            {
                throw new SignatureException("Error creating JWE: " + ex.Message, ex);
            }
        } 

Even a version with hardcoded info results in a "Signature invalid"

uherberg commented 1 year ago

@UkeHa You would not need to generate the JWE yourself. You can use the test JWE from the documentation with the Docker container. When you want to try it on eBay's sandbox or production API instead of the Docker container, you would have to get the JWE via the Key Management API as described in https://developer.ebay.com/develop/guides/digital-signatures-for-apis, Section 1

UkeHa commented 1 year ago

@uherberg even if i replace the jwe-function with a return of the public key as jwe from the readme.md i still get invalid signatures with above code. Am i correct in how this should work:

  1. Create a request with the headers "content-digest", "x-ebay-signature-key", "@method", "@path", "@authority"
  2. content-digest is the body of the request and is only filled if the request has a body. If so, the content has to be a sha256 hash as a byte array. Then this header info will be converted to a base64 encoded string
  3. x-ebay-signature-key is the public key as jwe (which we can easily fetch from the api via "getSigningKey" or with the static info if we use the public/private keys from the readme
  4. method is POST/GET in upper letters, depending on which method we use in our http requests
  5. path is the absolute path from the endpoint e.g. "@path": /verifysignature
  6. the authority is the domain-part of the host: e.g. "@authority": localhost:8080
  7. everything is concluded by the signature-params that must be in the same order as the info above: "@signature-params": ("content-digest" "x-ebay-signature-key" "@method" "@path" "@authority");created=1670428920;keyid="abc";cipher="EdDSA"

Is that correct?

uherberg commented 1 year ago

@UkeHa I just tried the example; I replaced the method GetJWE with return "eyJ6aXAiOiJERUYiLC..."; (the JWE from the readme) and it worked.

To your questions:

  1. The at-headers are pseudo-headers; you don't add them. This is explained in Section 2.2 of the IETF draft (https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#name-derived-components). Your HTTP request needs to contain the following headers (in addition to any header you want to send): content-digest (only for POST, contains the hash of the body), x-ebay-signature-key (contains the JWE), Signature-Input (contains the signature parameters), Signature (contains the signature) and -- for the next few months only -- x-ebay-enforce-signature. This is explained at https://developer.ebay.com/develop/guides/digital-signatures-for-apis
  2. Yes, it contains a hash of the body (if POST)
  3. Correct
  4. Yes, but you don't send this as header; this is inferred from the server side
  5. Same as 4, you don't send this as header
  6. Same as 4, you don't send this as header
  7. No, Signature-params is not sent as header; it is used to calculate the signature base (as done in the sample code on this Issue). But you need to send Signature-Input, which contains pretty much the same information. All this is explained in https://developer.ebay.com/develop/guides/digital-signatures-for-apis, and in the IETF drafts

Let me know if this helps. You can update the sample from this page and write to the Console all the headers as well as the signature base.

uherberg commented 1 year ago

I updated the code slightly from @jsaxdev to work with the sandbox API:

using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Security;
using RestSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace EbayHttpSignature
{
    internal class Program
    {
        private static string[] signatureParameters = new string[] { "content-digest", "x-ebay-signature-key", "@method", "@path", "@authority" };
        private static string signatureInput = string.Empty;

        static void Main(string[] args)
        {
            var token = "enter token here";
            var jwe = "enter jwe here";
            var privateKey = "enter private key from Key Management API without BEGIN and END PRIVATE KEY headers";

            var client = new RestClient("https://api.sandbox.ebay.com");
            var request = new RestRequest("/sell/fulfillment/v1/order/14-00032-43825/issue_refund", Method.Post);
            var uri = client.BuildUri(request);
            var message = "{\"orderLevelRefundAmount\": {\"currency\": \"USD\",\"value\": 10.39},\"reasonForRefund\": \"ITEM_NOT_AS_DESCRIBED\",\"comment\": \"public API test_order_partial_refund\"}";
            var contentHash = ComputeContentHash(message);

            request.AddStringBody(message, DataFormat.Json);
            request.AddHeader("x-ebay-signature-key", $"{jwe}");
            request.AddHeader("Content-Digest", $"sha-256=:{contentHash}:");
            request.AddHeader("Signature", $"sig1=:{SignSignature(privateKey, request, uri)}:");
            request.AddHeader("Signature-Input", $"sig1={signatureInput}");
            request.AddHeader("x-ebay-enforce-signature", "true");
            request.AddHeader("Authorization", $"Bearer {token}");
            var result = client.Execute(request);
            Console.WriteLine(result.StatusCode);
            Console.WriteLine(result.Content);
        }

        static string SignSignature(string privateKey, RestRequest request, Uri uri)
        {
            var signature = GetSignature(request, uri);
            var signatureBase = Encoding.UTF8.GetBytes(signature);
            var key = PrivateKeyFactory.CreateKey(Convert.FromBase64String(privateKey));
            var signer = new Ed25519Signer();
            signer.Init(true, key);
            signer.BlockUpdate(signatureBase, 0, signatureBase.Length);
            var sig = Convert.ToBase64String(signer.GenerateSignature(), Base64FormattingOptions.None);
            return sig;
        }

        static Org.BouncyCastle.Crypto.AsymmetricKeyParameter ReadAsymmetricKeyParameter(string pemFilename)
        {
            var fileStream = System.IO.File.OpenText(pemFilename);
            var pemReader = new Org.BouncyCastle.OpenSsl.PemReader(fileStream);
            var KeyParameter = (Org.BouncyCastle.Crypto.AsymmetricKeyParameter)pemReader.ReadObject();
            return KeyParameter;
        }

        static string GetSignature(RestRequest request, Uri uri)
        {
            var sb = new StringBuilder();
            var requestHeaders = request.Parameters.Where(x => x.Type == ParameterType.HttpHeader);
            foreach (var param in signatureParameters)
            {
                sb.Append($"\"{param.ToLower()}\": ");
                if (param.StartsWith("@"))
                {
                    switch (param.ToLower())
                    {
                        case "@method":
                            sb.Append(request.Method.ToString().ToUpper());
                            break;
                        case "@path":
                            sb.Append(uri.AbsolutePath);
                            break;
                        case "@authority":
                            sb.Append(uri.Authority);
                            break;
                    }
                }
                else
                {
                    var value = requestHeaders.FirstOrDefault(x => x.Name.ToLower() == param.ToLower());
                    if (value is null)
                        throw new Exception("Header " + param + " not included in message");
                    sb.Append(value.Value);
                }
                sb.AppendLine();
            }

            sb.Append("\"@signature-params\": ");
            signatureInput = GetSignatureInput();
            sb.Append(signatureInput);

            return sb.ToString();
        }

        static string GetSignatureInput()
        {
            var sb = new StringBuilder($"(");
            foreach (var param in signatureParameters)
            {
                if (sb.ToString().EndsWith("("))
                    sb.Append($"\"{param}\"");
                else
                    sb.Append($" \"{param}\"");
            }
            sb.Append($");created={DateTimeOffset.Now.ToUnixTimeSeconds()}");

            return sb.ToString();
        }

        static string ComputeContentHash(string content)
        {
            using (var sha256 = SHA256.Create())
            {
                byte[] hashedBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(content));
                return Convert.ToBase64String(hashedBytes);
            }
        }
    }
}
UkeHa commented 1 year ago

@uherberg, i've tried to reproduce your results and failed to do so with OPs version as well as a slightly changed version of your code (changed the target back to localhost and verifysignature and removed the bearer token). I wrote a small test with the example curl request and that verifies the signature correctly. Perhaps This is a timezone/localization problem. I'll try that next. Here's a gist of my logs/code.

https://gist.github.com/UkeHa/f7059ff504c742cdd6dc88a8212e496b

uherberg commented 1 year ago

@UkeHa Thanks for providing the code. I will try it out and then get back to you

uherberg commented 1 year ago

@UkeHa That's weird... I just tried it with the exact same code of yours and it works. Can you pull the latest container to be sure you run the same code?

docker pull ebay/digital-signature-verification-ebay-api
UkeHa commented 1 year ago

@uherberg even with the latest image it still fails.

grafik

uherberg commented 1 year ago

@UkeHa Can send me the image ID of the container? Just to be sure that it is the latest code. docker image ls

Also, can you add a line in the C# code to print out the signature base and compare it with the output from the container? Add Console.WriteLine(signature); after line 43

UkeHa commented 1 year ago

REPOSITORY TAG IMAGE ID CREATED SIZE ebay/digital-signature-verification-ebay-api latest 6777ff07361f About an hour ago 680MB

grafik

c# log:

"content-digest": sha-256=:jLO5LPb0rcfbRPjjUMDYVqjo7muhzU+WrkmKhnFYFd4=: "x-ebay-signature-key": eyJ6aXAiOiJERUYiLCJlbmMiOiJBMjU2R0NNIiwidGFnIjoiSXh2dVRMb0FLS0hlS0Zoa3BxQ05CUSIsImFsZyI6IkEyNTZHQ01LVyIsIml2IjoiaFd3YjNoczk2QzEyOTNucCJ9.2o02pR9SoTF4g_5qRXZm6tF4H52TarilIAKxoVUqjd8.3qaF0KJN-rFHHm_P.AMUAe9PPduew09mANIZ-O_68CCuv6EIx096rm9WyLZnYz5N1WFDQ3jP0RBkbaOtQZHImMSPXIHVaB96RWshLuJsUgCKmTAwkPVCZv3zhLxZVxMXtPUuJ-ppVmPIv0NzznWCOU5Kvb9Xux7ZtnlvLXgwOFEix-BaWNomUAazbsrUCbrp514GIea3butbyxXLNi6R9TJUNh8V2uan-optT1MMyS7eMQnVGL5rYBULk.9K5ucUqAu0DqkkhgubsHHw "@method": POST "@path": /verifysignature "@authority": localhost:8080 "@signature-params": ("content-digest" "x-ebay-signature-key" "@method" "@path" "@authority");created=1670495125

uherberg commented 1 year ago

@UkeHa Thanks. Definitely the same Docker image that I use... As I am developing from a mac, I am using a docker image for dotnet as well (and therefore don't point to localhost, but the IP of the other docker container, using docker network). But that shouldn't make a difference... I am in the same time zone (though US localization); but I fail to see how that would make a difference.

UkeHa commented 1 year ago

If i run my code with the created timestamp from the curl example i get a difference in the content-digest. Everything else is exactly the same:

curl example result:

"content-digest": **sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:**
"x-ebay-signature-key": eyJ6aXAiOiJERUYiLCJlbmMiOiJBMjU2R0NNIiwidGFnIjoiSXh2dVRMb0FLS0hlS0Zoa3BxQ05CUSIsImFsZyI6IkEyNTZHQ01LVyIsIml2IjoiaFd3YjNoczk2QzEyOTNucCJ9.2o02pR9SoTF4g_5qRXZm6tF4H52TarilIAKxoVUqjd8.3qaF0KJN-rFHHm_P.AMUAe9PPduew09mANIZ-O_68CCuv6EIx096rm9WyLZnYz5N1WFDQ3jP0RBkbaOtQZHImMSPXIHVaB96RWshLuJsUgCKmTAwkPVCZv3zhLxZVxMXtPUuJ-ppVmPIv0NzznWCOU5Kvb9Xux7ZtnlvLXgwOFEix-BaWNomUAazbsrUCbrp514GIea3butbyxXLNi6R9TJUNh8V2uan-optT1MMyS7eMQnVGL5rYBULk.9K5ucUqAu0DqkkhgubsHHw
"@method": POST
"@path": /verifysignature
"@authority": localhost:8080
"@signature-params": ("content-digest" "x-ebay-signature-key" "@method" "@path" "@authority");created=1658440308

my code with timestamp "sb.Append($");created=1658440308")":

"content-digest": **sha-256=:jLO5LPb0rcfbRPjjUMDYVqjo7muhzU+WrkmKhnFYFd4=:**
"x-ebay-signature-key": eyJ6aXAiOiJERUYiLCJlbmMiOiJBMjU2R0NNIiwidGFnIjoiSXh2dVRMb0FLS0hlS0Zoa3BxQ05CUSIsImFsZyI6IkEyNTZHQ01LVyIsIml2IjoiaFd3YjNoczk2QzEyOTNucCJ9.2o02pR9SoTF4g_5qRXZm6tF4H52TarilIAKxoVUqjd8.3qaF0KJN-rFHHm_P.AMUAe9PPduew09mANIZ-O_68CCuv6EIx096rm9WyLZnYz5N1WFDQ3jP0RBkbaOtQZHImMSPXIHVaB96RWshLuJsUgCKmTAwkPVCZv3zhLxZVxMXtPUuJ-ppVmPIv0NzznWCOU5Kvb9Xux7ZtnlvLXgwOFEix-BaWNomUAazbsrUCbrp514GIea3butbyxXLNi6R9TJUNh8V2uan-optT1MMyS7eMQnVGL5rYBULk.9K5ucUqAu0DqkkhgubsHHw
"@method": POST
"@path": /verifysignature
"@authority": localhost:8080
"@signature-params": ("content-digest" "x-ebay-signature-key" "@method" "@path" "@authority");created=1658440308

As the sha-256 generation works for you, i don't see how my code could affect anything here.

uherberg commented 1 year ago

@UkeHa Content-digest should not depend on the timestamp (as it is only part of a header, not the body). The content-digest is purely calculated on the request body. In the curl example, the body was different from this example though (hello world instead of a payload of the refund API)

UkeHa commented 1 year ago

good catch! If i run it with the same payload the signatures match exactly.

uherberg commented 1 year ago

@UkeHa I copied the C# code into a Docker container that works for me. That should exclude any differences in our environments: https://github.com/uherberg/digital-signature-dotnet-test You may have to update the IP in the URL and then rebuild the container. I added the two containers into the same network:

docker network create mynetwork
# run the two containers
docker network connect myNetwork containerId1
docker network connect myNetwork containerId2
UkeHa commented 1 year ago

Alright, that seems to work.... for whatever reason.

dotnet-test log:

2022-12-08 13:29:01 /App/signature2.csproj : warning NU1701: Package 'BouncyCastle 1.8.9' was restored using '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8, .NETFramework,Version=v4.8.1' instead of the project target framework 'net6.0'. This package may not be fully compatible with your project.
2022-12-08 13:29:01 /App/signature2.csproj : warning NU1701: Package 'BouncyCastle 1.8.9' was restored using '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8, .NETFramework,Version=v4.8.1' instead of the project target framework 'net6.0'. This package may not be fully compatible with your project.
2022-12-08 13:29:03 "content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
2022-12-08 13:29:03 "x-ebay-signature-key": eyJ6aXAiOiJERUYiLCJlbmMiOiJBMjU2R0NNIiwidGFnIjoiSXh2dVRMb0FLS0hlS0Zoa3BxQ05CUSIsImFsZyI6IkEyNTZHQ01LVyIsIml2IjoiaFd3YjNoczk2QzEyOTNucCJ9.2o02pR9SoTF4g_5qRXZm6tF4H52TarilIAKxoVUqjd8.3qaF0KJN-rFHHm_P.AMUAe9PPduew09mANIZ-O_68CCuv6EIx096rm9WyLZnYz5N1WFDQ3jP0RBkbaOtQZHImMSPXIHVaB96RWshLuJsUgCKmTAwkPVCZv3zhLxZVxMXtPUuJ-ppVmPIv0NzznWCOU5Kvb9Xux7ZtnlvLXgwOFEix-BaWNomUAazbsrUCbrp514GIea3butbyxXLNi6R9TJUNh8V2uan-optT1MMyS7eMQnVGL5rYBULk.9K5ucUqAu0DqkkhgubsHHw
2022-12-08 13:29:03 "@method": POST
2022-12-08 13:29:03 "@path": /verifysignature
2022-12-08 13:29:03 "@authority": 172.17.0.2:8080
2022-12-08 13:29:03 "@signature-params": ("content-digest" "x-ebay-signature-key" "@method" "@path" "@authority");created=1670502543
2022-12-08 13:29:03 OK
2022-12-08 13:29:03 OK
2022-12-08 13:29:03 2022-12-08 12:29:03.113  INFO 8 --- [nio-8080-exec-8] c.e.s.VerificationService                : Calculated base:
2022-12-08 13:29:03 "content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
2022-12-08 13:29:03 "x-ebay-signature-key": eyJ6aXAiOiJERUYiLCJlbmMiOiJBMjU2R0NNIiwidGFnIjoiSXh2dVRMb0FLS0hlS0Zoa3BxQ05CUSIsImFsZyI6IkEyNTZHQ01LVyIsIml2IjoiaFd3YjNoczk2QzEyOTNucCJ9.2o02pR9SoTF4g_5qRXZm6tF4H52TarilIAKxoVUqjd8.3qaF0KJN-rFHHm_P.AMUAe9PPduew09mANIZ-O_68CCuv6EIx096rm9WyLZnYz5N1WFDQ3jP0RBkbaOtQZHImMSPXIHVaB96RWshLuJsUgCKmTAwkPVCZv3zhLxZVxMXtPUuJ-ppVmPIv0NzznWCOU5Kvb9Xux7ZtnlvLXgwOFEix-BaWNomUAazbsrUCbrp514GIea3butbyxXLNi6R9TJUNh8V2uan-optT1MMyS7eMQnVGL5rYBULk.9K5ucUqAu0DqkkhgubsHHw
2022-12-08 13:29:03 "@method": POST
2022-12-08 13:29:03 "@path": /verifysignature
2022-12-08 13:29:03 "@authority": 172.17.0.2:8080
2022-12-08 13:29:03 "@signature-params": ("content-digest" "x-ebay-signature-key" "@method" "@path" "@authority");created=1670502543
2022-12-08 13:29:03 2022-12-08 12:29:03.114  INFO 8 --- [nio-8080-exec-8] c.e.s.VerificationService                : Message signature verified

Can i use the "createSigningKey" endpoint in the sandbox to fetch a keyset for the test-system? And what is the TTL for the Public JWE? Also 3 years as indicated by the documentation? "Note: All keys have an expiration date of three (3) years after their creationTime."

uherberg commented 1 year ago

@UkeHa Really strange that it works this way but not with your original set up...

Keys generated by the createSigningKey endpoint do not work with the docker container. I added a note to the README today to make that clear. The reason is that the JWE is encrypted using a masterkey, which is part of the container. If we were to use the same masterkey on our public endpoints, anyone could issue their own JWEs that aren't issued by eBay. You can, however, test the public APIs directly on sandbox or production with the script I posted further up in this thread, using the key generated from the Key Management API.

Do you want to further explore the issue you faced, or is it okay to close this issue?

UkeHa commented 1 year ago

Yeah i agree that it's really strange. As long as i can verify my requests against the sandbox i think it's okay to close this issue.

UkeHa commented 1 year ago

@uherberg on second thought i think i still need some support on this (and it's adjecent) issue. I'm struggling with the sandbox. I'm trying to run your sandbox-example with the code you provided.

If i see things correctly you can't create a keyset specifically for the sandbox but have to use the ones you create for the production environment as there is no endpoint to create them (according to this: https://developer.ebay.com/api-docs/developer/key-management/resources/signing_key/methods/createSigningKey).

Therefore i used my privatekey and it's jwe in the code you've posted above. For that to work i needed a valid oauth2 token and as it's sell/fulfillment the scope needs to be _https://api.ebay.com/oauth/api_scope/sell.fulfillment_?

Sadly, i can only use the scope https://api.ebay.com/oauth/api_scope with my client id/secret, anything else fails. Could you provide some more insight as to how things work here?

uherberg commented 1 year ago

@UkeHa You are correct that there is only a single endpoint for the createSigningKey API. However, based on which token you use for calling that API (sandbox or production), you will get either a prod or sandbox private key and JWE. In order to call the createSigningKey API, you need an application token (https://developer.ebay.com/my/auth?env=sandbox&index=0). Select your app (and sandbox or prod). Use that application token. In order to test your application, you need a user token on that same environment, also from https://developer.ebay.com/my/auth?env=sandbox&index=0 (select sandbox or prod, then Get User token here, then select OAuth).

UkeHa commented 1 year ago

Interestingly the error is the same with sandbox. On my local machine with my sandbox-api-keys i get this error:

Forbidden
{
  "errors": [
    {
      "errorId": 215122,
      "domain": "ACCESS",
      "category": "REQUEST",
      "message": "Signature validation failed",
      "longMessage": "Signature validation failed to fulfill the request."
    }
  ]
}

if i run the same code in a container:

2022-12-12 12:30:40 OK
2022-12-12 12:30:40 {"refundId":"5005000505","refundStatus":"PENDING"}

I have no clue as to what could be different between my windows host machine the docker container to provocate such a response though. Alas, i will try to implement it in our software in hopes that some integration test run in our CI/CD environment at least...

snizz666 commented 1 year ago

hey, i have the same error for C#. Is there a solution for this error?

UkeHa commented 1 year ago

Running this Dockerfile in my test-project let me at least try my code. I'm currently implementing it in our application in hopes that it at least works on a different machine/project.

Dockerfile:

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env
WORKDIR /App

# Copy everything
COPY . ./
# Restore as distinct layers
RUN dotnet restore

# Build runtime image
ENTRYPOINT ["dotnet", "run"]
woshizhifeiyua commented 1 year ago

i have the same error ,C#

vt-cloud commented 1 year ago

For anybody developing or running this on Windows, who can't figure out why you keep getting an invalid signature (Forbidden, error 215122) or can't replicate the signature in the example code.

Make sure you use the correct line-endings when building the signature message by stripping out any "\r" characters.

For example...

static string GetSignature(RestRequest request, Uri uri)
{
    var sb = new StringBuilder();
    ...
    return sb.ToString().Replace("\r", "");
}
UkeHa commented 1 year ago

oh man carriage return and new line was the problem all along? Man that's dumb.

UkeHa commented 1 year ago

using @vt-cloud fix the validation of the POST above works against the sandbox service. If i want to check against the payout_summary endpoint which is a GET request i removed the message part and removed the header for Content-Digest, as the readme.md mentions it can be skipped if there is no payload.

Heres the URL: var client = new RestClient("https://api.sandbox.ebay.com"); var request = new RestRequest("/sell/finances/v1/payout_summary", Method.Get);

Forbidden { "errors": [ { "errorId": 215122, "domain": "ACCESS", "category": "REQUEST", "message": "Signature validation failed", "longMessage": "Signature validation failed to fulfill the request." } ] }

Adding back a message (string.empty) and content-digest header, i get a 404 error. Has anyone an idea how to solve this with GET requests?

EDIT: Nevermind - the correct URL is https://apiz.sandbox.ebay.com/sell/finances/v1/payout_summary with a z!

KonradStefaniak1995 commented 1 year ago

Hi @UkeHa, when you use the sandbox, you also download the private key and jwe from apiz.sandbox.ebay.com or do you set the same as for docker tests?

I'm asking because I'm trying to make a test GET request on a sandbox but I'm getting an error:

{
  "errors": [
    {
      "errorId": 215002,
      "domain": "ACCESS",
      "category": "REQUEST",
      "message": "Internal errors as fetching master key",
      "longMessage": "Internal errors as fetching master key to fulfill the request."
    }
  ]
}

My curl result:

"x-ebay-signature-key": eyJ6aXAiOiJERUYiLCJraWQiOiJiNmI4ZWY2MC0zODU4LTRiMGUtYTI5My1mZjQyOGJkZmMyZmMiLCJlbmMiOiJBMjU2R0NNIiwidGFnIjoiaWlNdVFfdTl6TkFBLTFyc05oUGhIZyIsImFsZyI6IkEyNTZHQ01LVyIsIml2IjoiZjd6bU5iblRvY0xfdlU5TiJ9.nXowHrAmK6QHbl5hCSPWhbf4T2KiN_jdf7L_YyDF32U.dpjuHpg34OmAeE0q.dE8dti7KtLy2eznDWD7JBC6y5-z3V5intgSnxSXS2CwyCyadCC3S4rmmG2J_2Nkhcak6IpSyy0DOpLPbkIOhZsyMqQh6V9PbXHmwfXmm89vRHN1EJ0O3DZ3uhoVsFl7Q-sK7B-6P1D4zkkaBv_rdSlhCqgpbYzkPbcuNjuCi3Ix9QJSGXim72J4gUbpRzpsugAdCcmqlYTpkAMmfRDDBc91cNs3MwBLqZvbks_v25OhwW_lTI-FDcZH5N1ybUf4ORkKH.NiVVz16OM0iTRvTYmnE_1g
"@method": GET
"@path": /sell/finances/v1/payout_summary
"@authority": apiz.sandbox.ebay.com
"@signature-params": ("x-ebay-signature-key" "@method" "@path" "@authority");created=1673437963

I create the headers according to the documentation by adding x-ebay-signature-key, Signature, Signature-Input, x-ebay-enforce-signature and Authorization.

uherberg commented 1 year ago

@KonradStefaniak1995 The private/public/JWE keys from the Readme only work with the docker container. You will need to download a key from sandbox key management API for calling APIs and sandbox, and a different one from production key management API for production APIs

UkeHa commented 1 year ago

@uherberg - when i try to implement the solution that works in my POC, i receive the following error when trying to fetch data from /sell/finances/v1/payout_summary in PROD. Do you have any clue how to fix this issue? I'm using the jwe/privatekey from production as well. With the same private key and jwe i can successfully request from my test project.

Status: 403 Response: { "errors": [ { "errorId": 215002, "domain": "ACCESS", "category": "REQUEST", "message": "Internal errors as fetching master key", "longMessage": "Internal errors as fetching master key to fulfill the request." } ] }

KonradStefaniak1995 commented 1 year ago

I get the same error when querying with private key/jwe from production for /sell/finances/v1/payout_summary request. When passing sandbox token after oAuth authorization to key_management I was getting: {"errors":[{"errorId":2003,"domain":"ACCESS","category":"APPLICATION","message":"Internal error","longMessage":"There was a problem with an eBay internal system or process. Contact eBay developer support for assistance"}]}

UkeHa commented 1 year ago

@uherberg when i try to test my signature with the endpoint post-order/v2/return/14-00032-43825/issue_refund on sandbox i get the following error. Is it possible to test this endpoint somewhere? If i try to do the same on production i get the same error.

BadRequest {"error":[{"errorId":1616,"domain":"returnErrorDomain","severity":"ERROR","category":"REQUEST","message":"Invalid Input.","parameter":[{"value":"returnId","name":"parameter"}],"longMessage":"Invalid Input.","inputRefIds":[],"httpStatusCode":400}]}

When i try to refund a real item that has been bought i get this error:

2023-01-23 10:39:10.116 +01:00 [INF] Result: {"error":[{"errorId":1764,"domain":"returnErrorDomain","severity":"ERROR","category":"APPLICATION","message":"Your refund did not go through. Please try again later or contact us.","parameter":[{"value":"retriable internal error in OPMS REST - Execute money movement failed.;","name":"message"}],"inputRefIds":[],"httpStatusCode":400}]}

uherberg commented 1 year ago

@KonradStefaniak1995 @UkeHa I am not able to reproduce the errors you face with /sell/finances/v1/payout_summary in PROD. I downloaded private key and JWT from the production key management API (from Prod) and used them to create the signature and it works fine. Have you confirmed that you didn't use a key from either the README from this repo nor a sandbox private key and JWT? Neither of them would work on prod.

UkeHa commented 1 year ago

I was able to find the solution to my error. The problem was that the refund process we tried in prod was used with an offer that was paid locally so ebay wasn't able to process a refund. The error message is confusing, but my signature works.

uherberg commented 1 year ago

@UkeHa Great, thanks for confirming. Closing the issue.