<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kazuru</title>
    <description>The latest articles on DEV Community by Kazuru (@kazuru_73322ef9a7d6ed2b18).</description>
    <link>https://dev.to/kazuru_73322ef9a7d6ed2b18</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4014111%2F923ced4c-8fe9-450f-8224-d2ddb3524792.png</url>
      <title>DEV Community: Kazuru</title>
      <link>https://dev.to/kazuru_73322ef9a7d6ed2b18</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kazuru_73322ef9a7d6ed2b18"/>
    <language>en</language>
    <item>
      <title>Differential Testing Revealed What Conformance Testing Missed: A Case Study with Open-Source eMRTD Libraries</title>
      <dc:creator>Kazuru</dc:creator>
      <pubDate>Tue, 07 Jul 2026 21:06:27 +0000</pubDate>
      <link>https://dev.to/kazuru_73322ef9a7d6ed2b18/differential-testing-revealed-what-conformance-testing-missed-a-case-study-with-open-source-emrtd-1nie</link>
      <guid>https://dev.to/kazuru_73322ef9a7d6ed2b18/differential-testing-revealed-what-conformance-testing-missed-a-case-study-with-open-source-emrtd-1nie</guid>
      <description>&lt;p&gt;Conformance suites tell us whether a library passes its own tests. They do not tell us whether two independent implementations make the same decision when presented with identical protocol traffic. &lt;a href="https://www.icao.int/publications/doc-series/doc-9303" rel="noopener noreferrer"&gt;ICAO Doc 9303&lt;/a&gt; defines how an e-passport should behave. But the open-source library you choose determines what your application actually experiences. Libraries pass unit tests. Integrators pass interoperability tests. At some point, we stopped asking a much simpler question: if two independent implementations receive exactly the same protocol traffic, will they make the same security decision?&lt;/p&gt;

&lt;p&gt;We set out to find an answer. Our approach was a controlled experiment built around a deterministic synthetic protocol profile, with no passport on our desks, no NFC reader, and no RF variables to chase. We replayed the same protocol exchange against two widely used wire-tier eMRTD readers: &lt;a href="https://github.com/gmrtd/gmrtd" rel="noopener noreferrer"&gt;gmrtd&lt;/a&gt; in Go and &lt;a href="https://jmrtd.org" rel="noopener noreferrer"&gt;JMRTD&lt;/a&gt; in Java.&lt;/p&gt;

&lt;p&gt;What we found was not dramatic, but it was revealing. PACE failed during the initial negotiation. BAC then completed successfully. For an application that merely checks whether a secure session exists, there was no explicit indication that a protocol fallback had occurred. Both libraries produced the same observable behaviour under the same profile.&lt;/p&gt;

&lt;p&gt;The experiment was also entirely reproducible. We could rerun it later the same afternoon and obtain the same result, backed by the same traces and run metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  BAC, PACE, and why we bothered with differential testing
&lt;/h2&gt;

&lt;p&gt;If you have shipped TLS, you already know what negotiation failure feels like at the integration layer. eMRTD access control is the same shape of problem on a contactless chip.&lt;/p&gt;

&lt;p&gt;BAC (Basic Access Control) is the older path: session keys from MRZ data. PACE (Password Authenticated Connection Establishment) is what newer documents advertise in EF.CardAccess before authentication, and what auditors increasingly expect terminals to prefer when it is on offer. Most chips still speak both. That is normal.&lt;/p&gt;

&lt;p&gt;The interesting part is reader behaviour when PACE fails. Does the stack stop and report? Or does it try BAC and hand your application something that looks like a clean open session?&lt;/p&gt;

&lt;p&gt;Conformance testing asks whether each library passes its own checklist. Differential testing fixes the chip behaviour and asks what each library tells the application. We are interested in the second question.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we were trying to learn
&lt;/h2&gt;

&lt;p&gt;The hypothesis, in plain language: if two independent implementations see identical protocol traffic, should a typical application caller see the same outcome?&lt;/p&gt;

