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