crypto.createSign()
Creates a Sign object that hashes data with the given algorithm and signs the digest with a private key.
crypto.createSign(algorithm[, options])- Added in Node.js v0.1.94
Parameters
| Parameter | Type | Description |
|---|---|---|
$algorithm | string | The digest algorithm to hash the data with, such as 'sha256'. Use crypto.getHashes() to list the available names. |
$optionsoptional | Object | stream.Writable options, controlling the stream behaviour of the returned Sign object. |
Return value
SignA `Sign` object: a writable stream that accumulates data and produces a signature when `sign.sign()` is called.
Errors
sign.sign() throws ERR_CRYPTO_INVALID_STATE if called twice on the same object, and ERR_CRYPTO_UNSUPPORTED_OPERATION for Ed25519 and Ed448 keys, which must be signed with crypto.sign() instead.
Changelog
- PHP v6.0.0The default inputEncoding for sign.update() changed from 'binary' to 'utf8'.
- PHP v11.6.0sign.sign() accepts a KeyObject, not only PEM strings and Buffers.
createSign() builds a hash-then-sign pipeline. The algorithm you pass is the digest, not the signature scheme — the scheme comes from the key you hand to sign(). Pass an RSA key and you get RSASSA-PKCS1-v1_5 over SHA-256; pass an EC key and you get ECDSA over the same digest. That is why 'sha256' is correct and 'RS256' is not.
Two ways to feed it
Call update() once or many times and then sign(), or treat the object as a writable stream and pipe a file into it. The stream form is the reason to use createSign() over the one-shot crypto.sign(): it signs a large file without loading it into memory.
What trips people up
- A
Signobject is single use. Aftersign()returns, bothupdate()and a secondsign()throwERR_CRYPTO_INVALID_STATE. Create a fresh object per signature. - Without an
outputEncodingyou get aBuffer; pass'base64'or'hex'for text. - An encrypted private key needs the object form:
{ key, passphrase }. - RSA-PSS is not the default. Pass
{ key, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength }and use the same values when verifying.
For signing short strings with modern keys, prefer crypto.sign().
Example
When to use it
- Sign an outgoing webhook body so the receiver can prove it came from you.
- Produce a detached signature over a release tarball by streaming the file into the Sign object.
- Issue a JWT-style token by signing the base64url header and payload with an RSA or EC key.
- Sign a licence or entitlement blob that a client verifies offline with a pinned public key.
Frequently asked questions
How do I sign a string with a private key in Node.js?
Call crypto.createSign('sha256'), pass the string to update(), then call sign(privateKey, 'base64'). The algorithm name is the digest; the signature scheme (RSA or ECDSA) is decided by the key you supply.
Why does crypto.createSign throw ERR_CRYPTO_INVALID_STATE?
Because the Sign object has already produced a signature and cannot be reused. Every call to sign() finalises the object, so create a new one with crypto.createSign() for each signature instead of caching it.
Can crypto.createSign use an Ed25519 key?
No — it throws ERR_CRYPTO_UNSUPPORTED_OPERATION. EdDSA signs the whole message rather than a precomputed digest, so use the one-shot crypto.sign(null, data, privateKey) for Ed25519 and Ed448 keys.
More examples
Stream a file into the signer
pipeline() writes the file through the Sign object in chunks and ends the stream, after which sign() produces the signature over everything that was written. The output is true, showing that the streamed digest matches the one the verifier computed the same way — this is how you sign an artefact too large to read into a Buffer.
import { generateKeyPairSync, createSign, createVerify } from 'node:crypto';
import { pipeline } from 'node:stream/promises';
import fs from 'node:fs';
const { publicKey, privateKey } = generateKeyPairSync('ec', {
namedCurve: 'prime256v1',
});
fs.writeFileSync('release.tar', 'pretend this is a 2 GB archive\n');
// Sign is a writable stream, so the file never sits in memory.
const signer = createSign('sha256');
await pipeline(fs.createReadStream('release.tar'), signer);
const signature = signer.sign(privateKey);
fs.writeFileSync('release.tar.sig', signature);
const verifier = createVerify('sha256');
await pipeline(fs.createReadStream('release.tar'), verifier);
console.log('detached signature valid:',
verifier.verify(publicKey, fs.readFileSync('release.tar.sig')));
fs.unlinkSync('release.tar');
fs.unlinkSync('release.tar.sig');RSA-PSS padding and an encrypted key
The first check prints true and the second prints false. Padding is not carried inside the signature, so a verifier that omits RSA_PKCS1_PSS_PADDING simply returns false rather than raising an error — the single most common cause of a "valid signature that does not verify". The passphrase field is what lets an encrypted PKCS#8 key be used at all.
const { generateKeyPairSync, createSign, createVerify, constants } = require('node:crypto');
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: {
type: 'pkcs8', format: 'pem',
cipher: 'aes-256-cbc', passphrase: 's3cret',
},
});
const opts = {
padding: constants.RSA_PKCS1_PSS_PADDING,
saltLength: 32,
};
const signature = createSign('sha256').update('invoice-88').sign(
{ key: privateKey, passphrase: 's3cret', ...opts },
'base64',
);
console.log('PSS verify:',
createVerify('sha256').update('invoice-88')
.verify({ key: publicKey, ...opts }, signature, 'base64'));
// Verifying without the same padding fails silently.
console.log('default padding verify:',
createVerify('sha256').update('invoice-88')
.verify(publicKey, signature, 'base64'));Sign a JWT-style token
The output shows that the ECDSA signature Node produces is DER-encoded and between 70 and 72 bytes, not the fixed 64 bytes the JOSE spec requires for ES256. Anything speaking JWT must convert that DER structure to a raw r||s concatenation first; forgetting this step is why hand-rolled ES256 tokens are rejected by standards-compliant verifiers.
const { generateKeyPairSync, createSign } = require('node:crypto');
const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
const b64url = (obj) =>
Buffer.from(JSON.stringify(obj)).toString('base64url');
const header = b64url({ alg: 'ES256', typ: 'JWT' });
const claims = b64url({ sub: 'user-7', exp: 1893456000 });
const signingInput = `${header}.${claims}`;
// ES256 wants a raw r||s pair, not the DER blob createSign returns.
const der = createSign('sha256').update(signingInput).sign(privateKey);
console.log('DER signature, variable length:', der.length >= 70 && der.length <= 72);
console.log('token prefix:', signingInput.slice(0, 36) + '...');
Discussion