I want to make an API call to eBay for order cancellation (Add Dispute). I am following the document given by eBay https://developer.ebay.com/develop/guides/digital-signatures-for-apis. When I call an API I am getting the following error.
{
"errors": [
{
"errorId": 215120,
"domain": "ACCESS",
"category": "REQUEST",
"message": "Signature validation failed",
"longMessage": "Signature validation failed to fulfill the request."
}
]
}
Here is my code in Java, which I copied from https://github.com/eBay/digital-signature-java-sdk/blob/main/src/main/java/com/ebay/developer/SignatureService.java
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.Signer;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.signers.RSADigestSigner;
import org.bouncycastle.crypto.util.PrivateKeyFactory;
import org.bouncycastle.util.encoders.Base64;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* // https://github.com/eBay/digital-signature-java-sdk/blob/main/src/main/java/com/ebay/developer/SignatureService.java
* // https://github.com/eBay/digital-signature-java-sdk
*
*/
public class SignatureService {
private static final List<String> SIGNATURE_PARAMS = Arrays.asList("content-digest", "x-ebay-signature-key", "@method", "@path", "@authority");
public static final String CONTENT_DIGEST = "content-digest";
public static final String SIGNATURE_PREFIX = "sig1=:";
public static final String SIGNATURE_INPUT_PREFIX = "sig1=";
public void sign(HttpRequest request, PrivateKeys privateKeys) {
request.usingHeader("x-ebay-signature-key", privateKeys.getJwe());
if(HttpMethodName.POST.equals(request.getHttpMethod())
|| HttpMethodName.PUT.equals(request.getHttpMethod())){
String contentDigest = generateContentDigest(request.getPayload().getData(), "SHA-256");
request.usingHeader("Content-Digest", contentDigest);
}
String signature = getSignature(request, privateKeys);
request.usingHeader("Signature", signature);
request.usingHeader("Signature-Input", SIGNATURE_INPUT_PREFIX + getSignatureInput());
request.usingHeader("x-ebay-enforce-signature", "true");
}
/**
* Generate Content Digest
*
* @param body request body
* @param cipher ciper to use SHA-256 or SHA-512
* @return contentDigest content digest
*/
private String generateContentDigest(String body, String cipher){
if(StringUtil.nullOrEmpty(body)){
return null;
}
String contentDigest = "";
try {
MessageDigest messageDigest = MessageDigest
.getInstance(cipher.toUpperCase());
String digest = new String(Base64.encode(
messageDigest.digest(body.getBytes(StandardCharsets.UTF_8))));
if (StringUtil.nonNullNonEmpty(digest)) {
contentDigest = cipher + "=:" +
digest + ":";
}
} catch (Exception ex) {
throw new RuntimeException(
"Error generating Content-Digest header: " + ex.getMessage(),
ex);
}
return contentDigest;
}
/**
* Get 'Signature' header value
*
* @return signature signature
*/
private String getSignature(HttpRequest httpRequest, PrivateKeys privateKeys){
try {
String baseString = calculateBase(httpRequest);
System.out.println(baseString);
byte[] base = baseString.getBytes(StandardCharsets.UTF_8);
Signer signer = new RSADigestSigner(new SHA256Digest());
AsymmetricKeyParameter privateKeyParameters = PrivateKeyFactory
.createKey(getPrivateKey(privateKeys.getPrivateKey()).getEncoded());
signer.init(true, privateKeyParameters);
signer.update(base, 0, base.length);
byte[] signature = signer.generateSignature();
String signatureStr = new String(Base64.encode(signature));
return SIGNATURE_PREFIX + signatureStr +
":";
} catch (CryptoException | IOException ex) {
throw new RuntimeException(
"Error creating value for signature: " + ex.getMessage(), ex);
}
}
/**
* Method to calculate base string value
*
* @return calculatedBase base string
*/
private String calculateBase(HttpRequest httpRequest){
Map<String, String> headers = httpRequest.getHeaders();
try {
StringBuilder buf = new StringBuilder();
for (String header : SIGNATURE_PARAMS) {
if (header.equalsIgnoreCase(CONTENT_DIGEST)
&& headers.get(CONTENT_DIGEST) == null) {
continue;
}
buf.append("\"");
buf.append(header.toLowerCase());
buf.append("\": ");
if (header.startsWith("@")) {
switch (header.toLowerCase()) {
case "@method":
buf.append(httpRequest.getHttpMethod().toString().toUpperCase());
break;
case "@authority":
buf.append(httpRequest.getEndPoint().toString().replace("https://", "").replace("/", ""));
break;
case "@target-uri":
buf.append(headers.get("@target-uri"));
break;
case "@path":
buf.append(httpRequest.getResourcePath());
break;
case "@scheme":
buf.append(httpRequest.getAbsoluteURI().getScheme());
break;
case "@request-target":
buf.append(headers.get("@request-target"));
break;
default:
throw new RuntimeException("Unknown pseudo header " + header);
}
} else {
if (!headers.containsKey(header)) {
throw new RuntimeException("Header " + header + " not included in message");
}
buf.append(headers.get(header));
}
buf.append("\n");
}
buf.append("\"@signature-params\": ");
buf.append(getSignatureInput());
return buf.toString();
} catch (Exception ex) {
throw new RuntimeException("Error calculating signature base: " + ex.getMessage(), ex);
}
}
/**
* Generate Signature Input header
*
* @return signatureInputHeader
*/
private String getSignatureInput() {
StringBuilder signatureInputBuf = new StringBuilder();
signatureInputBuf.append("(");
for (int i = 0; i < SIGNATURE_PARAMS.size(); i++) {
String param = SIGNATURE_PARAMS.get(i);
if(param.equalsIgnoreCase(CONTENT_DIGEST)){
continue;
}
signatureInputBuf.append("\"");
signatureInputBuf.append(param);
signatureInputBuf.append("\"");
if (i < SIGNATURE_PARAMS.size() - 1) {
signatureInputBuf.append(" ");
}
}
signatureInputBuf.append(");created=");
signatureInputBuf.append(Instant.now().getEpochSecond());
return signatureInputBuf.toString();
}
/**
* Get private key value as a file or as a string value
*
* @return privateKey private key
*/
public PrivateKey getPrivateKey(String privateKeyString) {
byte[] clear = Base64.decode(privateKeyString.getBytes());
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(clear);
try {
KeyFactory fact = KeyFactory.getInstance("RSA");
return fact.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException(e);
}
}
}
Sample: The signature base is generated from the above code.
"x-ebay-signature-key": ************* (JWE from Key Management API)
"@method": POST
"@path": /post-order/v2/cancellation
"@authority": apiz.ebay.com
"@signature-params": ("x-ebay-signature-key" "@method" "@path" "@authority");created=1673328807
Note: Bearer token is attached from a different file.
Already visited the following solution but not working for me.
https://forums.developer.ebay.com/questions/50518/digital-signatures-for-apis.html
eBay Digital Signatures for APIs signature header generation
I had a similar problem to yours (EBay Digital Signature Validation Failed when trying to POST) and managed to solve it.
I'm using python but the principal should be the same. Hope this helps
my new signature base looks like this now:
note the order in which the properties are arranged
and the headers:
Note: Because I'm calling the
/post-order/v2/return/{return_id}/issue_refund
API endpoint I had to replaceBearer
withTOKEN
inside theAuthorization
header.Edit: Note that your signature input needs to have the
content-digest
property like this: