Blog
All Blog Posts | Next Post | Previous Post
Building a PKCS#12/PFX Exporter with TMS Crypto Pack: Inside TX509Certificate
Wednesday, September 23, 2026
TMS Cryptography Pack proposes functions/procedures to generate and decode many PKIX structures using the "PEM" format. That includes certificates, keys and signatures files. It also supported the decoding of PFX certificates (with either keyBag,certBag, or pkcs8ShroudedKeyBag) and as of 5.3.0.0, it supports the generation of PFX for "cert bags" and "key bags".
This post explains how the internal of this feature works.
Why PKCS#12 is trickier than it looks
A .pfx/.p12 file looks like a single opaque blob, but it's a small tower of nested ASN.1 structures, each with its own encoding rules, several of which are easy to get subtly wrong without any error message at write time the file parses fine, opens in some tools, and then fails silently in others (wrong MAC, unreadable key, orphaned certificate). TX509Certificate implements this format from scratch in Delphi, building and parsing the DER by hand rather than relying on an external ASN.1 library. This article walks through what the format actually requires, how the class implements each layer, and how to cross-check the output against OpenSSL at every step.
The structure, top to bottom
PFX ::= SEQUENCE {
version INTEGER {v3(3)},
authSafe ContentInfo,
macData MacData OPTIONAL
}authSafe is a ContentInfo of type id-data, whose payload is itself an AuthenticatedSafe ::= SEQUENCE OF ContentInfo. Each element of that sequence carries one "safe" a certificate, a key, or a group of either wrapped either in plain id-data or in encryptedData when confidentiality is wanted for that particular safe. Inside each safe sits a SafeContents ::= SEQUENCE OF SafeBag, and each SafeBag carries a typed payload (keyBag,certBag, pkcs8ShroudedKeyBag, CRLBag, SecretBag, and SafeContents) plus an optional set of attributes (localKeyID, friendlyName) used to associate a certificate with its private key.
That's four or five levels of nesting before you reach a single useful byte, and every level has its own tag, its own length, and its own rules about implicit versus explicit tagging. Get one length wrong, or wrap something in [0] IMPLICIT where [0] EXPLICIT was required, and the file becomes silently unreadable to strict parsers while looking fine in lenient ones a genuinely common failure mode when building this by hand.
New building blocks in TX509Certificate
BuildPKCS8PrivateKeyInfo
Wraps the raw key material (RSA or EC) into a standard PKCS#8 PrivateKeyInfo:
PrivateKeyInfo ::= SEQUENCE {
version INTEGER (0),
privateKeyAlgorithm AlgorithmIdentifier,
privateKey OCTET STRING
}For RSA, privateKeyAlgorithm carries rsaEncryption (1.2.840.113549.1.1.1) with NULL parameters. For EC, it carries id-ecPublicKey (1.2.840.10045.2.1) with the named curve's OID as parameters instead of NULL a detail that's easy to copy-paste wrong from the RSA branch, since both look identical except for that one field.
OpenSSL equivalent inspecting an unencrypted PKCS#8 key:
openssl pkey -in key.pem -text -nooutBuildPkcs8ShroudedKeyBag
Encrypts the PrivateKeyInfo with PBES2 (RFC 8018): PBKDF2-HMAC-SHA256 to derive the key, AES-256-CBC to encrypt it. The resulting AlgorithmIdentifier looks like:
PBES2 { PBKDF2 { salt, iterations, keyLength, prf }, AES-256-CBC { iv } }keyLength here is in bytes, not bits.
OpenSSL equivalent creating a PBES2/AES-256/PBKDF2-SHA256 encrypted PKCS#8 key directly:
openssl pkcs8 -topk8 -v2 aes-256-cbc -v2prf hmacWithSHA256 -in key.pem -out key_encrypted.pemBuildCertBag
For a certificate stored unencrypted, the SafeBag points directly at type x509Certificate (1.2.840.113549.1.9.22.1):
SafeBag { bagId=x509Certificate, bagValue=[0]{OCTET STRING cert}, bagAttributes }BuildBagForEncryption + BuildEncryptedContentInfo
For a certificate stored encrypted, OpenSSL and this class, once corrected to match it uses a different, doubly-nested bagId:
SafeBag {
bagId = certBag (1.2.840.113549.1.12.10.1.3),
bagValue = [0] EXPLICIT SEQUENCE {
certType = x509Certificate,
certValue = [0] EXPLICIT OCTET STRING (the cert DER)
},
bagAttributes
}This whole SafeBag not just the certificate is what gets encrypted, as the plaintext of an EncryptedContentInfo sitting inside a ContentInfo of type encryptedData (1.2.840.113549.1.7.6) at the AuthenticatedSafe level:
ContentInfo {
contentType = encryptedData,
content = [0] EXPLICIT EncryptedData {
version = 0,
encryptedContentInfo = SEQUENCE {
contentType = id-data,
contentEncryptionAlgorithm = PBES2 { ... same shape as the key ... },
encryptedContent = [0] IMPLICIT OCTET STRING
}
}
}Two details worth calling out because they don't announce themselves as bugs decryption just silently produces garbage:
encryptedContentmust be tagged[0] IMPLICIT(0x80, a primitive context tag directly in front of the ciphertext bytes), not[0] EXPLICIT(0xA0) wrapping an innerOCTET STRING. Both parse as valid DER, so a parser can misread one for the other without erroring the mismatch only surfaces if you try to decrypt and compare byte-for-byte against a reference.- The
bagValueof the certBag-wrappingSafeBag, and the innercertValue, both need their own[0] EXPLICITwrapper easy to drop one level when hand-assembling nested strings.
OpenSSL equivalent producing a PFX with the certificate encrypted (in addition to the key), with TMS CP supported algorithms:
openssl pkcs12 -export -in cert.pem -inkey key.pem -out out.pfx -certpbe AES-256-CBC -keypbe AES-256-CBC -macalg sha256(Without -certpbe, recent OpenSSL defaults to leaving the certificate in plain id-data which is also perfectly valid PKCS#12, and the simpler case this class supports as well.)
BuildMacData
The MacData structure authenticates the entire AuthenticatedSafe:
MacData ::= SEQUENCE {
mac DigestInfo { algorithm, digest },
macSalt OCTET STRING,
iterations INTEGER DEFAULT 1
}The key used for this HMAC is not derived with PBKDF2. RFC 7292's Annex B defines a separate, older key-derivation function specific to PKCS#12: the password is encoded as a BMPString (UTF-16BE, null-terminated), repeated together with the salt to fill whole hash-block-sized buffers, and the hash function is applied iteratively to an evolving buffer rather than through the standard PBKDF2 construction. Reusing the PBES2/PBKDF2 routine here an easy mistake, since both derive "a key from a password and a salt" produces a structurally perfect file with a MAC that never validates.
OpenSSL equivalent verifying a file's MAC and listing its contents:
openssl pkcs12 -info -in file.pfx -noout -passin pass:yourpassword # pick a strong password :-)A successful run without a MAC-mismatch error is the fastest real-world confirmation that both the AuthenticatedSafe structure and the HMAC are correct together cheaper than manual ASN.1 inspection for a quick sanity check, though it won't tell you why something is wrong if it fails. You can still use the https://lapo.it/asn1js/ decoder to visualize and check all ASN.1 structures.
Putting it together revisited: ExportToPFX
PKCS8Key := BuildPKCS8PrivateKeyInfo;
InitPfxParameters(Password);
FPfx.EncryptionKey := ASN1.GenerateKDFKey(FPfx.Password, FPfx.KdfSalt,
FPfx.KdfIterations, FPfx.KdfOutputSize * 8);
FPfx.EncryptedKeyset := ASN1.EncryptPrivateKeyInfo(FPfx.EncryptionKey, FPfx.EncryptionIV, PKCS8Key);
CertBag := BuildCertBag(FCrtRaw); // or BuildBagForEncryption, for the encrypted path
KeyBag := BuildPkcs8ShroudedKeyBag(FPfx.EncryptedKeyset);
SafeContents := BuildSafeContents(CertBag, KeyBag); // = the AuthenticatedSafe itself
AuthSafe := BuildContentInfoData(SafeContents); // outer ContentInfo wrapper
MacData := BuildMacData(SafeContents); // MAC over the AuthenticatedSafe, not SafeContents' inner bags
PFX := BuildPfxRoot(AuthSafe, MacData);The naming here is a little misleading on purpose: SafeContents is where the two per-bag ContentInfo elements are actually assembled, making it the true AuthenticatedSafe; AuthSafe is one further wrapper around that, the outer ContentInfo that becomes PFX.authSafe. Keeping straight which buffer the MAC is computed over the AuthenticatedSafe, always including its own SEQUENCE tag and length, never the bare concatenation of bags inside it, and never the doubly-wrapped AuthSafe is the single detail most worth getting right first, since every other structural mistake tends to surface as a parse error, while this one surfaces only as a MAC that mysteriously never matches.
Testing checklist
For anyone extending (not all OIDs are present) or auditing code like this, the fastest feedback loop is:
- Structural check
openssl asn1parse -inform DER -in file.pfx -i, or-strparse <offset>to descend past an outerOCTET STRINGthat the tool won't auto-recurse into. - Integrity check
openssl pkcs12 -info -in file.pfx -noout -passin pass:...; a MAC failure here means the buffer hashed at export didn't match the buffer written. - Round-trip check decrypt and re-import with a second, independent implementation (even a short Python script using
cryptographyandhashlib.pbkdf2_hmac) rather than trusting your own decoder, since a decoder written by the same hand as the encoder will happily agree with its own mistakes.
More OpenSSL examples are provided in the \Demo\VCL\PFX folder. Several PFX certificates to test the decode function can be found in the \Demo\VCL\PFX\Certs folder.
The VCL Demo has been updated to generate standard X509 certificates and PFX certificates, for which a password is required.
Bernard Roussely
This blog post has not received any comments yet. Add a comment.
All Blog Posts | Next Post | Previous Post