diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Constants/FormDataFields.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Constants/Internal/FormDataFields.cs similarity index 100% rename from src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Constants/FormDataFields.cs rename to src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Constants/Internal/FormDataFields.cs diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Constants/SignAlgorithms.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Constants/SignAlgorithms.cs new file mode 100644 index 00000000..56ffe932 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Constants/SignAlgorithms.cs @@ -0,0 +1,12 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Constants +{ + public static class SignAlgorithms + { + /// + /// WECHATPAY2-SHA256-RSA2048。 + /// + public const string WECHATPAY2_SHA256_RSA2048 = "WECHATPAY2-SHA256-RSA2048"; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventDecryptionExtensions.cs similarity index 96% rename from src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventExtensions.cs rename to src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventDecryptionExtensions.cs index 73afbc90..fdc60163 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventExtensions.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventDecryptionExtensions.cs @@ -3,9 +3,9 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 { /// - /// 为 提供回调通知事件的扩展方法。 + /// 为 提供回调通知事件敏感数据解密的扩展方法。 /// - public static class WechatTenpayClientEventExtensions + public static class WechatTenpayClientEventDecryptionExtensions { /// /// 反序列化得到 对象。 diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventVerificationExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventVerificationExtensions.cs new file mode 100644 index 00000000..8c51113a --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventVerificationExtensions.cs @@ -0,0 +1,80 @@ +using System; +using System.Text; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 +{ + /// + /// 为 提供回调通知事件签名验证的扩展方法。 + /// + public static class WechatTenpayClientEventVerificationExtensions + { + /// + /// 验证回调通知事件签名。 + /// 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) + where TResponse : WechatTenpayResponse + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (callbackTimestamp == null) throw new ArgumentNullException(nameof(callbackTimestamp)); + if (callbackNonce == null) throw new ArgumentNullException(nameof(callbackNonce)); + if (callbackBody == null) throw new ArgumentNullException(nameof(callbackBody)); + if (callbackSignature == null) throw new ArgumentNullException(nameof(callbackSignature)); + if (callbackSerialNumber == null) throw new ArgumentNullException(nameof(callbackSerialNumber)); + + if (client.WechatCertificateStorer == null) + { + throw new Exceptions.WechatTenpayResponseVerificationException($"You must set an instance of `{nameof(Settings.ICertificateStorer)}` at first."); + } + else + { + string? certificate = client.WechatCertificateStorer.Get(callbackSerialNumber); + if (certificate == null) + throw new Exceptions.WechatTenpayResponseVerificationException("Cannot get certificate by the serial number, may not be stored."); + + string? publicKey = null; + try + { + publicKey = Utilities.RSAUtility.ExportPublicKey(certificate); + } + catch (Exception ex) + { + throw new Exceptions.WechatTenpayResponseVerificationException("Cannot get public key of the certificate, may not be a valid certificate data.", ex); + } + + try + { + return Utilities.RSAUtility.VerifyWithSHA256( + publicKey: publicKey, + plainText: GetPlainTextForSignature(timestamp: callbackTimestamp, nonce: callbackNonce, body: callbackBody), + signature: callbackSignature + ); + } + catch (Exception ex) + { + throw new Exceptions.WechatTenpayResponseVerificationException("Verify event signature failed.", ex); + } + } + } + + private static string GetPlainTextForSignature(string timestamp, string nonce, string body) + { + return $"{timestamp}\n{nonce}\n{body}\n"; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerificationExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerificationExtensions.cs new file mode 100644 index 00000000..0b45bf7c --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerificationExtensions.cs @@ -0,0 +1,72 @@ +using System; +using System.Text; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 +{ + /// + /// 为 提供响应签名验证的扩展方法。 + /// + public static class WechatTenpayClientResponseVerificationExtensions + { + /// + /// 验证响应签名。 + /// 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) + where TResponse : WechatTenpayResponse + { + if (client == null) throw new ArgumentNullException(nameof(client)); + if (response == null) throw new ArgumentNullException(nameof(response)); + + if (client.WechatCertificateStorer == null) + { + throw new Exceptions.WechatTenpayResponseVerificationException($"You must set an instance of `{nameof(Settings.ICertificateStorer)}` at first."); + } + else + { + if (response.WechatpayCertSerialNumber == null) + throw new Exceptions.WechatTenpayResponseVerificationException("Cannot read the serial number in headers of this response."); + + string? certificate = client.WechatCertificateStorer.Get(response.WechatpayCertSerialNumber); + if (certificate == null) + throw new Exceptions.WechatTenpayResponseVerificationException("Cannot get certificate by the serial number, may not be stored."); + + string? publicKey = null; + try + { + publicKey = Utilities.RSAUtility.ExportPublicKey(certificate); + } + catch (Exception ex) + { + throw new Exceptions.WechatTenpayResponseVerificationException("Cannot get public key of the certificate, may not be a valid certificate data.", ex); + } + + try + { + return Utilities.RSAUtility.VerifyWithSHA256( + publicKey: publicKey, + plainText: GetPlainTextForSignature(response), + signature: response.WechatpaySignature + ); + } + catch (Exception ex) + { + throw new Exceptions.WechatTenpayResponseVerificationException("Verify response signature failed.", ex); + } + } + } + + private static string GetPlainTextForSignature(WechatTenpayResponse response) + { + string timestamp = response.WechatpayTimestamp; + string nonce = response.WechatpayNonce; + string body = Encoding.UTF8.GetString(response.RawBytes); + return $"{timestamp}\n{nonce}\n{body}\n"; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerifyExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerifyExtensions.cs deleted file mode 100644 index 2e86dd8d..00000000 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerifyExtensions.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Text; - -namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 -{ - /// - /// 为 提供响应签名验证的扩展方法。 - /// - public static class WechatTenpayClientResponseVerifyExtensions - { - /// - /// 验证响应签名(使用微信支付平台证书公钥)。 - /// 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, string publicKey) - where TResponse : WechatTenpayResponse - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (response == null) throw new ArgumentNullException(nameof(response)); - if (string.IsNullOrEmpty(publicKey)) throw new ArgumentNullException(publicKey); - - try - { - return Utilities.RSAUtility.VerifyWithSHA256( - publicKey: publicKey, - plainText: GetPlainTextForSignature(response), - signature: response.WechatpaySignature - ); - } - catch (Exception ex) - { - throw new Exceptions.WechatTenpayResponseVerificationException("Verify response signature failed.", ex); - } - } - - /// - /// 验证响应签名(使用微信支付平台证书)。 - /// 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 VerifyResponseSignatureByCertificate(this WechatTenpayClient client, TResponse response, string certificate) - where TResponse : WechatTenpayResponse - { - if (client == null) throw new ArgumentNullException(nameof(client)); - if (response == null) throw new ArgumentNullException(nameof(response)); - if (string.IsNullOrEmpty(certificate)) throw new ArgumentNullException(certificate); - - try - { - return Utilities.RSAUtility.VerifyWithSHA256ByCertificate( - certificate: certificate, - plainText: GetPlainTextForSignature(response), - signature: response.WechatpaySignature - ); - } - catch (Exception ex) - { - throw new Exceptions.WechatTenpayResponseVerificationException("Verify response signature failed.", ex); - } - } - - private static string GetPlainTextForSignature(WechatTenpayResponse response) - { - string timestamp = response.WechatpayTimestamp; - string nonce = response.WechatpayNonce; - string body = Encoding.UTF8.GetString(response.RawBytes); - return $"{timestamp}\n{nonce}\n{body}\n"; - } - } -} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Interceptors/WechatTenpaySignInterceptor.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Interceptors/WechatTenpaySignInterceptor.cs index 6f8e101e..2d426685 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Interceptors/WechatTenpaySignInterceptor.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Interceptors/WechatTenpaySignInterceptor.cs @@ -53,7 +53,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Interceptors switch (_scheme) { - case WechatTenpayAuthSchemes.WECHATPAY2_SHA256_RSA2048: + case Constants.SignAlgorithms.WECHATPAY2_SHA256_RSA2048: { try { diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/ICertificateStorer.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/ICertificateStorer.cs new file mode 100644 index 00000000..0c99f9ff --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/ICertificateStorer.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings +{ + /// + /// 微信商户平台证书存储器接口。 + /// + public interface ICertificateStorer + { + /// + /// 根据证书序列号获取证书(cer 格式)。 + /// + /// + /// + string? Get(string serialNumber); + + /// + /// 设置证书序列号与证书(cer 格式)的映射关系。 + /// + /// + /// + void Set(string serialNumber, string certificate); + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/InMemoryCertificateStorer.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/InMemoryCertificateStorer.cs new file mode 100644 index 00000000..37afe581 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/InMemoryCertificateStorer.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings +{ + /// + /// 一个基于内存实现的 。 + /// + public class InMemoryCertificateStorer : ICertificateStorer + { + public IDictionary _dict; + + public InMemoryCertificateStorer() + { + _dict = new ConcurrentDictionary(); + } + + string? ICertificateStorer.Get(string serialNumber) + { + if (serialNumber == null) throw new ArgumentNullException(nameof(serialNumber)); + + return _dict[serialNumber]; + } + + void ICertificateStorer.Set(string serialNumber, string certificate) + { + if (serialNumber == null) throw new ArgumentNullException(nameof(serialNumber)); + if (certificate == null) throw new ArgumentNullException(nameof(certificate)); + + _dict[serialNumber] = certificate; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayAuthSchemes.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayAuthSchemes.cs deleted file mode 100644 index 372fecdd..00000000 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayAuthSchemes.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 -{ - /// - /// 微信支付 API 接口签名认证方式。 - /// - public static class WechatTenpayAuthSchemes - { - /// - /// WECHATPAY2-SHA256-RSA2048(默认)。 - /// - public const string WECHATPAY2_SHA256_RSA2048 = "WECHATPAY2-SHA256-RSA2048"; - } -} diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClient.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClient.cs index a7afc7a2..d2e084fb 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClient.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClient.cs @@ -39,6 +39,11 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 /// internal string WechatMerchantV3Secret { get; } + /// + /// 获取当前客户端使用的微信商户平台证书存储器。 + /// + internal Settings.ICertificateStorer? WechatCertificateStorer { get; } + /// /// 获取当前客户端使用的 JSON 序列化器。 /// @@ -59,6 +64,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 WechatMerchantCertSerialNumber = options.MerchantCertSerialNumber; WechatMerchantCertPrivateKey = options.MerchantCertPrivateKey; WechatMerchantV3Secret = options.MerchantV3Secret; + WechatCertificateStorer = options.CertificateStorer; FlurlClient.BaseUrl = options.Endpoints ?? WechatTenpayEndpoints.DEFAULT; FlurlClient.Headers.Remove("Accept"); diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClientOptions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClientOptions.cs index a73a442d..3c339680 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClientOptions.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClientOptions.cs @@ -34,9 +34,9 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 /// /// 获取或设置微信支付 API 签名认证方式。 - /// 默认值: + /// 默认值: /// - public string AuthScheme { get; set; } = WechatTenpayAuthSchemes.WECHATPAY2_SHA256_RSA2048; + public string AuthScheme { get; set; } = Constants.SignAlgorithms.WECHATPAY2_SHA256_RSA2048; /// /// 获取或设置微信商户号。 @@ -57,5 +57,11 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 /// 获取或设置微信商户 API 证书私钥。 /// public string MerchantCertPrivateKey { get; set; } = default!; + + /// + /// 获取或设置微信商户平台证书存储器。 + /// 默认值: + /// + public Settings.ICertificateStorer? CertificateStorer { get; set; } = new Settings.InMemoryCertificateStorer(); } }