&lt;p&gt;We deliberately kept the first cut small. Two libraries, not five. One synthetic downgrade: PACE advertised, first PACE APDU returns &lt;code&gt;6FFF&lt;/code&gt;, BAC mutual authentication still works. One smoke run per library. This essay is one concrete case per stack, not a prevalence study.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the experiment is set up
&lt;/h2&gt;

&lt;p&gt;The chip is a JSON profile. A simulator answers APDUs from that profile in-process, with no silicon and no RF for this test. Each driver calls the real library negotiation path against the same byte stream. Whatever comes back, whether return value, session fields, or APDU log, gets written to a trace file.&lt;/p&gt;

&lt;p&gt;The profile we used is called &lt;code&gt;pace-then-bac-downgrade&lt;/code&gt;. The important fields look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TC-AC-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pace_fail_then_bac"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"injection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pace_fail_on"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"first_pace_apdu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pace_sw"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6FFF"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EF.CardAccess advertises PACE before authentication (our profile models this via &lt;code&gt;card_access_hex&lt;/code&gt;). The first PACE step dies with &lt;code&gt;6FFF&lt;/code&gt;. BAC still completes. That is the entire adversarial condition.&lt;/p&gt;

&lt;p&gt;We also score each run for whether a naive integration would notice the downgrade. Score 0 means the failure never has to surface to code that only checks "did it open?" Score 2 means the caller cannot proceed without handling the failure. For this post, score 0 is the whole story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the experiment
&lt;/h2&gt;

&lt;p&gt;The test case is TC-AC-01. We performed one execution per library. We cared about the JSON trace: &lt;code&gt;run_id&lt;/code&gt;, the APDU arrays, &lt;code&gt;bac_success&lt;/code&gt;, &lt;code&gt;observability_score&lt;/code&gt;, and whatever error surface each library actually exposes.&lt;/p&gt;

&lt;p&gt;On the wire, both readers should tell the same chip story. MSE:Set AT starts PACE; the chip returns &lt;code&gt;6FFF&lt;/code&gt;; GET CHALLENGE and EXTERNAL AUTHENTICATE still finish with &lt;code&gt;9000&lt;/code&gt;. The BAC ciphertexts differ run to run because each library generates its own random numbers. The failure point and the fallback should not differ.&lt;/p&gt;

&lt;p&gt;Each driver writes &lt;code&gt;logs/&amp;lt;run_id&amp;gt;.json&lt;/code&gt;. At pin &lt;code&gt;blog-b10-2026-07&lt;/code&gt;, &lt;code&gt;run_id&lt;/code&gt; uses microsecond UTC time plus a monotonic sequence suffix — pattern &lt;code&gt;TC-AC-01-{library}-YYYYMMDDTHHMMSS.microsecondsZ-NNNNNN&lt;/code&gt; (for example &lt;code&gt;TC-AC-01-gmrtd-20260707T135005.482913Z-000001&lt;/code&gt;). We captured smoke traces the afternoon we drafted this post; a colleague reran &lt;code&gt;make smoke&lt;/code&gt; on a fresh lab clone at the same pin and got the same outcome (&lt;code&gt;observability_score: 0&lt;/code&gt; on both drivers) with different &lt;code&gt;run_id&lt;/code&gt; values, as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What showed up on the wire
&lt;/h2&gt;

&lt;h3&gt;
  
  
  gmrtd
&lt;/h3&gt;

&lt;p&gt;gmrtd records the PACE failure on the session object. It does not fail the call your service is likely to treat as the success boundary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;docEx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apduLog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;atr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ats&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ReadDocument err: %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// &amp;gt;&amp;gt;&amp;gt; nil&lt;/span&gt;

&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PACE err: %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;docEx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaceErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// &amp;gt;&amp;gt;&amp;gt; [DoPACE] doApduMseSetAT error: ... MSE:Set AT failed (Status:6fff)&lt;/span&gt;

&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BAC success: %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;docEx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BacResult&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;docEx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BacResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// &amp;gt;&amp;gt;&amp;gt; true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PaceErr&lt;/code&gt; is populated. The APDU log shows &lt;code&gt;6fff&lt;/code&gt; before the BAC commands. &lt;code&gt;err&lt;/code&gt; is nil. If your handler only checks the return value, you log a successful read after a failed PACE and move on.&lt;/p&gt;

