diff --git a/README.md b/README.md index fa3e6c22..7b0d31f3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![JavaDoc](http://img.shields.io/badge/javadoc-reference-blue.svg)](https://www.javadoc.io/doc/com.github.wechatpay-apiv3/wechatpay-java/latest/index.html) -![Maven Central](https://img.shields.io/maven-central/v/com.github.wechatpay-apiv3/wechatpay-java?versionPrefix=0.2.12) +![Maven Central](https://img.shields.io/maven-central/v/com.github.wechatpay-apiv3/wechatpay-java?versionPrefix=0.2.14) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=wechatpay-apiv3_wechatpay-java&metric=security_rating)](https://sonarcloud.io/summary/overall?id=wechatpay-apiv3_wechatpay-java) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=wechatpay-apiv3_wechatpay-java&metric=sqale_rating)](https://sonarcloud.io/summary/overall?id=wechatpay-apiv3_wechatpay-java) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=wechatpay-apiv3_wechatpay-java&metric=coverage)](https://sonarcloud.io/summary/overall?id=wechatpay-apiv3_wechatpay-java) @@ -36,7 +36,7 @@ 在你的 build.gradle 文件中加入如下的依赖 ```groovy -implementation 'com.github.wechatpay-apiv3:wechatpay-java:0.2.12' +implementation 'com.github.wechatpay-apiv3:wechatpay-java:0.2.14' ``` #### Maven @@ -47,7 +47,7 @@ implementation 'com.github.wechatpay-apiv3:wechatpay-java:0.2.12' com.github.wechatpay-apiv3 wechatpay-java - 0.2.12 + 0.2.14 ``` @@ -233,6 +233,23 @@ Config config = .build(); ``` +## 使用本地平台公钥 + +如果你的商户可使用微信支付的公钥验证应答和回调的签名,可使用微信支付公钥和公钥ID初始化。 + +```java +// 可以根据实际情况使用publicKeyFromPath或publicKey加载公钥 +Config config = + new RSAPublicKeyConfig.Builder() + .merchantId(merchantId) + .privateKeyFromPath(privateKeyPath) + .publicKeyFromPath(publicKeyPath) + .publicKeyId(publicKeyId) + .merchantSerialNumber(merchantSerialNumber) + .apiV3Key(apiV3Key) + .build(); +``` + ## 回调通知 首先,你需要在你的服务器上创建一个公开的 HTTP 端点,接受来自微信支付的回调通知。 diff --git a/core/src/main/java/com/wechat/pay/java/core/AbstractRSAConfig.java b/core/src/main/java/com/wechat/pay/java/core/AbstractRSAConfig.java index 7e78092b..0d58f3f9 100644 --- a/core/src/main/java/com/wechat/pay/java/core/AbstractRSAConfig.java +++ b/core/src/main/java/com/wechat/pay/java/core/AbstractRSAConfig.java @@ -14,11 +14,13 @@ import com.wechat.pay.java.core.cipher.Signer; import com.wechat.pay.java.core.util.PemUtil; import java.security.PrivateKey; +import java.security.PublicKey; import java.security.cert.X509Certificate; /** RSAConfig抽象类 */ public abstract class AbstractRSAConfig implements Config { + /** 使用微信支付平台证书验签 */ protected AbstractRSAConfig( String merchantId, PrivateKey privateKey, @@ -28,6 +30,23 @@ protected AbstractRSAConfig( this.privateKey = privateKey; this.merchantSerialNumber = merchantSerialNumber; this.certificateProvider = certificateProvider; + this.publicKey = null; + this.publicKeyId = null; + } + + /** 使用微信支付公钥验签 */ + protected AbstractRSAConfig( + String merchantId, + PrivateKey privateKey, + String merchantSerialNumber, + PublicKey publicKey, + String publicKeyId) { + this.merchantId = merchantId; + this.privateKey = privateKey; + this.merchantSerialNumber = merchantSerialNumber; + this.certificateProvider = null; + this.publicKey = publicKey; + this.publicKeyId = publicKeyId; } /** 商户号 */ @@ -42,8 +61,17 @@ protected AbstractRSAConfig( /** 微信支付平台证书Provider */ private final CertificateProvider certificateProvider; + /** 微信支付平台公钥 */ + private final PublicKey publicKey; + + /** 微信支付平台公钥Id */ + private final String publicKeyId; + @Override public PrivacyEncryptor createEncryptor() { + if (publicKey != null) { + return new RSAPrivacyEncryptor(publicKey, publicKeyId); + } X509Certificate certificate = certificateProvider.getAvailableCertificate(); return new RSAPrivacyEncryptor( certificate.getPublicKey(), PemUtil.getSerialNumber(certificate)); @@ -61,6 +89,9 @@ public Credential createCredential() { @Override public Validator createValidator() { + if (publicKey != null) { + return new WechatPay2Validator(new RSAVerifier(publicKey, publicKeyId)); + } return new WechatPay2Validator(new RSAVerifier(certificateProvider)); } diff --git a/core/src/main/java/com/wechat/pay/java/core/RSAPublicKeyConfig.java b/core/src/main/java/com/wechat/pay/java/core/RSAPublicKeyConfig.java new file mode 100644 index 00000000..cc73aef8 --- /dev/null +++ b/core/src/main/java/com/wechat/pay/java/core/RSAPublicKeyConfig.java @@ -0,0 +1,120 @@ +package com.wechat.pay.java.core; + +import static com.wechat.pay.java.core.notification.Constant.AES_CIPHER_ALGORITHM; +import static com.wechat.pay.java.core.notification.Constant.RSA_SIGN_TYPE; +import static java.util.Objects.requireNonNull; + +import com.wechat.pay.java.core.cipher.AeadAesCipher; +import com.wechat.pay.java.core.cipher.AeadCipher; +import com.wechat.pay.java.core.cipher.RSAVerifier; +import com.wechat.pay.java.core.cipher.Verifier; +import com.wechat.pay.java.core.notification.NotificationConfig; +import com.wechat.pay.java.core.util.PemUtil; +import java.nio.charset.StandardCharsets; +import java.security.PublicKey; + +/** 使用微信支付平台公钥的RSA配置类。 每次构造都要求传入平台公钥以及平台公钥id,如果使用平台证书建议用RSAAutoCertificateConfig类 */ +public final class RSAPublicKeyConfig extends AbstractRSAConfig implements NotificationConfig { + + private final PublicKey publicKey; + private final AeadCipher aeadCipher; + private final String publicKeyId; + + private RSAPublicKeyConfig(Builder builder) { + super( + builder.merchantId, + builder.privateKey, + builder.merchantSerialNumber, + builder.publicKey, + builder.publicKeyId); + this.publicKey = builder.publicKey; + this.publicKeyId = builder.publicKeyId; + this.aeadCipher = new AeadAesCipher(builder.apiV3Key); + } + + /** + * 获取签名类型 + * + * @return 签名类型 + */ + @Override + public String getSignType() { + return RSA_SIGN_TYPE; + } + + /** + * 获取认证加解密器类型 + * + * @return 认证加解密器类型 + */ + @Override + public String getCipherType() { + return AES_CIPHER_ALGORITHM; + } + + /** + * 创建验签器 + * + * @return 验签器 + */ + @Override + public Verifier createVerifier() { + return new RSAVerifier(publicKey, publicKeyId); + } + + /** + * 创建认证加解密器 + * + * @return 认证加解密器 + */ + @Override + public AeadCipher createAeadCipher() { + return aeadCipher; + } + + public static class Builder extends AbstractRSAConfigBuilder { + protected byte[] apiV3Key; + protected PublicKey publicKey; + protected String publicKeyId; + + public Builder apiV3Key(String apiV3Key) { + this.apiV3Key = apiV3Key.getBytes(StandardCharsets.UTF_8); + return self(); + } + + public Builder publicKey(String publicKey) { + this.publicKey = PemUtil.loadPublicKeyFromString(publicKey); + return self(); + } + + public Builder publicKey(PublicKey publicKey) { + this.publicKey = publicKey; + return self(); + } + + public Builder publicKeyFromPath(String publicKeyPath) { + this.publicKey = PemUtil.loadPublicKeyFromPath(publicKeyPath); + return self(); + } + + public Builder publicKeyId(String publicKeyId) { + this.publicKeyId = publicKeyId; + return self(); + } + + @Override + protected Builder self() { + return this; + } + + public RSAPublicKeyConfig build() { + requireNonNull(merchantId); + requireNonNull(publicKey); + requireNonNull(publicKeyId); + requireNonNull(privateKey); + requireNonNull(merchantSerialNumber); + + return new RSAPublicKeyConfig(this); + } + } +} diff --git a/core/src/main/java/com/wechat/pay/java/core/certificate/CertificateDownloader.java b/core/src/main/java/com/wechat/pay/java/core/certificate/CertificateDownloader.java index 65a99ca7..ae5fa35d 100644 --- a/core/src/main/java/com/wechat/pay/java/core/certificate/CertificateDownloader.java +++ b/core/src/main/java/com/wechat/pay/java/core/certificate/CertificateDownloader.java @@ -6,13 +6,13 @@ 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.ServiceException; 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.MediaType; -import com.wechat.pay.java.core.util.PemUtil; import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.Base64; @@ -77,16 +77,17 @@ public Map download() { .addHeader(Constant.ACCEPT, " */*") .addHeader(Constant.CONTENT_TYPE, MediaType.APPLICATION_JSON.getValue()) .build(); - HttpResponse httpResponse = - httpClient.execute(httpRequest, DownloadCertificateResponse.class); - - Map downloaded = decryptCertificate(httpResponse); - validateCertificate(downloaded); - return downloaded; - } - - private void validateCertificate(Map certificates) { - certificates.forEach((serialNo, cert) -> certificateHandler.validateCertPath(cert)); + try { + HttpResponse httpResponse = + httpClient.execute(httpRequest, DownloadCertificateResponse.class); + return decryptCertificate(httpResponse); + } catch (ServiceException e) { + // 如果证书不存在,可能是切换为平台公钥,该处不报错 + if (e.getErrorCode().equals("NOT_FOUND")) { + return new HashMap<>(); + } + throw e; + } } /** @@ -109,7 +110,7 @@ private Map decryptCertificate( Base64.getDecoder().decode(encryptCertificate.getCiphertext())); certificate = certificateHandler.generateCertificate(decryptCertificate); - downloadCertMap.put(PemUtil.getSerialNumber(certificate), certificate); + downloadCertMap.put(data.getSerialNo(), certificate); } return downloadCertMap; } diff --git a/core/src/main/java/com/wechat/pay/java/core/certificate/CertificateHandler.java b/core/src/main/java/com/wechat/pay/java/core/certificate/CertificateHandler.java index bf43e218..cf12bd10 100644 --- a/core/src/main/java/com/wechat/pay/java/core/certificate/CertificateHandler.java +++ b/core/src/main/java/com/wechat/pay/java/core/certificate/CertificateHandler.java @@ -14,7 +14,7 @@ public interface CertificateHandler { X509Certificate generateCertificate(String certificate); /** - * * 验证证书链 + * * 验证证书链(不推荐验证,如果证书过期不及时更换会导致验证失败,从而影响业务) * * @param certificate 微信支付平台证书 * @throws com.wechat.pay.java.core.exception.ValidationException 证书验证失败 diff --git a/core/src/main/java/com/wechat/pay/java/core/certificate/RSACertificateHandler.java b/core/src/main/java/com/wechat/pay/java/core/certificate/RSACertificateHandler.java index c9ad3818..cecd753f 100644 --- a/core/src/main/java/com/wechat/pay/java/core/certificate/RSACertificateHandler.java +++ b/core/src/main/java/com/wechat/pay/java/core/certificate/RSACertificateHandler.java @@ -1,44 +1,10 @@ package com.wechat.pay.java.core.certificate; -import com.wechat.pay.java.core.exception.ValidationException; import com.wechat.pay.java.core.util.PemUtil; import java.security.cert.*; -import java.util.*; 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 = - new LinkedHashSet<>(Collections.singletonList(new TrustAnchor(tenpayCACert, null))); - @Override public X509Certificate generateCertificate(String certificate) { return PemUtil.loadX509FromString(certificate); @@ -46,24 +12,6 @@ public X509Certificate generateCertificate(String certificate) { @Override public void validateCertPath(X509Certificate certificate) { - try { - PKIXParameters params = new PKIXParameters(trustAnchor); - params.setRevocationEnabled(false); - - List 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); - } + // 为防止证书过期导致验签失败,从而影响业务,后续不再验证证书信任链 } } diff --git a/core/src/main/java/com/wechat/pay/java/core/cipher/AbstractVerifier.java b/core/src/main/java/com/wechat/pay/java/core/cipher/AbstractVerifier.java index d5f25f14..4c99a7e0 100644 --- a/core/src/main/java/com/wechat/pay/java/core/cipher/AbstractVerifier.java +++ b/core/src/main/java/com/wechat/pay/java/core/cipher/AbstractVerifier.java @@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.cert.X509Certificate; @@ -17,7 +18,8 @@ public abstract class AbstractVerifier implements Verifier { protected static final Logger logger = LoggerFactory.getLogger(AbstractVerifier.class); protected final CertificateProvider certificateProvider; - + protected final PublicKey publicKey; + protected final String publicKeyId; protected final String algorithmName; /** @@ -29,6 +31,41 @@ public abstract class AbstractVerifier implements Verifier { protected AbstractVerifier(String algorithmName, CertificateProvider certificateProvider) { this.certificateProvider = requireNonNull(certificateProvider); this.algorithmName = requireNonNull(algorithmName); + this.publicKey = null; + this.publicKeyId = null; + } + + /** + * AbstractVerifier 构造函数 + * + * @param algorithmName 获取Signature对象时指定的算法,例如SHA256withRSA + * @param publicKey 验签使用的微信支付平台公钥,非空 + * @param publicKeyId 验签使用的微信支付平台公钥id + */ + protected AbstractVerifier(String algorithmName, PublicKey publicKey, String publicKeyId) { + this.publicKey = requireNonNull(publicKey); + this.publicKeyId = publicKeyId; + this.algorithmName = requireNonNull(algorithmName); + this.certificateProvider = null; + } + + /** + * AbstractVerifier 构造函数,仅在平台证书和平台公钥灰度切换阶段使用 + * + * @param algorithmName 获取Signature对象时指定的算法,例如SHA256withRSA + * @param publicKey 验签使用的微信支付平台公钥,非空 + * @param publicKeyId 验签使用的微信支付平台公钥id + * @param certificateProvider 验签使用的微信支付平台证书管理器,非空 + */ + protected AbstractVerifier( + String algorithmName, + PublicKey publicKey, + String publicKeyId, + CertificateProvider certificateProvider) { + this.publicKey = requireNonNull(publicKey); + this.publicKeyId = publicKeyId; + this.algorithmName = requireNonNull(algorithmName); + this.certificateProvider = requireNonNull(certificateProvider); } protected boolean verify(X509Certificate certificate, String message, String signature) { @@ -47,12 +84,42 @@ protected boolean verify(X509Certificate certificate, String message, String sig } } + private boolean verify(String message, String signature) { + try { + Signature sign = Signature.getInstance(algorithmName); + sign.initVerify(publicKey); + sign.update(message.getBytes(StandardCharsets.UTF_8)); + return sign.verify(Base64.getDecoder().decode(signature)); + } catch (SignatureException e) { + return false; + } catch (InvalidKeyException e) { + throw new IllegalArgumentException("verify uses an illegal publickey.", e); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException( + "The current Java environment does not support " + algorithmName, e); + } + } + @Override public boolean verify(String serialNumber, String message, String signature) { + // 如果公钥不为空,使用公钥验签 + if (publicKey != null) { + if (serialNumber.equals(publicKeyId)) { + return verify(message, signature); + } + // 如果证书为空,则说明是传入的publicKeyId错误,如果不为空,则继续使用证书验签 + if (certificateProvider == null) { + logger.error( + "publicKeyId[{}] and serialNumber[{}] are not equal", publicKeyId, serialNumber); + return false; + } + } + // 使用证书验签 + requireNonNull(certificateProvider); X509Certificate certificate = certificateProvider.getCertificate(serialNumber); if (certificate == null) { logger.error( - "Verify the signature and get the WechatPay certificate corresponding to " + "Verify the signature and get the WechatPay certificate or publicKey corresponding to " + "serialNumber[{}] is empty.", serialNumber); return false; diff --git a/core/src/main/java/com/wechat/pay/java/core/cipher/RSAVerifier.java b/core/src/main/java/com/wechat/pay/java/core/cipher/RSAVerifier.java index 184822ff..561e6691 100644 --- a/core/src/main/java/com/wechat/pay/java/core/cipher/RSAVerifier.java +++ b/core/src/main/java/com/wechat/pay/java/core/cipher/RSAVerifier.java @@ -3,6 +3,7 @@ import static com.wechat.pay.java.core.cipher.Constant.SHA256WITHRSA; import com.wechat.pay.java.core.certificate.CertificateProvider; +import java.security.PublicKey; /** RSA验签器 */ public final class RSAVerifier extends AbstractVerifier { @@ -10,4 +11,12 @@ public final class RSAVerifier extends AbstractVerifier { public RSAVerifier(CertificateProvider provider) { super(SHA256WITHRSA, provider); } + + public RSAVerifier(PublicKey publicKey, String publicKeyId) { + super(SHA256WITHRSA, publicKey, publicKeyId); + } + + public RSAVerifier(PublicKey publicKey, String publicKeyId, CertificateProvider provider) { + super(SHA256WITHRSA, publicKey, publicKeyId, provider); + } } diff --git a/core/src/main/java/com/wechat/pay/java/core/cipher/Verifier.java b/core/src/main/java/com/wechat/pay/java/core/cipher/Verifier.java index 1e4fbcc2..e7b1b8a3 100644 --- a/core/src/main/java/com/wechat/pay/java/core/cipher/Verifier.java +++ b/core/src/main/java/com/wechat/pay/java/core/cipher/Verifier.java @@ -6,7 +6,7 @@ public interface Verifier { /** * 验证签名 * - * @param serialNumber 用于验证签名的证书序列号 + * @param serialNumber 用于验证签名的证书序列号或者公钥id * @param message 签名信息 * @param signature 待验证的签名 * @return 是否验证通过 diff --git a/core/src/main/java/com/wechat/pay/java/core/notification/AbstractNotificationConfig.java b/core/src/main/java/com/wechat/pay/java/core/notification/AbstractNotificationConfig.java index edc764f8..cd75994e 100644 --- a/core/src/main/java/com/wechat/pay/java/core/notification/AbstractNotificationConfig.java +++ b/core/src/main/java/com/wechat/pay/java/core/notification/AbstractNotificationConfig.java @@ -4,6 +4,7 @@ import com.wechat.pay.java.core.cipher.AeadCipher; import com.wechat.pay.java.core.cipher.RSAVerifier; import com.wechat.pay.java.core.cipher.Verifier; +import java.security.PublicKey; public abstract class AbstractNotificationConfig implements NotificationConfig { @@ -11,6 +12,8 @@ public abstract class AbstractNotificationConfig implements NotificationConfig { private final String cipherAlgorithm; private final CertificateProvider certificateProvider; private final AeadCipher aeadCipher; + private final PublicKey publicKey; + private final String publicKeyId; protected AbstractNotificationConfig( String signType, @@ -21,6 +24,37 @@ protected AbstractNotificationConfig( this.cipherAlgorithm = cipherAlgorithm; this.certificateProvider = certificateProvider; this.aeadCipher = aeadCipher; + this.publicKey = null; + this.publicKeyId = null; + } + + protected AbstractNotificationConfig( + String signType, + String cipherAlgorithm, + PublicKey publicKey, + String publicKeyId, + AeadCipher aeadCipher) { + this.signType = signType; + this.cipherAlgorithm = cipherAlgorithm; + this.publicKey = publicKey; + this.publicKeyId = publicKeyId; + this.aeadCipher = aeadCipher; + this.certificateProvider = null; + } + + protected AbstractNotificationConfig( + String signType, + String cipherAlgorithm, + CertificateProvider certificateProvider, + PublicKey publicKey, + String publicKeyId, + AeadCipher aeadCipher) { + this.signType = signType; + this.cipherAlgorithm = cipherAlgorithm; + this.publicKey = publicKey; + this.publicKeyId = publicKeyId; + this.aeadCipher = aeadCipher; + this.certificateProvider = certificateProvider; } @Override @@ -35,6 +69,12 @@ public String getCipherType() { @Override public Verifier createVerifier() { + if (publicKey != null && certificateProvider != null) { + return new RSAVerifier(publicKey, publicKeyId, certificateProvider); + } + if (publicKey != null) { + return new RSAVerifier(publicKey, publicKeyId); + } return new RSAVerifier(certificateProvider); } diff --git a/core/src/main/java/com/wechat/pay/java/core/notification/RSACombinedNotificationConfig.java b/core/src/main/java/com/wechat/pay/java/core/notification/RSACombinedNotificationConfig.java new file mode 100644 index 00000000..cc580bc6 --- /dev/null +++ b/core/src/main/java/com/wechat/pay/java/core/notification/RSACombinedNotificationConfig.java @@ -0,0 +1,94 @@ +package com.wechat.pay.java.core.notification; + +import static com.wechat.pay.java.core.notification.Constant.AES_CIPHER_ALGORITHM; +import static com.wechat.pay.java.core.notification.Constant.RSA_SIGN_TYPE; +import static java.util.Objects.requireNonNull; + +import com.wechat.pay.java.core.AbstractRSAConfigBuilder; +import com.wechat.pay.java.core.certificate.CertificateProvider; +import com.wechat.pay.java.core.certificate.RSAAutoCertificateProvider; +import com.wechat.pay.java.core.cipher.AeadAesCipher; +import com.wechat.pay.java.core.cipher.AeadCipher; +import com.wechat.pay.java.core.http.HttpClient; +import com.wechat.pay.java.core.util.PemUtil; +import java.nio.charset.StandardCharsets; +import java.security.PublicKey; + +/** 通知回调配置类 该类仅在商户由平台证书切换为平台公钥的灰度阶段使用,灰度完成后请切换为RSAPublicKeyNotificationConfig */ +public final class RSACombinedNotificationConfig extends AbstractNotificationConfig { + + private RSACombinedNotificationConfig( + CertificateProvider certificateProvider, + PublicKey publicKey, + String publicKeyId, + AeadCipher aeadAesCipher) { + super( + RSA_SIGN_TYPE, + AES_CIPHER_ALGORITHM, + certificateProvider, + publicKey, + publicKeyId, + aeadAesCipher); + } + + public static class Builder extends AbstractRSAConfigBuilder { + protected HttpClient httpClient; + protected byte[] apiV3Key; + + private PublicKey publicKey; + private String publicKeyId; + + public Builder apiV3Key(String apiV3Key) { + this.apiV3Key = apiV3Key.getBytes(StandardCharsets.UTF_8); + return this; + } + + public Builder httpClient(HttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + public Builder publicKey(String publicKey) { + this.publicKey = PemUtil.loadPublicKeyFromString(publicKey); + return this; + } + + public Builder publicKey(PublicKey publicKey) { + this.publicKey = publicKey; + return this; + } + + public Builder publicKeyFromPath(String publicKeyPath) { + this.publicKey = PemUtil.loadPublicKeyFromPath(publicKeyPath); + return this; + } + + public Builder publicKeyId(String publicKeyId) { + this.publicKeyId = publicKeyId; + return this; + } + + @Override + protected Builder self() { + return this; + } + + public RSACombinedNotificationConfig build() { + + RSAAutoCertificateProvider.Builder builder = + new RSAAutoCertificateProvider.Builder() + .apiV3Key(requireNonNull(apiV3Key)) + .privateKey(requireNonNull(privateKey)) + .merchantId(requireNonNull(merchantId)) + .merchantSerialNumber(requireNonNull(merchantSerialNumber)); + if (httpClient != null) { + builder.httpClient(httpClient); + } + return new RSACombinedNotificationConfig( + builder.build(), + requireNonNull(publicKey), + requireNonNull(publicKeyId), + new AeadAesCipher(requireNonNull(apiV3Key))); + } + } +} diff --git a/core/src/main/java/com/wechat/pay/java/core/notification/RSANotificationConfig.java b/core/src/main/java/com/wechat/pay/java/core/notification/RSANotificationConfig.java index 1627b4d0..7edf9fcb 100644 --- a/core/src/main/java/com/wechat/pay/java/core/notification/RSANotificationConfig.java +++ b/core/src/main/java/com/wechat/pay/java/core/notification/RSANotificationConfig.java @@ -15,7 +15,11 @@ import java.util.Arrays; import java.util.List; -/** 签名类型为RSA的通知配置参数 */ +/** + * 通知回调配置类 + * + * @deprecated 请使用 RSAAutoCertificateConfig,开发者应尽快迁移,我们将在未来某个时间移除这段废弃的代码。 + */ public final class RSANotificationConfig extends AbstractNotificationConfig { private RSANotificationConfig(CertificateProvider certificateProvider, AeadCipher aeadCipher) { diff --git a/core/src/main/java/com/wechat/pay/java/core/notification/RSAPublicKeyNotificationConfig.java b/core/src/main/java/com/wechat/pay/java/core/notification/RSAPublicKeyNotificationConfig.java new file mode 100644 index 00000000..2277482b --- /dev/null +++ b/core/src/main/java/com/wechat/pay/java/core/notification/RSAPublicKeyNotificationConfig.java @@ -0,0 +1,60 @@ +package com.wechat.pay.java.core.notification; + +import static com.wechat.pay.java.core.notification.Constant.AES_CIPHER_ALGORITHM; +import static com.wechat.pay.java.core.notification.Constant.RSA_SIGN_TYPE; +import static java.util.Objects.requireNonNull; + +import com.wechat.pay.java.core.cipher.AeadAesCipher; +import com.wechat.pay.java.core.cipher.AeadCipher; +import com.wechat.pay.java.core.util.PemUtil; +import java.nio.charset.StandardCharsets; +import java.security.PublicKey; + +/** 签名类型为RSA的通知配置参数 */ +public final class RSAPublicKeyNotificationConfig extends AbstractNotificationConfig { + + private RSAPublicKeyNotificationConfig( + PublicKey publicKey, String publicKeyId, AeadCipher aeadCipher) { + super(RSA_SIGN_TYPE, AES_CIPHER_ALGORITHM, publicKey, publicKeyId, aeadCipher); + } + + public static class Builder { + private byte[] apiV3Key; + + private PublicKey publicKey; + private String publicKeyId; + + public Builder publicKey(String publicKey) { + this.publicKey = PemUtil.loadPublicKeyFromString(publicKey); + return this; + } + + public Builder publicKey(PublicKey publicKey) { + this.publicKey = publicKey; + return this; + } + + public Builder publicKeyFromPath(String publicKeyPath) { + this.publicKey = PemUtil.loadPublicKeyFromPath(publicKeyPath); + return this; + } + + public Builder apiV3Key(String apiV3Key) { + this.apiV3Key = apiV3Key.getBytes(StandardCharsets.UTF_8); + return this; + } + + public Builder publicKeyId(String publicKeyId) { + this.publicKeyId = publicKeyId; + return this; + } + + public RSAPublicKeyNotificationConfig build() { + requireNonNull(publicKey); + requireNonNull(publicKeyId); + requireNonNull(apiV3Key); + return new RSAPublicKeyNotificationConfig( + publicKey, requireNonNull(publicKeyId), new AeadAesCipher(requireNonNull(apiV3Key))); + } + } +} diff --git a/core/src/main/java/com/wechat/pay/java/core/util/PemUtil.java b/core/src/main/java/com/wechat/pay/java/core/util/PemUtil.java index 9873a5a6..022bf987 100644 --- a/core/src/main/java/com/wechat/pay/java/core/util/PemUtil.java +++ b/core/src/main/java/com/wechat/pay/java/core/util/PemUtil.java @@ -12,11 +12,13 @@ import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PrivateKey; +import java.security.PublicKey; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; import java.util.Base64; /** PEM工具 */ @@ -71,6 +73,28 @@ public static PrivateKey loadPrivateKeyFromString( } } + /** + * 从字符串中加载RSA公钥。 + * + * @param keyString 公钥字符串 + * @return RSA公钥 + */ + public static PublicKey loadPublicKeyFromString(String keyString) { + try { + keyString = + keyString + .replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replaceAll("\\s+", ""); + return KeyFactory.getInstance("RSA") + .generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(keyString))); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } catch (InvalidKeySpecException e) { + throw new IllegalArgumentException(e); + } + } + /** * 从文件路径加载RSA私钥 * @@ -78,7 +102,7 @@ public static PrivateKey loadPrivateKeyFromString( * @return RSA私钥 */ public static PrivateKey loadPrivateKeyFromPath(String keyPath) { - return loadPrivateKeyFromString(readPrivateKeyStringFromPath(keyPath)); + return loadPrivateKeyFromString(readKeyStringFromPath(keyPath)); } /** @@ -91,10 +115,20 @@ public static PrivateKey loadPrivateKeyFromPath(String keyPath) { */ public static PrivateKey loadPrivateKeyFromPath( String keyPath, String algorithm, String provider) { - return loadPrivateKeyFromString(readPrivateKeyStringFromPath(keyPath), algorithm, provider); + return loadPrivateKeyFromString(readKeyStringFromPath(keyPath), algorithm, provider); + } + + /** + * 从文件路径加载RSA公钥 + * + * @param keyPath 公钥路径 + * @return RSA公钥 + */ + public static PublicKey loadPublicKeyFromPath(String keyPath) { + return loadPublicKeyFromString(readKeyStringFromPath(keyPath)); } - private static String readPrivateKeyStringFromPath(String keyPath) { + private static String readKeyStringFromPath(String keyPath) { try (FileInputStream inputStream = new FileInputStream(keyPath)) { return IOUtil.toString(inputStream); } catch (IOException e) { diff --git a/core/src/test/java/com/wechat/pay/java/core/RSAPublicKeyConfigTest.java b/core/src/test/java/com/wechat/pay/java/core/RSAPublicKeyConfigTest.java new file mode 100644 index 00000000..b84940d6 --- /dev/null +++ b/core/src/test/java/com/wechat/pay/java/core/RSAPublicKeyConfigTest.java @@ -0,0 +1,94 @@ +package com.wechat.pay.java.core; + +import static com.wechat.pay.java.core.model.TestConfig.API_V3_KEY; +import static com.wechat.pay.java.core.model.TestConfig.MERCHANT_CERTIFICATE_SERIAL_NUMBER; +import static com.wechat.pay.java.core.model.TestConfig.MERCHANT_ID; +import static com.wechat.pay.java.core.model.TestConfig.MERCHANT_PRIVATE_KEY; +import static com.wechat.pay.java.core.model.TestConfig.MERCHANT_PRIVATE_KEY_PATH; +import static com.wechat.pay.java.core.model.TestConfig.MERCHANT_PRIVATE_KEY_STRING; +import static com.wechat.pay.java.core.model.TestConfig.WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER; +import static com.wechat.pay.java.core.model.TestConfig.WECHAT_PAY_PUBLIC_KEY; +import static com.wechat.pay.java.core.model.TestConfig.WECHAT_PAY_PUBLIC_KEY_PATH; +import static com.wechat.pay.java.core.model.TestConfig.WECHAT_PAY_PUBLIC_KEY_STRING; +import static com.wechat.pay.java.core.notification.Constant.AES_CIPHER_ALGORITHM; +import static com.wechat.pay.java.core.notification.Constant.RSA_SIGN_TYPE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.wechat.pay.java.core.RSAPublicKeyConfig.Builder; +import com.wechat.pay.java.core.util.NonceUtil; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class RSAPublicKeyConfigTest implements ConfigTest { + + @ParameterizedTest + @MethodSource("BuilderProvider") + void testConfigWithBuilderProvider(Builder builder) { + RSAPublicKeyConfig config = builder.build(); + assertNotNull(config.createValidator()); + assertNotNull(config.createCredential()); + assertNotNull(config.createEncryptor()); + assertNotNull(config.createDecryptor()); + assertNotNull(config.createAeadCipher()); + assertNotNull(config.createVerifier()); + + assertEquals(RSA_SIGN_TYPE, config.getSignType()); + assertEquals(AES_CIPHER_ALGORITHM, config.getCipherType()); + } + + static Stream BuilderProvider() { + return Stream.of( + // from string + new Builder() + .merchantId("123456") + .privateKey(MERCHANT_PRIVATE_KEY_STRING) + .merchantSerialNumber(MERCHANT_CERTIFICATE_SERIAL_NUMBER) + .publicKey(WECHAT_PAY_PUBLIC_KEY_STRING) + .publicKeyId(WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER) + .apiV3Key(API_V3_KEY), + + // from path + new Builder() + .merchantId("223456") + .privateKeyFromPath(MERCHANT_PRIVATE_KEY_PATH) + .merchantSerialNumber(MERCHANT_CERTIFICATE_SERIAL_NUMBER) + .publicKeyFromPath(WECHAT_PAY_PUBLIC_KEY_PATH) + .publicKeyId(WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER) + .apiV3Key(API_V3_KEY), + + // with publickey + new Builder() + .merchantId("1123456") + .privateKeyFromPath(MERCHANT_PRIVATE_KEY_PATH) + .merchantSerialNumber(MERCHANT_CERTIFICATE_SERIAL_NUMBER) + .publicKey(WECHAT_PAY_PUBLIC_KEY) + .publicKeyId(WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER) + .apiV3Key(API_V3_KEY)); + } + + @Test + void testBuildConfigWithoutEnoughParam() { + Builder builder = + new Builder() + .merchantId(MERCHANT_ID) + .privateKey(MERCHANT_PRIVATE_KEY) + .merchantSerialNumber(MERCHANT_CERTIFICATE_SERIAL_NUMBER); + assertThrows(NullPointerException.class, builder::build); + } + + @Override + public Config createConfig() { + return new Builder() + .apiV3Key(API_V3_KEY) + .merchantId(NonceUtil.createNonce(6)) + .privateKey(MERCHANT_PRIVATE_KEY) + .merchantSerialNumber(MERCHANT_CERTIFICATE_SERIAL_NUMBER) + .publicKeyFromPath(WECHAT_PAY_PUBLIC_KEY_PATH) + .publicKeyId(WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER) + .build(); + } +} diff --git a/core/src/test/java/com/wechat/pay/java/core/certificate/RSACertificateHandlerTest.java b/core/src/test/java/com/wechat/pay/java/core/certificate/RSACertificateHandlerTest.java deleted file mode 100644 index cdf5ac06..00000000 --- a/core/src/test/java/com/wechat/pay/java/core/certificate/RSACertificateHandlerTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.wechat.pay.java.core.certificate; - -import static org.junit.jupiter.api.Assertions.*; - -import com.wechat.pay.java.core.exception.ValidationException; -import com.wechat.pay.java.core.util.PemUtil; -import java.security.cert.X509Certificate; -import org.junit.jupiter.api.Test; - -class RSACertificateHandlerTest { - - @Test - void testValidateCertPath() { - String validCertificate = - "-----BEGIN CERTIFICATE-----\n" - + "MIIEFDCCAvygAwIBAgIUXeoQ71WHfjz2wzJVwufe//fkJ+wwDQYJKoZIhvcNAQEL\n" - + "BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT\n" - + "FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg\n" - + "Q0EwHhcNMjMwOTE5MTUxNTU4WhcNMjgwOTE3MTUxNTU4WjBuMRgwFgYDVQQDDA9U\n" - + "ZW5wYXkuY29tIHNpZ24xEzARBgNVBAoMClRlbnBheS5jb20xHTAbBgNVBAsMFFRl\n" - + "bnBheS5jb20gQ0EgQ2VudGVyMQswCQYDVQQGDAJDTjERMA8GA1UEBwwIU2hlblpo\n" - + "ZW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3BqqoSXo3OhSFZzEE\n" - + "ZwGRqDy59WvxKywe5XaGJh/ohYkSCn8zzCrTOO4QFu112WfT2iXWXeDHSuMgB9XY\n" - + "9UtVnwel3q7PUnsMZu1Fa7OKVI+SDtIvTSadrWir9BQ0At2ythSB7mbfkqzsnPnm\n" - + "yJXQk5GnuT/tqRJiLzGXXLbo8muP+vJJXOVPcqu4yLn85+ToeH/tsJVhGDzg0McV\n" - + "yyGKomEtvq67uH67cYi3+4NsJaI7hkUa15Rj7s2ccEDE792mD1GR2K+oy1m98BHU\n" - + "RnKmZWNdlAKjZhR+ZLeYoeZcyoqmI7P4G1Vr/yOssXoBImtbLph+G3naJIUdIrWj\n" - + "Zff3AgMBAAGjgbkwgbYwCQYDVR0TBAIwADALBgNVHQ8EBAMCA/gwgZsGA1UdHwSB\n" - + "kzCBkDCBjaCBiqCBh4aBhGh0dHA6Ly9ldmNhLml0cnVzLmNvbS5jbi9wdWJsaWMv\n" - + "aXRydXNjcmw/Q0E9MUJENDIyMEU1MERCQzA0QjA2QUQzOTc1NDk4NDZDMDFDM0U4\n" - + "RUJEMiZzZz1IQUNDNDcxQjY1NDIyRTEyQjI3QTlEMzNBODdBRDFDREY1OTI2RTE0\n" - + "MDM3MTANBgkqhkiG9w0BAQsFAAOCAQEAHnjuI/OubrLb2UjYrUJfmv3OWwIacBzQ\n" - + "jl+0fpPviUkMEXHnwi4sKd5slGK30IeLocfRU+Tl+De7N4PrdiaAswVuMHSbiqPp\n" - + "0wEkogVqunMDyXX6eBa0ouKavyhbKP169dbbGqbqTgBFL0LuSD7finNbM23BVQ5E\n" - + "jep2M4Uqz5uDuZuMMiGYqx1cVkit4w196yoPOSgzaBNlKIAwwHICEgnj18oXIskn\n" - + "l2nzYY/ub+zw78jrkSLec259/Bby2LmhcJNL3Eo2TS0OI95Z6UbnHuHWP60yvPMs\n" - + "ck9llwSj1J9zyEWrG9TCtwdr38U8VmwIz6RQ9k6CK3Yq8tw/a5pYQQ==\n" - + "-----END CERTIFICATE-----"; - X509Certificate certificate = PemUtil.loadX509FromString(validCertificate); - - CertificateHandler handler = new RSACertificateHandler(); - assertDoesNotThrow( - () -> { - handler.validateCertPath(certificate); - }); - } - - @Test - void testInvalidCertPath() { - String validCertificate = - "-----BEGIN CERTIFICATE-----\n" - + "MIIEhDCCA2ygAwIBAgIUaZrpo6ACKL/hM/qaSpnjHU0lcbQwDQYJKoZIhvcNAQEF\n" - + "BQAwRjEbMBkGA1UEAwwSVGVucGF5LmNvbSBVc2VyIENBMRIwEAYDVQQLDAlDQSBD\n" - + "ZW50ZXIxEzARBgNVBAoMClRlbnBheS5jb20wHhcNMjExMTExMDI1NjM2WhcNMjYx\n" - + "MTEwMDI1NjM2WjCBlTEYMBYGA1UEAwwPVGVucGF5LmNvbSBzaWduMSUwIwYJKoZI\n" - + "hvcNAQkBFhZzdXBwb3J0QHN6aXRydXMuY29tLmNuMR0wGwYDVQQLDBRUZW5wYXku\n" - + "Y29tIENBIENlbnRlcjETMBEGA1UECgwKVGVucGF5LmNvbTERMA8GA1UEBwwIU2hl\n" - + "blpoZW4xCzAJBgNVBAYTAkNOMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\n" - + "AQEA7GvNxHf0MdTI0D4sWjL8fHc2HZ50WzuRwRItt1e7YmLpm4lKjlKNmKLRtrJO\n" - + "vmTXwqOmJNLKz6QHMnR0az9l5pIk8ZkzurexUpydHwTKs/OOD5ZKTtiaiwYy1kSC\n" - + "GTVzgEJ7Dw3PzqRMdM80+G30h+RwIDQIXMIZS3W0iLa9pxMXZVzD17N6BiBIpDup\n" - + "M/yErfWyxBd7jq1crvBoHrbyPh5ag4uiV4E0BptmWbn2nOIMq1vuY/LacozhxPcx\n" - + "nUVVPKLxWwxppvNQpWrJ0VjCxwgjhFU/DxZuqr50uyB0g4OEGAvlJiX7/l625ded\n" - + "AJmbiYoJWrOohcqauHdqJaIZ+QIDAQABo4IBGDCCARQwCQYDVR0TBAIwADALBgNV\n" - + "HQ8EBAMCBsAwTwYIKwYBBQUHAQEEQzBBMD8GCCsGAQUFBzAChjNvY3NwLGh0dHA6\n" - + "Ly9Zb3VyX1NlcnZlcl9OYW1lOlBvcnQvVG9wQ0EvbG9kcF9CYXNlRE4waQYDVR0f\n" - + "BGIwYDBeoFygWoZYaHR0cDovLzkuMTkuMTYxLjQ6ODA4MC9Ub3BDQS9wdWJsaWMv\n" - + "aXRydXNjcmw/Q0E9MzlCNDk3QUJDOEFFODg1NzQ1QkY1NjgxRTRGMDNCOEI2NDdG\n" - + "MjhFQTAfBgNVHSMEGDAWgBROc805tvupF/jOiYapcvSklvPrLjAdBgNVHQ4EFgQU\n" - + "SGGfum0liSULBRlrThkdsFe3au4wDQYJKoZIhvcNAQEFBQADggEBAHQBdNMRbLRA\n" - + "TaBnWvk9InV1R7WaO5uIKk3nx41SvBSiKTyKNKGTgro+1PL9aHPHCmnPZ0tQWSXe\n" - + "b78mFAmwCrz7LW7L9zQa2K+3Fk/X4A3ESlDpS4VY+xvFmujK7XfmzbqzvR5z/tFe\n" - + "HAMZ/NMqKc6rah9WcKfRn3EQ0DWfufQmpGPTuX5ZPl84TuPZG7MdApn3Vz4xhxGA\n" - + "5ohYCoCoBK8YNAcLeHNkmatb6GJfS8U+fVcNdDzbnurISYzJvH15yo1iaGNVAqjP\n" - + "Fwb9+n3hVZV6Jm1N9VIDgSmAaeBLj3Dm+T0og37FmLQ1cz148OJ+ScVJFjZ3I+9v\n" - + "IQz4B2jCWH4=\n" - + "-----END CERTIFICATE-----"; - X509Certificate certificate = PemUtil.loadX509FromString(validCertificate); - - CertificateHandler handler = new RSACertificateHandler(); - assertThrows( - ValidationException.class, - () -> { - handler.validateCertPath(certificate); - }); - } -} diff --git a/core/src/test/java/com/wechat/pay/java/core/cipher/RSAVerifierTest.java b/core/src/test/java/com/wechat/pay/java/core/cipher/RSAVerifierTest.java index cabf513c..cdf41d32 100644 --- a/core/src/test/java/com/wechat/pay/java/core/cipher/RSAVerifierTest.java +++ b/core/src/test/java/com/wechat/pay/java/core/cipher/RSAVerifierTest.java @@ -2,6 +2,8 @@ import static com.wechat.pay.java.core.model.TestConfig.MERCHANT_CERTIFICATE; import static com.wechat.pay.java.core.model.TestConfig.MERCHANT_CERTIFICATE_SERIAL_NUMBER; +import static com.wechat.pay.java.core.model.TestConfig.WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER; +import static com.wechat.pay.java.core.model.TestConfig.WECHAT_PAY_PUBLIC_KEY; import com.wechat.pay.java.core.certificate.InMemoryCertificateProvider; import java.security.cert.X509Certificate; @@ -13,7 +15,8 @@ public class RSAVerifierTest { - private static Verifier rsaVerifier; + private static Verifier certificateRsaVerifier; + private static Verifier publicKeyRsaVerifier; private static final String MESSAGE = "message"; /** signature为使用RSASigner和测试商户证书私钥对MESSAGE签名得到的结果 */ @@ -29,13 +32,22 @@ public class RSAVerifierTest { public static void init() { List list = new Vector<>(); list.add(MERCHANT_CERTIFICATE); - rsaVerifier = new RSAVerifier(new InMemoryCertificateProvider(list)); + certificateRsaVerifier = new RSAVerifier(new InMemoryCertificateProvider(list)); + publicKeyRsaVerifier = + new RSAVerifier(WECHAT_PAY_PUBLIC_KEY, WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER); } @Test - public void testVerify() { + public void testCertificateVerify() { Assert.assertTrue( - rsaVerifier.verify( + certificateRsaVerifier.verify( MERCHANT_CERTIFICATE_SERIAL_NUMBER, MESSAGE, SIGNATURE_RESULT.getSign())); } + + @Test + public void testPublicKeyVerify() { + Assert.assertFalse( + publicKeyRsaVerifier.verify( + WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER, MESSAGE, SIGNATURE_RESULT.getSign())); + } } diff --git a/core/src/test/java/com/wechat/pay/java/core/model/TestConfig.java b/core/src/test/java/com/wechat/pay/java/core/model/TestConfig.java index 1024321d..aa360ca3 100644 --- a/core/src/test/java/com/wechat/pay/java/core/model/TestConfig.java +++ b/core/src/test/java/com/wechat/pay/java/core/model/TestConfig.java @@ -3,6 +3,7 @@ import com.wechat.pay.java.core.util.IOUtil; import com.wechat.pay.java.core.util.PemUtil; import java.security.PrivateKey; +import java.security.PublicKey; import java.security.cert.X509Certificate; public class TestConfig { @@ -14,12 +15,15 @@ public class TestConfig { public static final String MERCHANT_CERTIFICATE_STRING; public static final String WECHAT_PAY_PRIVATE_KEY_PATH; public static final String WECHAT_PAY_CERTIFICATE_PATH; + public static final String WECHAT_PAY_PUBLIC_KEY_PATH; public static final String WECHAT_PAY_PRIVATE_KEY_STRING; public static final String WECHAT_PAY_CERTIFICATE_STRING; + public static final String WECHAT_PAY_PUBLIC_KEY_STRING; public static final PrivateKey MERCHANT_PRIVATE_KEY; public static final X509Certificate MERCHANT_CERTIFICATE; public static final PrivateKey WECHAT_PAY_PRIVATE_KEY; public static final X509Certificate WECHAT_PAY_CERTIFICATE; + public static final PublicKey WECHAT_PAY_PUBLIC_KEY; public static final String MERCHANT_CERTIFICATE_SERIAL_NUMBER; public static final String WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER; public static final String MERCHANT_ID; @@ -52,6 +56,7 @@ public class TestConfig { MERCHANT_CERTIFICATE_PATH = RESOURCES_DIR + "/merchant_certificate.pem"; WECHAT_PAY_PRIVATE_KEY_PATH = RESOURCES_DIR + "/wechat_pay_private_key.pem"; WECHAT_PAY_CERTIFICATE_PATH = RESOURCES_DIR + "/wechat_pay_certificate.pem"; + WECHAT_PAY_PUBLIC_KEY_PATH = RESOURCES_DIR + "/wechat_pay_public_key.pem"; MERCHANT_PRIVATE_KEY_STRING = IOUtil.loadStringFromPath(MERCHANT_PRIVATE_KEY_PATH); MERCHANT_PRIVATE_KEY = PemUtil.loadPrivateKeyFromString(MERCHANT_PRIVATE_KEY_STRING); MERCHANT_CERTIFICATE_STRING = IOUtil.loadStringFromPath(MERCHANT_CERTIFICATE_PATH); @@ -60,6 +65,8 @@ public class TestConfig { WECHAT_PAY_PRIVATE_KEY = PemUtil.loadPrivateKeyFromString(WECHAT_PAY_PRIVATE_KEY_STRING); WECHAT_PAY_CERTIFICATE_STRING = IOUtil.loadStringFromPath(WECHAT_PAY_CERTIFICATE_PATH); WECHAT_PAY_CERTIFICATE = PemUtil.loadX509FromString(WECHAT_PAY_CERTIFICATE_STRING); + WECHAT_PAY_PUBLIC_KEY_STRING = IOUtil.loadStringFromPath(WECHAT_PAY_PUBLIC_KEY_PATH); + WECHAT_PAY_PUBLIC_KEY = PemUtil.loadPublicKeyFromString(WECHAT_PAY_PUBLIC_KEY_STRING); MERCHANT_CERTIFICATE_SERIAL_NUMBER = "5F1C72E2A8931B72A2E13AF8DEE92471EB397115"; WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER = "440024045C4A427599D09BB4E3DE0279F2E813FD"; MERCHANT_ID = "1234567891"; diff --git a/core/src/test/java/com/wechat/pay/java/core/notification/RSACombinedNotificationConfigTest.java b/core/src/test/java/com/wechat/pay/java/core/notification/RSACombinedNotificationConfigTest.java new file mode 100644 index 00000000..657c07a9 --- /dev/null +++ b/core/src/test/java/com/wechat/pay/java/core/notification/RSACombinedNotificationConfigTest.java @@ -0,0 +1,137 @@ +package com.wechat.pay.java.core.notification; + +import static com.wechat.pay.java.core.model.TestConfig.API_V3_KEY; +import static com.wechat.pay.java.core.model.TestConfig.DOWNLOAD_CERTIFICATE_RESPONSE; +import static com.wechat.pay.java.core.model.TestConfig.MERCHANT_CERTIFICATE_SERIAL_NUMBER; +import static com.wechat.pay.java.core.model.TestConfig.MERCHANT_ID; +import static com.wechat.pay.java.core.model.TestConfig.MERCHANT_PRIVATE_KEY; +import static com.wechat.pay.java.core.model.TestConfig.MERCHANT_PRIVATE_KEY_STRING; +import static com.wechat.pay.java.core.model.TestConfig.WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER; +import static com.wechat.pay.java.core.model.TestConfig.WECHAT_PAY_PUBLIC_KEY; +import static com.wechat.pay.java.core.model.TestConfig.WECHAT_PAY_PUBLIC_KEY_PATH; +import static com.wechat.pay.java.core.model.TestConfig.WECHAT_PAY_PUBLIC_KEY_STRING; +import static java.net.HttpURLConnection.HTTP_OK; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.wechat.pay.java.core.auth.Validator; +import com.wechat.pay.java.core.auth.WechatPay2Credential; +import com.wechat.pay.java.core.cipher.RSASigner; +import com.wechat.pay.java.core.http.HttpClient; +import com.wechat.pay.java.core.http.HttpHeaders; +import com.wechat.pay.java.core.http.okhttp.OkHttpClientAdapter; +import com.wechat.pay.java.core.notification.RSACombinedNotificationConfig.Builder; +import java.util.stream.Stream; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class RSACombinedNotificationConfigTest implements NotificationConfigTest { + static HttpClient httpClient; + + @BeforeAll + static void initHttpClient() { + Validator validator = + new Validator() { + @Override + public boolean validate(HttpHeaders responseHeaders, String body) { + return true; + } + }; + OkHttpClient okHttpClient = + new OkHttpClient.Builder() + .addInterceptor( + chain -> + new Response.Builder() + .request(chain.request()) + .code(HTTP_OK) + .header("key", "val") + .message("ok") + .protocol(Protocol.HTTP_1_1) + .body( + ResponseBody.create( + DOWNLOAD_CERTIFICATE_RESPONSE, + MediaType.parse( + com.wechat.pay.java.core.http.MediaType.APPLICATION_JSON + .getValue()))) + .build()) + .build(); + httpClient = + new OkHttpClientAdapter( + new WechatPay2Credential( + MERCHANT_ID, + new RSASigner(MERCHANT_CERTIFICATE_SERIAL_NUMBER, MERCHANT_PRIVATE_KEY)), + validator, + okHttpClient); + } + + @ParameterizedTest + @MethodSource("BuilderProvider") + void testConfigWithBuilderProvider(Builder builder) { + RSACombinedNotificationConfig c = builder.build(); + + assertNotNull(c); + assertNotNull(c.createAeadCipher()); + assertNotNull(c.createVerifier()); + assertNotNull(c.getCipherType()); + assertNotNull(c.getSignType()); + } + + static Stream BuilderProvider() { + return Stream.of( + // from string + new Builder() + .merchantId(MERCHANT_ID) + .privateKey(MERCHANT_PRIVATE_KEY_STRING) + .merchantSerialNumber(MERCHANT_CERTIFICATE_SERIAL_NUMBER) + .publicKey(WECHAT_PAY_PUBLIC_KEY_STRING) + .publicKeyId(WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER) + .httpClient(httpClient) + .apiV3Key(API_V3_KEY), + + // from path + new Builder() + .merchantId(MERCHANT_ID) + .privateKey(MERCHANT_PRIVATE_KEY_STRING) + .merchantSerialNumber(MERCHANT_CERTIFICATE_SERIAL_NUMBER) + .publicKeyFromPath(WECHAT_PAY_PUBLIC_KEY_PATH) + .publicKeyId(WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER) + .httpClient(httpClient) + .apiV3Key(API_V3_KEY), + + // with publickey + new Builder() + .merchantId(MERCHANT_ID) + .privateKey(MERCHANT_PRIVATE_KEY_STRING) + .merchantSerialNumber(MERCHANT_CERTIFICATE_SERIAL_NUMBER) + .publicKey(WECHAT_PAY_PUBLIC_KEY) + .publicKeyId(WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER) + .httpClient(httpClient) + .apiV3Key(API_V3_KEY)); + } + + @Test + void testBuildConfigWithoutEnoughParam() { + Builder builder = new Builder().apiV3Key(API_V3_KEY); + assertThrows(NullPointerException.class, builder::build); + } + + @Override + public NotificationConfig buildNotificationConfig() { + return new Builder() + .merchantId(MERCHANT_ID) + .privateKey(MERCHANT_PRIVATE_KEY_STRING) + .merchantSerialNumber(MERCHANT_CERTIFICATE_SERIAL_NUMBER) + .publicKey(WECHAT_PAY_PUBLIC_KEY) + .publicKeyId(WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER) + .apiV3Key(API_V3_KEY) + .httpClient(httpClient) + .build(); + } +} diff --git a/core/src/test/java/com/wechat/pay/java/core/notification/RSAPublicKeyNotificationConfigTest.java b/core/src/test/java/com/wechat/pay/java/core/notification/RSAPublicKeyNotificationConfigTest.java new file mode 100644 index 00000000..baaf8d5e --- /dev/null +++ b/core/src/test/java/com/wechat/pay/java/core/notification/RSAPublicKeyNotificationConfigTest.java @@ -0,0 +1,66 @@ +package com.wechat.pay.java.core.notification; + +import static com.wechat.pay.java.core.model.TestConfig.API_V3_KEY; +import static com.wechat.pay.java.core.model.TestConfig.WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER; +import static com.wechat.pay.java.core.model.TestConfig.WECHAT_PAY_PUBLIC_KEY; +import static com.wechat.pay.java.core.model.TestConfig.WECHAT_PAY_PUBLIC_KEY_PATH; +import static com.wechat.pay.java.core.model.TestConfig.WECHAT_PAY_PUBLIC_KEY_STRING; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.wechat.pay.java.core.notification.RSAPublicKeyNotificationConfig.Builder; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class RSAPublicKeyNotificationConfigTest implements NotificationConfigTest { + + @ParameterizedTest + @MethodSource("BuilderProvider") + void testConfigWithBuilderProvider(Builder builder) { + RSAPublicKeyNotificationConfig c = builder.build(); + + assertNotNull(c); + assertNotNull(c.createAeadCipher()); + assertNotNull(c.createVerifier()); + assertNotNull(c.getCipherType()); + assertNotNull(c.getSignType()); + } + + static Stream BuilderProvider() { + return Stream.of( + // from string + new Builder() + .publicKey(WECHAT_PAY_PUBLIC_KEY_STRING) + .publicKeyId(WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER) + .apiV3Key(API_V3_KEY), + + // from path + new Builder() + .publicKeyFromPath(WECHAT_PAY_PUBLIC_KEY_PATH) + .publicKeyId(WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER) + .apiV3Key(API_V3_KEY), + + // with publickey + new Builder() + .publicKey(WECHAT_PAY_PUBLIC_KEY) + .publicKeyId(WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER) + .apiV3Key(API_V3_KEY)); + } + + @Test + void testBuildConfigWithoutEnoughParam() { + Builder builder = new Builder().apiV3Key(API_V3_KEY); + assertThrows(NullPointerException.class, builder::build); + } + + @Override + public NotificationConfig buildNotificationConfig() { + return new Builder() + .publicKey(WECHAT_PAY_PUBLIC_KEY) + .publicKeyId(WECHAT_PAY_CERTIFICATE_SERIAL_NUMBER) + .apiV3Key(API_V3_KEY) + .build(); + } +} diff --git a/core/src/test/resources/wechat_pay_public_key.pem b/core/src/test/resources/wechat_pay_public_key.pem new file mode 100644 index 00000000..b3787dd4 --- /dev/null +++ b/core/src/test/resources/wechat_pay_public_key.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4qBVumAat3oWPdWVuU24 +fkTVJ7hn6Lvp6m4dCq+d/txH9BDaE2ztUtuAYTb+WeugVoDZKphrQX5tismQq/aN +/rptyvBX8xCAzsmXFsYXWIsiG9cdU4FKk227akp5Yu3d74TXUjDyijwDampQ2HZT +u8O6CeyuuYYxHcpW0VkkStaIHXztqfBGV8K9EM67cbSmC+nq+KY+OIo9rHZUkCnc +LrrBdHf59Ik/KdRsEU1EzzUqPe5dVYDsuiPxWQvQf88SgKu/+/15YFimlpEya9Pd +kCcTV0xy/OSIisIFHek6j9Ii5jLvqhVwaSa+5zkhrrSq4/b10kXLjh3IPsTDenYt +7wIDAQAB +-----END PUBLIC KEY----- diff --git a/gradle.properties b/gradle.properties index 3902fe8b..69081712 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ projectPropGroup=com.github.wechatpay-apiv3 -projectPropVersion=0.2.12 +projectPropVersion=0.2.14 slf4jVersion=1.7.36 junitVersion=4.13.2