diff --git a/samples/SKIT.FlurlHttpClient.Wechat.TenpayV3.Sample_Net5/Services/BackgroundServices/WxpayCertificateRefreshingBackgroundService.cs b/samples/SKIT.FlurlHttpClient.Wechat.TenpayV3.Sample_Net5/Services/BackgroundServices/WxpayCertificateRefreshingBackgroundService.cs
index dc2b8d26..5d02dc85 100644
--- a/samples/SKIT.FlurlHttpClient.Wechat.TenpayV3.Sample_Net5/Services/BackgroundServices/WxpayCertificateRefreshingBackgroundService.cs
+++ b/samples/SKIT.FlurlHttpClient.Wechat.TenpayV3.Sample_Net5/Services/BackgroundServices/WxpayCertificateRefreshingBackgroundService.cs
@@ -50,9 +50,9 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Sample_Net5.Services.BackgroundSe
if (response.IsSuccessful())
{
client.DecryptResponseEncryptedData(ref response);
- foreach (var cert in response.CertificateList)
+ foreach (var certificateModel in response.CertificateList)
{
- _certificateManager.SetCertificate(cert.SerialNumber, cert.EncryptCertificate.CipherText);
+ _certificateManager.AddEntry(new CertificateEntry(certificateModel));
}
_logger.LogInformation("刷新微信商户平台证书成功。");
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Exceptions/WechatTenpayEventVerificationException.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Exceptions/WechatTenpayEventVerificationException.cs
new file mode 100644
index 00000000..56da1089
--- /dev/null
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Exceptions/WechatTenpayEventVerificationException.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Exceptions
+{
+ public class WechatTenpayEventVerificationException : WechatTenpayException
+ {
+ ///
+ internal WechatTenpayEventVerificationException()
+ {
+ }
+
+ ///
+ internal WechatTenpayEventVerificationException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ internal WechatTenpayEventVerificationException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Exceptions/WechatTenpayResponseVerificationException.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Exceptions/WechatTenpayResponseVerificationException.cs
new file mode 100644
index 00000000..0be0e102
--- /dev/null
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Exceptions/WechatTenpayResponseVerificationException.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Exceptions
+{
+ public class WechatTenpayResponseVerificationException : WechatTenpayException
+ {
+ ///
+ internal WechatTenpayResponseVerificationException()
+ {
+ }
+
+ ///
+ internal WechatTenpayResponseVerificationException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ internal WechatTenpayResponseVerificationException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventVerificationExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventVerificationExtensions.cs
index e782b3d5..fcb06151 100644
--- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventVerificationExtensions.cs
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventVerificationExtensions.cs
@@ -27,6 +27,32 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
string callbackBody,
string callbackSignature,
string callbackSerialNumber)
+ {
+ return VerifyEventSignature(client, callbackTimestamp, callbackNonce, callbackBody, callbackSignature, callbackSerialNumber, out _);
+ }
+
+ ///
+ /// 验证回调通知事件签名。
+ /// REF: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
+ /// REF: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_1.shtml
+ ///
+ ///
+ /// 微信回调通知中的 Wechatpay-Timestamp 字段。
+ /// 微信回调通知中的 Wechatpay-Nonce 字段。
+ /// 微信回调通知中请求正文。
+ /// 微信回调通知中的 Wechatpay-Signature 字段。
+ /// 微信回调通知中的 Wechatpay-Serial 字段。
+ ///
+ ///
+ ///
+ public static bool VerifyEventSignature(
+ this WechatTenpayClient client,
+ string callbackTimestamp,
+ string callbackNonce,
+ string callbackBody,
+ string callbackSignature,
+ string callbackSerialNumber,
+ out Exception? error)
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (callbackTimestamp == null) throw new ArgumentNullException(nameof(callbackTimestamp));
@@ -39,18 +65,28 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
{
try
{
- string certificate = client.CertificateManager.GetCertificate(callbackSerialNumber)!;
- string publicKey = Utilities.RSAUtility.ExportPublicKey(certificate);
+ var cert = client.CertificateManager.GetEntry(callbackSerialNumber)!;
+ if (!cert.HasValue)
+ {
+ error = new Exceptions.WechatTenpayEventVerificationException("Verify signature of event failed, because there is no platform certificate matched the serial number.");
+ return false;
+ }
- return Utilities.RSAUtility.VerifyWithSHA256(
- publicKey: publicKey,
+ error = null;
+ return Utilities.RSAUtility.VerifyWithSHA256ByCertificate(
+ certificate: cert.Value.Certificate,
plainText: GetPlainTextForSignature(timestamp: callbackTimestamp, nonce: callbackNonce, body: callbackBody),
signature: callbackSignature
);
}
- catch { }
+ catch (Exception ex)
+ {
+ error = new Exceptions.WechatTenpayEventVerificationException("Verify signature of event failed. Please see the `InnerException` for more details.", ex);
+ return false;
+ }
}
+ error = new Exceptions.WechatTenpayEventVerificationException("Verify signature of event failed, because there is no platform certificate in the manager.");
return false;
}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerificationExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerificationExtensions.cs
index 616b9ce9..ba3fd8ea 100644
--- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerificationExtensions.cs
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerificationExtensions.cs
@@ -19,6 +19,23 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
///
public static bool VerifyResponseSignature(this WechatTenpayClient client, TResponse response)
where TResponse : WechatTenpayResponse
+ {
+ return VerifyResponseSignature(client, response, out _);
+ }
+
+ ///
+ /// 验证响应签名。
+ /// REF: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
+ /// REF: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_1.shtml
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static bool VerifyResponseSignature(this WechatTenpayClient client, TResponse response, out Exception? error)
+ where TResponse : WechatTenpayResponse
{
if (client == null) throw new ArgumentNullException(nameof(client));
if (response == null) throw new ArgumentNullException(nameof(response));
@@ -27,18 +44,28 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
{
try
{
- string certificate = client.CertificateManager.GetCertificate(response.WechatpayCertSerialNumber)!;
- string publicKey = Utilities.RSAUtility.ExportPublicKey(certificate);
+ var cert = client.CertificateManager.GetEntry(response.WechatpayCertSerialNumber)!;
+ if (!cert.HasValue)
+ {
+ error = new Exceptions.WechatTenpayResponseVerificationException("Verify signature of response failed, because there is no platform certificate matched the serial number.");
+ return false;
+ }
- return Utilities.RSAUtility.VerifyWithSHA256(
- publicKey: publicKey,
+ error = null;
+ return Utilities.RSAUtility.VerifyWithSHA256ByCertificate(
+ certificate: cert.Value.Certificate,
plainText: GetPlainTextForSignature(response),
signature: response.WechatpaySignature
);
}
- catch { }
+ catch (Exception ex)
+ {
+ error = new Exceptions.WechatTenpayResponseVerificationException("Verify signature of response failed. Please see the `InnerException` for more details.", ex);
+ return false;
+ }
}
+ error = new Exceptions.WechatTenpayResponseVerificationException("Verify signature of response failed, because there is no platform certificate in the manager.");
return false;
}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateEntry.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateEntry.cs
new file mode 100644
index 00000000..087c9f81
--- /dev/null
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateEntry.cs
@@ -0,0 +1,85 @@
+using System;
+
+namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings
+{
+ ///
+ /// 表示一个微信商户平台证书实体。
+ ///
+ public struct CertificateEntry : IEquatable
+ {
+ ///
+ /// 获取证书序列号。
+ /// 序列号相同的实体将被视为同一个证书。
+ ///
+ public string SerialNumber { get; }
+
+ ///
+ /// 获取证书内容(CER 格式,即 -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE)
+ ///
+ public string Certificate { get; }
+
+ ///
+ /// 获取生效时间。
+ ///
+ public DateTimeOffset EffectiveTime { get; }
+
+ ///
+ /// 获取过期时间。
+ ///
+ public DateTimeOffset ExpireTime { get; }
+
+ public CertificateEntry(string serialNumber, string certificate, DateTimeOffset effectiveTime, DateTimeOffset expireTime)
+ {
+ if (string.IsNullOrEmpty(serialNumber))
+ throw new ArgumentException("The value of `serialNumber` can not be empty.", nameof(serialNumber));
+ if (string.IsNullOrEmpty(certificate))
+ throw new ArgumentException("The value of `certificate` can not be empty.", nameof(serialNumber));
+ if (!certificate.Trim().StartsWith("-----BEGIN CERTIFICATE-----") || !certificate.Trim().EndsWith("-----END CERTIFICATE-----"))
+ throw new ArgumentException("The value of `certificate` is an invalid .cer file content.", nameof(serialNumber));
+
+ SerialNumber = serialNumber;
+ Certificate = certificate;
+ EffectiveTime = effectiveTime;
+ ExpireTime = expireTime;
+ }
+
+ public CertificateEntry(Models.QueryCertificatesResponse.Types.Certificate cert)
+ : this(cert.SerialNumber, cert.EncryptCertificate.CipherText, cert.EffectiveTime, cert.ExpireTime)
+ {
+ }
+
+ public bool IsAvailable()
+ {
+ DateTimeOffset now = DateTimeOffset.Now;
+ return EffectiveTime <= now && now < ExpireTime;
+ }
+
+ public bool Equals(CertificateEntry other)
+ {
+ return string.Equals(SerialNumber, other.SerialNumber);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(null, obj))
+ return false;
+
+ return (obj is CertificateEntry other) && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ return SerialNumber.GetHashCode();
+ }
+
+ public static bool operator ==(CertificateEntry left, CertificateEntry right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(CertificateEntry left, CertificateEntry right)
+ {
+ return !left.Equals(right);
+ }
+ }
+}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateManager.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateManager.cs
index b9d04f96..eae7520b 100644
--- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateManager.cs
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateManager.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Collections.Concurrent;
+using System.Collections.Concurrent;
using System.Collections.Generic;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings
@@ -10,18 +9,30 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings
public abstract class CertificateManager
{
///
- /// 根据证书序列号获取证书(cer 格式)。
+ /// 获取存储的全部证书。
+ ///
+ ///
+ public abstract IEnumerable AllEntries();
+
+ ///
+ /// 添加一个证书实体。
+ ///
+ ///
+ public abstract void AddEntry(CertificateEntry entry);
+
+ ///
+ /// 根据证书序列号获取证书实体。
///
///
///
- public abstract string? GetCertificate(string serialNumber);
+ public abstract CertificateEntry? GetEntry(string serialNumber);
///
- /// 设置证书序列号与证书(cer 格式)的映射关系。
+ /// 移除指定的证书实体。
///
///
- ///
- public abstract void SetCertificate(string serialNumber, string certificate);
+ ///
+ public abstract bool RemoveEntry(string serialNumber);
}
///
@@ -29,26 +40,40 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings
///
public class InMemoryCertificateManager : CertificateManager
{
- private readonly IDictionary _dict;
+ private readonly ConcurrentDictionary _dict;
public InMemoryCertificateManager()
{
- _dict = new ConcurrentDictionary();
+ _dict = new ConcurrentDictionary();
}
- public override string? GetCertificate(string serialNumber)
+ public override IEnumerable AllEntries()
{
- if (serialNumber == null) throw new ArgumentNullException(nameof(serialNumber));
-
- return _dict[serialNumber];
+ return _dict.Values;
}
- public override void SetCertificate(string serialNumber, string certificate)
+ public override void AddEntry(CertificateEntry entry)
{
- if (serialNumber == null) throw new ArgumentNullException(nameof(serialNumber));
- if (certificate == null) throw new ArgumentNullException(nameof(certificate));
+ _dict.TryRemove(entry.SerialNumber, out _);
+ _dict.TryAdd(entry.SerialNumber, entry);
+ }
- _dict[serialNumber] = certificate;
+ public override CertificateEntry? GetEntry(string serialNumber)
+ {
+ if (_dict.TryGetValue(serialNumber, out var entry))
+ {
+ if (entry.IsAvailable())
+ return entry;
+
+ _dict.TryRemove(serialNumber, out _);
+ }
+
+ return null;
+ }
+
+ public override bool RemoveEntry(string serialNumber)
+ {
+ return _dict.TryRemove(serialNumber, out _);
}
}
}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Utilities/RSAUtility.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Utilities/RSAUtility.cs
index f06c4493..060ec4c5 100644
--- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Utilities/RSAUtility.cs
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Utilities/RSAUtility.cs
@@ -3,7 +3,6 @@ using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Org.BouncyCastle.Crypto;
-using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatTenpayResponseVerificationTests.cs b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatTenpayResponseVerificationTests.cs
index 9c5bcd16..c3fe8658 100644
--- a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatTenpayResponseVerificationTests.cs
+++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatTenpayResponseVerificationTests.cs
@@ -24,7 +24,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
TestClients.Instance.DecryptResponseEncryptedData(ref response);
foreach (var certificateModel in response.CertificateList)
{
- TestClients.GlobalCertificateManager.SetCertificate(certificateModel.SerialNumber, certificateModel.EncryptCertificate.CipherText);
+ TestClients.GlobalCertificateManager.AddEntry(new Settings.CertificateEntry(certificateModel));
}
Assert.True(TestClients.Instance.VerifyResponseSignature(response));