PHP替代C#客户端实现PDF签名:哈希签名一致性问题排查
解决PHP生成与C#一致的RSA SHA1签名问题
现有一套C#实现的PDF签名系统:
- 服务端预处理PDF待签名区域,生成SHA1哈希并Base64编码后传给客户端
- C#客户端通过
RSACryptoServiceProvider.SignHash方法完成签名,与服务端交互正常 - 改用PHP实现客户端签名逻辑时,使用
openssl_sign生成的签名与C#结果不一致,导致后续PDF签名失败
相关代码
C#服务端代码
Stream data = appearance.GetRangeStream(); byte[] hashByte = DigestAlgorithms.Digest(data, "SHA1"); string hash = Convert.ToBase64String(hashByte);
C#客户端签名代码
public static string SignHash(string hash, X509Certificate2 cert) { RSACryptoServiceProvider rsaCert = cert.PrivateKey as RSACryptoServiceProvider; byte[] hashSigned = rsaCert.SignHash(Convert.FromBase64String(hash), CryptoConfig.MapNameToOID("SHA1")); return Convert.ToBase64String(hashSigned); }
当前PHP签名代码
openssl_sign(base64_decode($hash), $sign, $private_key(), OPENSSL_ALGO_SHA1);
问题原因
C#的RSACryptoServiceProvider.SignHash方法并非直接对传入的哈希字节签名,而是遵循PKCS#1 v1.5标准,先将哈希值封装为ASN.1格式的DigestInfo结构,再进行RSA签名。而PHP的openssl_sign在传入原始哈希字节并指定OPENSSL_ALGO_SHA1时,会错误地将传入的字节视为原始数据,重新计算SHA1哈希后再签名,这就导致了签名结果的差异。
SHA1对应的DigestInfo结构DER编码前缀为固定字节:3021300906052b0e03021a05000414,拼接上20字节的SHA1哈希值后,才是C#实际签名的字节流。
解决方案
修改PHP代码,手动构造符合要求的DigestInfo字节流,再进行签名:
// 1. 解码服务端传来的Base64哈希字符串,得到原始SHA1哈希字节 $hashBytes = base64_decode($hash); // 2. 构造SHA1的DigestInfo结构(固定前缀 + 哈希字节) $digestInfoPrefix = hex2bin('3021300906052b0e03021a05000414'); $dataToSign = $digestInfoPrefix . $hashBytes; // 3. 加载私钥(替换为你的私钥路径或内容) $privateKey = openssl_pkey_get_private(file_get_contents('your_private_key.pem')); // 4. 执行RSA PKCS#1 v1.5签名 openssl_sign($dataToSign, $sign, $privateKey, OPENSSL_ALGO_SHA1); // 5. 将签名结果Base64编码,与C#返回格式一致 $signedHash = base64_encode($sign);
额外注意事项
- 确保PHP加载的私钥与C#客户端使用的证书私钥完全一致,私钥需为PEM格式(可从PFX证书中提取)
- 若C#中使用了非默认的签名填充方式(如OAEP),需调整PHP的签名参数(对应改为
OPENSSL_ALGO_RSA_SHA1_OAEP等),默认情况下RSACryptoServiceProvider使用PKCS#1 v1.5填充 - 服务端验证签名时,需使用对应公钥验证PHP生成的签名,逻辑与验证C#签名一致
内容的提问来源于stack exchange,提问作者Elvis Reis
相关产品推荐
相关产品推荐

