RSASSA-PSS in Node.js
This post is about encoding schemes for RSA signatures. The Public-Key Cryptography Standards (PKCS) #1 (RFC 3447) specifies two schemes:
Basically, PSS is the newer scheme and can optionally include some randomness in the encoding (though it doesn't rely on it). There are no known attacks against PKCS1-v1_5 but you have to be more careful when using it. PKCS#1 recommends PSS in new applications:
Although no attacks are known against RSASSA-PKCS1-v1_5, in the interest of increased robustness, RSASSA-PSS is recommended for eventual adoption in new applications. RSASSA-PKCS1-v1_5 is included for compatibility with existing applications, and while still appropriate for new applications, a gradual transition to RSASSA-PSS is encouraged.
I needed RSA signing for a Node.js project and it seemed sensible to use RSASSA-PSS since I control both ends of the communication path.
A nice module for doing RSA crypto in Node.js is ursa. It wraps OpenSSL, which is usually a good thing because you don't have to do it yourself.
ursa hardcodes the encoding type for signatures to PKCS1-v1_5 so I had to modify it to allow applications to choose PSS. Kris Brown has already done a good job of modifying ursa so applications can choose the encoding type for encryption. I forked his version of ursa and did the same for RSA signing.
Unfortunately, it wasn't as simple as I first thought. OpenSSL's RSA signing function is RSA_private_encrypt and its signature verification function is RSA_public_decrypt. They both take a padding (encoding) type as their last argument:
int RSA_private_encrypt(int flen, unsigned char *from, unsigned char *to, RSA *rsa, int padding); int RSA_public_decrypt(int flen, unsigned char *from, unsigned char *to, RSA *rsa, int padding);
So surely we just specify PSS as the padding argument we pass in? The answer is no! OpenSSL implements PSS in separate functions, RSA_padding_add_PKCS1_PSS and RSA_verify_PKCS1_PSS:
int RSA_padding_add_PKCS1_PSS(RSA *rsa, unsigned char *EM, const unsigned char *mHash, const EVP_MD *Hash, int sLen) int RSA_verify_PKCS1_PSS(RSA *rsa, const unsigned char *mHash, const EVP_MD *Hash, const unsigned char *EM, int sLen)
This is because PSS involves hashing some data and so requires us to pass the hash algorithm you want it to use (Hash). RSA is very slow so you usually only sign a digest of the data (mHash here) rather than the data itself. Usually you specify the same hash algorithm for Hash that you used to generate mHash.
Signing data using PSS is then done like this:
- Hash the data.
- Call RSA_padding_add_PKCS1_PSS to encode (pad) the digest.
- Call RSA_private_encrypt with the padded digest and specify RSA_NO_PADDING as the padding argument to ensure no more padding is done.
Verifying a signature against some data using PSS is done like this:
- Call RSA_public_decrypt with the signature to retrieve the padded digest.
- Hash the data.
- Call RSA_verify_PKCS1_PSS with the digest and the padded digest we retrieved from the signature. It returns whether the former after padding matches the latter.
I did this in the existing hashAndSign and hashAndVerify functions of the ursa module. I also exported a constant, RSA_PKCS1_PSS_PADDING, to specify use of PSS encoding with these functions.
For example, to sign some data using an RSA key you might do something like this:
signature = key.hashAndSign('sha256', data, 'base64', 'base64', ursa.RSA_PKCS1_PSS_PADDING);
and to verify the signature:
key.hashAndVerify('sha256', data, signature, 'base64', ursa.RSA_PKCS1_PSS_PADDING)
I'm assuming key here is an RSA key read by ursa and the data and signature are both encoded in Base64.
blog comments powered by Disqus