Using OpenSSL to Encrypt and Decrypt with CMS
Table of Contents
Introduction #
This practical guide is a follow-up to the theoretical foundation laid in my article End-to-End security for sensitive data using CMS (aka PKCS#7). This one explained which encryption capabilities but more importantly which key management mechanisms are provided by the CMS encapsulation format. Learning purely based on theory is not everyone’s favorite, so this article will provide you with a comprehensive guide with complete examples showing your how to use OpenSSL CLI to take advantage of the CMS capabilities for encryption and key management.
The following examples are performed on bash
and use the following OpenSSL version:
openssl version
OpenSSL 3.2.1 30 Jan 2024 (Library: OpenSSL 3.2.1 30 Jan 2024)
After a short overview, the article is structured based on the key management mechanisms. First, the mechanisms based on shared secrets are covered. Then, the mechanisms based on public key cryptography are explored.
CLI Overview #
The primary subcommand to perform CMS operations is cms
. When following along with the examples, you can always refer
to the cms man pages that provide a great level of detail for individual options.
man openssl-cms
Alternatively, you can also use the OpenSSL’s online documentation.
Since CMS is the basis for S/MIME, the cms
subcommand has many options that relate to common e-mail
terminology (e.g. -to, -from, -subject
) . Even the default output format is S/MIME. For our purposes, we will not take
advantage of these options.
For every OpenSSL cms
command, the following top-level options are important:
-print
: Prints out the CMS structure in a human-friendly way with many details best suited for debugging purposes-outform <format>
:PEM
: Outputs the CMS message in PEM formatDER
: Outputs the CMS message in DER formatSMIME
: Output format for S/MIME messages (default format if outform is missing)
To specify the cms
service to apply, several flags are available:
-encrypt
and-decrypt
: For encryption and decryption-sign
and-verify
: For signing and verifying-cmsout
: A special mode that instructs OpenSSL to only output the CMS structure without performing any encryption or decryption. This is useful for debugging purposes in combination with-print
.
Next, let’s look at the encryption capabilities and key management mechanisms that are supported.
Supported Encryption Capabilities #
As described in CMS Encryption Capabilities, CMS features in total three
encryption modes (i.e. content types) of which only EnvelopedData
and AuthEnvelopedData
are supported via the CLI
interface.
The mode used by OpenSSL CLI is implicitly decided based on the passed cipher. If an AEAD is
configured, AuthEnvelopedData
is used, else EnvelopedData
. In addition, OpenSSL does not support all algorithms that
are described by RFCs and on the other hand supports algorithms that have no formal RFC standing. The following table is
a non-exhaustive list of supported ciphers and the corresponding CMS mode:
Cipher | CMS Encryption Mode | Example OpenSSL CLI Param |
---|---|---|
AES-GCM (RFC 5084) | AuthEnvelopedData | -aes-128-gcm |
AES-GCM-SIV (RFC 8452) | Unsupported | - |
AES-CCM (RFC 5084) | Unsupported | - |
ChaCha20-Poly1305 (RFC 8103) | Unsupported | - |
AES-CBC (RFC 3565) | EnvelopedData | -aes-128-cbc |
AES-ECB (no formal RFC standing) | EnvelopedData | -aes-128-ecb |
AES-OFB (no formal RFC standing) | EnvelopedData | -aes-128-ofb |
AES-CFB (no formal RFC standing) | EnvelopedData | -aes-128-cfb |
The above listed AES modes are supported with block sizes of 128, 192 and 256 bits. In addition, the CLI supports all of
the RFC 3565 standardized AES key wrap
algorithms: aes128-wrap
, aes192-wrap
, aes256-wrap
.
Examples in this article will use AES-128-GCM AEAD together with AES-128-Wrap by default but you can freely change the algorthm and block sizes.
Supported Key Management Mechanisms #
CMS supports in total four key management mechanisms. All four mechanisms are supported by the OpenSSL CLI. The mechanism is chosen based on the options or the asymmetric key type. Here is the mapping between the mechanisms and CLI options:
- Symmetric Key Encryption:
-secretkey
and-secretkeyid
- Password-based Key Management:
-pwri_password
- Key Transport & Key Agreement:
-recip
Now we are getting to more complete examples.
Key management mechanisms based on shared secrets #
As described in my previous article, CMS offers two key management mechanisms based on shared secrets. Both mechanisms assume that the sender and receiver share a common secret but sharing this secret is out of CMS’ scope.
Symmetric Key Encryption #
Before getting started, we need some sample data to encrypt:
SAMPLE_FILE=sample.txt
echo '{ "accountBalance": 1000.0 }' > $SAMPLE_FILE
Next, we are going to generate the symmetric key that is used to encrypt and is somehow shared between sender and receiver. The symmetric key MUST have a length suitable for the chosen encryption cipher. Our example uses AES-128, thus a key with 16 bytes is required. OpenSSL requires this key in hex format. We generate this key as follows:
SECRET_KEY=$(openssl rand -hex 16)
Sharing this key between senders and receivers is out CMS’ scope. However, CMS provides a mechanism to add an
identifier for the secret key using the option secretkeyid
. This allows an recipient to easily identify its key,
though the distribution of this identifier is also out of CMS’ scope. Mind that the key identifier must be a valid hex
string. With that, we can proceed to encrypt the data:
openssl cms -encrypt -in $SAMPLE_FILE \
-secretkey $SECRET_KEY -secretkeyid "ad2024" \
-wrap aes128-wrap -aes-128-gcm \
-outform DER > "$SAMPLE_FILE"_enc-kek.p7m
Let’s inspect the generated CMS data structure:
openssl cms -cmsout -in "$SAMPLE_FILE"_enc-kek.p7m -inform DER -print
We see that we have a authEnvelopedData
structure (line 3) with the mac
field set (line 33-34) which is in line with
our AEAD algorithm. The recipient information is stored in the KEKRecipientInfo (kekri)
(line 7) structure as defined
in RFC 5652 section 6.2.3. We see the expected key
identifier (line 10-11), the key encryption algorithm (line 15) and the encryption algorithm (line 23). The key is
encrypted using AES-128-Wrap (line 15) and is stored in the encryptedKey
field (line 28).
|
|
Using the same secret key and key identifier, the recipient can decrypt the encrypted message as follows:
openssl cms -decrypt -in "$SAMPLE_FILE"_enc-kek.p7m -inform DER \
-secretkey $SECRET_KEY -secretkeyid "ad2024"
This should print our initial value { "accountBalance": 1000.0 }
.
Hey, do you like what you are reading? Subscribe and don't miss any news from my blog. No spam, just good reads.
Password-based Key Management #
After using a symmetric key directly, we now derive a symmetric key from a password by using PBKDF2
. This is the only
supported KDF. Important to note is a bug tracked in issue #23634
that prevents us from using a AEAD algorithm together with password-based key management. Hence, only non-authenticated
encryption is currently possible and we fall back to AES-CBC
for this example.
We again generate some sample data to encrypt:
SAMPLE_FILE=sample.txt
echo '{ "accountBalance": 1000.0 }' > $SAMPLE_FILE
Then, we can encrypt the data using some arbitrary password:
CMS_PWD=alexdippel.de2024
openssl cms -encrypt -in $SAMPLE_FILE \
-aes-128-cbc -pwri_password $CMS_PWD \
-outform DER > "$SAMPLE_FILE"_enc-pwri.p7m
Let’s analyze the generated CMS data structure:
openssl cms -cmsout -in "$SAMPLE_FILE"_enc-pwri.p7m -inform DER -print
There is nothing surprising. We have a PasswordRecipientInfo (pwri)
data structure (line 7) as defined
in RFC 5652 section 6.2.4 together with the KDF
algorithm PBKDF2
(line 10). Since we changed to non-AEAD (line 28), we have a envelopedData
structure (line 3).
|
|
Let’s see if we can decrypt the data using the previously used data:
openssl cms -decrypt -pwri_password $CMS_PWD -in "$SAMPLE_FILE"_enc-pwri.p7m -inform DER
This should print our initial value { "accountBalance": 1000.0 }
.
Key Management Mechanisms based on Public Key Cryptography #
As you can read in my previous article, CMS provides two key management mechanisms based on public key cryptography. They assume that an originator has access to the public keys of the recipients. In the following examples, we will generate self-signed certificates to illustrate the usage of these mechanisms. In a real-world scenario, a more sophisticated approach for distributing certificates is required.
Using Key Transport with RSA #
Since key transport is only described for RSA, we start off by generating a self-signed certificate with an RSA
key pair. Since we intend to encrypt a symmetric key with the public key, we add the keyEncipherment
key usage
to the certificate:
KT_RSA_RECIPIENT_CERT=rsa_recipient.crt
KT_RSA_RECIPIENT_KEY=rsa_recipient.key
openssl req -new -newkey rsa:2048 -noenc -keyout $KT_RSA_RECIPIENT_KEY \
-out $KT_RSA_RECIPIENT_CERT -outform PEM -x509 -days 365 \
-subj "/O=alexdippel.de/OU=CMS Test Laboratory/CN=rsa-recipient" \
-addext "keyUsage=keyEncipherment"
Next, generate some sample data and save it to a file:
SAMPLE_FILE=sample.txt
echo '{ "accountBalance": 1000.0 }' > $SAMPLE_FILE
Now, we can encrypt the data using our previously generated certificate. By default, OpenSSL uses the discouraged PKCS#1
v1.5 padding. We change that by setting the rsa_padding_mode
to oaep
thus using the preferred RSA-OAEP padding.
openssl cms -encrypt -in $SAMPLE_FILE \
-recip $KT_RSA_RECIPIENT_CERT \
-aes-128-gcm \
-keyopt rsa_padding_mode:oaep \
-outform DER > "$SAMPLE_FILE"_enc-ktri.p7m
Let’s inspect the resulting CMS structure:
openssl cms -cmsout -in "$SAMPLE_FILE"_enc-ktri.p7m -inform DER -print
We used AES-GCM (line 22) which performs AEAD and thus produces an authEnvelopedData
(line 3) structure with the mac
field set (line 32-33). Since we are using key transport, the recipient information is stored in
a KeyTransRecipientInfo (ktri)
(line 7) structure as defined
in RFC 5652 section 6.2.1. RSA-OAEP (line 13) is used to
encrypt the CEK (line 16) using the recipient’s public key.
|
|
Since we can encrypt our message to several recipients leading to several KeyTransRecipientInfo (ktri)
structures.
A mechanism is needed helping a recipients efficiently identify the structure intended for them. For this, the
certificate’s issuer line and the recipient’s serial number are copied into the structure (see line 10 and 11). The
copied information match information on the certificate:
openssl x509 -in $KT_RSA_RECIPIENT_CERT -issuer -serial -noout
issuer=O=alexdippel.de, OU=CMS Test Laboratory, CN=rsa-recipient
serial=32DD28D7266ACA5A5EDEED0C3B5F9DCFAA1E6FD6
To decrypt the content again, we use the recipient’s private key that we conveniently stored in a file previously. In addition, we need the certificate again to identify the recipient structure using the issuer and serial number information:
openssl cms -decrypt -in "$SAMPLE_FILE"_enc-ktri.p7m -inform DER \
-inkey $KT_RSA_RECIPIENT_KEY \
-recip $KT_RSA_RECIPIENT_CERT
This should print our initial value { "accountBalance": 1000.0 }
.
Using Key Agreement with Standard ECDH #
Key agreement is a key establishment mechanism that is only described for ECC in CMS. Since key transport and key agreement are mutually exclusive, OpenSSL implicitly chooses the correct key management mechanism based on the key pair used. We will generate a self-signed certificate with an ECC key pair:
KA_ECC_RECIPIENT_CERT=ecc_recipient.crt
KA_ECC_RECIPIENT_KEY=ecc_recipient.key
openssl req -new -newkey ec -pkeyopt group:prime256v1 -noenc -keyout $KA_ECC_RECIPIENT_KEY \
-out $KA_ECC_RECIPIENT_CERT -outform PEM -x509 -days 365 \
-subj "/O=alexdippel.de/OU=CMS Test Laboratory/CN=ecc-recipient" \
-addext keyUsage=critical,keyAgreement
Since we perform a key agreement with the public key, we reflect that in the key usage extension of the certificate. Next, we again require our sample data in a file:
SAMPLE_FILE=sample.txt
echo '{ "accountBalance": 1000.0 }' > $SAMPLE_FILE
Now, we can encrypt the data using the recipient’s certificate. Only the hash algorithm that is used together
with the KDF is changed to sha256
since OpenSSL uses sha1
by default but SHA1 is discouraged and should be avoided.
openssl cms -encrypt -in $SAMPLE_FILE \
-recip $KA_ECC_RECIPIENT_CERT \
-aes-128-gcm \
-keyopt ecdh_kdf_md:sha256 -aes128-wrap \
-outform DER > "$SAMPLE_FILE"_enc-kari.p7m
Let’s inspect the resulting CMS structure:
openssl cms -cmsout -in "$SAMPLE_FILE"_enc-kari.p7m -inform DER -print
We used AES-GCM (line 23) which performs AEAD and thus produces an authEnvelopedData
(line 3) structure with the mac
field set (line 45,47). OpenSSL uses Standard ECDH (line 21) to establish a shared secret that is used to encrypt the
CEK and which is then stored in the structure (line 29). As such, recipient information are stored in
the KeyAgreeRecipientInfo (kari)
(line 7) as defined
in RFC 5652 section 6.2.2. OpenSSL only implements ECDH
in an Ephemeral-Static mode (aka C(1e, 1s, ECC DH)
. To allow the recipient to perform ECDH on his site with the
ephemeral public key, the originator key field contains the ephemeral public key (line 13). And like before, the
recipients certificate information are copied into the structure (line 27, 28).
|
|
To decrypt the content again, we use the recipient’s private key that we conveniently stored in a file previously as well as the certificate required to identify the recipient using the issuer and serial number information:
openssl cms -decrypt -in "$SAMPLE_FILE"_enc-kari.p7m -inform DER \
-inkey $KA_ECC_RECIPIENT_KEY \
-recip $KA_ECC_RECIPIENT_CERT
This should print our initial value { "accountBalance": 1000.0 }
.
That’s it for now. I hope that this guide and the examples provide you with a good starting point for your journey of adopting CMS for your encryption needs. If you have any questions or feedback, feel free to reach out to me.