Calypso Cards embrace PKI: a practical demo with SpringCard couplers and Python

Public transport cards are going PKI

In a previous article, we introduced the MIFARE DESFire DuoX in its VDE EV configuration for secure e-mobility services. That demo illustrated a broader trend: asymmetric cryptography schemes are gaining ground over symmetric schemes. With ECC now fully mature, and silicon costs low enough to run ECDSA and/or ECDH efficiently in contactless chips, the identification industry seems to be on the verge of a massive shift from shared-secret security to PKI-based solutions.

The Calypso specification, well-known for its use in transit ticketing systems, is part of this wave. Since 2022, the specification integrates a PKI-based security model, allowing cards to carry their own certificates, signed by an issuing authority, enabling stronger authentication, scalable trust, and interoperability with modern infrastructures.

In this article, we’ll walk through this PKI feature of modern Calypso cards, and the related Account-Based Ticketing (ABT) approach, before digging into the concrete implementation of the ECC-authenticated card transaction and the validation of the certificate chain.

From Symmetric Keys to PKI and ABT

Traditionally, the security of Calypso cards relied on symmetric cryptography, i.e. a secret key that is shared between the card and the terminal. Since the terminal itself is — generally speaking — not a secure device, its copy of the shared secret key stays in a dedicated smart card, the Calypso Secure Access Module (SAM).

While efficient, this approach has well-known limitations:

  • Key distribution and management is heavy; trust domains are limited,
  • The logistics and the management of the SAM is a burden,
  • The cost of the (contact) smart card reader function to handle the SAM, and the cost of the SAM itself, add to the global price of the terminal,

With the PKI extension of Calypso, each card carries its own public/private keypair and a certificate (CardCert) signed by a trusted certification authority. The certificate of this authority (CACert) is in turn signed by Calypso Networks Association’s CA, that acts as the root of trust in the infrastructure.

This allows:

  • Authentication without sharing secrets across domains,
  • Verifiable chain of trust (Calypso → CA → Card),
  • Easier integration with IT systems already relying on PKI,
  • Removal of the SAM in many use cases, dramatically reducing costs and complexity.

According to the Calypso Networks Association, the PKI-enabled Calypso Prime cards are designed to support both traditional card-centric infrastructures and modern Account-Based Ticketing (ABT) systems where traveller’s settings and entitlements are stored centrally.

In the case of non-transit applications (e.g. car park, bike sharing, EV charging, etc.), PKI makes it possible to verify the authenticity of the Calypso card without a SAM, and potentially without any local processing since the transaction log and the certificates can be transferred to the backend for asynchronous processing. This ultimately provides the expected level of security, without the complexity and the cost of a high-end terminal.

Demo Scenario

A Calypso Prime PKI-enabled card on a SpringCard M519-SRK.

Why Python and PC/SC?

For this demo we deliberately chose Python together with the standard PC/SC interface to interact with SpringCard couplers. Of course, all SpringCard’s contactless PC/SC couplers (like Prox’N’RollM519SpringParkPUCK, etc.) are fully compatible with PKI-enabled Calypso Prime cards. The demo has been tested with Thales (formerly Gemalto) Calypso Prime G3. The above illustration picture shows a M519 OEM module mounted on the M519-SRK, but it would work the same with any other SpringCard coupler.

Python makes it easy to write concise, readable code that demonstrates the algorithm step by step. But what matters most here is the algorithm itself: the signature verification logic is independent of the language or platform. If tomorrow the market requires it, the very same logic could be transposed to C and embedded directly into a reader’s firmware. Another option would be to run it on an embedded Linux platform (for example, a Raspberry Pi), using an optimized variant of the Python script.

Remark: we could also have built the demo with Eclipse Keyple — the open-source SDK maintained by the Calypso Networks Association. Available in Java and C++, Keyple is a great framework for application developers, but it is less suited to a future migration into resource-constrained embedded systems. That is why we chose to show the algorithm in its most straightforward form here.

The Calypso PKI Transaction

