I have the following code that signs some data in a .js script:
const { RSA_PKCS1_PSS_PADDING } = require('constants');
const crypto = require('crypto');
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
},
});
const fs = require('fs');
const keys = fs.createWriteStream('keys.txt');
keys.write(`${publicKey}\n`);
keys.write(`${privateKey}\n`);
function signature(verifyData) {
return crypto.createSign('sha256').sign({
keyLike: Buffer.from(verifyData),
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
}).toString('base64');
}
The script will create a txt file with my public and private keys, such as follows:
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
I tried several ways to generate the same hash as the .js script for the same input and no success. It also cannot verify any hashes created by the .js script. Below are my implementations:
private RsaKeyParameters readPrivateKey(string privateKeyFileName)
{
RsaKeyParameters keyPair;
using (var reader = File.OpenText(privateKeyFileName))
keyPair = (RsaKeyParameters)new PemReader(reader).ReadObject();
return keyPair;
}
bool VerifyDataBouncyCastle(string bodyData, string signature)
{
var data = bodyData;
var signatureBytes = Convert.FromBase64String(signature);
var signer = SignerUtilities.GetSigner("SHA256WITHRSA");
signer.Init(false, readPrivateKey($"{DiretorioBase}\\public.txt"));
signer.BlockUpdate(Encoding.UTF8.GetBytes(data), 0, data.Length);
var success = signer.VerifySignature(signatureBytes);
return success;
}
string SignDataBouncyCastle(string data)
{
// Verify using the public key
var signer = SignerUtilities.GetSigner("SHA256WITHRSA");
signer.Init(true, readPrivateKey($"{DiretorioBase}\\private.txt"));
signer.BlockUpdate(Encoding.UTF8.GetBytes(data), 0, data.Length);
return Convert.ToBase64String(signer.GenerateSignature());
}
public byte[] SignDataNetCore(byte[] data)
{
// privateKey does not have the ---BEGIN and ---END headers.
var privateKey = File.ReadAllText($"{DiretorioBase}\\private.txt");
var rsaPrivateKey = RSA.Create();
rsaPrivateKey.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKey), out _);
return rsaPrivateKey.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
public bool VerifyDataNetCore(byte[] data, byte[] signature)
{
var publicKey = File.ReadAllText($"{DiretorioBase}\\public.txt");
var rsaPublicKey = RSA.Create();
rsaPublicKey.ImportFromPem(publicKey);
return rsaPublicKey.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
None of the above methods will produce the same signature using the same input and same key generated by the .js script. What am I missing?
--Edit--
I changed the .js signature method like this:
function signature(verifyData) {
var cSign = crypto.createSign('sha256');
cSign.update(verifyData);
return cSign.sign({
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
});
}
And the C# verified code to this:
bool isVerified()
{
string x509Pem = @"-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----";
byte[] message = Encoding.UTF8.GetBytes(validar);
byte[] signature = Convert.FromBase64String(hash64);
PemReader pr = new PemReader(new StringReader(x509Pem));
AsymmetricKeyParameter publicKey = (AsymmetricKeyParameter)pr.ReadObject();
RSAParameters rsaParams = DotNetUtilities.ToRSAParameters((RsaKeyParameters)publicKey);
RSACng rsaCng = new RSACng();
rsaCng.ImportParameters(rsaParams);
bool verified = rsaCng.VerifyData(message, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
return verified;
}
It still returns false.
CodePudding user response:
PSS has a number of parameters, including the salt length. RFC8017, A.2.3. RSASSA-PSS defines a default salt length that corresponds to the output length of the digest, i.e. 32 bytes for SHA256.
Your recent C# code applies the C# built-in classes that use this default salt length. A different salt length cannot be specified!
The NodeJS code, on the other hand, defaults to the maximum possible salt length (crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN
), which is given by:
<keysize> - <digest output length> - 2 = 256 - 32 - 2 = 222
.
Thus, the two codes are incompatible!
Unlike the C# built-in classes, BouncyCastle allows the salt length to be configured:
string x509Pem = @"-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----";
string validar = "...";
string hash64 = "...";
byte[] message = Encoding.UTF8.GetBytes(validar);
byte[] signature = Convert.FromBase64String(hash64);
PemReader pr = new PemReader(new StringReader(x509Pem));
AsymmetricKeyParameter publicKey = (AsymmetricKeyParameter)pr.ReadObject();
PssSigner pssSigner = new PssSigner(new RsaEngine(), new Sha256Digest(), 256 - 32 - 2);
pssSigner.Init(false, publicKey);
pssSigner.BlockUpdate(message, 0, message.Length);
bool valid = pssSigner.VerifySignature(signature); // succeeds when the maximum possible salt length is used
With this, verification is successful.
Note that in the NodeJS code you can explicitly change the salt length to the output length of the digest (crypto.constants.RSA_PSS_SALTLEN_DIGEST
). Then verification will also work with the built-in C# classes.