Public-key cryptography

Sign and verify data

Use digital signatures to bind exact bytes to a private-key holder and verify identity, context, format, and freshness before trusting the result.

A digital signature is created with a private key and verified with the matching public key. It proves more than a checksum, but less than “this action is safe”.

Unlike an HMAC, verification needs no secret. Anyone with the public key can check the signature, and nobody without the private key can forge one. That asymmetry gives you non-repudiation. The signer cannot claim a verifier forged the message, because verifiers never had the ability to sign.

Ed25519 is the common modern choice, and Node supports it directly. Let’s sign a release manifest:

import { generateKeyPairSync, sign, verify } from 'node:crypto'

const { publicKey, privateKey } = generateKeyPairSync('ed25519')

const manifest = Buffer.from(JSON.stringify({
  project: 'shipd', version: '2.4.1',
  artifact: 'sha256:7c2df1a9...', purpose: 'release', ts: 1754236800
}))

const signature = sign(null, manifest, privateKey)
verify(null, manifest, publicKey, signature) // true

Change one byte of the manifest and verify returns false. The null first argument tells Node to use the algorithm’s built-in hashing, which is what Ed25519 expects.

Sign exact bytes, with context

Signatures cover bytes, not meaning. Decide on one exact byte representation and verify the bytes you received, before any parsing or re-serialization. JSON with unstable key order is the classic way to sign one thing and verify another.

Put the purpose and the version inside the signed payload. A signature over bare data can be replayed in a different context. A signature over purpose: 'release' cannot be presented as a login token, because the verifier checks the purpose field too.

A valid signature is not approval

Verify with the public key you expected. Never with a key that arrived alongside the message. Then keep going, because the policy questions are still open.

Say a deployment manifest carries a valid signature but names an old artifact with a known vulnerability. The signature proves who signed those bytes. It does not prove the release is current or approved right now. This is a real attack on update systems, called a rollback attack: the attacker replays your own legitimately signed old release.

So a valid signature from an unexpected signer, or over an old message, should still be rejected. Your verification code has to check the signer, the project, the freshness, and whether this action is allowed. The cryptography answers one question: who signed these exact bytes. Everything after that is your application’s decision.

I think of it as two gates. The first gate is math and it is binary. The second gate is policy and it is where most of the bugs live.

Try this on your own: define the exact signed bytes for a release manifest with project, version, artifact digest, purpose, and timestamp. Verify a current manifest with the expected public key. Then replay an old valid manifest and make sure a freshness or release-policy check rejects it, even though the signature is fine.

Lesson completed