You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#客户端生成RSA-SHA256签名 Node.js服务端验签失败

问题背景
  • 业务流程初始请求阶段,客户端会收到服务端下发的随机字符串。C#实现的客户端需要针对该字符串生成RSA+SHA256数字签名,回传至基于Node.js(Express、TypeScript技术栈)搭建的API服务端;服务端收到签名后执行校验,校验通过返回true,失败返回false。
  • 服务端已提前存储客户端公钥与对应用户标识符,用于校验数字签名的归属身份。

核心场景

  • 客户端(C#):接收服务端下发的随机字符串,使用RSA+SHA256算法生成该字符串的数字签名,通过API请求回传给服务端。
  • 服务端(Node.js+Express+TypeScript):接收请求中携带的数字签名并执行校验,校验通过返回true,不通过返回false。

问题现象

跨端校验始终无法返回true。本地测试结果如下:

  • C#客户端生成同字符串的签名后自行校验,结果为true
  • 服务端使用相同密钥对生成同字符串的签名后自行校验,结果为true
  • 两端使用相同密钥对、针对相同字符串生成的签名内容不一致
  • 将服务端本地生成的同字符串签名通过客户端发送给服务端,服务端可正常校验通过返回true

注:以下展示的代码已做脱敏调整,部分命名已修改。


客户端(C#)实现代码

请求方法实现

private async Task<string> APIRequestSignature(RSA my_rsa, string randomString)
{
  byte[] randomBytes = Encoding.UTF8.GetBytes(randomString);
  byte[] signature = Crypto.GetDigitalSignatur256(randomBytes, my_rsa);

  MeRequest meRequest = new(identifier, Convert.ToBase64String(signature));
  var request = new HttpRequestMessage
  {
    Method = HttpMethod.Get,
    RequestUri = APIUri.ValidME,
    Content = new StringContent(meRequest.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json),
  };
  var response = await client.SendAsync(request).ConfigureAwait(false);
  response.EnsureSuccessStatusCode();

  string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
  Console.WriteLine(responseBody);
}
internal class MeRequest: Request
{
  private string sign;
  public MeRequest(string identifier, string sign) : base(identifier, "")
  {
    this.sign = sign;
  }
  public string signature
  {
    get
    {
      return this.sign;
    }
  }
}

Crypto工具类

public static byte[] GetDigitalSignatur256(byte[] str, RSA rsa)
{
  byte[] hash = Hash256(str);
  RSAPKCS1SignatureFormatter rsaFormatter = new(rsa);
  rsaFormatter.SetHashAlgorithm("SHA256");
  return rsaFormatter.CreateSignature(hash);
}

public static byte[] Hash256(byte[] arr) =>SHA256.Create().ComputeHash(arr);

服务端(Node.js+Express+TypeScript)实现代码
import {Encryption} from "../class/Encryption";
var randomString = "TESTTESTTESTTEST";

/UserExist 接口(下发随机字符串)

UserRoutes.get('/UserExist', function(req, res) {
    //Create Hash from randomString
    var hash = crypto.createHash("SHA256");
    hash.update(randomString, "base64");

    var randomStringHash = hash.digest("base64");

    //Read Private Key from Client for Test Signature
    var clientPrivateKey = fs.readFileSync(config.server.clientKeyPath + "identlarspk.txt", 'utf8');
    //Create Sign with Hash from RandomString
    var sign = crypto.createSign("SHA256");
    sign.update(randomStringHash, "base64");
    sign.end();
    var signature = sign.sign({
        key: clientPrivateKey,
        padding: crypto.constants.RSA_PKCS1_PADDING
    });
    console.log("Signature Created By Server: " + signature);
    res.json({
        randomString: randomString
    })
});

/UserSigning 接口(校验签名)

UserRoutes.get('/UserSigning', async function(req, res) {
    var encryption = new Encryption();
    //Formating Public key
    var userPKeyDB = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqdKBCUssRLefK9EzzRKxm+ftQ26PLmI5utmmGY6LgwEnKuIrJw/cWA5Fn+2ebJNAgdH9uFBAh8CEtHHjnfMB0SWCl6Qv8R62x8wJs8xNmoTIVLENe5lvHGi2FLVmzLg1LInfwqfsLBLmCM6re5WtQPX4BiXjfDaeYxYCLg3plfaQyYe9/PCzpswC4U4R3yMKzW3ptj4D+L0BGwp4EKFkh2qGJ1Bnm2PkDwott7CT8a8ef36vS7x1fb0XUuw4b5c87ilQZ+mG85aWDK45R7Tl2ZMebZsAe/kpLIRDsG3nnxdzpBelePFCGWSdCCwvFjygOcLyKXQtqJyp3tHvENGTjQIDAQAB";
    var formatedEncryptionKey = await encryption.FormatePKey(userPKeyDB);

    //Create Hash from RandomString
    var hash = crypto.createHash("SHA256");
    hash.update(randomString, "base64");
    var randomStringHash = hash.digest("base64");
    var bufferRandomStringHash = Buffer.from(randomStringHash, "base64");
    var bufferClientSignature = Buffer.from(req.body.signature, "base64");

    var verify = crypto.createVerify("SHA256");
    verify.update(randomStringHash);
    verify.end();
    var isValidated = verify.verify(formatedEncryptionKey, bufferClientSignature);

    res.json({
        token: isValidated
    })
});

公钥格式化方法

public async FormatePKey(pKey: string): Promise<string> {
    var bufferStartKey = Buffer.from("-----BEGIN PUBLIC KEY-----\n", "utf8");
    var bufferStartKeyUTF8 = bufferStartKey.toString("utf8");

    var bufferPubKey = Buffer.from(pKey, "base64");
    var pubKeyBase64 = bufferPubKey.toString("base64");

    var bufferEndKey = Buffer.from("\n-----END PUBLIC KEY-----", "utf8");
    var bufferEndKeyUTF8 = bufferEndKey.toString("utf8");


    var formatedPublicKey = bufferStartKeyUTF8 + pubKeyBase64 + bufferEndKeyUTF8;

    var bufferPubicKey = Buffer.from(formatedPublicKey, "utf8");
    var bufferPublicKeyUTF8 = bufferPubicKey.toString("utf8");

    return bufferPublicKeyUTF8;
}

问题根因与修复方案

跨端校验失败核心是两端签名/校验时的待签名内容编码、哈希处理逻辑不一致,具体问题点和修复如下:

  1. 编码配置错误:服务端所有对randomString的哈希处理都错误指定输入编码为base64,但实际randomString是普通UTF8字符串,和C#端用Encoding.UTF8.GetBytes取字节的逻辑不匹配。
    • 修复:将所有hash.update(randomString, "base64")、sign.update(randomStringHash, "base64")中的编码改为"utf8",和C#端对齐。
  2. 重复哈希错误:C#端的签名逻辑是直接对原始字符串字节做SHA256哈希后签名,但服务端在校验时先对原始字符串做了一次SHA256得到哈希值,又把这个哈希值的base64字符串传入verify.update,相当于做了两次哈希,和C#端的待签名内容完全不一致。
    • 修复:服务端签名和校验时不需要手动提前计算SHA256,Node.js的createSign("SHA256")/createVerify("SHA256")会自动完成哈希计算,直接传入原始字符串的UTF8字节即可,和C#端逻辑对齐。
  3. 请求方法隐患:C#端发签名校验请求用的是HttpMethod.Get,但把签名参数放在了请求Body里,多数HTTP服务端实现、中间件会默认忽略GET请求的Body,会导致服务端拿不到客户端传的signature字段。
    • 修复:将校验接口改为POST方法,客户端对应发POST请求,或者把签名参数放在URL Query中传递。
  4. 公钥格式化冗余:现有FormatePKey方法里多次做Buffer转字符串的无意义操作,容易引入编码问题,直接拼接PEM头尾即可,不需要反复转码。

修复后的服务端校验核心逻辑示例:

UserRoutes.post('/UserSigning', async function(req, res) {
    const userPKeyDB = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqdKBCUssRLefK9EzzRKxm+ftQ26PLmI5utmmGY6LgwEnKuIrJw/cWA5Fn+2ebJNAgdH9uFBAh8CEtHHjnfMB0SWCl6Qv8R62x8wJs8xNmoTIVLENe5lvHGi2FLVmzLg1LInfwqfsLBLmCM6re5WtQPX4BiXjfDaeYxYCLg3plfaQyYe9/PCzpswC4U4R3yMKzW3ptj4D+L0BGwp4EKFkh2qGJ1Bnm2PkDwott7CT8a8ef36vS7x1fb0XUuw4b5c87ilQZ+mG85aWDK45R7Tl2ZMebZsAe/kpLIRDsG3nnxdzpBelePFCGWSdCCwvFjygOcLyKXQtqJyp3tHvENGTjQIDAQAB";
    const formatedEncryptionKey = `-----BEGIN PUBLIC KEY-----\n${userPKeyDB}\n-----END PUBLIC KEY-----`;

    const verify = crypto.createVerify("SHA256");
    verify.update(randomString, "utf8");
    verify.end();
    const bufferClientSignature = Buffer.from(req.body.signature, "base64");
    const isValidated = verify.verify({
        key: formatedEncryptionKey,
        padding: crypto.constants.RSA_PKCS1_PADDING
    }, bufferClientSignature);

    res.json({
        token: isValidated
    })
});

内容的提问来源于stack exchange,提问作者Larisio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 12:45:35