BAC (Basic Access Control) and PACE (Password Authenticated Connection Establishment) are the two protocols an electronic ID or passport chip uses to decide whether a reader is allowed to see its contents. Both are defined in ICAO Doc 9303, the public standard behind every ICAO-compliant travel document. The difference in one sentence: BAC protects the chip with keys derived from data printed on the document, which gives it a well-known entropy weakness, while PACE runs a password-authenticated Diffie-Hellman exchange that produces strong session keys even when the underlying password is weak. Modern documents mandate PACE; readers keep BAC around only as a fallback for older chips.
I ran into this distinction the hard way while building a production eID SDK for a banking app. If you want the wider picture of how a chip is read and verified over NFC, I wrote that up separately in reading an eID chip on Android. This post zooms in on one stage of that flow: access control, and why the choice between BAC and PACE matters.
Key takeaways
- Access control is a gate, not a proof of authenticity. It controls who may read the chip; a separate step (Passive Authentication) proves the data is genuine.
- BAC derives its keys from the MRZ (document number, date of birth, date of expiry). Those fields are printed on the document and partly predictable, so the effective entropy is low.
- PACE is a password-authenticated key exchange. Its session keys stay strong even when the password is short, which closes BAC's offline-attack weakness.
- Readers negotiate. A reader checks the chip's
EF.CardAccessfile, uses PACE when the chip advertises it, and falls back to BAC otherwise.
Why an ID chip needs access control
A contactless chip is, physically, just a small memory you can talk to over radio (ISO/IEC 14443). Left open, anyone standing close enough could skim your name, date of birth and photo without you noticing, or silently record the exchange between your document and a legitimate terminal. Access control exists to stop that. Before the chip hands over a single data group, the reader has to prove it is entitled to read the document.
The clever part of the ICAO design is how "entitled" is defined. The reader proves it by demonstrating knowledge of a secret that is only available if you physically hold the document, namely the data in the machine-readable zone (MRZ), the two or three lines of text at the bottom of a passport page or the back of an ID card. The assumption is simple: if you can read the MRZ, you are holding the document, and the holder has effectively consented to the read. Access control is therefore about privacy and consent, not about trusting the contents. Proving the data is authentic and untampered is a completely separate job done afterwards by Passive Authentication, which I cover in the eID post linked above. Keep those two ideas apart and the rest of this gets much clearer.
BAC: keys derived from the MRZ
BAC was the original mechanism, and it does exactly what the previous section describes, in the most literal way possible. The reader takes three MRZ fields, each with its check digit, concatenates them, and hashes the result with SHA-1 to produce a key seed. From that seed it derives a pair of 3DES keys, one for encryption and one for message authentication, and uses them to open a Secure Messaging channel with the chip.
These are the three fields that feed the derivation:
/**
* The three MRZ fields that seed a BAC key. All three are printed on the
* document, which is the root of BAC's entropy problem. The same inputs can
* also serve as the PACE password when a CAN is not used.
*/
data class DocumentKeyInput(
val documentNumber: String,
val dateOfBirth: String, // YYMMDD
val dateOfExpiry: String, // YYMMDD
)
The weakness is structural, not a bug. The key is static (it never changes for a given document) and it is built entirely from data printed on the page. Worse, those fields carry far less entropy than they look like they do. A date of birth has on the order of tens of thousands of plausible values, an expiry date only a few thousand, and document numbers tend to follow predictable issuer patterns rather than being random. Published analyses put the effective entropy of a BAC key well below the keyspace of the 3DES keys it ultimately produces. Because the keys come entirely from these low-entropy MRZ fields, BAC's resistance to a determined attacker is limited by design, not by any implementation flaw.
What this means in practice: an attacker who passively records a BAC session can run an offline brute-force search over the MRZ field space and recover the session keys, decrypting everything that was exchanged. No interaction with the chip is needed once the traffic is captured. Deriving a long-lived secret from short, guessable, printed data is the kind of mistake that reads fine on paper and falls apart under a motivated adversary, a theme that runs through most of mobile security too; I collected a batch of those lessons in SecureCheck.
PACE: a password-authenticated handshake
PACE fixes the entropy problem without asking users to memorise anything stronger. It is a password-authenticated key exchange (PAKE), and the whole point of a PAKE is to take a possibly weak shared password and bootstrap it into strong session keys that an eavesdropper cannot recover offline.
The password itself is still something available to the holder. It can be derived from the MRZ, exactly as with BAC, or it can be the CAN (Card Access Number), a short number printed separately on many modern documents specifically for this purpose. Either way, the password is low entropy. The difference is entirely in what PACE does with it.
At a high level the handshake runs in three moves. First, a mapping step: the chip picks a random nonce, sends it encrypted under a key derived from the password, and both sides combine that nonce with a fresh Diffie-Hellman exchange to produce a new, ephemeral group generator. ICAO Doc 9303 Part 11 defines more than one way to do this, notably Generic Mapping (the decrypted nonce is combined with an anonymous Diffie-Hellman result to map the generator) and Integrated Mapping. Second, both sides run a Diffie-Hellman (or ECDH) key agreement over that freshly mapped generator. Third, they derive the session keys and exchange authentication tokens to confirm that both ends arrived at the same keys.
The property that matters is this: the strength of the resulting session keys does not depend on the entropy of the password. A passive observer holding a full transcript cannot test password guesses against it offline, because each candidate would require its own live Diffie-Hellman exchange. That is the design goal stated in the formal analysis behind the protocol, and it is precisely the attack that sinks BAC. PACE does not make a weak password magically unguessable in an online sense, a reader that can repeatedly probe the chip is still bounded by how fast the chip responds, but it removes the offline recovery that makes BAC fragile. Modern documents also pair PACE with AES-based Secure Messaging rather than BAC's 3DES.
When each applies: negotiation and fallback
A reader does not get to choose freely; it negotiates based on what the chip supports. The chip exposes a small file, EF.CardAccess, that can be read before any authentication. It carries the chip's security parameters, including which PACE variants and domain parameters it supports. The reader reads that file first and decides:
sealed interface AccessControl {
data object Pace : AccessControl // chip advertises PACE in EF.CardAccess
data object Bac : AccessControl // legacy fallback, keys derived from the MRZ
}
/**
* EF.CardAccess is readable without authentication. If the chip publishes
* PACE parameters there, prefer PACE; otherwise fall back to BAC.
*/
fun selectAccessControl(cardAccess: ByteArray?): AccessControl =
if (cardAccess != null && cardAccess.isNotEmpty()) AccessControl.Pace
else AccessControl.Bac
(In real code you would parse EF.CardAccess into its SecurityInfos and check for a PACEInfo entry rather than just testing for a non-empty file, but the decision shape is the same.)
The rollout followed this negotiate-and-fall-back logic. ICAO groups PACE under Supplemental Access Control (SAC), and for global interoperability the rule has been that a chip must not implement PACE without also implementing BAC, so that an older reader can still get in. That interoperability requirement carried an ICAO transition sunset (commonly cited as 1 January 2018), after which PACE-only chips are permitted. The European Commission made PACE mandatory for travel documents issued from the end of 2014, with full compliance across member states from 2015. The long-term direction is unambiguous: PACE is the default, and BAC is being phased out. If you are writing a reader today, you implement PACE first and keep BAC only for documents old enough to need it.
What it means for an Android NFC reader
On Android the mechanics are the same regardless of which protocol you end up running. You put the adapter into reader mode, get an IsoDep connection, and exchange APDUs. Reader mode is the right choice in production because it stops the system and other NFC apps from interfering mid-scan:
import android.app.Activity
import android.nfc.NfcAdapter
import android.nfc.NfcManager
import android.nfc.tech.IsoDep
class PassportReader(private val activity: Activity) {
// getDefaultAdapter is deprecated since API 29; prefer NfcManager.
private val adapter: NfcAdapter? =
activity.getSystemService(NfcManager::class.java)?.defaultAdapter
fun startReading(onConnected: (IsoDep) -> Unit) {
adapter?.enableReaderMode(
activity,
{ tag ->
IsoDep.get(tag)?.use { isoDep ->
isoDep.timeout = 10_000
isoDep.connect()
onConnected(isoDep)
}
},
NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
null,
)
}
fun stopReading() {
adapter?.disableReaderMode(activity)
}
}
Inside onConnected the order is fixed: read EF.CardAccess, pick the access-control protocol, run the handshake to establish Secure Messaging, and only then start selecting and reading data groups. Skip the handshake and the chip simply refuses every read. For the parsing and verification that follow, open-source libraries like jMRTD are a solid reference, and the reading and trust steps are the subject of the eID post linked earlier.
A practical note from the field: do not treat the access-control step as a single pass/fail. "Could not reach the chip," "handshake failed," and "data read but verification failed" are three completely different problems with different fixes, and your diagnostics need to tell them apart. That discipline, treating every security signal as something you can explain rather than a black-box boolean, is the same one I argue for in the SecureCheck write-up.
Closing
BAC versus PACE is not just old versus new. It is a compact lesson in cryptographic hygiene: never derive a long-lived secret from short, printed, guessable data, because entropy you cannot see is entropy an attacker will find. PACE keeps the convenience of a password the holder already has while making the session keys independent of how weak that password is. Build your reader to prefer PACE, keep BAC only for compatibility, and above all keep access control (who may read) cleanly separated from authentication (whether to trust what you read). Get that separation right and the rest of an eID flow becomes far easier to reason about.