From 03d21a335decd636807b4250b9ea5726dcffc302 Mon Sep 17 00:00:00 2001
From: zhanyan <zhanyan.work@outlook.com>
Date: Fri, 29 Nov 2024 16:29:41 +0800
Subject: [PATCH] =?UTF-8?q?new:=20#3402=20[=E5=BE=AE=E4=BF=A1=E6=94=AF?=
 =?UTF-8?q?=E4=BB=98]=E6=94=AF=E6=8C=81=E9=85=8D=E7=BD=AE=E5=BE=AE?=
 =?UTF-8?q?=E4=BF=A1=E6=94=AF=E4=BB=98=E5=85=AC=E9=92=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../binarywang/wxpay/config/WxPayConfig.java  |  67 ++++++--
 .../v3/auth/PublicCertificateVerifier.java    |  39 +++++
 .../wxpay/v3/auth/X509PublicCertificate.java  | 150 ++++++++++++++++++
 .../binarywang/wxpay/v3/util/PemUtils.java    |  29 +++-
 .../src/test/resources/test-config.sample.xml |   1 +
 5 files changed, 267 insertions(+), 19 deletions(-)
 create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PublicCertificateVerifier.java
 create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/X509PublicCertificate.java

diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java
index 637d46e986..857b937d8e 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java
@@ -6,26 +6,26 @@
 import com.github.binarywang.wxpay.v3.WxPayV3HttpClientBuilder;
 import com.github.binarywang.wxpay.v3.auth.*;
 import com.github.binarywang.wxpay.v3.util.PemUtils;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.SneakyThrows;
-import lombok.ToString;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.RegExUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.ssl.SSLContexts;
-
-import javax.net.ssl.SSLContext;
 import java.io.*;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.security.KeyStore;
 import java.security.PrivateKey;
+import java.security.PublicKey;
 import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
 import java.util.Base64;
 import java.util.Optional;
+import javax.net.ssl.SSLContext;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.SneakyThrows;
+import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.RegExUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.ssl.SSLContexts;
 
 /**
  * 微信支付配置
@@ -138,6 +138,25 @@ public class WxPayConfig {
    */
   private byte[] privateCertContent;
 
+  /**
+   * 公钥ID
+   */
+  private String publicKeyId;
+
+  /**
+   * pub_key.pem证书base64编码
+   */
+  private String publicKeyString;
+
+  /**
+   * pub_key.pem证书文件的绝对路径或者以classpath:开头的类路径.
+   */
+  private String publicKeyPath;
+
+  /**
+   * pub_key.pem证书文件内容的字节数组.
+   */
+  private byte[] publicKeyContent;
   /**
    * apiV3 秘钥值.
    */
@@ -241,7 +260,7 @@ public SSLContext initSSLContext() throws WxPayException {
     }
 
     try (InputStream inputStream = this.loadConfigInputStream(this.keyString, this.getKeyPath(),
-      this.keyContent, "p12证书");) {
+      this.keyContent, "p12证书")) {
       KeyStore keystore = KeyStore.getInstance("PKCS12");
       char[] partnerId2charArray = this.getMchId().toCharArray();
       keystore.load(inputStream, partnerId2charArray);
@@ -284,7 +303,6 @@ public CloseableHttpClient initApiV3HttpClient() throws WxPayException {
           this.privateKeyContent, "privateKeyPath")) {
           merchantPrivateKey = PemUtils.loadPrivateKey(keyInputStream);
         }
