Skip to main content

Payload Signing

Stem can sign every task envelope so workers can detect tampering or untrusted publishers. This guide focuses on newcomers: how signing works, how to wire it into your app, and where to look when something fails.

Why sign envelopes?

Signing lets workers verify that the envelope payload (args, headers, metadata, and timing fields) is unchanged between the producer and the broker. When signing is enabled on workers, any envelope missing a signature or carrying an invalid signature is rejected and moved to the DLQ with a signature-invalid reason.

How signing works in Stem

  • Producers create a PayloadSigner from environment-derived config and pass it into Stem to sign new envelopes.
  • Workers create the same signer (or verification-only config) and pass it into Worker to verify each delivery.
  • Schedulers/Beat that enqueue tasks should also sign.
  • Signatures are stored in envelope headers: stem-signature and stem-signature-key.

Signing is opt-in: if no signing keys are configured, envelopes are sent and accepted unsigned.

Quick start (HMAC)

  1. Generate a shared secret and export signing variables on every producer and worker (and any scheduler that enqueues tasks):
export STEM_SIGNING_ALGORITHM=hmac-sha256
export STEM_SIGNING_KEYS="v1:$(openssl rand -base64 32)"
export STEM_SIGNING_ACTIVE_KEY=v1
  1. Wire the signer into producers, workers, and schedulers.

These snippets come from the example/microservice project so you can see the full context.

Load signing config once at startup:

example/microservice/enqueuer/bin/main.dart
  final config = StemConfig.fromEnvironment();

Create a signer from that config:

example/microservice/enqueuer/bin/main.dart
  final signer = PayloadSigner.maybe(config.signing);

Attach the signer to the producer so envelopes are signed:

example/microservice/enqueuer/bin/main.dart
  final stem = Stem(
broker: broker,
registry: registry,
backend: backend,
signer: signer,
);

Ed25519 (asymmetric signing)

Ed25519 keeps private keys only on producers while workers verify with public keys.

  1. Generate keys and export the values:
dart run scripts/security/generate_ed25519_keys.dart
  1. Set variables on producers, workers, and schedulers:
export STEM_SIGNING_ALGORITHM=ed25519
export STEM_SIGNING_PUBLIC_KEYS=primary:<base64-public>
export STEM_SIGNING_PRIVATE_KEYS=primary:<base64-private>
export STEM_SIGNING_ACTIVE_KEY=primary
  1. For workers, you may omit STEM_SIGNING_PRIVATE_KEYS if you only want to verify signatures.

Key rotation (safe overlap)

  1. Add the new key alongside the old one in your key list.
  2. Update STEM_SIGNING_ACTIVE_KEY on producers first.
  3. Roll workers (they accept all configured keys).
  4. Remove the old key after the backlog drains.

Example: producer logging the active key and enqueuing during rotation (from example/signing_key_rotation):

example/signing_key_rotation/bin/producer.dart
  final keyId = config.signing.activeKeyId ??
Platform.environment['STEM_SIGNING_ACTIVE_KEY'] ??
'unknown';
stdout.writeln(
'[producer] broker=${config.brokerUrl} backend=$backendUrl '
'signing=${config.signing.isEnabled ? 'on' : 'off'} '
'activeKey=$keyId tasks=$taskCount',
);

Reference: signing environment variables

VariablePurposeNotes
STEM_SIGNING_ALGORITHMhmac-sha256 (default) or ed25519Defaults to HMAC.
STEM_SIGNING_KEYSHMAC secrets (keyId:base64)Comma-separated list. Required for HMAC.
STEM_SIGNING_ACTIVE_KEYKey id used for new signaturesRequired when signing.
STEM_SIGNING_PUBLIC_KEYSEd25519 public keys (keyId:base64)Comma-separated list. Required for Ed25519.
STEM_SIGNING_PRIVATE_KEYSEd25519 private keys (keyId:base64)Only needed by signers.

Failure behavior & troubleshooting

  • Missing or invalid signatures are dead-lettered with reason signature-invalid and increment the stem.tasks.signature_invalid metric.
  • If you see signature-invalid in the DLQ, confirm all producers are signing and that workers have the same key set.
  • If the active key id is not present in the key list, producers will fail fast when trying to sign.

Next steps