Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 使用证书信任链验证通过API下载的微信支付平台证书 #138

Merged
merged 8 commits into from
Feb 24, 2023
2 changes: 1 addition & 1 deletion .code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ source:
# 提供产品代码库中编写的测试代码存放目录或文件名格式,以便后续代码统计环节进行排除等特殊处理
test_source:
#用于匹配文件; 匹配方式为正则表达式
filepath_regex: [ ".*Test.java" ]
filepath_regex: [ ".*/src/test/.*" ]

# 提供产品代码库中工具或框架自动生成的且在代码库中的代码,没有可为空。以便后续代码统计环节进行排除等特殊处理。
auto_generate_source:
Expand Down
8 changes: 5 additions & 3 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ dependencies {

testImplementation "junit:junit:${junitVersion}"
testImplementation "org.mockito:mockito-inline:${mockitoInlineVersion}"
testImplementation "com.squareup.okhttp3:mockwebserver:${okhttpVersion}"
testImplementation "org.awaitility:awaitility:${awaitilityVersion}"

testImplementation platform("org.junit:junit-bom:${junit5Version}")
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation "com.squareup.okhttp3:mockwebserver:${okhttpVersion}"
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:${junit5Version}")
testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'

testRuntimeOnly "org.slf4j:slf4j-simple:${slf4jVersion}"
}

