crypto.createSign()

Creates a Sign object that hashes data with the given algorithm and signs the digest with a private key.

Signature
crypto.createSign(algorithm[, options])
  • Added in Node.js v0.1.94

Parameters

ParameterTypeDescription
$algorithmstringThe digest algorithm to hash the data with, such as 'sha256'. Use crypto.getHashes() to list the available names.
$optionsoptionalObjectstream.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 Sign object is single use. After sign() returns, both update() and a second sign() throw ERR_CRYPTO_INVALID_STATE. Create a fresh object per signature.
  • Without an outputEncoding you get a Buffer; 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · javascript
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.

Example · javascript
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.

Example · javascript
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

  • Be the first to comment on this lesson.