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; }
问题根因与修复方案
跨端校验失败核心是两端签名/校验时的待签名内容编码、哈希处理逻辑不一致,具体问题点和修复如下:
- 编码配置错误:服务端所有对
randomString的哈希处理都错误指定输入编码为base64,但实际randomString是普通UTF8字符串,和C#端用Encoding.UTF8.GetBytes取字节的逻辑不匹配。- 修复:将所有
hash.update(randomString, "base64")、sign.update(randomStringHash, "base64")中的编码改为"utf8",和C#端对齐。
- 修复:将所有
- 重复哈希错误:C#端的签名逻辑是直接对原始字符串字节做SHA256哈希后签名,但服务端在校验时先对原始字符串做了一次SHA256得到哈希值,又把这个哈希值的base64字符串传入
verify.update,相当于做了两次哈希,和C#端的待签名内容完全不一致。- 修复:服务端签名和校验时不需要手动提前计算SHA256,Node.js的
createSign("SHA256")/createVerify("SHA256")会自动完成哈希计算,直接传入原始字符串的UTF8字节即可,和C#端逻辑对齐。
- 修复:服务端签名和校验时不需要手动提前计算SHA256,Node.js的
- 请求方法隐患:C#端发签名校验请求用的是
HttpMethod.Get,但把签名参数放在了请求Body里,多数HTTP服务端实现、中间件会默认忽略GET请求的Body,会导致服务端拿不到客户端传的signature字段。- 修复:将校验接口改为POST方法,客户端对应发POST请求,或者把签名参数放在URL Query中传递。
- 公钥格式化冗余:现有
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
相关产品推荐
相关产品推荐

