ebayAPI请求新增的签名机制处理流程,java
部分站点因为新的安全政策,API请求需要额外新增四个header
- x-ebay-signature-key将RSA公钥或ed25519公钥 转为jwe格式,公钥文件从ebay后续api下载
- Content-Digest http请求body的内容摘要,使用SHA-256算法处理body生成摘要
- Signature-Input 决定由哪些字段拼接为signature-base
- Signature 通过私钥对signature-base进行签名的得到
- 请求头的名字大小写不敏感
流程
-
通过ebay未来提供的api下载公钥私钥文件。
-
将公钥转为jwe格式的字符串作为请求header x-ebay-signature-key。
-
为api请求的body内容计算摘要content-digest,作为请求header Content-Digest
-
ebay已经指定 header signature-Input 为sig1=(“content-digest” “x-ebay-signature-key” “@method” “@path” “@authority”);created=1658272908代表要根据content-digest,x-ebay-signature-key两个请求头的内容和http请求的"@method" “@path” “@authority"属性拼接signatureBase。 可直接通过URI对象获取这些@开头的属性。
URI uri = URI.create(url); String method = uri.getMethod();
-
使用私钥文件为刚刚构造的signatureBase签名作为请求header Signature。
拼接signature base的格式要求
-
详细的格式要求和签名机制可参考文档https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-11.html
-
signature base每一行都是"key”: value\n的形式,key用"“括起来并且全部小写,冒号后附加单独的空格,value结束后换行。最后一行是@signature-params。 ebay指定了请求header signature-Input为
signature-Input:sig1=("content-digest" "x-ebay-signature-key" "@method" "@path" "@authority");created=1658272908
-
created后面附加的是时间戳。
-
@signature-params指拼接signture-base用到的所有参数,与signature-Input是对应的,放在最后一行
-
拼接完成的sinatureBase
"content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
"x-ebay-signature-key": publicKey
"@method": POST
"@path": /verifysignature
"@authority": localhost:8080
"@signature-params": ("content-digest" "x-ebay-signature-key" "@method" "@path" "@authority");created=1660628866
- 拼接signatureBase并将签名加入请求headers
//与signature-input对应
private List<String> signatureParams = Arrays.asList("content-digest", "x-ebay-signature-key", "@method", "@path", "@authority");
//构造http message签名header
void buildSignatureHeaders(Map<String, String> headers, String url) throws SignatureException, IOException, CryptoException {
URI uri = URI.create(url);
StringBuilder buf = new StringBuilder();
if (!headers.containsKey("content-digest")) {
//post请求没有body时,则不需要内容摘要content-digest
signatureParams = signatureParams.stream().filter(a -> !a.equals("content-digest")).collect(Collectors.toList());
}
for (String header : signatureParams) {
buf.append("\"");
buf.append(header.toLowerCase());
buf.append("\": ");
if (header.startsWith("@")) {
switch (header.toLowerCase()) {
case "@method":
buf.append("POST");
break;
case "@authority":
buf.append(uri.getAuthority());
break;
case "@target-uri":
buf.append(url);
break;
case "@scheme":
buf.append(uri.getScheme());
break;
case "@path":
buf.append(uri.getPath());
break;
case "@query":
buf.append(uri.getQuery());
break;
default:
throw new SignatureException("Unknown pseudo header " + header);
}
} else {
buf.append(headers.get(header));
}
buf.append("\n");
}
buf.append("\"@signature-params\": ");
StringBuilder signatureInputBuf = new StringBuilder();
signatureInputBuf.append("(");
for (int i = 0; i < signatureParams.size(); i++) {
String param = signatureParams.get(i);
signatureInputBuf.append("\"");
signatureInputBuf.append(param);
signatureInputBuf.append("\"");
if (i < signatureParams.size() - 1) {
signatureInputBuf.append(" ");
}
}
signatureInputBuf.append(");created=");
signatureInputBuf.append(Instant.now().getEpochSecond());
buf.append(signatureInputBuf);
String signatureInput = "sig1=" + signatureInputBuf;
String signatureBase = buf.toString();
headers.put("signature", getSignature(signatureBase));
headers.put("signature-input", signatureInput);
}
计算Content-Digest
/**
* 获得body的内容摘要
*
* @param content
* @return
*/
public static String getContentDigest(String content) {
byte[] hash = org.apache.commons.codec.digest.DigestUtils.sha256(content);
String encoded = Base64.getEncoder().encodeToString(hash);
return "sha-256=:" + encoded + ":";
}
读取密钥对文件
private PrivateKey privateKey;
private PublicKey publicKey;
@Test
public void readPublicKey() throws SignatureException {
Path path = Paths.get("xxxx\\rsa\\publickey.pem");
try (FileReader keyReader = new FileReader(path.toFile())) {
PEMParser pemParser = new PEMParser(keyReader);
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemParser.readObject());
//方法要调用核心类库的keyFactory,java-8不支持ed25519算法,无法根据对应的ed25519公钥文件构造对象
this.publicKey = converter.getPublicKey(publicKeyInfo);
} catch (IOException ex) {
throw new SignatureException("Error loading public key: " + ex.getMessage(), ex);
}
}
@Test
public void readPrivateKey() throws SignatureException {
Path path = Paths.get("xxxx\\rsa\\privatekey.pem");
try (FileReader keyReader = new FileReader(path.toFile())) {
PEMParser pemParser = new PEMParser(keyReader);
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
PrivateKeyInfo privateKeyInfo = PrivateKeyInfo.getInstance(pemParser.readObject());
//方法要调用核心类库的keyFactory,java-8不支持ed25519算法,无法根据对应的ed25519私钥文件构造对象
this.privateKey = converter.getPrivateKey(privateKeyInfo);
} catch (IOException ex) {
throw new SignatureException("Error loading private key: " + ex.getMessage(), ex);
}
}
使用私钥为signature-base签名
public String getSignature(String baseString) throws CryptoException, IOException {
byte[] base = baseString.getBytes(StandardCharsets.UTF_8);
Signer signer;
//如果要使用ed25519密钥签名使用signer = new Ed25519Signer(new SHA256Digest())
signer = new RSADigestSigner(new SHA256Digest());
AsymmetricKeyParameter privateKeyParameters = PrivateKeyFactory.createKey(this.privateKey.getEncoded());
signer.init(true, privateKeyParameters);
signer.update(base, 0, base.length);
byte[] signature = signer.generateSignature();
return "sig1=:" + new String(org.bouncycastle.util.encoders.Base64.encode(signature)) + ":";
}
将公钥转为JWE格式的字符串
//把公钥文件转为JWE格式字符串
public String getJWE(PublicKey publicKey) throws JOSEException , IOException {
//masterkey.key文件 ebay提供
Path masterkeyPath = Paths.get("xxx\\masterkey.key");
String secretKeyBase64 = Files.readAllLines(masterkeyPath).get(0);//目前文件只有一行
final byte[] secretKey = Base64.getDecoder().decode(secretKeyBase64);
JWEEncrypter jweEncrypter = new AESEncrypter(secretKey);
JWEDecrypter jweDecrypter = new AESDecrypter(secretKey);
// Compose the JWT claims set
Date now = new Date();
JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder()
.expirationTime(new Date(now.getTime() + 1000L * 60 * 60 * 24 * 365 * 10)) // expires in 3 years
.notBeforeTime(now)
.issueTime(now)
.jwtID(UUID.randomUUID().toString())
.claim("appid", "app1") // this is set to the appId
.claim("pkey", new String(org.bouncycastle.util.encoders.Base64.encode(publicKey.getEncoded()))) // public ed25519 key
.build();
// Request JWT encrypted with DIR and 256-bit AES/GCM
JWEHeader header = new JWEHeader.Builder(JWEAlgorithm.A256GCMKW, EncryptionMethod.A256GCM)
.compressionAlgorithm(CompressionAlgorithm.DEF)
.build();
// Create the encrypted JWT object
EncryptedJWT jwt = new EncryptedJWT(header, jwtClaims);
// Do the actual encryption
jwt.encrypt(jweEncrypter);
// Serialise to JWT compact form
String jwtString = jwt.serialize();
return jwtString;
}