-
       }
       if (certificate == null && StringUtils.isBlank(this.getCertSerialNo())) {
         try (InputStream certInputStream = this.loadConfigInputStream(this.getPrivateCertString(), this.getPrivateCertPath(),
@@ -293,13 +311,28 @@ public CloseableHttpClient initApiV3HttpClient() throws WxPayException {
         }
         this.certSerialNo = certificate.getSerialNumber().toString(16).toUpperCase();
       }
+      PublicKey publicKey = null;
+      if (this.getPublicKeyString() != null || this.getPublicKeyPath() != null || this.publicKeyContent != null) {
+        try (InputStream pubInputStream =
+            this.loadConfigInputStream(this.getPublicKeyString(), this.getPublicKeyPath(),
+              this.publicKeyContent, "publicKeyPath")) {
+          publicKey = PemUtils.loadPublicKey(pubInputStream);
+        }
+      }
 
       //构造Http Proxy正向代理
       WxPayHttpProxy wxPayHttpProxy = getWxPayHttpProxy();
 
-      AutoUpdateCertificatesVerifier certificatesVerifier = new AutoUpdateCertificatesVerifier(
-        new WxPayCredentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey)),
-        this.getApiV3Key().getBytes(StandardCharsets.UTF_8), this.getCertAutoUpdateTime(), this.getPayBaseUrl(), wxPayHttpProxy);
+      Verifier certificatesVerifier;
+      if (publicKey == null) {
+        certificatesVerifier =
+            new AutoUpdateCertificatesVerifier(
+                new WxPayCredentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey)),
+                this.getApiV3Key().getBytes(StandardCharsets.UTF_8), this.getCertAutoUpdateTime(),
+                this.getPayBaseUrl(), wxPayHttpProxy);
+      } else {
+        certificatesVerifier = new PublicCertificateVerifier(publicKey, publicKeyId);
+      }
 
       WxPayV3HttpClientBuilder wxPayV3HttpClientBuilder = WxPayV3HttpClientBuilder.create()
         .withMerchant(mchId, certSerialNo, merchantPrivateKey)
