diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Constants/SecurityApiAsymmetricAlgorithms.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Constants/SecurityApiAsymmetricAlgorithms.cs new file mode 100644 index 00000000..3b435200 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Constants/SecurityApiAsymmetricAlgorithms.cs @@ -0,0 +1,9 @@ +namespace SKIT.FlurlHttpClient.Wechat.Api.Constants +{ + public static class SecurityApiAsymmetricAlgorithms + { + public const string RSA = "RSA"; + + public const string SM2 = "SM2"; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Constants/SecurityApiSymmetricAlgorithms.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Constants/SecurityApiSymmetricAlgorithms.cs new file mode 100644 index 00000000..b8056411 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Constants/SecurityApiSymmetricAlgorithms.cs @@ -0,0 +1,9 @@ +namespace SKIT.FlurlHttpClient.Wechat.Api.Constants +{ + public static class SecurityApiSymmetricAlgorithms + { + public const string AES = "AES"; + + public const string SM4 = "SM4"; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Interceptors/WechatApiSecurityApiInterceptor.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Interceptors/WechatApiSecurityApiInterceptor.cs new file mode 100644 index 00000000..cf67b4e3 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Interceptors/WechatApiSecurityApiInterceptor.cs @@ -0,0 +1,451 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using Flurl.Http; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Interceptors +{ + using SKIT.FlurlHttpClient.Internal; + using SKIT.FlurlHttpClient.Wechat.Api.Constants; + + internal class WechatApiSecurityApiInterceptor : HttpInterceptor + { + /** + * REF: + * https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/getting_started/api_signature.html + * https://developers.weixin.qq.com/community/develop/article/doc/00028ca675c708b23f100b8e161013 + * https://developers.weixin.qq.com/community/develop/article/doc/000e68b8038ed8796f00f6c2f68c13 + */ + private static readonly ISet SIGN_REQUIRED_URLS = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "/cgi-bin/clear_quota", + "/cgi-bin/openapi/quota/get", + "/cgi-bin/openapi/rid/get", + "/wxa/getpluginopenpid", + "/wxa/business/checkencryptedmsg", + "/wxa/business/getuserencryptkey", + "/wxa/business/getuserphonenumber", + "/wxa/getwxacode", + "/wxa/getwxacodeunlimit", + "/cgi-bin/wxaapp/createwxaqrcode", + "/cgi-bin/message/custom/send", + "/cgi-bin/message/wxopen/updatablemsg/send", + "/wxaapi/newtmpl/deltemplate", + "/cgi-bin/message/subscribe/send", + "/wxaapi/newtmpl/addtemplate", + "/wxa/msg_sec_check", + "/wxa/media_check_async", + "/wxa/getuserriskrank", + "/datacube/getweanalysisappidweeklyretaininfo", + "/datacube/getweanalysisappidmonthlyretaininfo", + "/datacube/getweanalysisappiddailyretaininfo", + "/datacube/getweanalysisappidmonthlyvisittrend", + "/datacube/getweanalysisappiddailyvisittrend", + "/datacube/getweanalysisappidweeklyvisittrend", + "/datacube/getweanalysisappiddailysummarytrend", + "/datacube/getweanalysisappidvisitpage", + "/datacube/getweanalysisappiduserportrait", + "/wxa/business/performance/boot", + "/datacube/getweanalysisappidvisitdistribution", + "/wxa/getwxadevinfo", + "/wxaapi/log/get_performance", + "/wxaapi/log/jserr_detail", + "/wxaapi/log/jserr_list", + "/wxa/devplugin", + "/wxa/plugin", + "/cgi-bin/express/business/account/getall", + "/cgi-bin/express/business/delivery/getall", + "/cgi-bin/express/business/printer/getall", + "/wxa/servicemarket", + "/cgi-bin/soter/verify_signature", + "/cgi-bin/express/intracity/apply", + "/cgi-bin/express/intracity/createstore", + "/cgi-bin/express/intracity/querystore", + "/cgi-bin/express/intracity/updatestore", + "/cgi-bin/express/intracity/storecharge", + "/cgi-bin/express/intracity/storerefund", + "/cgi-bin/express/intracity/queryflow", + "/cgi-bin/express/intracity/balancequery", + "/cgi-bin/express/intracity/preaddorder", + "/cgi-bin/express/intracity/addorder", + "/cgi-bin/express/intracity/queryorder", + "/cgi-bin/express/intracity/cancelorder", + "/cgi-bin/express/intracity/setpaymode", + "/cgi-bin/express/intracity/getpaymode", + "/cgi-bin/express/intracity/getcity", + "/cgi-bin/express/intracity/mocknotify" + }; + + private readonly string _baseUrl; + private readonly string _appId; + private readonly string _symmetricAlg; + private readonly string _symmetricNum; + private readonly string _symmetricKey; + private readonly string _asymmetricAlg; + private readonly string _asymmetricNum; + private readonly string _asymmetricPrivateKey; + private readonly Func? _customRequestPathMatcher; + + public WechatApiSecurityApiInterceptor(string baseUrl, string appId, string symmetricAlg, string symmetricNum, string symmetricKey, string asymmetricAlg, string asymmetricNum, string asymmetricPrivateKey, Func? customRequestPathMatcher) + { + _baseUrl = baseUrl; + _appId = appId; + _symmetricAlg = symmetricAlg; + _symmetricNum = symmetricNum; + _symmetricKey = symmetricKey; + _asymmetricAlg = asymmetricAlg; + _asymmetricNum = asymmetricNum; + _asymmetricPrivateKey = asymmetricPrivateKey; + _customRequestPathMatcher = customRequestPathMatcher; + } + + public override async Task BeforeCallAsync(HttpInterceptorContext context, CancellationToken cancellationToken = default) + { + if (context is null) throw new ArgumentNullException(nameof(context)); + if (context.FlurlCall.Completed) throw new WechatApiException("Failed to encrypt and sign request. This interceptor must be called before request completed."); + + if (context.FlurlCall.HttpRequestMessage.Method != HttpMethod.Post) + return; + if (context.FlurlCall.HttpRequestMessage.RequestUri is null) + return; + if (!IsRequestUrlRequireEncryption(context.FlurlCall.HttpRequestMessage.RequestUri)) + return; + + string urlpath = GetRequestUrl(context.FlurlCall.HttpRequestMessage.RequestUri); + string timestamp = DateTimeOffset.Now.ToLocalTime().ToUnixTimeSeconds().ToString(); + string postData = "{}"; + string postDataEncrypted; + if (context.FlurlCall.HttpRequestMessage?.Content is not null) + { + if (context.FlurlCall.HttpRequestMessage.Content is MultipartFormDataContent) + return; + + HttpContent httpContent = context.FlurlCall.HttpRequestMessage.Content; + postData = await +#if NET5_0_OR_GREATER + httpContent.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); +#else + _AsyncEx.RunTaskWithCancellationTokenAsync(httpContent.ReadAsStringAsync(), cancellationToken).ConfigureAwait(false); +#endif + if (string.IsNullOrEmpty(postData)) + postData = "{}"; + } + + // 对称加密 + if (string.IsNullOrEmpty(_symmetricKey)) + { + throw new WechatApiException("Failed to encrypt request, because the AES/SM4 key is not set."); + } + else + { + string nonce = GenerateSymmetricEncryptionNonce(12); + string associatedData = GenerateSymmetricEncryptionAssociatedData(urlpath, _appId, timestamp); + string plainData; + try + { + JsonObject jsonObj = JsonObject.Parse(postData)!.AsObject(); + jsonObj["_n"] = GenerateSymmetricEncryptionNonce(16); + jsonObj["_appid"] = _appId; + jsonObj["_timestamp"] = timestamp; + + NameValueCollection queryParams = HttpUtility.ParseQueryString(context.FlurlCall.HttpRequestMessage!.RequestUri.Query); + foreach (string? key in queryParams.AllKeys) + { + if (key is null) continue; + if (key == "access_token") continue; + jsonObj[key] = queryParams[key]; + } + + plainData = jsonObj.ToJsonString(SystemTextJsonSerializer.GetDefaultSerializerOptions()); + } + catch (Exception ex) + { + throw new WechatApiException("Failed to encrypt request. Please see the inner exception for more details.", ex); + } + + string sData, sIV, sAuthTag; + switch (_symmetricAlg) + { + case SecurityApiSymmetricAlgorithms.AES: + { + try + { + const int TAG_LENGTH_BYTE = 16; + byte[] cipherBytes = Utilities.AESUtility.EncryptWithGCM( + keyBytes: Convert.FromBase64String(_symmetricKey), + nonceBytes: Convert.FromBase64String(nonce), + associatedDataBytes: Encoding.UTF8.GetBytes(associatedData), + plainBytes: Encoding.UTF8.GetBytes(plainData) + )!; + byte[] encdataBytes = new byte[cipherBytes.Length - TAG_LENGTH_BYTE]; + byte[] authtagBytes = new byte[TAG_LENGTH_BYTE]; + Buffer.BlockCopy(cipherBytes, 0, encdataBytes, 0, encdataBytes.Length); + Buffer.BlockCopy(cipherBytes, encdataBytes.Length, authtagBytes, 0, authtagBytes.Length); + + sIV = nonce; + sData = Convert.ToBase64String(encdataBytes); + sAuthTag = Convert.ToBase64String(authtagBytes); + } + catch (Exception ex) + { + throw new WechatApiException("Failed to encrypt request. Please see the inner exception for more details.", ex); + } + } + break; + + case SecurityApiSymmetricAlgorithms.SM4: + { + try + { + const int TAG_LENGTH_BYTE = 16; + byte[] cipherBytes = Utilities.SM4Utility.EncryptWithGCM( + keyBytes: Convert.FromBase64String(_symmetricKey), + nonceBytes: Convert.FromBase64String(nonce), + associatedDataBytes: Encoding.UTF8.GetBytes(associatedData), + plainBytes: Encoding.UTF8.GetBytes(plainData) + )!; + byte[] encdataBytes = new byte[cipherBytes.Length - TAG_LENGTH_BYTE]; + byte[] authtagBytes = new byte[TAG_LENGTH_BYTE]; + Buffer.BlockCopy(cipherBytes, 0, encdataBytes, 0, encdataBytes.Length); + Buffer.BlockCopy(cipherBytes, encdataBytes.Length, authtagBytes, 0, authtagBytes.Length); + + sIV = nonce; + sData = Convert.ToBase64String(encdataBytes); + sAuthTag = Convert.ToBase64String(authtagBytes); + } + catch (Exception ex) + { + throw new WechatApiException("Failed to encrypt request. Please see the inner exception for more details.", ex); + } + } + break; + + default: + throw new WechatApiException($"Failed to encrypt request. Unsupported encryption algorithm: \"{_symmetricAlg}\"."); + } + + IDictionary temp = new Dictionary(capacity: 4) + { + { "iv", sIV }, + { "data", sData }, + { "authtag", sAuthTag } + }; + postDataEncrypted = JsonSerializer.Serialize(temp, SystemTextJsonSerializer.GetDefaultSerializerOptions()); + context.FlurlCall.HttpRequestMessage!.Content?.Dispose(); + context.FlurlCall.HttpRequestMessage!.Content = new StringContent(postDataEncrypted, Encoding.UTF8, MimeTypes.Json); + } + + // 非对称签名 + if (string.IsNullOrEmpty(_asymmetricPrivateKey)) + { + throw new WechatApiException("Failed to sign request, because the RSA/SM2 private key is not set."); + } + else + { + string signData = $"{urlpath}\n{_appId}\n{timestamp}\n{postDataEncrypted}"; + string sign; + + switch (_asymmetricAlg) + { + case SecurityApiAsymmetricAlgorithms.RSA: + { + try + { + sign = Utilities.RSAUtility.SignWithSHA256PSS(_asymmetricPrivateKey, signData).Value!; + } + catch (Exception ex) + { + throw new WechatApiException("Failed to sign request. Please see the inner exception for more details.", ex); + } + } + break; + + case SecurityApiAsymmetricAlgorithms.SM2: + { + try + { + sign = Utilities.SM2Utility.SignWithSM3(_asymmetricPrivateKey, _asymmetricNum, signData).Value!; + } + catch (Exception ex) + { + throw new WechatApiException("Failed to sign request. Please see the inner exception for more details.", ex); + } + } + break; + + default: + throw new WechatApiException($"Failed to sign request. Unsupported signing algorithm: \"{_asymmetricAlg}\"."); + } + + context.FlurlCall.Request.WithHeader("Wechatmp-Appid", _appId); + context.FlurlCall.Request.WithHeader("Wechatmp-TimeStamp", timestamp); + context.FlurlCall.Request.WithHeader("Wechatmp-Signature", sign); + } + } + + public override async Task AfterCallAsync(HttpInterceptorContext context, CancellationToken cancellationToken = default) + { + if (context is null) throw new ArgumentNullException(nameof(context)); + if (!context.FlurlCall.Completed) throw new WechatApiException("Failed to decrypt and verify response. This interceptor must be called after request completed."); + + if (context.FlurlCall.HttpRequestMessage.Method != HttpMethod.Post) + return; + if (context.FlurlCall.HttpRequestMessage.RequestUri is null) + return; + if (!IsRequestUrlRequireEncryption(context.FlurlCall.HttpRequestMessage.RequestUri)) + return; + if (context.FlurlCall.HttpResponseMessage is null) + return; + + string urlpath = GetRequestUrl(context.FlurlCall.HttpRequestMessage.RequestUri); + byte[] respBytes = Array.Empty(); + if (context.FlurlCall.HttpResponseMessage.Content is not null) + { + HttpContent httpContent = context.FlurlCall.HttpResponseMessage.Content; + respBytes = await +#if NET5_0_OR_GREATER + httpContent.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); +#else + _AsyncEx.RunTaskWithCancellationTokenAsync(httpContent.ReadAsByteArrayAsync(), cancellationToken).ConfigureAwait(false); +#endif + } + + // 对称解密 + if (string.IsNullOrEmpty(_symmetricKey)) + { + throw new WechatApiException("Failed to decrypt response, because the AES/SM4 key is not set."); + } + else if (_StringSyntaxAssert.MaybeJson(respBytes)) + { + bool requireDecrypt = false; + string? sData = null, sIV = null, sAuthTag = null; + + try + { + JsonObject jsonObj = JsonObject.Parse(respBytes)!.AsObject(); + requireDecrypt = context.FlurlResponse!.Headers.Contains("Wechatmp-Signature") + && jsonObj.ContainsKey("data") + && jsonObj.ContainsKey("iv") + && jsonObj.ContainsKey("authtag"); + + if (requireDecrypt) + { + sData = jsonObj["data"]!.GetValue(); + sIV = jsonObj["iv"]!.GetValue(); + sAuthTag = jsonObj["authtag"]!.GetValue(); + } + } + catch (Exception ex) + { + throw new WechatApiException("Failed to decrypt response. Please see the inner exception for more details.", ex); + } + + if (requireDecrypt) + { + byte[] respBytesDecrypted; + string appId = context.FlurlResponse!.Headers.GetAll("Wechatmp-Appid")?.FirstOrDefault() ?? string.Empty; + string timestamp = context.FlurlResponse!.Headers.GetAll("Wechatmp-TimeStamp")?.FirstOrDefault() ?? string.Empty; + string associatedData = GenerateSymmetricEncryptionAssociatedData(urlpath, appId, timestamp); + + switch (_symmetricAlg) + { + case SecurityApiSymmetricAlgorithms.AES: + { + try + { + byte[] encdataBytes = Convert.FromBase64String(sData!); + byte[] authtagBytes = Convert.FromBase64String(sAuthTag!); + byte[] cipherBytes = new byte[encdataBytes.Length + authtagBytes.Length]; + Buffer.BlockCopy(encdataBytes, 0, cipherBytes, 0, encdataBytes.Length); + Buffer.BlockCopy(authtagBytes, 0, cipherBytes, encdataBytes.Length, authtagBytes.Length); + + respBytesDecrypted = Utilities.AESUtility.DecryptWithGCM( + keyBytes: Convert.FromBase64String(_symmetricKey), + nonceBytes: Convert.FromBase64String(sIV!), + associatedDataBytes: Encoding.UTF8.GetBytes(associatedData), + cipherBytes: cipherBytes + )!; + } + catch (Exception ex) + { + throw new WechatApiException("Failed to decrypt response. Please see the inner exception for more details.", ex); + } + } + break; + + case SecurityApiSymmetricAlgorithms.SM4: + { + try + { + byte[] encdataBytes = Convert.FromBase64String(sData!); + byte[] authtagBytes = Convert.FromBase64String(sAuthTag!); + byte[] cipherBytes = new byte[encdataBytes.Length + authtagBytes.Length]; + Buffer.BlockCopy(encdataBytes, 0, cipherBytes, 0, encdataBytes.Length); + Buffer.BlockCopy(authtagBytes, 0, cipherBytes, encdataBytes.Length, authtagBytes.Length); + + respBytesDecrypted = Utilities.SM4Utility.DecryptWithGCM( + keyBytes: Convert.FromBase64String(_symmetricKey), + nonceBytes: Convert.FromBase64String(sIV!), + associatedDataBytes: Encoding.UTF8.GetBytes(associatedData), + cipherBytes: cipherBytes + )!; + } + catch (Exception ex) + { + throw new WechatApiException("Failed to decrypt response. Please see the inner exception for more details.", ex); + } + } + break; + + default: + throw new WechatApiException($"Failed to decrypt response. Unsupported encryption algorithm: \"{_symmetricAlg}\"."); + } + + context.FlurlCall.HttpResponseMessage!.Content?.Dispose(); + context.FlurlCall.HttpResponseMessage!.Content = new ByteArrayContent(respBytesDecrypted); + } + } + } + + private string GenerateSymmetricEncryptionNonce(int byteLength) + { + byte[] bytes = new byte[byteLength]; + new Random().NextBytes(bytes); + return Convert.ToBase64String(bytes); + } + + private string GenerateSymmetricEncryptionAssociatedData(string urlpath, string appId, string timestamp) + { + return $"{urlpath}|{appId}|{timestamp}|{_symmetricNum}"; + } + + private string GetRequestUrl(Uri uri) + { + return uri.AbsoluteUri.Substring(0, uri.AbsoluteUri.Length - uri.Query.Length); + } + + private bool IsRequestUrlRequireEncryption(Uri uri) + { + string absoluteUrl = GetRequestUrl(uri); + if (!absoluteUrl.StartsWith(_baseUrl)) + return false; + + string relativeUrl = absoluteUrl.Substring(_baseUrl.TrimEnd('/').Length); + if (!SIGN_REQUIRED_URLS.Contains(relativeUrl)) + { + if (_customRequestPathMatcher is not null) + return _customRequestPathMatcher(relativeUrl); + } + + return true; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Models/Wxa/RiskControl/WxaGetUserRiskRankResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Models/Wxa/RiskControl/WxaGetUserRiskRankResponse.cs index 64b406d9..77480495 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Api/Models/Wxa/RiskControl/WxaGetUserRiskRankResponse.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Models/Wxa/RiskControl/WxaGetUserRiskRankResponse.cs @@ -1,4 +1,4 @@ -namespace SKIT.FlurlHttpClient.Wechat.Api.Models +namespace SKIT.FlurlHttpClient.Wechat.Api.Models { /// /// 表示 [POST] /wxa/getuserriskrank 接口的响应。 diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/RSAUtility.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/RSAUtility.cs index 0ce71bb0..29c9ecfe 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/RSAUtility.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/RSAUtility.cs @@ -3,9 +3,13 @@ using System.IO; using System.Security.Cryptography; using System.Text.RegularExpressions; using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; using Org.BouncyCastle.OpenSsl; using Org.BouncyCastle.Security; +using Org.BouncyCastle.X509; namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities { @@ -16,11 +20,6 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities /// public static class RSAUtility { - /// - /// 签名算法:SHA-256withRSA。 - /// - private const string DIGEST_ALGORITHM_SHA256 = "SHA-256withRSA"; - private static byte[] ConvertPrivateKeyPemToByteArray(string privateKeyPem) { const string PKCS8_HEADER = "-----BEGIN PRIVATE KEY-----"; @@ -103,6 +102,15 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities return Convert.FromBase64String(publicKeyPem); } + private static X509Certificate ParseCertificatePemToX509(string certificatePem) + { + using (TextReader sreader = new StringReader(certificatePem)) + { + PemReader pemReader = new PemReader(sreader); + return (X509Certificate)pemReader.ReadObject(); + } + } + #if NET5_0_OR_GREATER #else private static RsaKeyParameters ParsePrivateKeyToParameters(byte[] privateKeyBytes) @@ -115,17 +123,17 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities return (RsaKeyParameters)PublicKeyFactory.CreateKey(publicKeyBytes); } - private static byte[] Sign(RsaKeyParameters rsaPrivateKeyParams, byte[] messageBytes, string digestAlgorithm) + private static byte[] SignWithSHA256PSS(RsaKeyParameters rsaPrivateKeyParams, byte[] messageBytes) { - ISigner signer = SignerUtilities.GetSigner(digestAlgorithm); + ISigner signer = new PssSigner(new RsaEngine(), new Sha256Digest()); signer.Init(true, rsaPrivateKeyParams); signer.BlockUpdate(messageBytes, 0, messageBytes.Length); return signer.GenerateSignature(); } - private static bool Verify(RsaKeyParameters rsaPublicKeyParams, byte[] messageBytes, byte[] signBytes, string digestAlgorithm) + private static bool VerifyWithSHA256PSS(RsaKeyParameters rsaPublicKeyParams, byte[] messageBytes, byte[] signBytes) { - ISigner signer = SignerUtilities.GetSigner(digestAlgorithm); + ISigner signer = new PssSigner(new RsaEngine(), new Sha256Digest()); signer.Init(false, rsaPublicKeyParams); signer.BlockUpdate(messageBytes, 0, messageBytes.Length); return signer.VerifySignature(signBytes); @@ -133,13 +141,12 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities #endif /// - /// 使用私钥基于 SHA-256 算法生成签名。 + /// 使用私钥基于 SHA-256/PSS 算法生成签名。 /// /// PKCS#8 私钥字节数组。 /// 待签名的数据字节数组。 - /// 签名算法。(默认值:) /// 签名字节数组。 - public static byte[] SignWithSHA256(byte[] privateKeyBytes, byte[] messageBytes) + public static byte[] SignWithSHA256PSS(byte[] privateKeyBytes, byte[] messageBytes) { if (privateKeyBytes is null) throw new ArgumentNullException(nameof(privateKeyBytes)); if (messageBytes is null) throw new ArgumentNullException(nameof(messageBytes)); @@ -148,67 +155,64 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities using (RSA rsa = RSA.Create()) { rsa.ImportPkcs8PrivateKey(privateKeyBytes, out _); - return rsa.SignData(messageBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + return rsa.SignData(messageBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); } #else RsaKeyParameters rsaPrivateKeyParams = ParsePrivateKeyToParameters(privateKeyBytes); - return Sign(rsaPrivateKeyParams, messageBytes, DIGEST_ALGORITHM_SHA256); + return SignWithSHA256PSS(rsaPrivateKeyParams, messageBytes); #endif } /// - /// 使用私钥基于 SHA-256 算法生成签名。 + /// 使用私钥基于 SHA-256/PSS 算法生成签名。 /// /// PKCS#1/PKCS#8 私钥(PEM 格式)。 /// 待签名的数据。 - /// 签名算法。(默认值:) /// 经过 Base64 编码的签名。 - public static EncodedString SignWithSHA256(string privateKeyPem, string messageData) + public static EncodedString SignWithSHA256PSS(string privateKeyPem, string messageData) { if (privateKeyPem is null) throw new ArgumentNullException(nameof(privateKeyPem)); if (messageData is null) throw new ArgumentNullException(nameof(messageData)); byte[] privateKeyBytes = ConvertPrivateKeyPemToByteArray(privateKeyPem); byte[] messageBytes = EncodedString.FromLiteralString(messageData); - byte[] signBytes = SignWithSHA256(privateKeyBytes, messageBytes); + byte[] signBytes = SignWithSHA256PSS(privateKeyBytes, messageBytes); return EncodedString.ToBase64String(signBytes); } /// - /// 使用公钥基于 SHA-256 算法验证签名。 + /// 使用公钥基于 SHA-256/PSS 算法验证签名。 /// /// PKCS#8 公钥字节数组。 /// 待验证的数据字节数组。 /// 签名字节数组。 - /// 签名算法。(默认值:) /// 验证结果。 - public static bool VerifyWithSHA256(byte[] publicKeyBytes, byte[] messageBytes, byte[] signBytes) + public static bool VerifyWithSHA256PSS(byte[] publicKeyBytes, byte[] messageBytes, byte[] signBytes) { if (publicKeyBytes is null) throw new ArgumentNullException(nameof(publicKeyBytes)); if (messageBytes is null) throw new ArgumentNullException(nameof(messageBytes)); if (signBytes is null) throw new ArgumentNullException(nameof(signBytes)); -#if NET5_0_OR_GREATER +#if NETCOREAPP3_0_OR_GREATER || NET5_0_OR_GREATER using (RSA rsa = RSA.Create()) { rsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _); - return rsa.VerifyData(messageBytes, signBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + return rsa.VerifyData(messageBytes, signBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); } #else RsaKeyParameters rsaPublicKeyParams = ParsePublicKeyToParameters(publicKeyBytes); - return Verify(rsaPublicKeyParams, messageBytes, signBytes, DIGEST_ALGORITHM_SHA256); + return VerifyWithSHA256PSS(rsaPublicKeyParams, messageBytes, signBytes); #endif } /// - /// 使用公钥基于 SHA-256 算法验证签名。 + /// 使用公钥基于 SHA-256/PSS 算法验证签名。 /// /// PKCS#1/PKCS#8 公钥(PEM 格式)。 /// 待验证的数据。 /// 经过编码后的(通常为 Base64)签名。 - /// 签名算法。(默认值:) /// 验证结果。 - public static bool VerifyWithSHA256(string publicKeyPem, string messageData, EncodedString encodingSignature) + public static bool VerifyWithSHA256PSS(string publicKeyPem, string messageData, EncodedString encodingSignature) { if (publicKeyPem is null) throw new ArgumentNullException(nameof(publicKeyPem)); if (messageData is null) throw new ArgumentNullException(nameof(messageData)); @@ -217,7 +221,46 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities byte[] publicKeyBytes = ConvertPublicKeyPemToByteArray(publicKeyPem); byte[] messageBytes = EncodedString.FromLiteralString(messageData); byte[] signBytes = EncodedString.FromString(encodingSignature, fallbackEncodingKind: EncodingKinds.Base64); - return VerifyWithSHA256(publicKeyBytes, messageBytes, signBytes); + return VerifyWithSHA256PSS(publicKeyBytes, messageBytes, signBytes); + } + + /// + /// 使用证书基于 SHA-256/PSS 算法验证签名。 + /// + /// 证书内容(PEM 格式)。 + /// 待验证的数据。 + /// 经过编码后的(通常为 Base64)签名。 + /// 验证结果。 + public static bool VerifyWithSHA256PSSByCertificate(string certificatePem, string messageData, EncodedString encodingSignature) + { + if (certificatePem is null) throw new ArgumentNullException(nameof(certificatePem)); + + string publicKeyPem = ExportPublicKeyFromCertificate(certificatePem); + return VerifyWithSHA256PSS(publicKeyPem, messageData, encodingSignature); + } + + /// + /// 从 CRT/CER 证书中导出 PKCS#8 公钥。 + /// + /// 即从 -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE----- + /// 转为 -----BEGIN PUBLIC KEY----- ..... -----END PUBLIC KEY----- + /// + /// + /// 证书内容(PEM 格式)。 + /// PKCS#8 公钥(PEM 格式)。 + public static string ExportPublicKeyFromCertificate(string certificate) + { + if (certificate is null) throw new ArgumentNullException(nameof(certificate)); + + using (TextWriter swriter = new StringWriter()) + { + X509Certificate x509cert = ParseCertificatePemToX509(certificate); + RsaKeyParameters rsaPublicKeyParams = (RsaKeyParameters)x509cert.GetPublicKey(); + PemWriter pemWriter = new PemWriter(swriter); + pemWriter.WriteObject(rsaPublicKeyParams); + pemWriter.Writer.Flush(); + return swriter.ToString()!; + } } } } diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/SM2Utility.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/SM2Utility.cs index 5ee65c9c..2b4ecaae 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/SM2Utility.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/SM2Utility.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using System.Text.RegularExpressions; using Org.BouncyCastle.Asn1; @@ -7,9 +8,11 @@ using Org.BouncyCastle.Asn1.X9; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Math; +using Org.BouncyCastle.OpenSsl; using Org.BouncyCastle.Security; using Org.BouncyCastle.Utilities; using Org.BouncyCastle.Utilities.Encoders; +using Org.BouncyCastle.X509; namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities { @@ -44,6 +47,15 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities return Convert.FromBase64String(publicKeyPem); } + private static X509Certificate ParseCertificatePemToX509(string certificatePem) + { + using (TextReader sreader = new StringReader(certificatePem)) + { + PemReader pemReader = new PemReader(sreader); + return (X509Certificate)pemReader.ReadObject(); + } + } + private static ECPrivateKeyParameters ParsePrivateKeyToParameters(byte[] privateKeyBytes) { return (ECPrivateKeyParameters)PrivateKeyFactory.CreateKey(privateKeyBytes); @@ -218,6 +230,28 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities return EncodedString.ToBase64String(signBytes); } + /// + /// 使用私钥基于 SM3 算法生成签名。 + /// + /// PKCS#8 私钥(PEM 格式)。 + /// 用户标识符。 + /// 待签名的数据。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 经过 Base64 编码的签名。 + public static EncodedString SignWithSM3(string privateKeyPem, string uidData, string messageData, bool asn1Encoding = true) + { + if (privateKeyPem is null) throw new ArgumentNullException(nameof(privateKeyPem)); + if (messageData is null) throw new ArgumentNullException(nameof(messageData)); + + byte[] signBytes = SignWithSM3( + privateKeyBytes: ConvertPrivateKeyPemToByteArray(privateKeyPem), + uidBytes: EncodedString.FromLiteralString(uidData), + messageBytes: EncodedString.FromLiteralString(messageData), + asn1Encoding: asn1Encoding + ); + return EncodedString.ToBase64String(signBytes); + } + /// /// 使用 EC 私钥基于 SM3 算法生成签名。 /// @@ -353,6 +387,27 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities ); } + /// + /// 使用证书基于 SM3 算法验证签名。 + /// + /// 证书内容(PEM 格式)。 + /// 待验证的数据。 + /// 经过编码后的(通常为 Base64)签名。 + /// 指示签名结果是否为 ASN.1 编码的形式。(默认值:true) + /// 验证结果。 + public static bool VerifyWithSM3ByCertificate(string certificatePem, string messageData, EncodedString encodingSignature, bool asn1Encoding = true) + { + if (certificatePem is null) throw new ArgumentNullException(nameof(certificatePem)); + + string publicKeyPem = ExportPublicKeyFromCertificate(certificatePem); + return VerifyWithSM3( + publicKeyPem: publicKeyPem, + messageData: messageData, + encodingSignature: encodingSignature, + asn1Encoding: asn1Encoding + ); + } + /// /// 使用 EC 公钥基于 SM3 算法生成签名。 /// @@ -433,5 +488,29 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities asn1Encoding: asn1Encoding ); } + + /// + /// 从 CRT/CER 证书中导出 PKCS#8 公钥。 + /// + /// 即从 -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE----- + /// 转为 -----BEGIN PUBLIC KEY----- ..... -----END PUBLIC KEY----- + /// + /// + /// 证书内容(PEM 格式)。 + /// PKCS#8 公钥(PEM 格式)。 + public static string ExportPublicKeyFromCertificate(string certificatePem) + { + if (certificatePem is null) throw new ArgumentNullException(nameof(certificatePem)); + + using (TextWriter swriter = new StringWriter()) + { + X509Certificate x509cert = ParseCertificatePemToX509(certificatePem); + ECPublicKeyParameters exPublicKeyParams = (ECPublicKeyParameters)x509cert.GetPublicKey(); + PemWriter pemWriter = new PemWriter(swriter); + pemWriter.WriteObject(exPublicKeyParams); + pemWriter.Writer.Flush(); + return swriter.ToString()!; + } + } } } diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/WechatApiClient.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/WechatApiClient.cs index 9cf4864b..212d7451 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Api/WechatApiClient.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/WechatApiClient.cs @@ -40,6 +40,21 @@ namespace SKIT.FlurlHttpClient.Wechat.Api FlurlClient.BaseUrl = options.Endpoint ?? WechatApiEndpoints.DEFAULT; FlurlClient.WithTimeout(options.Timeout <= 0 ? Timeout.InfiniteTimeSpan : TimeSpan.FromMilliseconds(options.Timeout)); + + if (options.SecurityApiEnabled) + { + Interceptors.Add(new Interceptors.WechatApiSecurityApiInterceptor( + baseUrl: FlurlClient.BaseUrl, + appId: string.IsNullOrEmpty(options.SecurityApiAppId) ? options.AppId : options.SecurityApiAppId, + symmetricAlg: options.SecurityApiSymmetricAlgorithm!, + symmetricNum: options.SecurityApiSymmetricNumber!, + symmetricKey: options.SecurityApiSymmetricKey!, + asymmetricAlg: options.SecurityApiAsymmetricAlgorithm!, + asymmetricNum: options.SecurityApiAsymmetricNumber!, + asymmetricPrivateKey: options.SecurityApiAsymmetricPrivateKey!, + customRequestPathMatcher: options.SecurityApiCustomRequestPathMatcher + )); + } } /// diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/WechatApiClientOptions.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/WechatApiClientOptions.cs index 65c81a35..e0d1de2f 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Api/WechatApiClientOptions.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/WechatApiClientOptions.cs @@ -1,3 +1,5 @@ +using System; + namespace SKIT.FlurlHttpClient.Wechat.Api { /// @@ -75,5 +77,56 @@ namespace SKIT.FlurlHttpClient.Wechat.Api /// 获取或设置米大师平台 AppKey(用于小游戏虚拟支付 2.0 相关接口)。 /// public string? MidasAppKeyV2 { get; set; } + + /// + /// 获取或设置 API 安全鉴权模式是否开启。 + /// + public bool SecurityApiEnabled { get; set; } + + /// + /// 获取或设置 API 安全鉴权模式所使用的 AppId。如果不指定将使用 。 + /// + public string? SecurityApiAppId { get; set; } + + /// + /// 获取或设置 API 安全鉴权模式对称加密算法。 + /// + /// 默认值: + /// + /// + public string SecurityApiSymmetricAlgorithm { get; set; } = Constants.SecurityApiSymmetricAlgorithms.AES; + + /// + /// 获取或设置 API 安全鉴权模式对称加密密钥编号。 + /// + public string? SecurityApiSymmetricNumber { get; set; } + + /// + /// 获取或设置 API 安全鉴权模式对称加密密钥。 + /// + public string? SecurityApiSymmetricKey { get; set; } + + /// + /// 获取或设置 API 安全鉴权模式非对称加密算法。 + /// + /// 默认值: + /// + /// + public string SecurityApiAsymmetricAlgorithm { get; set; } = Constants.SecurityApiAsymmetricAlgorithms.RSA; + + /// + /// 获取或设置 API 安全鉴权模式非对称加密私钥编号。 + /// + public string? SecurityApiAsymmetricNumber { get; set; } + + /// + /// 获取或设置 API 安全鉴权模式非对称加密私钥。 + /// + public string? SecurityApiAsymmetricPrivateKey { get; set; } + + /// + /// 获取或设置 API 安全鉴权模式自定义请求路径匹配器。如果不指定将只匹配关键 API。 + /// + public Func? SecurityApiCustomRequestPathMatcher { get; set; } } } diff --git a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_SecurityApiTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_SecurityApiTests.cs new file mode 100644 index 00000000..bb682bc8 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_SecurityApiTests.cs @@ -0,0 +1,102 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests +{ + public partial class TestCase_SecurityApiTests + { + [Fact(DisplayName = "测试用例:API 安全鉴权模式")] + public async Task TestSecurityApiRequestSignature() + { + var mockClient = WechatApiClientBuilder + .Create(new WechatApiClientOptions() + { + AppId = "wxba6223c06417af7b", + SecurityApiEnabled = true, + SecurityApiSymmetricNumber = "fa05fe1e5bcc79b81ad5ad4b58acf787", + SecurityApiSymmetricKey = "otUpngOjU+nVQaWJIC3D/yMLV17RKaP6t4Ot9tbnzLY=", + SecurityApiAsymmetricNumber = "97845f6ed842ea860df6fdf65941ff56", + SecurityApiAsymmetricPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA3FoQOmOl5/CF5hF7ta4EzCy2LaU3Eu2k9DBwQ73J82I53Sx9\nLAgM1DH3IsYohRRx/BESfbdDI2powvr6QYKVIC+4Yavwg7gzhZRxWWmT1HruEADC\nZAgkUCu+9Il/9FPuitPSoIpBd07NqdkkRe82NBOfrKTdhge/5zd457fl7J81Q5VT\nIxO8vvq7FSw7k6Jtv+eOjR6SZOWbbUO7f9r4UuUkXmvdGv21qiqtaO1EMw4tUCEL\nzY73M7NpCH3RorlommYX3P6q0VrkDHrCE0/QMhmHsF+46E+IRcJ3wtEj3p/mO1Vo\nCpEhawC1U728ZUTwWNEii8hPEhcNAZTKaQMaTQIDAQABAoIBAQCXv5p/a5KcyYKc\n75tfgekh5wTLKIVmDqzT0evuauyCJTouO+4z/ZNAKuzEUO0kwPDCo8s1MpkU8boV\n1Ru1M8WZNePnt65aN+ebbaAl8FRzNvltoeg9VXIUmBvYcjzhOVAE4V2jW7M8A9QU\nzUpyswuED6OeFKfOHtYk2In2IipAqhfbyc6gn7uZSWTQsoO6hGBRQ7Ejx+vgwrbx\nZKVZ7UXbPHD0lOEPraA3PH/QUeUKpNwK2NXQoBxWcR283/HxFSAjjSSsGSBKsCnw\nDN55P2FQ0HNi5YrwUNT9190NIXSeygaRy1b+D+yBfm+yE7/qXwHLZCHsjO+2tMSS\n3KGjllTBAoGBAP9FPeYNKZuu5jt9RpZwXCc9E7Iz7bmM7zws6dun6dQH0xVVWFVm\niGIu07eqyB8HNagXseFzoXLV5EQx+3DaB0bAH+ZEpHGJJpAWSLusigssFUFuTvTF\nw+rC5hxOfidMa6+93SU5pWeJb0zJF8PRDaJ3UmwlwpYubF17sT4PD6p9AoGBANz7\nRlhRSFvggJjhEMpek3OIYWrrlRNO2MVcP7i/fGNTHhrw7OHcNGRof54QZ2Y0baL7\n1vHNokbK2mnT+cQXY/gXMmcE/eV4xyRGYiIL9nBdrkLerc43EYPv+evDvgyji6+y\n4np5cKqHrS8F+YzATk82Jt9HgdI2MvfbJTkSbmgRAoGAHNPL9rPb1An/VA6Ery6H\nKaM7Gy/EE+U3ixsjWbvvqxMrIkieDh7jHftdy2sM6Hwe8hmi6+vr+pTvD0h5tbfZ\nhILj11Q/Idc0NKdflVoZyMM0r0vuvLOsuVFDPUUb+AIoUxNk6vREmpmpqQk4ltN/\n763779yfyef6MuBqFrEKut0CgYB9FfsuuOv1nfINF7EybDCZAETsiee7ozEPHnWv\ndSzK6FytMV1VSBmcEI7UgUKWVu0MifOUsiq+WcsihmvmNLtQzoioSeoSP7ix7ulT\njmP0HQMsNPI7PW67uVZFv2pPqy/Bx8dtPlqpHN3KNV6Z7q0lJ2j/kHGK9UUKidDb\nKnS2kQKBgHZ0cYzwh9YnmfXx9mimF57aQQ8aFc9yaeD5/3G2+a/FZcHtYzUdHQ7P\nPS35blD17/NnhunHhuqakbgarH/LIFMHITCVuGQT4xS34kFVjFVhiT3cHfWyBbJ6\nGbQuzzFxz/UKDDKf3/ON41k8UP20Gdvmv/+c6qQjKPayME81elus\n-----END RSA PRIVATE KEY-----" + }) + .UseHttpClient(new MockHttpClient()) + .Build(); + + var request = new Models.WxaGetUserRiskRankRequest() + { + OpenId = "oEWzBfmdLqhFS2mTXCo2E4Y9gJAM", + Scene = 0, + ClientIp = "127.0.0.1", + AccessToken = "ACCESS_TOKEN" + }; + var response = await mockClient.ExecuteWxaGetUserRiskRankAsync(request); + + Assert.True(Boolean.TrueString == response.GetRawHeaders().GetFirstValueOrEmpty(MOCK_RESP_HEADER_RESULT)); + Assert.Equal(0, response.ErrorCode); + Assert.Equal("getuserriskrank succ", response.ErrorMessage); + Assert.Equal(0, response.RiskRank); + Assert.Equal("2258658297", response.RequestId); + } + } + + partial class TestCase_SecurityApiTests + { + private const string MOCK_RESP_HEADER_RESULT = "x-result"; + + public class MockHttpClient : HttpClient + { + public MockHttpClient() + : base(new MockHttpMessageHandler(new HttpClientHandler())) + { + } + } + + public class MockHttpMessageHandler : DelegatingHandler + { + public MockHttpMessageHandler(HttpMessageHandler innerHandler) + : base(innerHandler) + { + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var resp = new HttpResponseMessage(); + resp.RequestMessage = request; + + if (request.Method == HttpMethod.Post && request.RequestUri?.AbsolutePath == "/wxa/getuserriskrank") + { + resp.StatusCode = HttpStatusCode.OK; + resp.Content = new StringContent("{\"iv\":\"r2WDQt56rEAmMuoR\",\"data\":\"HExs66Ik3el+iM4IpeQ7SMEN934FRLFYOd3EmeaIrpP4EPTHckoco6O+PaoRZRa3lqaPRZT7r52f7LUok6gLxc6cdR8C4vpIIfh4xfLC4L7FNy9GbuMK1hcoi8b7gkWJcwZMkuCFNEDmqn3T49oWzAQOrY4LZnnnykv6oUJotdAsnKvmoJkLK7hRh7M2B1d2UnTnRuoIyarXc5Iojwoghx4BOvnV\",\"authtag\":\"z2BFD8QctKXTuBlhICGOjQ==\"}", null, "application/json"); + resp.Headers.Add("Wechatmp-Appid", "wxba6223c06417af7b"); + resp.Headers.Add("Wechatmp-TimeStamp", "1635927956"); + resp.Headers.Add("Wechatmp-Serial", "79ba700ea147819f640941bceb38b1d1"); + resp.Headers.Add("Wechatmp-Signature", "Ht0VfQkkEweJ4hU266C14Aj64H9AXfkwNi5zxUZETCvR2svU1ZYdosDhFX/voLj1TyszqKsVxAlENGt7PPZZ8RQX7jnA4SKhiPUhW4LTbyTenisHJ+ohSfDjYnXavjQsBHspFS+BlPHuSSJ2xyQzw1+HuC6nid09ZL4FnGSYo4OI5MJrSb9xLzIVZMIDuUQchGKi/KaB1KzxECLEZcfjqbAgmxC7qOmuBLyO1WkHYDM95NJrHJWba5xv4wrwPru9yYTJSNRnlM+zrW5w9pOubC4Jtj3szTAEuOz9AcqUmgaAvMLNAIa8hfODLRe3n/cu4SgYlN/ZkNRU4QXVNbPGMg=="); + + var reqData = await request.Content!.ReadAsStringAsync(); + var reqJson = JsonObject.Parse(reqData)?.AsObject(); + bool isReqValid = reqJson is not null + && reqJson.ContainsKey("iv") + && reqJson.ContainsKey("data") + && reqJson.ContainsKey("authtag") + && request.Headers.Contains("Wechatmp-Appid") + && request.Headers.Contains("Wechatmp-TimeStamp") + && request.Headers.Contains("Wechatmp-Signature"); + resp.Headers.Add(MOCK_RESP_HEADER_RESULT, isReqValid ? Boolean.TrueString : Boolean.FalseString); + } + else + { + resp.StatusCode = HttpStatusCode.ServiceUnavailable; + resp.Content = new ByteArrayContent(Array.Empty()); + } + + return resp; + } + } + + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsAESUtilityTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsAESUtilityTests.cs index a5a1b88a..b425a06d 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsAESUtilityTests.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsAESUtilityTests.cs @@ -1,3 +1,5 @@ +using System; +using System.Text; using Xunit; namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests @@ -45,6 +47,28 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests string expectedPlain = "Awesome SKIT.FlurlHttpClient.Wechat.Api!"; Assert.Equal(expectedPlain, actualPlain); + + // 以下是根据微信官方文档提供的示例数据的测试用例 + { + string wxsampleKey = "otUpngOjU+nVQaWJIC3D/yMLV17RKaP6t4Ot9tbnzLY="; + string wxsampleNonce = "r2WDQt56rEAmMuoR"; + string wxsampleAad = "https://api.weixin.qq.com/wxa/getuserriskrank|wxba6223c06417af7b|1635927956|fa05fe1e5bcc79b81ad5ad4b58acf787"; + string wxsampleData = "HExs66Ik3el+iM4IpeQ7SMEN934FRLFYOd3EmeaIrpP4EPTHckoco6O+PaoRZRa3lqaPRZT7r52f7LUok6gLxc6cdR8C4vpIIfh4xfLC4L7FNy9GbuMK1hcoi8b7gkWJcwZMkuCFNEDmqn3T49oWzAQOrY4LZnnnykv6oUJotdAsnKvmoJkLK7hRh7M2B1d2UnTnRuoIyarXc5Iojwoghx4BOvnV"; + string wxsampleTag = "z2BFD8QctKXTuBlhICGOjQ=="; + + byte[] keyBytes = Convert.FromBase64String(wxsampleKey); + byte[] nonceBytes = Convert.FromBase64String(wxsampleNonce); + byte[] aadBytes = Encoding.UTF8.GetBytes(wxsampleAad); + byte[] encdataBytes = Convert.FromBase64String(wxsampleData); + byte[] authtagBytes = Convert.FromBase64String(wxsampleTag); + byte[] cipherBytes = new byte[encdataBytes.Length + authtagBytes.Length]; + Buffer.BlockCopy(encdataBytes, 0, cipherBytes, 0, encdataBytes.Length); + Buffer.BlockCopy(authtagBytes, 0, cipherBytes, encdataBytes.Length, authtagBytes.Length); + + byte[] plainBytes = Utilities.AESUtility.DecryptWithGCM(keyBytes, nonceBytes, aadBytes, cipherBytes); + string plainData = Encoding.UTF8.GetString(plainBytes).Trim(); + Assert.True(plainData.StartsWith("{") && plainData.EndsWith("}")); + } } } } diff --git a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsRSAUtilityTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsRSAUtilityTests.cs index d69b1909..2fc5ed30 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsRSAUtilityTests.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/TestCase_ToolsRSAUtilityTests.cs @@ -7,36 +7,44 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests public class TestCase_ToolsRSAUtilityTests { // 此处测试的 RSA 证书/公钥/私钥是自签名生成的,仅供执行 RSA 相关的单元测试,不能用于调用微信 API。 - private const string RSA_PEM_CERTIFICATE = "-----BEGIN CERTIFICATE-----\nMIIFRzCCAy8CFDBQ9y4tzgPn7+SVV90jHRdmSa+9MA0GCSqGSIb3DQEBCwUAMGAx\nCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhTaGFuZ2hhaTERMA8GA1UEBwwIU2hhbmdo\nYWkxDTALBgNVBAoMBFNLSVQxDTALBgNVBAsMBFNLSVQxDTALBgNVBAMMBFNLSVQw\nHhcNMjExMTI1MTgzNzQ4WhcNMjExMjI1MTgzNzQ4WjBgMQswCQYDVQQGEwJDTjER\nMA8GA1UECAwIU2hhbmdoYWkxETAPBgNVBAcMCFNoYW5naGFpMQ0wCwYDVQQKDART\nS0lUMQ0wCwYDVQQLDARTS0lUMQ0wCwYDVQQDDARTS0lUMIICIjANBgkqhkiG9w0B\nAQEFAAOCAg8AMIICCgKCAgEA52DszUZzPKPo1d9Hi5Hjlu7OINwADaeXifA4rvmJ\nJaA+jm4DCMwrAMzyS12EiW31xCAF8LZ/xkrFHO5CZgvK87Y+kY9DmhvNX6FVYsn4\nay7KER0zo87zqQjC+njUu1rYuKnio7MYb354PitwQ3SWNv2qTCbCNCXTN9pJXNhl\nCudWCEWrNrYc4/hKz3bqu1DjpY0oHuuKPk/iRr2TTUIAwahNkNQheQNB2a8hL7L2\nOG1Sn1vaDWe+5RJYlMRZ3NgYDTqoy8GMs+6q091MQMDlQ90jtW/JEoM5DUyI8zfQ\nfDLGnU7FuY0rrZ/+6OQT/o7ISf0OR5TISS0lqnDN3vVaph0ftDGRdGqJk2SJAHIo\nxp5gt410rfWS9kpSDFJs3Pvt4rtNZBYvkGD8obSm91brAkoX4+u1Y4p1qZpWJ4LI\nKw8oyeieqlLZtF/VGKOtKxe/IKn8GwoQJLx4dUGFOqM7HPwR9cyjMaC1o3V1NQG+\n1wD9TLtGh3WXUFJRYDmePaSp39GFPupTMlPRbD0RK80B6xv2rYTyYyd8s2LN6P6H\nh/nFIkc1rekIf9JhPy0WKzrXdmnfjSHKPxmz0WSYN8FxKasqcJhncOdhLTzzVEhj\n9xHSI8ejP2fJ4v+ARoD3GURPD9H7KMa7xmzRSAZ8A8LM3uvdJNhbKBwWqvo45ncz\n+7cCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAVTS6oMfDA3XTwEel0BvaXMCdo7yM\ns5ueM87151eywnPlConYDXeqhfF0OCSBnY2g7Fpmn+YAUoa/L+FNOx/gMC9QV/lP\nHhhAcWpiCRy52RX/IyTDxFD6OqtH0BaBtDTb+QBXZuFypMUkPy6EUYs5Cl9qYepy\nHcgGVomx7tcwWcvI4o/KZtj8hXC5wu/k4Y0GGUriTt8xmnJ+RTRedZ6hzAFVHtXm\n/YIT9Lc1IIYZuHVyCbX/HXwa0E4r8lghwZRg94HUvpbfabNA3obt5auwtJUfW1tK\n2ERgFrtBRBWf9EGb8TstXqksqYZ04U4OjLm/3ZJhSSYKNbriRLlSEzAlHikNVW+t\n6cTh+sasrGt/qNIRMs5PiipwmV/T3z1LbyoiU7fXZ4GqiWBnZARFC9KiPPTzLszh\nBKJGYHaC8wkGb3WfNWFBqVRfFL8kdME+shLB8/ETQ31gIFeudnW1QlujJ7ZSZtwz\nxT3HxzZIIbNEqLFP+d37kmuKjRmI4KWc+pKOUw9BOl4g/TJH6ySljSNs8LSDWwQY\n76Dsnr+ovz8ZVLNUCmedZCyumeJo2tLkJmsPo5GuMnXpL94mhqpCoUS4l4JbJl44\nT2lmqp1Ueoz+Qlkqyt2lj3heTv9bvB7NO9KHTsDy1hhWHOG1QyXzajyWETU+1XdW\nx1hGvYxtpQPLUE8=\n-----END CERTIFICATE-----"; private const string RSA_PEM_PUBLIC_KEY_PKCS8 = "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA52DszUZzPKPo1d9Hi5Hj\nlu7OINwADaeXifA4rvmJJaA+jm4DCMwrAMzyS12EiW31xCAF8LZ/xkrFHO5CZgvK\n87Y+kY9DmhvNX6FVYsn4ay7KER0zo87zqQjC+njUu1rYuKnio7MYb354PitwQ3SW\nNv2qTCbCNCXTN9pJXNhlCudWCEWrNrYc4/hKz3bqu1DjpY0oHuuKPk/iRr2TTUIA\nwahNkNQheQNB2a8hL7L2OG1Sn1vaDWe+5RJYlMRZ3NgYDTqoy8GMs+6q091MQMDl\nQ90jtW/JEoM5DUyI8zfQfDLGnU7FuY0rrZ/+6OQT/o7ISf0OR5TISS0lqnDN3vVa\nph0ftDGRdGqJk2SJAHIoxp5gt410rfWS9kpSDFJs3Pvt4rtNZBYvkGD8obSm91br\nAkoX4+u1Y4p1qZpWJ4LIKw8oyeieqlLZtF/VGKOtKxe/IKn8GwoQJLx4dUGFOqM7\nHPwR9cyjMaC1o3V1NQG+1wD9TLtGh3WXUFJRYDmePaSp39GFPupTMlPRbD0RK80B\n6xv2rYTyYyd8s2LN6P6Hh/nFIkc1rekIf9JhPy0WKzrXdmnfjSHKPxmz0WSYN8Fx\nKasqcJhncOdhLTzzVEhj9xHSI8ejP2fJ4v+ARoD3GURPD9H7KMa7xmzRSAZ8A8LM\n3uvdJNhbKBwWqvo45ncz+7cCAwEAAQ==\n-----END PUBLIC KEY-----"; private const string RSA_PEM_PUBLIC_KEY_PKCS1 = "-----BEGIN RSA PUBLIC KEY-----\nMIICCgKCAgEA52DszUZzPKPo1d9Hi5Hjlu7OINwADaeXifA4rvmJJaA+jm4DCMwr\nAMzyS12EiW31xCAF8LZ/xkrFHO5CZgvK87Y+kY9DmhvNX6FVYsn4ay7KER0zo87z\nqQjC+njUu1rYuKnio7MYb354PitwQ3SWNv2qTCbCNCXTN9pJXNhlCudWCEWrNrYc\n4/hKz3bqu1DjpY0oHuuKPk/iRr2TTUIAwahNkNQheQNB2a8hL7L2OG1Sn1vaDWe+\n5RJYlMRZ3NgYDTqoy8GMs+6q091MQMDlQ90jtW/JEoM5DUyI8zfQfDLGnU7FuY0r\nrZ/+6OQT/o7ISf0OR5TISS0lqnDN3vVaph0ftDGRdGqJk2SJAHIoxp5gt410rfWS\n9kpSDFJs3Pvt4rtNZBYvkGD8obSm91brAkoX4+u1Y4p1qZpWJ4LIKw8oyeieqlLZ\ntF/VGKOtKxe/IKn8GwoQJLx4dUGFOqM7HPwR9cyjMaC1o3V1NQG+1wD9TLtGh3WX\nUFJRYDmePaSp39GFPupTMlPRbD0RK80B6xv2rYTyYyd8s2LN6P6Hh/nFIkc1rekI\nf9JhPy0WKzrXdmnfjSHKPxmz0WSYN8FxKasqcJhncOdhLTzzVEhj9xHSI8ejP2fJ\n4v+ARoD3GURPD9H7KMa7xmzRSAZ8A8LM3uvdJNhbKBwWqvo45ncz+7cCAwEAAQ==\n-----END RSA PUBLIC KEY-----\r\n"; private const string RSA_PEM_PRIVATE_KEY_PKCS8 = "-----BEGIN PRIVATE KEY-----\nMIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDnYOzNRnM8o+jV\n30eLkeOW7s4g3AANp5eJ8Diu+YkloD6ObgMIzCsAzPJLXYSJbfXEIAXwtn/GSsUc\n7kJmC8rztj6Rj0OaG81foVViyfhrLsoRHTOjzvOpCML6eNS7Wti4qeKjsxhvfng+\nK3BDdJY2/apMJsI0JdM32klc2GUK51YIRas2thzj+ErPduq7UOOljSge64o+T+JG\nvZNNQgDBqE2Q1CF5A0HZryEvsvY4bVKfW9oNZ77lEliUxFnc2BgNOqjLwYyz7qrT\n3UxAwOVD3SO1b8kSgzkNTIjzN9B8MsadTsW5jSutn/7o5BP+jshJ/Q5HlMhJLSWq\ncM3e9VqmHR+0MZF0aomTZIkAcijGnmC3jXSt9ZL2SlIMUmzc++3iu01kFi+QYPyh\ntKb3VusCShfj67VjinWpmlYngsgrDyjJ6J6qUtm0X9UYo60rF78gqfwbChAkvHh1\nQYU6ozsc/BH1zKMxoLWjdXU1Ab7XAP1Mu0aHdZdQUlFgOZ49pKnf0YU+6lMyU9Fs\nPRErzQHrG/athPJjJ3yzYs3o/oeH+cUiRzWt6Qh/0mE/LRYrOtd2ad+NIco/GbPR\nZJg3wXEpqypwmGdw52EtPPNUSGP3EdIjx6M/Z8ni/4BGgPcZRE8P0fsoxrvGbNFI\nBnwDwsze690k2FsoHBaq+jjmdzP7twIDAQABAoICAQDTJ+hT2eRWxfs6G39uhyBd\nYOhqonvF+llYgAsq2/3mgZw1XX6Va8Ye/+prDxhiVyB/8P2a1OI884V5xpKAEGkS\nCxKEwmreXFsL1+9VrZ5xKo0sGytCZh6F98IA1X7G0LyRojB8VniJX7CahAf6944S\n92KQBpsa/h4JjcN/4NgtoDsqZ3I+BurMvY6AUTUc51ApiG3B8hECluKYzm98hSyt\nj0viTUWS638QCzxNDJSZoGNTnX6c1z4mTZzbf2nHGsqwYAUlligzGS97FC1/tspE\nKa9p6G9m3qyVT3B4DkrM3YXWj8nwcT4YQLhgj60TlfiBVVjPyJ8T8Qi7yCCJRf6H\nd8/YT9Nh/uaHh9DUmgiN6SL2v7kRnfJ9+5nXfyxjC/jiJjMwoFvSzkWYNdz0AOiw\nqVFVAzIBvNS4he6blXlpxvi8vtx4Bkg86uwUlauKtUbtRxy7PaUYJ966dgvYH6oB\nEqRPXqSc0d4GaY+RS6LzcXmwLYmsXwZV+GwY9Q8Y445vuP20Ae/dc2l9R19Dp90U\nYWKU49QgXIrGqZ0vL4StWuy10Y3tpBdW12qBpVGwUIxMhY2dAR5nWrXnqbqXZ3KK\novWPPKj0+SUN/RKglzNjezkvJqcfTHWn32+wqjTzAivYIYZhFtYRje95OzGnjp3q\nVQm/hXZGWaJdNCmu94oFcQKCAQEA/iRxbMxY3ZC2E3VD9PzTatRWxJ0ZgR2ZvXQZ\nDZe+Ut1bzuKerPQIkGNDAqRjicYSS6QbtopNbVjyNpz7lJduXXohpTSDrWlIjfto\n/dQ8AFHOEeM2ynp/s+Q8/fzXAbgmBmgFpGOf/bYzDWuweQ9G29msJ7G8py+Lo5RH\nb6ZmhvkGVez4m3mR7B3fbRMO/K/4fyRRJm40Nc3aAk+UbnhL/Nl8nMRC+bkjJv0N\nG4Pf6Fhf99sqJR7EbS2B5p9C+m6Du9zVC/zmIhOSg7Cg6/VGLdSX/el7QgL9r8Ld\n71a1Bn4hTeWnRgkyyC2c/oiCx2GcLFMNXZECIqUNhpZDsaNz/wKCAQEA6RHiywU+\niVyRW28RP3UvoKhm0RqWH8kFJ6SjATi0QDTNUAOEtTOXAmyc9FuxkBQjoIi8qVby\nYwZF9YFXb1o823J4EafEKX1D9gGHeV22FlzhMSBOzf0KTi1R9IAJoIScBIyNyamZ\nKwAfa7bLCbxNBiQG3JYmQqI3OE6VFFM7uuIWvZHF26Rt8HLKYXtRzZ/phO3mJ4Ke\nyQYfl+yF5PWueGpLJAjNYI3E2TxxudQMtYkWDV6o8OJrQ66bnUcHMxi1XPNYDlBM\nAQsGHIN7+qYx5EY7fHK1kzChYOoORsqjGwj9SSEdnNTM3uB6PLXnJsoG0NTaaoVo\nW5rfnCPjI0gYSQKCAQBlMj24BOad0zGtLdSRiNrmfwbN44B0WUUOm1wefX3boSkd\niD+GvuVqGRxlwO+hvK0sUXx3gzqxf+lyta+3y1S3BBrBndeRBYtOff2glRIPToOv\nu7nlhkGzb/6ZZER4+sqpYmJcww7CB/rsLSVoDx04DcTvSWbFa7k+uZx4aNoKhL5x\nGJslzZK9YmfFFwGwvKFGfz+Q/fDsO7vDj8ya8GvRkwh7o+rHZWEJ9Vlyy2AtNIOC\nPlLZ1RaCIszG+EPDVJ4///8Vdu5sQz7kEUECs/ft5+ldwcrCzk4V3pJg6zXKEA9S\n5U9mI+OEsiUBdXodylBVlfyMdWFUSkTIgq0R3vQhAoIBABtLb+7st00o3REDKdbv\np1s+PYRBg9FHHmZtHnXXKSzXwi+bqd/6obWz+JGZZ2sDIMT9HnMKbqpwIqNEuXOd\n8sCUYEFZD1z4gYv+09m/wsJNsEWrje8LsjhDkHR8xiPZQ9g4iaZTSU/C3OslZhPG\nzJJqh68vml11V9gtQ8I0mSsirR0YRD6bvBBLsS3HXmYhUxyxK6H25xeNswd8uJV+\nvCb388LNkRe8oo/6RytHDRH5cu6v5kMHkR5FBY5eshYmz56KFQbgGnaIzvdp4owR\nCIi+PNsvJ9qL+Go8Ht3lf0J8RAVbbndeaHu1eDtB5kcho7izJL0S0Izhz0we28vW\n9pkCggEAbxVbSvo1zwI6rJ5V5hNA3mLfyQfZbdGa3DvsJNpYkkKfcDDCY0A5c87v\naIXJs+Mv2Ec/jNlQnIgrAavrM4Q8QxsBCfQREfb2GK9xZPINAZ9BZAyMcqO5FIUG\n2b5SKxXWVaFpt52CsKXQIIJUy3VI9lyvKNQc9xKIXarYiMyC9X4/tVmqZqIJwPZZ\nZqWeptNm5dyIGHbKsxIXdYBgD8TKb22nFaKbRX7dB11zGfs3o5rOftWWew7/ha3Q\nePN9vy8x0PXfKzBbWNgOwu/uv4uQF0mrhHb+sn6N2XSj3v20nJz562ropN3tI8oe\nhpUq0eKgdGHc2R4r57soRvGoGy2DtA==\n-----END PRIVATE KEY-----"; private const string RSA_PEM_PRIVATE_KEY_PKCS1 = "-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----\r\n"; - [Fact(DisplayName = "测试用例:SHA256WithRSA 签名生成")] - public void TestRSASignatureSHA256WithRSASign() + [Fact(DisplayName = "测试用例:SHA256WithRSA/PSS 签名生成")] + public void TestRSASignatureSHA256WithRSAPSSSign() { string msgText = "SHA256WithRSASignTest"; - string actualSignByPrivateKeyPkcs8 = Utilities.RSAUtility.SignWithSHA256(RSA_PEM_PRIVATE_KEY_PKCS8, msgText)!; - string actualSignByPrivateKeyPkcs1 = Utilities.RSAUtility.SignWithSHA256(RSA_PEM_PRIVATE_KEY_PKCS1, msgText)!; - string expectedSign = "EzeVEIoBhzOhXpwbXdJjIuGIGRc6ArKO7sVo2fuAdzYTDgorAEufEnw7lPPXV1GTfFcHOnsAJH9kGJmg7Orwlkh7x7TyOGrkMEhWGulA9SIdmou4iBsHpIZ/TERSgGt/VTmqmfpkzJqrvbQWIQENwo7Lr6uJSJBND0YT3nIBP8TzbO3cHnQb6chHIBHrDF5vOO7HHu+Cga2MZnAtRizhO8BhK0jOmyro32CgIML3EVX8yuPy0kOk6aN1R8xFblZUD4NU2M6zzQpydmxaHr9B1WNFoMwmpoAS5BuKJMYOHO5cc6DhB+0fAGxaWtKp6759KhKCf8M65zh3WKS4l262SGuWq4qG1+AKf2DOgCon769+A4z8flOmvl0iIwoH9FThGJoP156rpAJW7v/bWputSeC6WToUTBRmGWdwWySVwW5AZ26OAFFWs1CmrGp3jF5E2oUy1mQwgfM0QN6DW+wD769ggIYH9HLHqDHbF5UyF7eNh3s8Qy23xXEKZWNMAJ0IdtdMQ7FRRgLFSCai7HELLlBJSCz7P5WTeYZGQpbvnUShkRvzujjO6XlGiKKI0EwKb121z8N6KRpvs4SnRztWBGoXbzHZgnXKXU/BWWADemqB2cvaT3Bj0k3/N3sea0dAEtlNEklRWoyyNUUlscK9zg4LBlHrhbbFo66uuub8ySo="; + string actualSignByPrivateKeyPkcs8 = Utilities.RSAUtility.SignWithSHA256PSS(RSA_PEM_PRIVATE_KEY_PKCS8, msgText)!; + string actualSignByPrivateKeyPkcs1 = Utilities.RSAUtility.SignWithSHA256PSS(RSA_PEM_PRIVATE_KEY_PKCS1, msgText)!; - Assert.Equal(expectedSign, actualSignByPrivateKeyPkcs8); - Assert.Equal(expectedSign, actualSignByPrivateKeyPkcs1); - Assert.True(Utilities.RSAUtility.VerifyWithSHA256(RSA_PEM_PUBLIC_KEY_PKCS8, msgText, (EncodedString)actualSignByPrivateKeyPkcs8)); - Assert.True(Utilities.RSAUtility.VerifyWithSHA256(RSA_PEM_PUBLIC_KEY_PKCS1, msgText, (EncodedString)actualSignByPrivateKeyPkcs1)); + Assert.NotNull(actualSignByPrivateKeyPkcs8); + Assert.NotNull(actualSignByPrivateKeyPkcs1); + Assert.NotEqual(actualSignByPrivateKeyPkcs8, actualSignByPrivateKeyPkcs1); + Assert.True(Utilities.RSAUtility.VerifyWithSHA256PSS(RSA_PEM_PUBLIC_KEY_PKCS8, msgText, (EncodedString)actualSignByPrivateKeyPkcs8)); + Assert.True(Utilities.RSAUtility.VerifyWithSHA256PSS(RSA_PEM_PUBLIC_KEY_PKCS1, msgText, (EncodedString)actualSignByPrivateKeyPkcs1)); } - [Fact(DisplayName = "测试用例:SHA256WithRSA 签名验证")] - public void TestRSASignatureSHA256WithRSAVerify() + [Fact(DisplayName = "测试用例:SHA256WithRSA/PSS 签名验证")] + public void TestRSASignatureSHA256WithRSAPSSVerify() { string msgText = "SHA256WithRSAVerifyTest"; - string signText = "aHX+MrmZHDEraMKBEPV2Vnps1B9b25lGbv/rdppx/S7+oaXtjKJprzCq5H7RCpvrKS3xYIeTEPwQGC3Vots7dCdLi8v8ew1vvtXf8qNAnd7CTMHqu3wSohXzgyASTmNbXE2ml9LbWYPPYMvPJXROQbGVjoOrsErWBPPJYXuO3lIckIfwI05OTdl4H3+BvpD/ZoljRp8Qgo9+paGvarBc++TaAh0FXnQf0TGNFUIeHHiAKBee5oCBTuZZM9J5RPw0oIq/g7Wun+e/zWiwVBPHltOgZrV46uagSAE6nBDHk+hlNxDivCxkJdBVCSIYFFmBXIcnGZ/u4ZfBui/k1jGoKibyvPK4z2+6GSlj41Yo81kuSBfzLiSsx33EPR1eIJJkwDTsvap0ymL9pfIqMiLuiteH5kGmL/dyONy9oAJywLEeITfoVyElM/CY6Dc+xDhRnjN7Hu54meYyXRZrnCtQ3YhzEr1immNBn6npgA/qi9aHsuWFOw8b8aSwOHDHTDmjmvV+axI8CVMrR0MjB9QNCWrKLq2B9iQX9MtLgcUyDsQvzAsxUJm/OEfzUjs9SHvmgmyAvzNAuTdO7wLQ+ZmKg0yZne6nvcrJVvfh3lD5ZPt7NY57Y6OIJluqKUT5H+a3H6W9Q1Z+cBMnHGYaaK7Tv8IcDdEYqTIG8hc5BqjFOzE="; + string signText = "xK9mJSuEfR4nHTBlTiBL3mLYZKZU8sXMiYkmvE1nNWdTfaEoWJ98i9uglFvE3U/dS8SsoHuZ3UNX8SL7v9Jpu4pK5opuqbH2hxMDOrEt+KDUxlZb6NDPCkWAQJoTtAgLHcKXrH9u3DiV18OcbjaWuAC8kWZqmxmkmx9Hzbl/L/CR0C1tj/2ujn8IElH0iKaxB3fR3oYFxwv0v2pgk0gv/AxeynrZD51wx3SfEAWggs8vdVvUHAYXJwg8A9dCK9bRPChFjJEQTdvhXv9lKYf3UHwEQOUtK83yE0FXn6JZuBYdRB0E9Am5Un5gu0DuyxV1a4OUTepimV519rFBDoN6qIsRSItns4G56T/6mHos5Z8Qu3OYcJhI6nw0gYzK7cdTZIm88QvB+e8/+qdqq5tcQ4hhrzznhfc0onXkKDuPGNoGHKTWCdU4gjc0qabFPovD+wsLb97pTM/IXbUa0XywjNrvrM6bUIBlLgeUajbQfKxDgPn1oJVUJHJO7Se+l5XnxY6IA/ntUpuI260ZTh5E9cfa6LRIHIaFVOORwEBIZYXfVfj0Nbk6N4uiqktswxBpyuHO3UG7xRuUOcB8bZSUuuySNjVzAfX2sgGnM8liNlBi5qUBH+a1tUraYtkIg/SWm4QtbPKQCwP4TaQv5Y6+rufqZv2vTq07+KanwnCMHbM="; - Assert.True(Utilities.RSAUtility.VerifyWithSHA256(RSA_PEM_PUBLIC_KEY_PKCS8, msgText, (EncodedString)signText)); - Assert.True(Utilities.RSAUtility.VerifyWithSHA256(RSA_PEM_PUBLIC_KEY_PKCS1, msgText, (EncodedString)signText)); - Assert.False(Utilities.RSAUtility.VerifyWithSHA256(RSA_PEM_PUBLIC_KEY_PKCS8, msgText, (EncodedString)"FAKE SIGN")); - Assert.False(Utilities.RSAUtility.VerifyWithSHA256(RSA_PEM_PUBLIC_KEY_PKCS1, msgText, (EncodedString)"FAKE SIGN")); + Assert.True(Utilities.RSAUtility.VerifyWithSHA256PSS(RSA_PEM_PUBLIC_KEY_PKCS8, msgText, (EncodedString)signText)); + Assert.True(Utilities.RSAUtility.VerifyWithSHA256PSS(RSA_PEM_PUBLIC_KEY_PKCS1, msgText, (EncodedString)signText)); + Assert.False(Utilities.RSAUtility.VerifyWithSHA256PSS(RSA_PEM_PUBLIC_KEY_PKCS8, msgText, (EncodedString)"FAKE SIGN")); + Assert.False(Utilities.RSAUtility.VerifyWithSHA256PSS(RSA_PEM_PUBLIC_KEY_PKCS1, msgText, (EncodedString)"FAKE SIGN")); + + // 以下是根据微信官方文档提供的示例数据的测试用例 + { + string wxsampleCertificate = "-----BEGIN CERTIFICATE-----\nMIID0jCCArqgAwIBAgIUeE+Yy7vM/o+eHHsfM+1bGJJEZTQwDQYJKoZIhvcNAQEL\nBQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT\nFFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg\nQ0EwHhcNMjIwOTA1MDgzOTIyWhcNMjcwOTA0MDgzOTIyWjBkMRswGQYDVQQDDBJ3\neGQ5MzBlYTVkNWEyNThmNGYxFTATBgNVBAoMDFRlbmNlbnQgSW5jLjEOMAwGA1UE\nCwwFV3hnTXAxCzAJBgNVBAYMAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM5D9qlkCmk1kr3FpF0e9pc3kGsvz5RA\n0/YRny9xPKIyV2UVMDZvRQ+mDHsiQQFE6etg457KFYSxTDKtItbdl6hJQVGeAvg0\nmqPYE9SkHRGTfL/AnXRbKBG2GC2OcaPSAprsLOersjay2me+9pF8VHybV8aox78A\nNsU75G/OO3V1iEE0s5Pmglqk8DEiw9gB/dGJzsNfXwzvyJyiUP9ZujYexyjsS+/Z\nGdSOUkqL/th+16yHj8alcdyga6YGfWEDyWkt/i/B28cwx4nzwk8xgrurifPaLuMk\n0+9wJQLCfAn/f7zyHrC8PcD1XvvRt9VBNMBASXs3710ODyyVf2lkMgkCAwEAAaOB\ngTB/MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMGUGA1UdHwReMFwwWqBYoFaGVGh0\ndHA6Ly9ldmNhLml0cnVzLmNvbS5jbi9wdWJsaWMvaXRydXNjcmw/Q0E9MUJENDIy\nMEU1MERCQzA0QjA2QUQzOTc1NDk4NDZDMDFDM0U4RUJEMjANBgkqhkiG9w0BAQsF\nAAOCAQEAL2MK9tYu+ljLVBlSbfEeaKyF07TN+G31Ya5NBzeS1ZCx4joUEIyACWmG\nfUkKNKiKV+EMzxeEhKRso1Qif3E7Ipl+PQBoQw6OSR/jFHciYurnGR9CLkL03Zo1\nqw1Xetv9OipsvlpA0SOWc207e/XpGdm8C7FMXM6bzvVp8I/STTjC1vqjIZu9WavI\nRgGM4jyAPz2XogUq0BNijef8BXbbav9fAsXjHSwn5BQv4iLms3fiLm/eoyQ6dZ2R\noTudrlcyr1bG4vwETLmHF+3yfVp9dpvJ+lyfiviwDwyfa8t2WlJm27DuF4vWoxir\nmjgj9tDutIFqxLIovLyg3uiAYtSQ/Q==\n-----END CERTIFICATE-----"; + string wxsampleMessage = "https://api.weixin.qq.com/wxa/getuserriskrank\nwxba6223c06417af7b\n1635927956\n{\"iv\":\"r2WDQt56rEAmMuoR\",\"data\":\"HExs66Ik3el+iM4IpeQ7SMEN934FRLFYOd3EmeaIrpP4EPTHckoco6O+PaoRZRa3lqaPRZT7r52f7LUok6gLxc6cdR8C4vpIIfh4xfLC4L7FNy9GbuMK1hcoi8b7gkWJcwZMkuCFNEDmqn3T49oWzAQOrY4LZnnnykv6oUJotdAsnKvmoJkLK7hRh7M2B1d2UnTnRuoIyarXc5Iojwoghx4BOvnV\",\"authtag\":\"z2BFD8QctKXTuBlhICGOjQ==\"}"; + string wxsampleSignature = "Ht0VfQkkEweJ4hU266C14Aj64H9AXfkwNi5zxUZETCvR2svU1ZYdosDhFX/voLj1TyszqKsVxAlENGt7PPZZ8RQX7jnA4SKhiPUhW4LTbyTenisHJ+ohSfDjYnXavjQsBHspFS+BlPHuSSJ2xyQzw1+HuC6nid09ZL4FnGSYo4OI5MJrSb9xLzIVZMIDuUQchGKi/KaB1KzxECLEZcfjqbAgmxC7qOmuBLyO1WkHYDM95NJrHJWba5xv4wrwPru9yYTJSNRnlM+zrW5w9pOubC4Jtj3szTAEuOz9AcqUmgaAvMLNAIa8hfODLRe3n/cu4SgYlN/ZkNRU4QXVNbPGMg=="; + + Assert.True(Utilities.RSAUtility.VerifyWithSHA256PSSByCertificate(wxsampleCertificate, wxsampleMessage, new EncodedString(wxsampleSignature, EncodingKinds.Base64))); + } } } }