privateEncrypt()
says "..., the padding property can be passed. Otherwise, this function uses RSA_PKCS1_PADDING." sign()
says "padding Optional padding value for RSA, one of the following:
crypto.constants.RSA_PKCS1_PADDING (default)".
So naive expectation would be that both returns the same buffer
as the padding scheme and hash function used are identical. (I suppose that that privateEncrypt()
uses the signing variant of the scheme, when publicEncrypt()
uses encryption variant; please, count this as part of the question, as I could find this one in docs, and sunk mapping OpenSSL manuals to node:crypto
, your expertise is helpful!)
But they don't. Maybe I read the docs incorrectly, maybe it's common knowledge I'm missing, or maybe it's something else. Please explain the differences between them in this sense, or correct the snippet so it would be visually clear.
// this snippet is solely for discussion purpose and shouldn't be used in production
import {
generateKeyPairSync, createSign, privateEncrypt, createVerify, createHash
} from "node:crypto";
const keyPair = generateKeyPairSync("rsa", {modulusLength: 1024});
const encrypted = privateEncrypt(
keyPair.privateKey,
createHash('sha256').update("stack overflow").digest()
);
// console.debug(encrypted);
const signed = createSign("SHA256").update("stack overflow").end().sign(keyPair.privateKey);
// console.debug(signed);
// console.debug(createVerify("SHA256").update("stack overflow").end().verify(
// keyPair.publicKey, signed
// )); // "true"
console.assert(!Buffer.compare(encrypted, signed)); // "Assertion failed"
Sign#sign()
is used for signing and applies the RSASSA-PKCS1-v1_5 variant for PKCS#1v1.5 padding.
RSASSA-PKCS1-v1_5 is described in RFC 8017, Section 8.2. This uses the EMSA-PKCS1-v1_5 encoding described in Section 9.2, which pads the message as follows:
EM = 0x00 || 0x01 || PS || 0x00 || T
where T is the DER encoding of the DigstInfo value (containing the digest OID and the message hash) and PS is a sequence of as many 0xFF bytes until the length of the key/modulus is reached.
While sign()
implicitly determines T from the message, crypto.privateEncrypt()
uses the message directly as T. Thus, for privateEncrypt()
to work like sign()
, T must be explicitly determined from the message and passed instead of the message. Since the posted code already hashes, only the digest-specific byte sequence containing the OID (see RFC8017, p. 47) needs to be prepended.
const digestInfo = Buffer.from('3031300d060960864801650304020105000420','hex');
const dataHashed = crypto.createHash('sha256').update("stack overflow").digest();
const dataToSign = Buffer.concat([digestInfo, dataHashed]);
const encrypted = crypto.privateEncrypt(keyPair.privateKey, dataToSign);
Because RSASSA-PKCS1-v1_5 is deterministic, both approaches provide the same signature (for an identical key).
Why are there both methods?
Sign#sign()
is used for regular signing, crypto.privateEncrypt()
is meant for a low level signig, e.g. when not the message itself but only the hashed data is available.
Since v12.0.0 there is also crypto.sign()
which performs a high level signing like Sign#sign()
. This new variant also supports newer algorithms like Ed25519:
const signedv12 = crypto.sign('sha256', 'stack overflow', keyPair.privateKey);
In contrast, crypto.publicEncrypt()
performs an encryption and uses the RSAES-PKCS1-v1_5 variant for PKCS#1v1.5 padding.
Note that 1024 bit keys are insecure nowadays. The key size should be at least 2048 bits.