Those who are already familiar with the Calypso specification will feel right at home.
The PKI transaction begins like a “classic” transaction with the Open Secure Session command. The difference lies in the P2 parameter, which allows the card to be asked to provide an ECC signature at the end of the session, instead of the traditional CMAC exchange. The challenge (a random number that makes the transaction dynamic in order to prevent a replay attack) is provided by the reading application instead of being provided by the SAM.

In our example, we take advantage of the session opening to read the Environment file, which provides, among other things, the network number (i.e., the ID of the public transport authority managing the card) and its expiration date. We then move on, exactly as in CNA application note TN325, to reading the Contract List and a Contract record. This is where we expect to find either a valid weekly or monthly subscription, or individual transport tickets ready to be used.

In order to avoid cluttering up the code unnecessarily, we do not process the contents of these files in any way; our goal in this demo is only to verify that the card is authentic. Obviously, in a real-world application, the developer would also need to ensure not only that the card is authentic, but also that it contains the right data to authorize its use at that place and at that time.

After reading what interests us, we close the session with the Close Secure Session command. The card then returns its session signature, calculated over all APDUs exchanged during the session (both commands and responses).

Remark: just as the CMAC in a “classic” Calypso transaction, this signature provides a proof of integrity and authenticity of the communication at the session level (OSI layer #5). It is not, however, a proof of integrity of the stored data itself.

Obtaining the Card Public Key (CardPub) and the Certificates (CardCert and CACert)

Before going any further, we must obtain the keys and certificates that will allow us to validate the card’s authenticity.

This is done using the ISO/IEC 7816-4 GET DATA command, which is fairly generic: you specify in P1 and P2 the identifier of the object you want to retrieve, and the card returns it in the response (wrapped in a TLV, because why make life too simple?).

One important detail: both CardCert and CACert are longer than what a short APDU can carry in a single block. Since extended APDUs are not used in Calypso, each certificate must be read in two steps — effectively chaining two APDUs per certificate.

At the end of the process, the tally looks like this:

  • The Calypso transaction itself costs 4 APDUs (or 5 if we include the initial SELECT APPLICATION command) and we’ve received 189 bytes (230 with the response to the initial command).
  • Retrieving the card’s public key (CardPub) and the two certificates (CardCert and CACert) adds 5 more APDUs and about 784 extra bytes on the wire (well… on the air).

This overhead may sound significant, but it is the cryptographic anchor of trust:

  • CardPub is the card’s ECC public key; we need it to verify whether the (dynamic) signature of the session,
  • CardCert proves that this CardPub has not been tampered since the card was issued,
  • CACert links CardCert back to the Calypso issuing authority, thereby establishing the PKI trust chain.

Verifying the Session Signature against CardPub

At the end of the secure session, the Calypso card returns an ECDSA signature that covers all the APDUs exchanged during the session. This signature is generated with the card’s private key. To check it, the terminal must re-assemble all commands and responses into a canonical byte sequence and then verify the signature using the Card Public Key (CardPub) retrieved earlier using GET DATA.

In our Python demo, we start by concatenating all the inputs and outputs of the session, with length prefixes as required by the specification. The result is a single INPUT buffer that represents the session transcript.

The heavy lifting is done by the function verify_signature_of_session(). It takes as input the session transcript, the raw ECDSA signature (r||s), and the card’s public key (X||Y). It reconstructs the signature in DER format, instantiates the EC public key on curve P-256 (aka secp256r1 or prime256v1), and runs a standard ECDSA(SHA-256) verification:

This way, the scripts validates that the signature produced by the card is indeed correct for the session transcript.

Remark: this is plain, standard ECDSA P-256 / SHA-256, exactly as specified by ANSI X9.62, NIST (FIPS 186-4) and SEC 1 v2.0. The same algorithm is available out-of-the-box in all major cryptographic libraries such as OpenSSL, BouncyCastle (Java/.NET), Apple CryptoKit, Python cryptography, etc. Any developer can therefore verify the signature using standard tooling, without Calypso-specific code.

Verifying CACert and CardCert – Principles

At this stage, we know that the signature was actually produced by the private key that corresponds to the public key provided by the card. But this in itself is not enough: any card could generate such signature with its own key pair. We now need to unroll the certificate chain, to determine whether or not we can trust the card that owns this private key/public key pair.

This part of the process is slightly unusual — at least compared to what most developers are used to in the IT world. In a classic PKI system with X.509 certificates, the public key to be trusted is explicitly carried in the certificate itself, and verification means climbing upwards from the leaf certificate to the root of trust.

In Calypso PKI, there are a few specificities:

  • The certificates are not X.509. They follow a dedicated format, defined in Calypso specifications, consistent with other Calypso datasets that rely on fixed-length structures as per ASN.1 PER (Packed Encoding Rules).
  • All certificates are based on 1024-bit RSA signatures (even if the leaf key pair — the card’s own — is ECC). This is why the certificates are too large to fit into a short APDU and must be retrieved in multiple chunks.
  • The certificate signature algorithm is a “low-level” scheme based on ISO/IEC 9796-2, where the public key (or a part of it) must be recovered during the verification process.
  • Because the signed public key is embedded inside the signature of the certificate (instead of being available in cleartext as in X.509) we needed a dedicated GET DATA command to retrieve CardPub directly from the card, instead of reading it immediately from CardCert.
  • Likewise, the public key of the CA that has signed CardCert (CAPub) is not directly available in CACert. In a classical X.509 chain, we would extract CAPub from CACert, then use it to verify CardCert, and only afterwards verify CACert against the upper-level root CA. In Calypso PKI, the logic is reversed: we must start from the root public key (known a priori), use it to verify CACert and recover CAPub, and only then verify CardCert and recover CardPub. Finally, we compared this recovered CardPub with the one retrieved earlier, to prevent impersonation of the card.

Remark: These design choices may look a bit exotic, since most PKI ecosystems rely on the more familiar PKCS#1 v1.5 or PKCS#1 v2.2 RSA-PSS standards. But there are a least two good reasons that could be invoked to justify this choice:

  • ISO/IEC 9796-2 is an international standard (de jure) whereas PKCS remains essentially a de facto standard,
  • ISO/IEC 9796-2 is efficient in terms of bytes exchanged over the medium, since part of the message can be recovered directly from the signature, instead of being transmitted explicitly — which matters in contactless ticketing where every millisecond counts.

Verifying CACert and CardCert – Implementation

Now that we’ve understood the principles, it’s time to implement the actual verification! In our Python demo, this is done in three stages:

1. Process CACert

  • We drop the TLV header and parse the certificate to fetch all its fields.
  • We then call verify_cacert(), which applies the ISO/IEC 9796-2 RSA verification against the root public key (RootPub) that is, of course, public, and supposed to be known by all the terminals.
  • If the verification succeeds, we not only confirm the authenticity of CACert but also recover CAPub, the CA’s RSA public key that will be used at the next step.

2. Process CardCert

  • Similarly, we drop the TLV header and parse the certificate.
  • We call verify_cardcert(), which applies ISO/IEC 9796-2 again, this time using CAPub.
  • The verification produces two results: (a) confirmation that CardCert is valid and (b) recovery of the card’s ECC public key (CardPub).

3. Consistency check

  • Finally, we compare the recovered CardPub with the value retrieved earlier using the GET DATA command.
  • If both match, we know the card’s ECC keypair is genuine and belongs to a card certified by the Calypso PKI.

The heart of this process is the verify_certificate_iso9796_2() function, which implements the RSA “decryption” step (sig^e mod n), checks the trailer field (0xBC), extracts H*, regenerates D* through MGF1-SHA256, and compares H’ with H*. If everything matches, the certificate is authentic, and the M1* portion of the recovered data gives us the missing part of the message, e.g. the public key asserted by this certificate.

Download

The calypso-pki-example.py Python script is here:

(the extension is .txt to please some paranoid security tools, change for .py and chmod +x to allow execution)

The root public keys are taken from Calypso TN #021, and the script has a (huge) self-test part based on the vectors provided in Calypso TN #235.

Conclusion

PKI-enabled Calypso cards remove the need for a SAM in many scenarios, which lowers costs and simplifies the design of the reader and its associated host, with no compromise on security.

This demo shows that PKI in Calypso is not only a theoretical specification, but something that can be implemented and used today with any SpringCard coupler, open-source tools and standard crypto libraries.

Leave a Comment

Prove your humanity: 6   +   2   =