test {
Expand Down Expand Up @@ -97,4 +99,4 @@ signing {
def signingPassword = System.getenv("SIGNING_PASSWORD")
useInMemoryPgpKeys(signingKey, signingPassword)
sign publishing.publications.maven
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.wechat.pay.java.core;

import static com.wechat.pay.java.core.cipher.Constant.HEX;

import com.wechat.pay.java.core.auth.Credential;
import com.wechat.pay.java.core.auth.Validator;
import com.wechat.pay.java.core.auth.WechatPay2Credential;
Expand All @@ -14,6 +12,7 @@
import com.wechat.pay.java.core.cipher.RSASigner;
import com.wechat.pay.java.core.cipher.RSAVerifier;
import com.wechat.pay.java.core.cipher.Signer;
import com.wechat.pay.java.core.util.PemUtil;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;

Expand Down Expand Up @@ -44,7 +43,7 @@ protected AbstractRSAConfig(
public PrivacyEncryptor createEncryptor() {
X509Certificate certificate = certificateProvider.getAvailableCertificate();
return new RSAPrivacyEncryptor(
certificate.getPublicKey(), certificate.getSerialNumber().toString(HEX));
certificate.getPublicKey(), PemUtil.getSerialNumber(certificate));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
package com.wechat.pay.java.core.certificate;

import static com.wechat.pay.java.core.cipher.Constant.HEX;

import com.wechat.pay.java.core.auth.Validator;
import com.wechat.pay.java.core.auth.WechatPay2Validator;
import com.wechat.pay.java.core.certificate.model.Data;
import com.wechat.pay.java.core.certificate.model.DownloadCertificateResponse;
import com.wechat.pay.java.core.certificate.model.EncryptCertificate;
import com.wechat.pay.java.core.cipher.AeadCipher;
import com.wechat.pay.java.core.exception.ValidationException;
import com.wechat.pay.java.core.http.Constant;
import com.wechat.pay.java.core.http.HttpClient;
import com.wechat.pay.java.core.http.HttpMethod;
import com.wechat.pay.java.core.http.HttpRequest;
import com.wechat.pay.java.core.http.HttpResponse;
import com.wechat.pay.java.core.http.JsonResponseBody;
import com.wechat.pay.java.core.http.MediaType;
import com.wechat.pay.java.core.util.PemUtil;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
Expand All @@ -30,19 +24,19 @@
/** 自动更新平台证书提供器抽象类 */
public abstract class AbstractAutoCertificateProvider implements CertificateProvider {
private static final Logger log = LoggerFactory.getLogger(AbstractAutoCertificateProvider.class);
protected static final int UPDATE_INTERVAL_MINUTE = 60; // 定时更新时间,1小时
protected static final int UPDATE_INTERVAL_MINUTE = 60; // 定时更新时间,60分钟,即1小时
protected final SafeSingleScheduleExecutor executor =
SafeSingleScheduleExecutor.getInstance(); // 安全的单线程定时执行器实例
protected String requestUrl; // 请求URl

protected String merchantId; // 商户号

protected CertificateHandler certificateHandler; // 证书处理器
protected AeadCipher aeadCipher; // 解密平台证书的aeadCipher;
protected HttpClient httpClient; // 下载平台证书的httpClient
private final HttpRequest httpRequest; // http请求
private Validator validator; // 验证器

private int updateTime; // 自动更新次数
private int updateCount; // 自动更新次数
private int succeedCount; // 成功次数
private final Map<String, Map<String, X509Certificate>> certificateMap; // 证书map

protected AbstractAutoCertificateProvider(
Expand All @@ -52,6 +46,24 @@ protected AbstractAutoCertificateProvider(
HttpClient httpClient,
String merchantId,
Map<String, Map<String, X509Certificate>> wechatPayCertificateMap) {
this(
requestUrl,
certificateHandler,
aeadCipher,
httpClient,
merchantId,
wechatPayCertificateMap,
UPDATE_INTERVAL_MINUTE * 60);
}

protected AbstractAutoCertificateProvider(
String requestUrl,
CertificateHandler certificateHandler,
AeadCipher aeadCipher,
HttpClient httpClient,
String merchantId,
Map<String, Map<String, X509Certificate>> wechatPayCertificateMap,
int updateInterval) {
this.merchantId = merchantId;
synchronized (AbstractAutoCertificateProvider.class) {
if (!wechatPayCertificateMap.containsKey(merchantId)) {
Expand All @@ -61,7 +73,7 @@ protected AbstractAutoCertificateProvider(
"The corresponding provider for the merchant already exists.");
}
}
this.requestUrl = requestUrl;

this.certificateHandler = certificateHandler;
this.aeadCipher = aeadCipher;
this.httpClient = httpClient;
Expand All @@ -73,42 +85,49 @@ protected AbstractAutoCertificateProvider(
.addHeader(Constant.ACCEPT, " */*")
.addHeader(Constant.CONTENT_TYPE, MediaType.APPLICATION_JSON.getValue())
.build();
// 下载证书,如果失败会抛出异常
downloadAndUpdate(wechatPayCertificateMap);

Runnable runnable =
() -> {
log.info(
"Begin update Certificates.merchantId:{},total updates:{}", merchantId, updateTime);
downloadAndUpdate(wechatPayCertificateMap);
"Begin update Certificates.merchantId:{},total updates:{}", merchantId, updateCount);
try {
updateCount++;
downloadAndUpdate(wechatPayCertificateMap);
succeedCount++;
} catch (Exception e) {
// 已经有证书了,失败暂时忽略
log.error("Download and update WechatPay certificates failed.", e);
}

log.info(
"Finish update Certificates.merchantId:{},total updates:{}", merchantId, updateTime);
"Finish update Certificates.merchantId:{},total updates:{}, succeed updates:{}",
merchantId,
updateCount,
succeedCount);
};
executor.scheduleAtFixedRate(
runnable, UPDATE_INTERVAL_MINUTE, UPDATE_INTERVAL_MINUTE, TimeUnit.MINUTES);
executor.scheduleAtFixedRate(runnable, updateInterval, updateInterval, TimeUnit.SECONDS);
}

/** 下载和更新证书 */
/**
* 下载并更新证书
*
* @param wechatPayCertificateMap 存放多商户对应证书的Map
*/
protected void downloadAndUpdate(
Map<String, Map<String, X509Certificate>> wechatPayCertificateMap) {
try {
HttpResponse<DownloadCertificateResponse> httpResponse = downloadCertificate(httpClient);
validateCertificate(httpResponse);
updateCertificate(httpResponse, wechatPayCertificateMap);
validator =
new WechatPay2Validator(
certificateHandler.generateVerifier(
new ArrayList<>(wechatPayCertificateMap.get(merchantId).values())));
updateTime++;
} catch (Exception e) {
if (validator == null) {
throw e;
}
log.error("Download and update WechatPay certificates failed.", e);
}
HttpResponse<DownloadCertificateResponse> httpResponse = downloadCertificate(httpClient);

Map<String, X509Certificate> downloaded = decryptCertificate(httpResponse);
validateCertificate(downloaded);
wechatPayCertificateMap.put(merchantId, downloaded);
}

/**
* 下载证书
*
* @param httpClient 下载使用的HttpClient
* @return httpResponse
*/
protected HttpResponse<DownloadCertificateResponse> downloadCertificate(HttpClient httpClient) {
Expand All @@ -117,30 +136,18 @@ protected HttpResponse<DownloadCertificateResponse> downloadCertificate(HttpClie
return httpResponse;
}

/**
* 校验下载证书
*
* @param httpResponse httpResponse
*/
protected void validateCertificate(HttpResponse<DownloadCertificateResponse> httpResponse) {
JsonResponseBody responseBody = (JsonResponseBody) (httpResponse.getBody());
if (validator != null
&& !validator.validate(httpResponse.getHeaders(), responseBody.getBody())) {
throw new ValidationException(
String.format(
"Validate response failed,the WechatPay signature is incorrect.responseHeader[%s]\tresponseBody[%.1024s]",
httpResponse.getHeaders(), httpResponse.getServiceResponse()));
}
protected void validateCertificate(Map<String, X509Certificate> certificates) {
certificates.forEach((serialNo, cert) -> certificateHandler.validateCertPath(cert));
}

/**
* 更新证书
* 从应答报文中解密证书
*
* @param httpResponse httpResponse
* @return 应答报文解密后,生成X.509证书对象的Map
*/
protected void updateCertificate(
HttpResponse<DownloadCertificateResponse> httpResponse,
Map<String, Map<String, X509Certificate>> wechatPayCertificateMap) {
protected Map<String, X509Certificate> decryptCertificate(
HttpResponse<DownloadCertificateResponse> httpResponse) {
List<Data> dataList = httpResponse.getServiceResponse().getData();
Map<String, X509Certificate> downloadCertMap = new HashMap<>();
for (Data data : dataList) {
Expand All @@ -152,9 +159,9 @@ protected void updateCertificate(
encryptCertificate.getNonce().getBytes(StandardCharsets.UTF_8),
Base64.getDecoder().decode(encryptCertificate.getCiphertext()));
certificate = certificateHandler.generateCertificate(decryptCertificate);
downloadCertMap.put(certificate.getSerialNumber().toString(HEX).toUpperCase(), certificate);
downloadCertMap.put(PemUtil.getSerialNumber(certificate), certificate);
}
wechatPayCertificateMap.put(merchantId, downloadCertMap);
return downloadCertMap;
}

public X509Certificate getAvailableCertificate(Map<String, X509Certificate> certificateMap) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.wechat.pay.java.core.certificate;

import com.wechat.pay.java.core.cipher.Verifier;
import java.security.cert.X509Certificate;
import java.util.List;

/** 证书处理器 */
public interface CertificateHandler {
Expand All @@ -16,10 +14,10 @@ public interface CertificateHandler {
X509Certificate generateCertificate(String certificate);

/**
* 使用微信支付平台证书生成Verifier
* * 验证证书链
*
* @param certificateList 微信支付平台证书列表
* @return verifier
* @param certificate 微信支付平台证书
* @throws com.wechat.pay.java.core.exception.ValidationException 证书验证失败
*/
Verifier generateVerifier(List<X509Certificate> certificateList);
void validateCertPath(X509Certificate certificate);
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,69 @@
package com.wechat.pay.java.core.certificate;

import com.wechat.pay.java.core.cipher.RSAVerifier;
import com.wechat.pay.java.core.cipher.Verifier;
import com.wechat.pay.java.core.exception.ValidationException;
import com.wechat.pay.java.core.util.PemUtil;
import java.security.cert.X509Certificate;
import java.util.List;
import java.security.cert.*;
import java.util.*;

class RSACertificateHandler implements CertificateHandler {
final class RSACertificateHandler implements CertificateHandler {

private static final X509Certificate tenpayCACert =
PemUtil.loadX509FromString(
"-----BEGIN CERTIFICATE-----\n"
+ "MIIEcDCCA1igAwIBAgIUG9QiDlDbwEsGrTl1SYRsAcPo69IwDQYJKoZIhvcNAQEL\n"
+ "BQAwcDELMAkGA1UEBhMCQ04xEzARBgNVBAoMCmlUcnVzQ2hpbmExHDAaBgNVBAsM\n"
+ "E0NoaW5hIFRydXN0IE5ldHdvcmsxLjAsBgNVBAMMJWlUcnVzQ2hpbmEgQ2xhc3Mg\n"
+ "MiBFbnRlcnByaXNlIENBIC0gRzMwHhcNMTcwODA5MDkxNTU1WhcNMzIwODA5MDkx\n"
+ "NTU1WjBeMQswCQYDVQQGEwJDTjETMBEGA1UEChMKVGVucGF5LmNvbTEdMBsGA1UE\n"
+ "CxMUVGVucGF5LmNvbSBDQSBDZW50ZXIxGzAZBgNVBAMTElRlbnBheS5jb20gUm9v\n"
+ "dCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALvnPD6k39BdPYAH\n"
+ "+6lnWPjuHH+2pcmZUf2E8cNFQFNr+ECRZylYV2iKyItCQt3I2/7VIDZl6aR9TE7n\n"
+ "sZrtSmOXCw635QOrq2yF9LTSDotAhf3ER0+216w3age/VzGcNVQpTf6gRCHCuQIk\n"
+ "8pe/oh06JagGvX0wERa+I6NfuG58ZHQY9d6RqLXKQl0Up95v73HDsG487z8k6jcn\n"
+ "qpGngmHQxdWiWRJugqxNRUD+awv2/DUsqGOffPX4jzJ6rLSJSlQXvuniDYxmaiaD\n"
+ "cK0bUbB5aM+1zMwogoHSYxWj/6B+vgcnHQCUrwGdiQR5+F+yRWzy5bO09IzaFgeO\n"
+ "PNPLPOsCAwEAAaOCARIwggEOMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/\n"
+ "BAQDAgEGMCAGA1UdEQQZMBekFTATMREwDwYDVQQDDAhzd2JlLTI2NjAdBgNVHQ4E\n"
+ "FgQUTFo4GLdm9oHX52HcWnzuL4tui2gwHwYDVR0jBBgwFoAUK1vVxWgI69vN5LA5\n"
+ "MqJf/8dPmEUwRgYDVR0gBD8wPTA7BgoqgRyG7xcBAQECMC0wKwYIKwYBBQUHAgEW\n"
+ "H2h0dHBzOi8vd3d3Lml0cnVzLmNvbS5jbi9jdG5jcHMwPgYDVR0fBDcwNTAzoDGg\n"
+ "L4YtaHR0cDovL3RvcGNhLml0cnVzLmNvbS5jbi9jcmwvaXRydXNjMmNhZzMuY3Js\n"
+ "MA0GCSqGSIb3DQEBCwUAA4IBAQBwZhL/eiOQmMyo1D0IR9mu1DPWl5J3XXhjc4R6\n"
+ "mFgsN/FCeVP9M4U9y2FJH6i5Ha5YCecKGw5pwhA0rjZr/6okWwo22GF+nzI/gQiz\n"
+ "6ugAKs5VjFbeiEb04Ncz4HT8FP1idK3tyCjqCUTkLNt0U3tR7wy26hgOqlT2wCZ9\n"
+ "X4MfT8dUMdt9nCZx4ujN5yZOzaLOCHmzoGDGxgKg91bbu0TG2Yzd2ylhrxxRtFH9\n"
+ "aZ/J1x5UoF7uwhTM8P92DuAldWC1/bX1kciOtQvQEZeAy+9y/1BtFxoBnmDxnqkX\n"
+ "+lirIUYTLDaL7HaLrOLECUlaxZCU/Nkwm3tmqQxtCh+XQBdd\n"
+ "-----END CERTIFICATE-----");

private static final Set<TrustAnchor> trustAnchor =
new LinkedHashSet<>(Collections.singletonList(new TrustAnchor(tenpayCACert, null)));

@Override
public X509Certificate generateCertificate(String certificate) {
return PemUtil.loadX509FromString(certificate);
}

@Override
public Verifier generateVerifier(List<X509Certificate> certificateList) {
return new RSAVerifier(new InMemoryCertificateProvider(certificateList));
public void validateCertPath(X509Certificate certificate) {
try {
PKIXParameters params = new PKIXParameters(trustAnchor);
params.setRevocationEnabled(false);

List<X509Certificate> certs = new ArrayList<>();
certs.add(certificate);

CertificateFactory cf = CertificateFactory.getInstance("X.509");
CertPath certPath = cf.generateCertPath(certs);

CertPathValidator validator = CertPathValidator.getInstance("PKIX");
validator.validate(certPath, params);
} catch (Exception e) {
throw new ValidationException(
String.format(
"certificate[%s] validation failed: %s",
PemUtil.getSerialNumber(certificate), e.getMessage()),
e);
}
}
}
6 changes: 6 additions & 0 deletions core/src/main/java/com/wechat/pay/java/core/util/PemUtil.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.wechat.pay.java.core.util;

import static com.wechat.pay.java.core.cipher.Constant.HEX;

import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
Expand Down Expand Up @@ -190,4 +192,8 @@ public static X509Certificate loadX509FromString(String certificateString, Strin
throw new UncheckedIOException(e);
}
}

public static String getSerialNumber(X509Certificate certificate) {
return certificate.getSerialNumber().toString(HEX).toUpperCase();
}
}
Loading