@@ -422,7 +455,7 @@ private Object[] p12ToPem() {
 
     // 分解p12证书文件
     try (InputStream inputStream = this.loadConfigInputStream(this.keyString, this.getKeyPath(),
-      this.keyContent, "p12证书");) {
+      this.keyContent, "p12证书")) {
       KeyStore keyStore = KeyStore.getInstance("PKCS12");
       keyStore.load(inputStream, key.toCharArray());
 
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PublicCertificateVerifier.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PublicCertificateVerifier.java
new file mode 100644
index 0000000000..9344fc6f83
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PublicCertificateVerifier.java
@@ -0,0 +1,39 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+import java.security.*;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import me.chanjar.weixin.common.error.WxRuntimeException;
+
+public class PublicCertificateVerifier implements Verifier{
+
+    private final PublicKey publicKey;
+
+    private final X509PublicCertificate publicCertificate;
+
+    public PublicCertificateVerifier(PublicKey publicKey, String publicId) {
+        this.publicKey = publicKey;
+        this.publicCertificate = new X509PublicCertificate(publicKey, publicId);
+    }
+
+    @Override
+    public boolean verify(String serialNumber, byte[] message, String signature) {
+        try {
+            Signature sign = Signature.getInstance("SHA256withRSA");
+            sign.initVerify(publicKey);
+            sign.update(message);
+            return sign.verify(Base64.getDecoder().decode(signature));
+        } catch (NoSuchAlgorithmException e) {
+            throw new WxRuntimeException("当前Java环境不支持SHA256withRSA", e);
+        } catch (SignatureException e) {
+            throw new WxRuntimeException("签名验证过程发生了错误", e);
+        } catch (InvalidKeyException e) {
+            throw new WxRuntimeException("无效的证书", e);
+        }
+    }
+
+    @Override
+    public X509Certificate getValidCertificate() {
+        return this.publicCertificate;
+    }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/X509PublicCertificate.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/X509PublicCertificate.java
new file mode 100644
index 0000000000..39d147c6ac
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/X509PublicCertificate.java
@@ -0,0 +1,150 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+import java.math.BigInteger;
+import java.security.*;
+import java.security.cert.*;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Set;
+
+public class X509PublicCertificate extends X509Certificate {
+
+    private final PublicKey publicKey;
+
+    private final String publicId;
+
+    public X509PublicCertificate(PublicKey publicKey, String publicId) {
+        this.publicKey = publicKey;
+        this.publicId = publicId;
+    }
+
+    @Override
+    public PublicKey getPublicKey() {
+        return this.publicKey;
+    }
+
+    @Override
+    public BigInteger getSerialNumber() {
+        return new BigInteger(publicId.replace("PUB_KEY_ID_", ""), 16);
+    }
+
+    @Override
+    public void checkValidity() throws CertificateExpiredException, CertificateNotYetValidException {
+    }
+
+    @Override
+    public void checkValidity(Date date) throws CertificateExpiredException, CertificateNotYetValidException {
+
+    }
+
+    @Override
+    public int getVersion() {
+        return 0;
+    }
+
+    @Override
+    public Principal getIssuerDN() {
+        return null;
+    }
+
+    @Override
+    public Principal getSubjectDN() {
+        return null;
+    }
+
+    @Override
+    public Date getNotBefore() {
+        return null;
+    }
+
+    @Override
+    public Date getNotAfter() {
+        return null;
+    }
+
+    @Override
+    public byte[] getTBSCertificate() throws CertificateEncodingException {
+        return new byte[0];
+    }
+
+    @Override
+    public byte[] getSignature() {
+        return new byte[0];
+    }
+
+    @Override
+    public String getSigAlgName() {
+        return "";
+    }
+
+    @Override
+    public String getSigAlgOID() {
+        return "";
+    }
+
+    @Override
+    public byte[] getSigAlgParams() {
+        return new byte[0];
+    }
+
+    @Override
+    public boolean[] getIssuerUniqueID() {
+        return new boolean[0];
+    }
+
+    @Override
+    public boolean[] getSubjectUniqueID() {
+        return new boolean[0];
+    }
+
+    @Override
+    public boolean[] getKeyUsage() {
+        return new boolean[0];
+    }
+
+    @Override
+    public int getBasicConstraints() {
+        return 0;
+    }
+
+    @Override
+    public byte[] getEncoded() throws CertificateEncodingException {
+        return new byte[0];
+    }
+
+    @Override
+    public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException {
+
+    }
+
+    @Override
+    public void verify(PublicKey key, String sigProvider) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException {
+
+    }
+
+    @Override
+    public String toString() {
+        return "";
+    }
+
+
+    @Override
+    public boolean hasUnsupportedCriticalExtension() {
+        return false;
+    }
+
+    @Override
+    public Set<String> getCriticalExtensionOIDs() {
+        return Collections.emptySet();
+    }
+
+    @Override
+    public Set<String> getNonCriticalExtensionOIDs() {
+        return Collections.emptySet();
+    }
+
+    @Override
+    public byte[] getExtensionValue(String oid) {
+        return new byte[0];
+    }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java
index 1983fb3387..a885ea0950 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java
@@ -1,13 +1,12 @@
 package com.github.binarywang.wxpay.v3.util;
 
-import me.chanjar.weixin.common.error.WxRuntimeException;
-
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.KeyFactory;
 import java.security.NoSuchAlgorithmException;
 import java.security.PrivateKey;
+import java.security.PublicKey;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateExpiredException;
 import java.security.cert.CertificateFactory;
@@ -15,7 +14,9 @@
 import java.security.cert.X509Certificate;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
 import java.util.Base64;
+import me.chanjar.weixin.common.error.WxRuntimeException;
 
 public class PemUtils {
 
@@ -59,4 +60,28 @@ public static X509Certificate loadCertificate(InputStream inputStream) {
       throw new WxRuntimeException("无效的证书", e);
     }
   }
+
+  public static PublicKey loadPublicKey(InputStream inputStream){
+    try {
+      ByteArrayOutputStream array = new ByteArrayOutputStream();
+      byte[] buffer = new byte[1024];
+      int length;
+      while ((length = inputStream.read(buffer)) != -1) {
+        array.write(buffer, 0, length);
+      }
+
+      String publicKey = array.toString("utf-8")
+        .replace("-----BEGIN PUBLIC KEY-----", "")
+        .replace("-----END PUBLIC KEY-----", "")
+        .replaceAll("\\s+", "");
+      return KeyFactory.getInstance("RSA")
+        .generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)));
+    } catch (NoSuchAlgorithmException e) {
+      throw new WxRuntimeException("当前Java环境不支持RSA", e);
+    } catch (InvalidKeySpecException e) {
+      throw new WxRuntimeException("无效的密钥格式");
+    } catch (IOException e) {
+      throw new WxRuntimeException("无效的密钥");
+    }
+  }
 }
diff --git a/weixin-java-pay/src/test/resources/test-config.sample.xml b/weixin-java-pay/src/test/resources/test-config.sample.xml
index a63cd8dc30..e9d383dd19 100644
--- a/weixin-java-pay/src/test/resources/test-config.sample.xml
+++ b/weixin-java-pay/src/test/resources/test-config.sample.xml
@@ -18,6 +18,7 @@
   <certSerialNo>apiV3 证书序列号值</certSerialNo>
   <privateKeyPath>apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径.</privateKeyPath>
   <privateCertPath>apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径.</privateCertPath>
+  <publicKeyPath>pub_key.pem证书文件的绝对路径或者以classpath:开头的类路径.</publicKeyPath>
 
   <!-- other配置 -->
   <openid>某个openId</openid>