Sign, encrypt and verify mail with OpenSSL S/MIME
You will create a MIME message signed with an X.509 certificate, encrypt it for a recipient, then decrypt and verify it with the matching private keys. The workflow uses the installed openssl smime command from OpenSSL 3.6.1. Allow 20 to 30 minutes if the certificates already exist. Certificate issuance and trust design take longer and are outside this guide.
The route
Jump straight to the step you need, or tick off Done means at the end.
You need OpenSSL, a readable input message, the sender's certificate and private key, and the recipient's certificate and private key. The examples use obvious placeholder paths. Replace them with real files, but do not paste private keys into a shared shell history or commit them to a repository.
1. Check the installed command
This is a normal, read-only check and does not need elevated privileges:
$ command -v openssl
/usr/bin/openssl
$ openssl version
OpenSSL 3.6.1 27 Jan 2026
The command is invoked as openssl smime. It handles S/MIME messages for signing, encryption, decryption and verification. The installed manual page is dated 18 August 2026 and describes this OpenSSL 3.6.1 build. Options and defaults can differ on an older system, so check the local manual before copying this workflow to another host.
Checkpoint
Confirm that openssl smime -help runs, and keep the version output with any troubleshooting notes.
2. Prepare a MIME-friendly message and key files
Create a small plain-text message in a working directory that is not shared with other users:
$ umask 077
$ printf '%s\n' 'The deployment completed successfully.' > message.txt
$ test -r message.txt && echo 'input is readable'
input is readable
For a real run, set these paths to your existing files:
$ SENDER_CERT=/path/to/sender-cert.pem
$ SENDER_KEY=/path/to/sender-key.pem
$ RECIPIENT_CERT=/path/to/recipient-cert.pem
$ RECIPIENT_KEY=/path/to/recipient-key.pem
$ for f in "$SENDER_CERT" "$SENDER_KEY" "$RECIPIENT_CERT" "$RECIPIENT_KEY"; do test -r "$f" || { echo "cannot read $f"; exit 1; }; done
$ echo 'certificate and key files are readable'
certificate and key files are readable
The sender certificate must match the sender private key. The recipient certificate is public and is used for encryption; the corresponding recipient private key is needed later for decryption. A certificate can be public, but a private key must be protected. Do not use sudo to make a key broadly readable. Fix ownership and permissions through your normal administrator process instead.
3. Create a cleartext signed message
Use -sign with -text to add plain-text MIME headers. Without -nodetach, the default is a cleartext, detached signature in a multipart/signed message:
$ openssl smime -sign \
-in message.txt \
-text \
-signer "$SENDER_CERT" \
-inkey "$SENDER_KEY" \
-out signed.msg
$ test -s signed.msg && echo 'signed message written'
signed message written
If the private key is encrypted, OpenSSL will ask for its passphrase. Avoid putting a passphrase directly in a command line. The -passin option accepts a passphrase source when automation is genuinely required, but every source has its own exposure risks.
Inspect the beginning of the result without treating the MIME text as proof that the signature is valid:
$ sed -n '1,12p' signed.msg
MIME-Version: 1.0
Content-Type: multipart/signed;
Exact boundary and header lines vary. The useful check is that the output is a MIME message and the command exited successfully. With -nodetach, OpenSSL instead creates an opaque signed message. That form travels more reliably through systems that alter line endings, but some mail clients cannot display it.
Checkpoint
Keep message.txt and signed.msg. The input remains unchanged, and the output can be replaced safely by choosing a new filename.
4. Verify the signature
Verify the signed message and write the recovered content to a separate file. This checks the signature and, by default, attempts certificate verification using the certificates supplied in the message and the configured trust store:
$ openssl smime -verify \
-in signed.msg \
-text \
-out verified.txt
Verification successful
$ diff -u message.txt <(tr -d '\r' < verified.txt) && echo 'content matches'
content matches
A successful signature does not automatically make the signer trusted for your purpose. For a controlled trust decision, supply the appropriate CA bundle with -CAfile /path/to/ca-bundle.pem, or use the relevant verification options for your environment. Do not add -noverify just to silence a trust failure: that option deliberately skips verification of the signer's certificate.
If the message has a detached signature in DER or PEM form, provide the original content with -content and select the matching -inform. The -content file must be the exact signed content, including relevant line endings.
5. Encrypt the signed message for a recipient
Encrypt the already signed MIME message with the recipient certificate. The input already has MIME headers, so do not add -text a second time:
$ openssl smime -encrypt \
-aes-128-cbc \
-in signed.msg \
-out encrypted.msg \
"$RECIPIENT_CERT"
$ test -s encrypted.msg && echo 'encrypted message written'
encrypted message written
The certificate at the end of the command identifies the recipient. This guide selects AES-128-CBC explicitly so the choice is visible in a script. The manpage says that triple DES is used when no cipher is specified, but do not rely on an implicit legacy choice when you can state the intended algorithm.
Security warning
Encryption protects the message content for the matching private key, not the surrounding operational metadata. The -to, -from and -subject options add mail headers outside the signed portion, and those headers are not a substitute for transport or endpoint security.
6. Decrypt and then verify
On the recipient side, decrypt to a new file with the recipient certificate and private key:
$ openssl smime -decrypt \
-in encrypted.msg \
-recip "$RECIPIENT_CERT" \
-inkey "$RECIPIENT_KEY" \
-out recovered-signed.msg
$ test -s recovered-signed.msg && echo 'decrypted message written'
decrypted message written
Now verify the recovered signed message, rather than assuming that successful decryption proves who signed it:
$ openssl smime -verify \
-in recovered-signed.msg \
-text \
-out recovered.txt
Verification successful
$ diff -u message.txt <(tr -d '\r' < recovered.txt) && echo 'round trip verified'
round trip verified
Decryption fails if the supplied certificate does not match one of the message recipients. Verification fails if the signed bytes have changed, if the signing certificate cannot be verified, or if the MIME parser cannot handle the message. The manpage describes the parser as limited, so preserve the original message when diagnosing a failure.
7. Handle failures without weakening the check
Use the exit status to distinguish a completed operation from a file that merely exists:
$ openssl smime -verify -in signed.msg -text -out verified.txt
$ status=$?
$ printf 'verification exit status: %s\n' "$status"
verification exit status: 0
OpenSSL documents status 1 for option parsing, 2 for an unreadable input file, 3 for creating or reading the PKCS#7 or MIME message, 4 for decryption or verification failure, and 5 when verification worked but writing signer certificates failed. An incomplete output file can still be left after an error, so write to a temporary name and move it into place only after a successful command.
$ tmp_output=$(mktemp ./verified.XXXXXX)
$ if openssl smime -verify -in signed.msg -out "$tmp_output"; then
mv -- "$tmp_output" verified.txt
else
status=$?
rm -- "$tmp_output"
printf 'verification failed with status %s\n' "$status" >&2
exit "$status"
fi
That recovery block removes only its own temporary file. If you need to discard generated messages, check the exact paths first and remove them deliberately. Never delete the only copy of an encrypted message or private key while investigating a failure.
Done means
- The installed OpenSSL version and local
smimeoptions were checked. - A signed MIME message was created without changing the source message.
- Verification succeeded, with trust configured deliberately rather than bypassed.
- The signed message was encrypted for the intended recipient certificate.
- Decryption with the matching recipient key was followed by a separate signature verification.
- Private keys stayed protected, and failed output was not promoted over a known-good file.