When a chip-card payment terminal cannot or does not go online, it still wants some evidence that the card in front of it was issued by a real bank and that its data has not been tampered with. Static Data Authentication (SDA) was the simplest answer to that problem: the issuing bank signs some of the card’s fixed data once, and every terminal can check that signature offline. This article explains what SDA proves, why it cannot stop a copied card, how the dynamic methods (DDA and CDA) close that gap, and demonstrates the difference with a small, tested Node.js model.
Scope. This is a conceptual explanation for developers and security engineers, built from openly published research and standards (listed at the end). It is not an implementation specification. The exact record formats, key lengths and certification rules for payment cards are defined in specifications published by EMVCo under its own terms of use; they are not reproduced or paraphrased here, and the code below deliberately uses a format of its own.
Why a terminal authenticates the card offline
An online transaction can be checked by the issuer, which holds the secret keys needed to verify the card’s cryptogram. An offline decision has to be made by the terminal alone, using only public information. Public-key signatures make that possible: a terminal that trusts a small set of public keys can verify data signed by keys it has never seen before, as long as a chain of certificates connects them.
How SDA works
SDA uses a two-level chain:
Payment scheme CA key pair
| signs
v
Issuer public-key certificate ----- stored on the card
| (contains the issuer public key)
v
Issuer signature over selected static card data ----- stored on the card
(account number, expiry date and other fixed data)
Terminal: holds the scheme CA public keys
1. verify the issuer certificate with the CA public key
2. take the issuer public key out of the certificate
3. verify the static-data signature with the issuer public key
Everything the card contributes is fixed at the time the card is prepared. The card does not compute anything during the check; it only hands over stored data, and the terminal does all the cryptographic work. That is what made SDA cheap: it worked on chips that could not perform public-key operations themselves.
What SDA proves, and what it cannot
| Question | SDA’s answer |
|---|---|
| Was this data signed by an issuer the payment scheme vouches for? | Yes: the certificate chain ends at a CA key the terminal already trusts. |
| Has the signed data been altered since the issuer signed it? | Yes: changing any signed byte breaks the signature. |
| Is this physical card the one the issuer produced? | No: anyone who reads the stored data and signature can copy them to another chip. |
| Is the card taking part in this transaction, right now? | No: the same signature is presented every time, so it carries no freshness. |
The last two rows are the crux. Published research describes counterfeit cards (“yes cards”) that carried data copied from genuine SDA cards and simply answered the terminal’s questions in whatever way the attacker wanted. In offline transactions, where nobody checks the card’s cryptogram online, such cards were used in practice; France saw a wave of these attacks.
Why a static signature cannot provide freshness
A digital signature answers two questions: who signed the data, and whether the data has changed since. It says nothing about when the signed message was produced or who is presenting it. Protocols get freshness by adding something unpredictable to each run, a nonce such as a random challenge, a counter or a timestamp, and making the responder include it in what it signs. A recorded answer then fails against the next challenge. A replay attack is exactly the attempt to reuse such a recorded message, and SDA is defenceless against it because there is no challenge to answer: the “answer” is the same stored signature every time.
DDA and CDA: signatures made by the card
| SDA (static) | DDA (dynamic) | CDA (combined) | |
|---|---|---|---|
| Who signs during the transaction? | Nobody: the issuer signed once, in advance | The card, with its own private key | The card, with its own private key |
| What is signed? | Fixed card data | Dynamic data including a number chosen by the terminal | As DDA, plus transaction details such as the amount |
| Certificate chain | CA → issuer | CA → issuer → card | CA → issuer → card |
| Detects altered data | Yes | Yes | Yes |
| Detects a copy of the card’s data | No | Yes: a copy has no private key | Yes |
| Binds the card to this transaction | No | Partly: to the terminal’s challenge | Yes: to the challenge and the transaction data |
| Needs public-key operations on the chip | No | Yes | Yes |
Because the card’s private key never leaves the chip, reading the card no longer gives an attacker everything needed to impersonate it. Card authentication is only one part of a transaction’s security, though. Published attacks such as the “no-PIN” man-in-the-middle attack on cardholder verification work regardless of whether the card uses SDA or DDA.
Where SDA stands today
Ross Anderson’s Security Engineering describes SDA as the default card-authentication variant in many countries until about 2011, with DDA the current default. Murdoch and colleagues reported in 2010 that UK banks had issued SDA cards until 2009 and had begun issuing DDA cards since then. If you meet SDA today it will usually be in older cards, test material or legacy terminal configurations. Which methods a given card and terminal support, and in which order they are tried, is set by the payment schemes’ own rules.
A working model in Node.js
The module below models the three ideas on this page with Node.js’s built-in crypto (RSA signatures with SHA-256): a certificate authority vouching for an issuer key, an issuer signing static data once, and a card signing a fresh challenge with its own key. The “certificate” is a minimal structure of our own, not the format, padding or key size used on payment cards. Its purpose is to make the security argument executable.
// static-vs-dynamic.mjs — a conceptual model of static vs dynamic offline card authentication.
// Ambimat Electronics, 2026. Node.js 18+ (node:crypto). RSA PKCS #1 v1.5 signatures with SHA-256.
//
// This is a TEACHING MODEL, not a payment-card implementation: the "certificate" here is a minimal
// structure of our own, not the record format, padding or key sizes used by any card scheme.
import { generateKeyPairSync, sign, verify, createPublicKey, randomBytes } from 'node:crypto';
export const newKeyPair = () => generateKeyPairSync('rsa', { modulusLength: 2048 });
const spki = (publicKey) => publicKey.export({ type: 'spki', format: 'der' });
const certBody = (subject, der) => Buffer.concat([Buffer.from(subject, 'utf8'), Buffer.from([0]), der]);
// A certificate authority vouches for an issuer's public key by signing it.
export function issueCertificate(caPrivateKey, subject, subjectPublicKey) {
const publicKeyDer = spki(subjectPublicKey);
return { subject, publicKeyDer, signature: sign('sha256', certBody(subject, publicKeyDer), caPrivateKey) };
}
export function verifyCertificate(caPublicKey, cert) {
return verify('sha256', certBody(cert.subject, cert.publicKeyDer), caPublicKey, cert.signature);
}
// STATIC: the issuer signs the card's fixed data once, at personalisation. The card only stores the result.
export function signStaticData(issuerPrivateKey, staticData) {
return sign('sha256', staticData, issuerPrivateKey);
}
// What a terminal can check offline with nothing but the CA public key.
export function checkStatic(caPublicKey, card) {
if (!verifyCertificate(caPublicKey, card.issuerCert)) return 'FAIL: issuer certificate';
const issuerKey = createPublicKey({ key: card.issuerCert.publicKeyDer, format: 'der', type: 'spki' });
return verify('sha256', card.staticData, issuerKey, card.staticSignature) ? 'PASS' : 'FAIL: static data altered';
}
// DYNAMIC: the card holds its own private key and signs a fresh terminal challenge together with its data.
export function cardRespond(card, challenge) {
if (!card.privateKey) return null; // a copy of the stored data has no private key
return sign('sha256', Buffer.concat([challenge, card.staticData]), card.privateKey);
}
export function checkDynamic(caPublicKey, card, challenge, response) {
if (!response || !verifyCertificate(caPublicKey, card.issuerCert) || !verifyCertificate(card.issuerPublicKey, card.cardCert)) return 'FAIL';
const cardKey = createPublicKey({ key: card.cardCert.publicKeyDer, format: 'der', type: 'spki' });
return verify('sha256', Buffer.concat([challenge, card.staticData]), cardKey, response) ? 'PASS' : 'FAIL';
}
export const newChallenge = () => randomBytes(8);
Example: copying the card’s data
The demonstration builds one genuine card, then a copy containing everything that can be read from it (but not the private key, which never leaves a real chip), and then a copy whose expiry date has been edited:
// clone-demo.mjs — why a static signature cannot tell a genuine card from a copy of its data.
import * as m from '../static-vs-dynamic.mjs';
const ca = m.newKeyPair(), issuer = m.newKeyPair(), cardKeys = m.newKeyPair();
const staticData = Buffer.from('ACCOUNT=TEST-0001;EXPIRY=2812;HOLDER=TEST CARD'); // made-up values
const card = {
staticData,
issuerCert: m.issueCertificate(ca.privateKey, 'Example Issuer', issuer.publicKey),
staticSignature: m.signStaticData(issuer.privateKey, staticData),
issuerPublicKey: issuer.publicKey,
cardCert: m.issueCertificate(issuer.privateKey, 'Example Card', cardKeys.publicKey),
privateKey: cardKeys.privateKey, // never leaves a real chip
};
const copy = { ...card, privateKey: undefined }; // everything that can be read from the card
const tampered = { ...copy, staticData: Buffer.from(staticData.toString().replace('2812', '2912')) };
console.log('static check, genuine card :', m.checkStatic(ca.publicKey, card));
console.log('static check, copied data :', m.checkStatic(ca.publicKey, copy));
console.log('static check, edited expiry:', m.checkStatic(ca.publicKey, tampered));
const c1 = m.newChallenge(), c2 = m.newChallenge();
const r1 = m.cardRespond(card, c1);
console.log('dynamic check, genuine card:', m.checkDynamic(ca.publicKey, card, c1, r1));
console.log('dynamic check, replayed old answer to a new challenge:', m.checkDynamic(ca.publicKey, card, c2, r1));
console.log('dynamic check, copied data :', m.checkDynamic(ca.publicKey, copy, c2, m.cardRespond(copy, c2)));
Output:
static check, genuine card : PASS
static check, copied data : PASS
static check, edited expiry: FAIL: static data altered
dynamic check, genuine card: PASS
dynamic check, replayed old answer to a new challenge: FAIL
dynamic check, copied data : FAIL
The second line is the whole story of SDA: the copy passes the static check exactly like the genuine card. The edited copy fails, which is what SDA does protect against. Under the dynamic check, a recorded answer fails against a new challenge, and the copy cannot answer at all.
How the model was tested
The tests (Node.js built-in test runner) check that genuine data verifies through the chain, that a copy also verifies under the static check, that flipping any single bit of the signed data is detected, that an issuer key signed by the wrong CA or swapped inside the certificate is rejected, and that the dynamic check accepts a fresh answer but rejects a replayed one, a missing one and one signed with a different key.
// static-vs-dynamic.test.mjs — node --test static-vs-dynamic.test.mjs (SDA_IMPL=<path> tests another copy)
import { test } from 'node:test';
import assert from 'node:assert/strict';
const impl = process.env.SDA_IMPL ? new URL(process.env.SDA_IMPL, `file://${process.cwd()}/`).href : './static-vs-dynamic.mjs';
const m = await import(impl);
const ca = m.newKeyPair(), issuer = m.newKeyPair(), cardKeys = m.newKeyPair(), rogue = m.newKeyPair();
const data = Buffer.from('static application data');
const card = {
staticData: data,
issuerCert: m.issueCertificate(ca.privateKey, 'Issuer', issuer.publicKey),
staticSignature: m.signStaticData(issuer.privateKey, data),
issuerPublicKey: issuer.publicKey,
cardCert: m.issueCertificate(issuer.privateKey, 'Card', cardKeys.publicKey),
privateKey: cardKeys.privateKey,
};
const copy = { ...card, privateKey: undefined };
test('static: genuine data verifies through the CA -> issuer chain', () => assert.equal(m.checkStatic(ca.publicKey, card), 'PASS'));
test('static: a copy of the data verifies too (the limitation)', () => assert.equal(m.checkStatic(ca.publicKey, copy), 'PASS'));
test('static: any change to the signed data is detected', () => {
for (let i = 0; i < data.length; i++) {
const d = Buffer.from(data); d[i] ^= 0x01;
assert.equal(m.checkStatic(ca.publicKey, { ...card, staticData: d }), 'FAIL: static data altered');
}
});
test('static: an issuer key not certified by the CA is rejected', () => {
const fake = { ...card, issuerCert: m.issueCertificate(rogue.privateKey, 'Issuer', issuer.publicKey) };
assert.equal(m.checkStatic(ca.publicKey, fake), 'FAIL: issuer certificate');
const swapped = { ...card, issuerCert: { ...card.issuerCert, publicKeyDer: rogue.publicKey.export({ type: 'spki', format: 'der' }) } };
assert.equal(m.checkStatic(ca.publicKey, swapped), 'FAIL: issuer certificate');
});
test('dynamic: fresh challenge passes, replay and copy fail', () => {
const c1 = m.newChallenge(), c2 = m.newChallenge();
assert.notDeepEqual(c1, c2);
const r1 = m.cardRespond(card, c1);
assert.equal(m.checkDynamic(ca.publicKey, card, c1, r1), 'PASS');
assert.equal(m.checkDynamic(ca.publicKey, card, c2, r1), 'FAIL');
assert.equal(m.cardRespond(copy, c2), null);
assert.equal(m.checkDynamic(ca.publicKey, copy, c2, null), 'FAIL');
const forged = m.cardRespond({ ...card, privateKey: rogue.privateKey }, c2);
assert.equal(m.checkDynamic(ca.publicKey, card, c2, forged), 'FAIL');
});
Looking at a card’s authentication data as a developer
The certificates and signed data a card uses for offline authentication are ordinary data objects in its records, so you can read them with the same tools you use for any other card data: send the commands with an APDU library, strip the status bytes, and parse the BER-TLV structure. What you can do with them beyond that depends on access to the scheme CA public keys and the scheme specifications, and on the rules that govern handling card data. Verifying a production card’s chain is a job for certified terminal software, not for a quick script.
Common misconceptions
- “A valid signature means a genuine card.” It means genuine data. Under SDA the data can be copied.
- “SDA protects against skimming.” It protects against editing the signed data, not against copying it.
- “DDA makes a transaction secure.” It authenticates the card. Cardholder verification, the issuer’s online checks and terminal behaviour are separate questions.
- “Offline means unprotected.” With DDA or CDA an offline terminal can still detect a cloned card; the limit is SDA specifically.
Frequently asked questions
What does SDA stand for in chip cards?
Static Data Authentication: offline card authentication based on an issuer signature over fixed card data, verified by the terminal through a certificate chain.
What is the difference between SDA and DDA?
Under SDA the issuer signs once, in advance, and the card replays that signature. Under DDA the card holds its own private key and signs data that includes a value chosen by the terminal, so each answer is new and a copy of the card’s data cannot produce one.
Is SDA still used?
It is a legacy method: according to the sources above, DDA has been the default for years and banks moved away from SDA around 2009–2011. Check the current rules of the payment scheme concerned for what is accepted today.
Can I use this code to verify real cards?
No. It is a teaching model with its own simple certificate format. Real card certificates use formats defined in the payment specifications, and production verification belongs in certified terminal software.
Related reading on Ambimat
- Smart card APDU commands – the command and response layer underneath.
- Application discovery and selection on chip cards – what happens before authentication.
- How to parse BER-TLV data in JavaScript – reading card data objects.
- Payment and smart-card BER-TLV tag reference – looking up data-object tags.
Sources
- S. J. Murdoch, S. Drimer, R. Anderson, M. Bond, Chip and PIN is Broken, IEEE Symposium on Security and Privacy 2010, §II (card authentication: SDA, DDA, CDA) and §VI: cl.cam.ac.uk
- D. Basin, R. Sasse, J. Toro-Pozo, The EMV Standard: Break, Fix, Verify, IEEE Symposium on Security and Privacy 2021, §III (offline data authentication): arxiv.org/abs/2006.08249
- R. Anderson, Security Engineering, 3rd edition, chapter 12 “Banking and Bookkeeping” (§12.6.1, SDA, yescards, DDA, CDA) and chapter 4 “Protocols” (§4.3, nonces and freshness), free author copies: ch. 12, ch. 4
- NIST Computer Security Resource Center glossary, replay attack: csrc.nist.gov
- NIST FIPS 186-5, Digital Signature Standard (2023): nvlpubs.nist.gov; IETF RFC 8017, PKCS #1: RSA Cryptography Specifications v2.2, §8 signature schemes with appendix: rfc-editor.org
- Node.js documentation, Crypto (
sign,verify,generateKeyPairSync): nodejs.org/api/crypto.html
Written and tested by the Ambimat engineering team. The model and its tests were run on Node.js 25.8.2.