&lt;p&gt;From our gmrtd smoke trace (&lt;code&gt;logs/TC-AC-01-gmrtd-*.json&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C: 0022c1a412800a04007f0007020204020483010184010d
R: 6fff

C: 0084000008
R: 4608f919887022129000

C: 0082000028d7b3b9d55cb313556804d6afedd55db24f0e6389dc7403dbd4d4be510d784711fcafd7dfd68a387628
R: 46b9342a41396cd7d74fff620ba529aa76ed1d79623747d5383fd073b453010e3a6c7e2c59e3ea3e9000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Observability score: 0.&lt;/p&gt;

&lt;h3&gt;
  
  
  JMRTD
&lt;/h3&gt;

&lt;p&gt;JMRTD is louder at the library boundary. It throws &lt;code&gt;PACEException&lt;/code&gt; on &lt;code&gt;6FFF&lt;/code&gt;. Production glue is often quieter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;PACEException&lt;/span&gt; &lt;span class="n"&gt;paceFailure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;doPACE&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bacKey&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paceInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getObjectIdentifier&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
        &lt;span class="nc"&gt;PACEInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toParameterSpec&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paceInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getParameterId&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PACEException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;paceFailure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// SW = 0x6fff&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendSelectApplet&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;doBAC&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bacKey&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have seen this pattern enough times to treat it as the default risk, not a corner case: catch, maybe log at DEBUG, call &lt;code&gt;doBAC()&lt;/code&gt;, return &lt;code&gt;authenticated: true&lt;/code&gt;. The experiment models that wrapper on purpose.&lt;/p&gt;

&lt;p&gt;From our JMRTD smoke trace (&lt;code&gt;logs/TC-AC-01-jmrtd-*.json&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C: 0022c1a40f800a04007f00070202040204830101
R: 6fff

C: 00a4040c07a0000002471001
R: 9000

C: 0084000008
R: 4608f919887022129000

C: 008200002892e5c592d2995c4f76a0e0b7f71e8d915f28e27a5a6bc11aa5706ddc781e05cbd3f6534575ebc19b28
R: 46b9342a41396cd798d2cc8b293b4fb78abfd26d4ef5bd5bd8475bf0f290ed59325904a018b7d5419000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same chip. Different API shape. Same gap if the application only asks whether the session opened. Observability score: 0 with the catch-and-continue wrapper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we think integrators should care
&lt;/h2&gt;

&lt;p&gt;When PACE fails and BAC succeeds, the stack has accepted a weaker mechanism than the chip advertised. Your middleware may record &lt;code&gt;authenticated: true&lt;/code&gt; and never store that PACE was attempted and rejected. SIEM rules built on session success will not see it. Policy that says "use PACE when EF.CardAccess offers it" cannot enforce what your API never surfaces.&lt;/p&gt;

&lt;p&gt;If you have ever searched a vendor codebase for &lt;code&gt;doBAC&lt;/code&gt; after a failed &lt;code&gt;doPACE&lt;/code&gt;, you know the feeling. The README says the library supports PACE. Your wrapper might still treat BAC success as mission accomplished. Audit the tag you pinned, not the marketing page.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fix that is boring on purpose
&lt;/h2&gt;

&lt;p&gt;The failure mode is implicit downgrade. The fix we care about is equally boring: do not fall back unless fallback is an explicit policy decision.&lt;/p&gt;

&lt;p&gt;On gmrtd that means surfacing &lt;code&gt;PaceErr&lt;/code&gt; on the return path, not burying it on the session struct. On JMRTD it means rethrowing &lt;code&gt;PACEException&lt;/code&gt; instead of catch-and-continue. In pseudocode the rule is simply: if PACE failed and policy does not allow BAC fallback, return an error; do not silently open BAC and call it success.&lt;/p&gt;

&lt;p&gt;We wired a thin middleware layer in the experiment repo to show the before and after. Baseline drivers score 0. Mitigated drivers force the caller to handle PACE failure. Nothing clever, just separating protocol outcome from transaction success.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same story elsewhere
&lt;/h2&gt;

&lt;p&gt;Passports are a convenient place to run this because the open source readers exist and the specification is public. The pattern is not passport-specific. Long-lived hardware with short-lived software on top shows up in PIV, national eID, payment terminals, anywhere a secure element outlives the middleware version you pinned last quarter. TLS cipher negotiation and SSH KEX fallback rhyme with the same question: which mechanism actually ran?&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;Synthetic traces only, with no RF lab, no real chip silicon, no live PKD or CRL infrastructure. Two named libraries at one deterministic condition, smoke depth, pinned reproduction commit. We are describing behaviour divergence, and we are not claiming a deployed exploit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it yourself
&lt;/h2&gt;

&lt;p&gt;This is the first place in the essay we point at the codebase.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kazuru-chidumbwe/emrtd-differential-harness" rel="noopener noreferrer"&gt;https://github.com/kazuru-chidumbwe/emrtd-differential-harness&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Checkout tag &lt;code&gt;blog-b10-2026-07&lt;/code&gt; (commit &lt;code&gt;ef15b10&lt;/code&gt;, 7 July 2026). That pin is what this post describes; &lt;code&gt;main&lt;/code&gt; may move on without changing the essay's claims.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/kazuru-chidumbwe/emrtd-differential-harness.git
&lt;span class="nb"&gt;cd &lt;/span&gt;emrtd-differential-harness
git checkout blog-b10-2026-07
bash scripts/bootstrap-vendor.sh
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GOTOOLCHAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;auto
make smoke
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should get JSON traces with &lt;code&gt;observability_score: 0&lt;/code&gt; on both baseline drivers. Look in &lt;code&gt;logs/&lt;/code&gt; — the &lt;code&gt;run_id&lt;/code&gt; field inside each file matches its filename. On Ubuntu 24.04 with Go 1.25+ (&lt;code&gt;GOTOOLCHAIN=auto&lt;/code&gt;), Java 17, and Maven, &lt;code&gt;make smoke&lt;/code&gt; finished in under a minute on a warmed lab VM where vendors were already bootstrapped. A cold first clone pays extra time for &lt;code&gt;bootstrap-vendor.sh&lt;/code&gt; and compiling JMRTD from vendor source; budget several minutes, not seconds.&lt;/p&gt;

&lt;p&gt;One practical wrinkle: the &lt;code&gt;jmrtd-0.5.2&lt;/code&gt; artifact on Maven Central is empty for our build. The bootstrap script compiles JMRTD from vendor source. Budget a few extra minutes the first time.&lt;/p&gt;

&lt;p&gt;MIT license. If you extend the work, cite the &lt;code&gt;run_id&lt;/code&gt; from your trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Two mainstream OSS passport readers. One synthetic downgrade. Both complete BAC after PACE fails. Both stay silent to a naive caller.&lt;/p&gt;

&lt;p&gt;If that matters in your stack, rerun the pin before you ship. The traces are public. The check takes minutes. We are not asking you to trust our summary — we are asking you to run the same profile and read your own &lt;code&gt;run_id&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;Further reading: &lt;a href="https://www.icao.int/publications/doc-series/doc-9303" rel="noopener noreferrer"&gt;ICAO Doc 9303&lt;/a&gt; · &lt;a href="https://github.com/gmrtd/gmrtd" rel="noopener noreferrer"&gt;gmrtd&lt;/a&gt; · &lt;a href="https://jmrtd.org" rel="noopener noreferrer"&gt;JMRTD&lt;/a&gt; · &lt;a href="https://doi.org/10.1145/2825026" rel="noopener noreferrer"&gt;Avoine et al., ePassport protocol survey&lt;/a&gt; · &lt;a href="https://www.bsi.bund.de/dok/TR-03110-en" rel="noopener noreferrer"&gt;BSI TR-03110&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Synthetic test environment only. No physical travel document was used. Results apply to gmrtd and JMRTD at TC-AC-01 smoke depth, tag &lt;code&gt;blog-b10-2026-07&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>cryptography</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
