Reporting a vulnerability
Please do not open a public issue. Use GitHub's private vulnerability reporting on this repository ("Security" tab, "Report a vulnerability"). You should get an answer within a week. Fixes are released for the latest minor version.
Threat model
The library is meant to read files from anyone: a certificate uploaded by a user, a file from a partner, a key store found on disk. It assumes the file may be hostile and the password may be wrong. When it writes, the input comes from the application and is trusted; the section on writing below covers what the written file protects.
Malformed input
PKCS12.decode/3returns{:error, %PKCS12.Error{}}for any input it cannot read. It does not raise on malformed input; the test suite feeds it damaged and random files to check that.- The parser limits nesting (32 levels of ASN.1, 4 levels of nested SafeContents) and length fields (4 bytes), and checks every length against the input before it reads.
- Nothing from the file becomes an atom. Unknown algorithms and bag types are reported by their OID, as a string.
- The memory used is a small multiple of the input size. Bound the size of the files you accept; the library does not.
CPU
A PKCS#12 file sets its own iteration counts. Without limits, one small file could ask for minutes of CPU. Two options cap the work:
:max_iterations(default 1,000,000) is checked for every key derivation while the file is parsed, before any of them runs.:max_total_iterations(default 4,000,000) caps the work of all derivations together, counted in iterations of the hash: the MAC checks, one per form of the password tried, and every decryption. A file with many encrypted parts, or a wrong password tried in several forms, stops at this limit.
At the defaults the worst case is about a second of one core. The work runs
in the calling process: PBKDF2 inside OpenSSL on a dirty scheduler, the
PKCS#12 key derivation as an Elixir loop that the BEAM preempts. For files
from untrusted sources, lower both limits and put a timeout around the call
(for example, with Task.async/1 and Task.yield/2).
Integrity and the password
- When the file has a MAC, it is checked first, in constant time
(
:crypto.hash_equals/2), and nothing is decrypted before it matches. A wrong password gives:invalid_passwordafter the MAC check alone. - Some tools encode the password in other ways than RFC 7292 says. The
library tries those forms (at most two for any password), and the MAC
decides which is right. Each try counts towards
:max_total_iterations. - A file without a MAC has no integrity protection: anyone can change its
unencrypted parts, or replace a certificate.
info.macisnilfor such files; refuse them if you need integrity. Without a MAC, a wrong password is detected by the padding and the structure of what it decrypts. verify_mac: falsereads a file without checking its MAC. Use it only to recover a file you trust.
Weak algorithms
Files in the wild still use RC2-40 and RC4-40 for certificates, 3DES and
SHA-1 for keys and MACs, and low iteration counts. The library reads them,
because refusing would make it useless for many real files. They protect the
file at rest poorly. p12.info reports the algorithms and iteration counts
of each file, so that an application can warn about them or refuse them.
The library implements RC2 and the PKCS#12 key derivation itself, on top of
the SHA-1 of :crypto. They therefore work even where the OpenSSL that OTP
uses refuses legacy algorithms, such as under a FIPS policy. If your policy
forbids reading such files, check p12.info.
Secrets in memory
inspect/1ofPKCS12andPKCS12.Bagleaves the private key out, and no error, exception message or argument check includes the password or key material.- The decrypted key is a binary like any other. The BEAM copies binaries
and frees them when the garbage collector decides; there is no way to wipe
them. Keep the key in as few processes as possible, and do not log the
result of
PKCS12.decode/3with tools that bypassinspect/1. - The returned key and certificates are binaries of their own, not references into the input or the decrypted buffers.
Side channels
The MAC comparison is constant-time. RC2 and the PKCS#12 key derivation are written in Elixir and are not hardened against timing or cache attacks. In the files that use RC2, it protects the certificates, which are public. The library is meant for reading a file once, such as at startup or after an upload, not for an oracle that answers many queries with the same key.
What the library does not check
It does not validate certificates: not the chain, the dates, the key usage
or revocation. p12.certificate is the certificate that belongs to the key
by localKeyId or by public key, and p12.ca_certificates is everything
else, in file order. Validate them as your application requires, for
example with :public_key.pkix_path_validation/3.
Writing
- The salts (16 bytes) and the IVs come from
:crypto.strong_rand_bytes/1, the random generator of OpenSSL. - The password is all that protects a written file. Anyone who gets the file can try passwords offline, as fast as the key derivation allows. Every derivation in a file uses the same iteration count, because the cheapest one would set the pace. The default is 100,000; use a long random password for files that leave your systems, and a higher count for files that live long.
- The
:legacyprofile is weak. Its key derivation is SHA-1 with 2,048 iterations, so guessing is cheap, and the 40-bit key of RC2-40 can be found without the password, which exposes the certificates (public anyway). Use it only for importers that need it, and keep such files short-lived. key_encryption: :noneleaves the key readable by anyone with the file, andmac: :noneleaves the file without integrity protection. They are meant for trust stores and for files protected by other means.- The library checks the shape of the key and of the certificates it writes, not that the certificate belongs to the key, nor that it is valid.
- As when reading, the key and the password are binaries in the memory of the calling process, which the BEAM cannot wipe.