Browse Source

修复 ECDSA 验签时写死缓冲区长度的问题,改用实际数组长度,避免异常输入导致越界读取。

HerobrineXia 2 days ago
parent
commit
a7f1fc17ca
2 changed files with 41 additions and 3 deletions
  1. 5 3
      Shared/Utils/CryptographyUtils.cs
  2. 36 0
      Tests/Utils/CryptographyUtilsTest.cs

+ 5 - 3
Shared/Utils/CryptographyUtils.cs

@@ -138,20 +138,22 @@ public static class CryptographyUtils {
 
     /// <summary>
     /// 使用 ECDSA 算法,验证一个字符串的签名是否使用私钥生成。
-    /// 如果验证失败则抛出 <see cref="CryptographicException"/>。
+    /// 如果验证失败则抛出 <see cref="SecurityException"/>。
     /// </summary>
     public static void EcdsaVerify(string sourceString, string sign, string publicKey) {
         using var sha256 = GetHashAlgorithm(HashMethod.Sha256);
         byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(sourceString));
+        byte[] publicKeyBytes = Convert.FromBase64String(publicKey);
+        byte[] signature = Convert.FromBase64String(sign);
         IntPtr algorithmHandle = IntPtr.Zero;
         IntPtr keyHandle = IntPtr.Zero;
         // 直接调用 DLL,以避免 .NET API 依赖的系统服务存在问题
         try {
             int status = BCryptOpenAlgorithmProvider(out algorithmHandle, "ECDSA_P256", null, 0);
             if (status < 0) throw new CryptographicException($"{nameof(BCryptOpenAlgorithmProvider)} 失败,错误码 {status}");
-            status = BCryptImportKeyPair(algorithmHandle, IntPtr.Zero, "ECCPUBLICBLOB", out keyHandle, Convert.FromBase64String(publicKey), 72, 0);
+            status = BCryptImportKeyPair(algorithmHandle, IntPtr.Zero, "ECCPUBLICBLOB", out keyHandle, publicKeyBytes, publicKeyBytes.Length, 0);
             if (status < 0) throw new CryptographicException($"{nameof(BCryptImportKeyPair)} 失败,错误码 {status}");
-            status = BCryptVerifySignature(keyHandle, IntPtr.Zero, hash, hash.Length, Convert.FromBase64String(sign), 64, 0);
+            status = BCryptVerifySignature(keyHandle, IntPtr.Zero, hash, hash.Length, signature, signature.Length, 0);
             if (status == unchecked((int) 0xC000A000)) throw new SecurityException("签名验证失败");
             if (status < 0) throw new CryptographicException($"{nameof(BCryptVerifySignature)} 失败,错误码 {status}");
         } finally {

+ 36 - 0
Tests/Utils/CryptographyUtilsTest.cs

@@ -1,3 +1,4 @@
+using System.Security;
 using System.Security.Cryptography;
 
 namespace MeloongCore.Tests;
@@ -45,4 +46,39 @@ public class CryptographyUtilsTest : TestBase {
 
     #endregion
 
+    #region ECDSA 签名
+
+    [Test]
+    public async Task ECDSA_字符串与标准API互通() {
+        using CngKey key = CngKey.Create(CngAlgorithm.ECDsaP256, null, new CngKeyCreationParameters {ExportPolicy = CngExportPolicies.AllowPlaintextExport});
+        string publicKey = Convert.ToBase64String(key.Export(CngKeyBlobFormat.EccPublicBlob));
+        string privateKey = Convert.ToBase64String(key.Export(CngKeyBlobFormat.EccPrivateBlob));
+        const string text = "土豆码 / 配置签名 中文\r\n";
+        byte[] data = Encoding.UTF8.GetBytes(text);
+        string signature = CryptographyUtils.EcdsaSign(text, privateKey);
+        using ECDsaCng ecdsa = new(key) {HashAlgorithm = CngAlgorithm.Sha256};
+        await Assert.That(Convert.FromBase64String(signature).Length).IsEqualTo(64);
+        await Assert.That(ecdsa.VerifyData(data, Convert.FromBase64String(signature))).IsTrue();
+        CryptographyUtils.EcdsaVerify(text, Convert.ToBase64String(ecdsa.SignData(data)), publicKey);
+        Assert.Throws<SecurityException>(() => CryptographyUtils.EcdsaVerify(text.Replace("\r\n", "\n"), signature, publicKey));
+    }
+
+    [Test]
+    [Arguments(0)]
+    [Arguments(63)]
+    [Arguments(65)]
+    public async Task ECDSA_错误签名长度被拒绝(int length) {
+        using CngKey key = CngKey.Create(CngAlgorithm.ECDsaP256);
+        string publicKey = Convert.ToBase64String(key.Export(CngKeyBlobFormat.EccPublicBlob));
+        Exception? error = null;
+        try {
+            CryptographyUtils.EcdsaVerify("test", Convert.ToBase64String(new byte[length]), publicKey);
+        } catch (Exception ex) {
+            error = ex;
+        }
+        await Assert.That(error).IsNotNull();
+    }
+
+    #endregion
+
 }