From bfa6557314f6511186d06e870aaeb59836c008ea Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 25 Nov 2021 11:58:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(tenpayv3):=20=E9=87=8D=E6=96=B0=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=20CertificateManager=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E7=94=9F=E6=95=88=E6=97=B6=E9=97=B4=E3=80=81?= =?UTF-8?q?=E8=BF=87=E6=9C=9F=E6=97=B6=E9=97=B4=E7=AD=89=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...yCertificateRefreshingBackgroundService.cs | 4 +- .../WechatTenpayEventVerificationException.cs | 24 ++++++ ...chatTenpayResponseVerificationException.cs | 24 ++++++ ...TenpayClientEventVerificationExtensions.cs | 46 ++++++++-- ...payClientResponseVerificationExtensions.cs | 37 ++++++-- .../Settings/CertificateEntry.cs | 85 +++++++++++++++++++ .../Settings/CertificateManager.cs | 59 +++++++++---- .../Utilities/RSAUtility.cs | 1 - .../WechatTenpayResponseVerificationTests.cs | 2 +- 9 files changed, 251 insertions(+), 31 deletions(-) create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Exceptions/WechatTenpayEventVerificationException.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Exceptions/WechatTenpayResponseVerificationException.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/CertificateEntry.cs 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));