Blog

All Blog Posts  |  Next Post  |  Previous Post

Building a PKCS#12/PFX Exporter with TMS Crypto Pack: Inside TX509Certificate

Today

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, pkcs8ShroudedKeyBagCRLBagSecretBag, 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 -noout

BuildPkcs8ShroudedKeyBag

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.pem

BuildCertBag

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:

  • encryptedContent must be tagged [0] IMPLICIT (0x80, a primitive context tag directly in front of the ciphertext bytes), not [0] EXPLICIT (0xA0) wrapping an inner OCTET 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 bagValue of the certBag-wrapping SafeBag, and the inner certValue, both need their own [0] EXPLICIT wrapper — 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

Up to version 5.2.x.y, ExportToPFX was a call to OpenSSL with a series of parameters. The new code looks like this:
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:

  1. Structural checkopenssl asn1parse -inform DER -in file.pfx -i, or -strparse <offset> to descend past an outer OCTET STRING that the tool won't auto-recurse into.
  2. Integrity checkopenssl pkcs12 -info -in file.pfx -noout -passin pass:...; a MAC failure here means the buffer hashed at export didn't match the buffer written.
  3. Round-trip check — decrypt and re-import with a second, independent implementation (even a short Python script using cryptography and hashlib.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