Skip to content

Encryption and decryption

An EnvelopedData message carries content encrypted with a random content key and that key encrypted once for each recipient. This library supports recipients with RSA certificates.

Encrypt for a recipient

enveloped, err := pkcs7.Encrypt([]byte("for the recipient only"), []*x509.Certificate{recipientCert}, pkcs7.EncryptOptions{
    ContentEncryption: pkcs7.AES256CBC,
    KeyEncryption:     pkcs7.RSAOAEP,
})
if err != nil {
    log.Fatal(err)
}

Encrypt generates a content key and an IV, encrypts the content in CBC mode with PKCS #7 padding and encrypts the content key for every certificate in the list. Each recipient is identified by the issuer and serial number of its certificate.

Encrypt fails with pkcs7: no recipients for an empty list and with ErrUnsupportedKeyType when a certificate does not hold an RSA key. It does not check the validity period or the key usage of the certificates, so check them before encrypting.

Choosing the algorithms

EncryptOptions selects the content cipher and the key transport:

Field Default Choices
ContentEncryption AES128CBC AES128CBC, AES192CBC, AES256CBC, DESEDE3CBC and DESCBC
KeyEncryption RSAPKCS1v15 RSAPKCS1v15 and RSAOAEP
OAEPHash SHA-256 SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512. Used only with RSAOAEP

AES-128-CBC with PKCS #1 v1.5 key transport is the default because SCEP peers expect it. Use AES-256-CBC and RSAES-OAEP when the receiver supports them. RSAES-OAEP uses OAEPHash for both the OAEP hash and MGF1. The parameters name both hashes explicitly and carry no label, which OpenSSL reads correctly.

A responder that answers in the cipher the requester chose reads it with ContentEncryptionAlgorithm:

p7, err := pkcs7.Parse(request)
if err != nil {
    log.Fatal(err)
}
alg, err := p7.ContentEncryptionAlgorithm()
if err != nil {
    log.Fatal(err)
}
reply, err := pkcs7.Encrypt([]byte("reply"), []*x509.Certificate{clientCert}, pkcs7.EncryptOptions{ContentEncryption: alg})
if err != nil {
    log.Fatal(err)
}
fmt.Println("reply encrypted with", alg)

Decrypt

p7, err := pkcs7.Parse(enveloped)
if err != nil {
    log.Fatal(err)
}
plaintext, err := p7.Decrypt(recipientCert, recipientKey, pkcs7.DecryptOptions{})
if err != nil {
    log.Fatal(err)
}
fmt.Printf("%s\n", plaintext)

Decrypt picks the recipient whose issuer and serial number match the certificate and fails with pkcs7: no enveloped recipient for provided certificate when there is none. It does not check that the key belongs to the certificate. A wrong key is treated like corrupted input, as Integrity describes.

The key transport is read from the message. PKCS #1 v1.5 and RSAES-OAEP are accepted. For RSAES-OAEP the hash, the MGF1 hash and the label come from the parameters. Absent parameters mean SHA-1 as RFC 8017 specifies.

Accepted ciphers

Decrypt accepts AES-128-CBC, AES-192-CBC, AES-256-CBC and DES-EDE3-CBC by default, the list in DefaultContentDecryptionAlgorithms. A message encrypted with another cipher fails with an error that wraps ErrContentEncryptionNotAllowed. DecryptOptions.ContentEncryptionAlgorithms replaces the list. Single DES must be listed explicitly:

_, err = p7.Decrypt(cert, key, pkcs7.DecryptOptions{})
fmt.Println("refused by default:", errors.Is(err, pkcs7.ErrContentEncryptionNotAllowed))

opts := pkcs7.DecryptOptions{
    ContentEncryptionAlgorithms: append(slices.Clone(pkcs7.DefaultContentDecryptionAlgorithms), pkcs7.DESCBC),
}
plaintext, err := p7.Decrypt(cert, key, opts)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("%s\n", plaintext)

Accept DES-CBC only from peers that cannot do better. Its 56-bit key is broken.

Do not modify DefaultContentDecryptionAlgorithms itself. Every Decrypt and DecryptUsingPSK call reads it, so a change affects the whole program. Pass ContentEncryptionAlgorithms instead.

Several keys

A receiver that holds several keys, for example during a key rollover, reads the recipients first and decrypts with the key that matches:

type candidate struct {
    cert *x509.Certificate
    key  crypto.Decrypter
}
candidates := []candidate{{currentCert, currentKey}, {nextCert, nextKey}}

p7, err := pkcs7.Parse(enveloped)
if err != nil {
    log.Fatal(err)
}
recipients, err := p7.Recipients()
if err != nil {
    log.Fatal(err)
}
for _, r := range recipients {
    for _, c := range candidates {
        if bytes.Equal(r.Issuer, c.cert.RawIssuer) && r.SerialNumber.Cmp(c.cert.SerialNumber) == 0 {
            plaintext, err := p7.Decrypt(c.cert, c.key, pkcs7.DecryptOptions{})
            if err != nil {
                log.Fatal(err)
            }
            fmt.Printf("%s with %s\n", plaintext, c.cert.Subject.CommonName)
        }
    }
}

Recipients lists them in encoded order. DER sorts the recipient set, so the order need not match the certificates given to Encrypt.

Keys in an HSM

Decrypt takes a crypto.Decrypter, so the private key may stay in an HSM. The library calls its Decrypt method with these options:

  • *rsa.PKCS1v15DecryptOptions with SessionKeyLen set to the content key length, for PKCS #1 v1.5 key transport.
  • *rsa.OAEPOptions with Hash, MGFHash and Label from the message, for RSAES-OAEP.

The crypto.Decrypter must honor them. With SessionKeyLen set, *rsa.PrivateKey returns a random key of that length when the padding is wrong instead of an error, which is the RFC 3218 countermeasure against Bleichenbacher's attack. A crypto.Decrypter that returns an error instead still produces ErrDecryptionFailed, but whether it leaks timing then depends on that implementation.

Errors

Every failure to decrypt the content key or the content returns ErrDecryptionFailed, without saying which step failed. That includes an error from the crypto.Decrypter, a content key of the wrong length and bad padding. A sender therefore cannot use the error as a padding oracle.

Other errors describe the message rather than the keys:

Error Cause
ErrNotEncryptedContent The message is not EnvelopedData
ErrUnsupportedAsymmetricEncryptionAlgorithm The recipient uses a key transport other than PKCS #1 v1.5 or RSAES-OAEP
ErrUnsupportedAlgorithm The content cipher is not one this library implements
ErrContentEncryptionNotAllowed The content cipher is not in the accepted list
pkcs7: encryption algorithm parameters are malformed The IV is missing or has the wrong length
pkcs7: ciphertext is not a whole number of cipher blocks The ciphertext is truncated or empty

Integrity

CBC mode does not authenticate the content. A wrong key or a changed ciphertext usually ends in ErrDecryptionFailed, but sometimes the padding happens to be valid and Decrypt returns garbage. When the receiver must detect changes, sign the EnvelopedData message, as SCEP does.