diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Utilities/SM3Utility.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Utilities/SM3Utility.cs
new file mode 100644
index 00000000..22730c36
--- /dev/null
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Utilities/SM3Utility.cs
@@ -0,0 +1,370 @@
+using System;
+using System.Text;
+using Org.BouncyCastle.Crypto;
+
+namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Utilities
+{
+ ///
+ /// SM3 算法工具类。
+ ///
+ public static class SM3Utility
+ {
+ internal static class BitOperator
+ {
+ public static int URShift(int number, int bits)
+ {
+ if (number >= 0)
+ return number >> bits;
+ else
+ return (number >> bits) + (2 << ~bits);
+ }
+
+ public static int URShift(int number, long bits)
+ {
+ return URShift(number, (int)bits);
+ }
+
+ public static long URShift(long number, int bits)
+ {
+ if (number >= 0)
+ return number >> bits;
+ else
+ return (number >> bits) + (2L << ~bits);
+ }
+
+ public static long URShift(long number, long bits)
+ {
+ return URShift(number, (int)bits);
+ }
+ }
+
+ internal abstract class GeneralDigest : IDigest
+ {
+ private const int ByteLength = 64;
+ private readonly byte[] XBuf;
+ private int XBufOff;
+ private long ByteCount;
+
+ public abstract string AlgorithmName { get; }
+
+ protected GeneralDigest()
+ {
+ XBuf = new byte[4];
+ }
+
+ protected GeneralDigest(GeneralDigest t)
+ {
+ XBuf = new byte[t.XBuf.Length];
+ Array.Copy(t.XBuf, 0, XBuf, 0, t.XBuf.Length);
+
+ XBufOff = t.XBufOff;
+ ByteCount = t.ByteCount;
+ }
+
+ public void Update(byte input)
+ {
+ XBuf[XBufOff++] = input;
+
+ if (XBufOff == XBuf.Length)
+ {
+ ProcessWord(XBuf, 0);
+ XBufOff = 0;
+ }
+
+ ByteCount++;
+ }
+
+ public void BlockUpdate(byte[] input, int inOff, int length)
+ {
+ //更新当前消息摘要
+ while ((XBufOff != 0) && (length > 0))
+ {
+ Update(input[inOff]);
+ inOff++;
+ length--;
+ }
+
+ //处理完整的消息摘要
+ while (length > XBuf.Length)
+ {
+ ProcessWord(input, inOff);
+
+ inOff += XBuf.Length;
+ length -= XBuf.Length;
+ ByteCount += XBuf.Length;
+ }
+
+ //填充剩余的消息摘要
+ while (length > 0)
+ {
+ Update(input[inOff]);
+
+ inOff++;
+ length--;
+ }
+ }
+
+ public void Finish()
+ {
+ long bitLength = (ByteCount << 3);
+
+ //添加字节
+ Update(unchecked((byte)128));
+
+ while (XBufOff != 0) Update(unchecked((byte)0));
+ ProcessLength(bitLength);
+ ProcessBlock();
+ }
+
+ public virtual void Reset()
+ {
+ ByteCount = 0;
+ XBufOff = 0;
+ Array.Clear(XBuf, 0, XBuf.Length);
+ }
+
+ public int GetByteLength()
+ {
+ return ByteLength;
+ }
+
+ internal abstract void ProcessWord(byte[] input, int inOff);
+
+ internal abstract void ProcessLength(long bitLength);
+
+ internal abstract void ProcessBlock();
+
+ public abstract int GetDigestSize();
+
+ public abstract int DoFinal(byte[] output, int outOff);
+ }
+
+ internal sealed class SM3Digest : GeneralDigest
+ {
+ private const int DIGEST_LENGTH = 32;
+ private const int T_1 = 0x79cc4519;
+ private const int T_2 = 0x7a879d8a;
+
+ private static readonly int[] IV = new int[] { 0x7380166f, 0x4914b2b9, 0x172442d7, unchecked((int)0xda8a0600), unchecked((int)0xa96f30bc), 0x163138aa, unchecked((int)0xe38dee4d), unchecked((int)0xb0fb0e4e) };
+ private static readonly int[] X0 = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
+
+ private readonly int[] _v1 = new int[8];
+ private readonly int[] _v2 = new int[8];
+ private readonly int[] _x = new int[68];
+ private int _xOff;
+
+ public override string AlgorithmName { get { return "SM3"; } }
+
+ public SM3Digest()
+ : base()
+ {
+ Reset();
+ }
+
+ public SM3Digest(SM3Digest t)
+ : base(t)
+ {
+ Array.Copy(t._x, 0, _x, 0, t._x.Length);
+ _xOff = t._xOff;
+ Array.Copy(t._v1, 0, _v1, 0, t._v1.Length);
+ }
+
+ private static void ConvertIntegerToBigEndian(int num, byte[] bytes, int offset)
+ {
+ bytes[offset] = (byte)(BitOperator.URShift(num, 24));
+ bytes[++offset] = (byte)(BitOperator.URShift(num, 16));
+ bytes[++offset] = (byte)(BitOperator.URShift(num, 8));
+ bytes[++offset] = (byte)(num);
+ }
+
+ private static int Rotate(int x, int n)
+ {
+ return (x << n) | (BitOperator.URShift(x, (32 - n)));
+ }
+
+ private static int P0(int x)
+ {
+ return (x) ^ Rotate(x, 9) ^ Rotate(x, 17);
+ }
+
+ private static int P1(int x)
+ {
+ return (x) ^ Rotate(x, 15) ^ Rotate(x, 23);
+ }
+
+ private static int FFOne(int x, int y, int z)
+ {
+ return (x ^ y ^ z);
+ }
+
+ private static int FFSecond(int x, int y, int z)
+ {
+ return ((x & y) | (x & z) | (y & z));
+ }
+
+ private static int GGOne(int x, int y, int z)
+ {
+ return (x ^ y ^ z);
+ }
+
+ private static int GGSecond(int x, int y, int z)
+ {
+ return ((x & y) | (~x & z));
+ }
+
+ public override void Reset()
+ {
+ base.Reset();
+
+ Array.Copy(IV, 0, _v1, 0, IV.Length);
+
+ _xOff = 0;
+ Array.Copy(X0, 0, _x, 0, X0.Length);
+ }
+
+ internal override void ProcessWord(byte[] input, int inOff)
+ {
+ int n = input[inOff] << 24;
+ n |= (input[++inOff] & 0xff) << 16;
+ n |= (input[++inOff] & 0xff) << 8;
+ n |= (input[++inOff] & 0xff);
+ _x[_xOff] = n;
+
+ if (++_xOff == 16)
+ {
+ ProcessBlock();
+ }
+ }
+
+ internal override void ProcessLength(long bitLength)
+ {
+ if (_xOff > 14)
+ {
+ ProcessBlock();
+ }
+
+ _x[14] = (int)(BitOperator.URShift(bitLength, 32));
+ _x[15] = (int)(bitLength & unchecked((int)0xffffffff));
+ }
+
+ internal override void ProcessBlock()
+ {
+ int j;
+
+ int[] ww = _x;
+ int[] ww_ = new int[64];
+
+ for (j = 16; j < 68; j++)
+ {
+ ww[j] = P1(ww[j - 16] ^ ww[j - 9] ^ (Rotate(ww[j - 3], 15))) ^ (Rotate(ww[j - 13], 7)) ^ ww[j - 6];
+ }
+ for (j = 0; j < 64; j++)
+ {
+ ww_[j] = ww[j] ^ ww[j + 4];
+ }
+
+ int[] vv = _v1;
+ int[] vv_ = _v2;
+
+ Array.Copy(vv, 0, vv_, 0, IV.Length);
+
+ int SS1, SS2, TT1, TT2;
+ int aaa;
+
+ for (j = 0; j < 16; j++)
+ {
+ aaa = Rotate(vv_[0], 12);
+ SS1 = aaa + vv_[4] + Rotate(T_1, j);
+ SS1 = Rotate(SS1, 7);
+ SS2 = SS1 ^ aaa;
+
+ TT1 = FFOne(vv_[0], vv_[1], vv_[2]) + vv_[3] + SS2 + ww_[j];
+ TT2 = GGOne(vv_[4], vv_[5], vv_[6]) + vv_[7] + SS1 + ww[j];
+
+ vv_[3] = vv_[2];
+ vv_[2] = Rotate(vv_[1], 9);
+ vv_[1] = vv_[0];
+ vv_[0] = TT1;
+ vv_[7] = vv_[6];
+ vv_[6] = Rotate(vv_[5], 19);
+ vv_[5] = vv_[4];
+ vv_[4] = P0(TT2);
+ }
+
+ for (j = 16; j < 64; j++)
+ {
+ aaa = Rotate(vv_[0], 12);
+ SS1 = aaa + vv_[4] + Rotate(T_2, j);
+ SS1 = Rotate(SS1, 7);
+ SS2 = SS1 ^ aaa;
+
+ TT1 = FFSecond(vv_[0], vv_[1], vv_[2]) + vv_[3] + SS2 + ww_[j];
+ TT2 = GGSecond(vv_[4], vv_[5], vv_[6]) + vv_[7] + SS1 + ww[j];
+
+ vv_[3] = vv_[2];
+ vv_[2] = Rotate(vv_[1], 9);
+ vv_[1] = vv_[0];
+ vv_[0] = TT1;
+ vv_[7] = vv_[6];
+ vv_[6] = Rotate(vv_[5], 19);
+ vv_[5] = vv_[4];
+ vv_[4] = P0(TT2);
+ }
+
+ for (j = 0; j < 8; j++)
+ {
+ vv[j] ^= vv_[j];
+ }
+
+ _xOff = 0;
+ Array.Copy(X0, 0, _x, 0, X0.Length);
+ }
+
+ public override int GetDigestSize()
+ {
+ return DIGEST_LENGTH;
+ }
+
+ public override int DoFinal(byte[] output, int outOff)
+ {
+ Finish();
+ for (int i = 0; i < 8; i++)
+ {
+ ConvertIntegerToBigEndian(_v1[i], output, outOff + i * 4);
+ }
+ Reset();
+ return DIGEST_LENGTH;
+ }
+ }
+
+ ///
+ /// 获取 SM3 哈希值。
+ ///
+ /// 信息字节数组。
+ /// 哈希字节数组。
+ public static byte[] Hash(byte[] bytes)
+ {
+ if (bytes == null) throw new ArgumentNullException(nameof(bytes));
+
+ SM3Digest sm3 = new SM3Digest();
+ sm3.BlockUpdate(bytes, 0, bytes.Length);
+ byte[] hashBytes = new byte[sm3.GetDigestSize()];
+ sm3.DoFinal(hashBytes, 0);
+ return hashBytes;
+ }
+
+ ///
+ /// 获取 SM3 哈希值。
+ ///
+ /// 文本信息。
+ /// 哈希值。
+ public static string Hash(string message)
+ {
+ if (message == null) throw new ArgumentNullException(nameof(message));
+
+ byte[] msgBytes = Encoding.UTF8.GetBytes(message);
+ byte[] hashBytes = Hash(msgBytes);
+ return BitConverter.ToString(hashBytes).Replace("-", string.Empty);
+ }
+ }
+}
diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/TestCase_SM3UtilityTests.cs b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/TestCase_SM3UtilityTests.cs
new file mode 100644
index 00000000..edaf878a
--- /dev/null
+++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/TestCase_SM3UtilityTests.cs
@@ -0,0 +1,18 @@
+using System;
+using Xunit;
+
+namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
+{
+ public class TestCase_SM3UtilityTests
+ {
+ [Fact(DisplayName = "测试用例:计算 SM3 哈希值")]
+ public void TestSM3Hash()
+ {
+ byte[] msgBytes = Convert.FromBase64String("QXdlc29tZSBTS0lULkZsdXJsSHR0cENsaWVudC5XZWNoYXQuVGVucGF5VjMh");
+ string expectedHashText = "3F5EC5A79871755905E34013FD86D906B334281D2CA5B9FF612A136C7C377815";
+ string actualHashText = BitConverter.ToString(Utilities.SM3Utility.Hash(msgBytes)).Replace("-", "");
+
+ Assert.Equal(actualHashText, expectedHashText, ignoreCase: true);
+ }
+ }
+}