A minimal, clean, fully native Python SDK for communicating with Keycard smart cards. One KeyCard object works with applet 3.x and 4.x; it picks the matching secure channel when the card is selected.
Requirements
- Python 3.10 or higher; tested on 3.10 to 3.14
- A PC/SC smart card reader. On Linux, run the
pcscdservice; buildingpyscardfrom source also needslibpcsclite-dev.
Installation
pip install keycard
The package is published on PyPI as keycard (repo name is keycard-py).
From source
git clone https://github.com/mmlado/keycard-py.git
cd keycard-py
python -m venv venv
source venv/bin/activate
pip install -e .
pytest
Usage
Connect and select
from keycard import KeyCard
with KeyCard() as card:
card.select()
print(card.version) # e.g. (4, 0)
print(card.is_initialized)
KeyCard() uses the first PC/SC reader. For another reader, connect a Transport yourself:
from keycard import KeyCard
from keycard.transport import Transport
transport = Transport()
transport.connect(1)
card = KeyCard(transport)
The examples below continue with a selected card.
Open a secure channel: applet 4.x
When the secure channel opens, the card proves it is genuine with its Identity Certificate, which must be signed by a trusted Keycard CA. The official CA is trusted by default; any other card raises UntrustedCardError.
PIN = '123456'
PUK = '987654321098'
if not card.is_initialized:
card.init(PIN, PUK) # opens the secure channel itself
else:
card.open_secure_channel()
card.verify_pin(PIN) # False on a wrong PIN
init also takes pin_retries, puk_retries and an alt_pin. Without alt_pin, the card uses the first 6 digits of the PUK as the Alternate PIN. PIN verification tries the Alternate PIN first, so if the PUK starts with the PIN, verify_pin(PIN) unlocks the alternate keys instead of the main ones.
Development cards carry a certificate from the Test CA (see tools/load_test_identity.py). Trust it only during development, since anyone can sign with it:
from keycard import KeyCard, constants
card = KeyCard(ca_public_keys=[
constants.OFFICIAL_CA_PUBLIC_KEY,
constants.TEST_CA_PUBLIC_KEY,
])
Open a secure channel: applet 3.x
3.x cards need a pairing, a shared key stored in one of the card's pairing slots. Pair once with the pairing secret set at init, and keep the result for later sessions:
PAIRING_SECRET = 'my pairing secret'
if not card.is_initialized:
card.init(PIN, PUK, PAIRING_SECRET)
card.select()
pairing = card.pair(PAIRING_SECRET) # Pairing(slot, key): store it
card.open_secure_channel()
card.verify_pin(PIN)
In a later session, reuse the stored pairing instead of pairing again:
from keycard.pairing import Pairing
card.pairing = Pairing(slot, key)
card.open_secure_channel()
The card has a limited number of pairing slots; card.unpair(slot) frees one.
Keys
Generate a key on the card, or load one from a BIP-39 mnemonic. The passphrase goes into the seed, so the card never sees it:
from mnemonic import Mnemonic # pip install mnemonic
from keycard.constants import LoadKeyType
card.generate_key()
seed = Mnemonic.to_seed('<your twelve or twenty-four words>', passphrase='')
card.load_key(LoadKeyType.BIP39_SEED, bip39_seed=seed)
The card can also generate the mnemonic, returning word indexes:
indexes = card.generate_mnemonic()
words = ' '.join(Mnemonic('english').wordlist[i] for i in indexes)
Export a public key:
from keycard.constants import DerivationOption
exported = card.export_key(
DerivationOption.DERIVE, public_only=True, keypath="m/44'/60'/0'/0/0")
exported.public_key # 65-byte uncompressed point
Sign
from hashlib import sha256
digest = sha256(b'message').digest()
result = card.sign(digest) # the current key on 3.x, the master key on 4.x
result = card.sign_with_path(digest, "m/44'/60'/0'/0/0")
result.signature # r || s, 64 bytes
result.signature_der
result.recovery_id
result.public_key
BIP-340 Schnorr signatures (applet 4.x) take an optional 32-byte tweak that the card adds to the private key, such as a BIP-341 Taproot tweak. result.public_key is the untweaked key:
from keycard.constants import SigningAlgorithm
result = card.sign_with_path(
digest,
"m/86'/0'/0'/0/0",
algorithm=SigningAlgorithm.SCHNORR_BIP340,
tweak=tweak,
)
Applet 4.x features
card.get_challenge(32) # random bytes from the card
# BIP-85 entropy, here for a 12-word English mnemonic
card.export_bip85("m/83696968'/39'/0'/12'/0'", 16)
# Raw ECDH secret; run it through your protocol's KDF
card.ecdh(peer_public_key, "m/44'/1237'/0'/0/0")
ECDH works only under the NIP-44 (m/44'/1237') and EIP-1581 (m/43'/60'/1581') paths, at least 5 levels deep.
Loading a seed in LEE mode also derives the LEE keys:
card.load_key(LoadKeyType.LEE_SEED, bip39_seed=seed)
keys = card.export_lee("m/0'") # ask, nsk, vsk_d, vsk_z
Differences between 3.x and 4.x
| 3.x | 4.x | |
|---|---|---|
| Secure channel | Pairing, Secure Channel V1 | Secure Channel V2; the card is checked against trusted Keycard CAs |
Current key, derive_key, PIN-less signing |
Yes | No |
| Schnorr, LEE, BIP-85, ECDH, GET CHALLENGE | No | Yes |
Using a 4.x feature on a 3.x card raises APDUError with the card's status word. The pairing calls raise InvalidStateError on 4.x.
Errors
All errors derive from keycard.exceptions.KeyCardError:
APDUError: the card returned a failure; the status word is in.swInvalidStateError: a precondition isn't met, such as no secure channel or PIN not verifiedUntrustedCardError: the card's Identity Certificate isn't signed by a trusted CAInvalidResponseError: the card's response is malformedTransportError: no reader found
Example script
example/example.py walks through the commands on a 3.x or 4.x card. It factory-resets the card first, so use a test card. It needs the mnemonic package.
Development
pip install -e '.[dev]'
pytest
flake8 keycard tests
mypy keycard
Hardware tests run against a real card only when asked. KEYCARD_READER is the reader index; tests that factory-reset the card also need KEYCARD_DESTRUCTIVE=1:
KEYCARD_READER=0 pytest tests/hardware
KEYCARD_READER=0 KEYCARD_DESTRUCTIVE=1 pytest tests/hardware
4.x development cards need a Test CA certificate, provisioned with python tools/load_test_identity.py.
License
MIT
Contributions
Contributions are welcome as this SDK grows.
Metadata
Release files for keycard 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| keycard-0.4.0.tar.gz | 38.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| keycard-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 93.3 kB
Release files / keycard-0.4.0.tar.gz
| Download URL | keycard-0.4.0.tar.gz |
|---|---|
| Size | 38.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d38c935cd86cdf6c697480a69b1eed5295bad18f54e67621ad642cf1bd7ed2e2
|
|
BLAKE2b-256 checksum How to use checksums |
fee4f47b9d8fc3a20a8bdf149d56e1341e68c0cd9057d3b58c5c3967b5ca5611
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency logRelease files / keycard-0.4.0-py3-none-any.whl
| Download URL | keycard-0.4.0-py3-none-any.whl |
|---|---|
| Size | 55.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
92a9aea6b0645fd4d62be2e4241dede3e07bb47f506633788667b4c181ae7fbb
|
|
BLAKE2b-256 checksum How to use checksums |
e47f02a2348126a2c6e8721c566beffb0bf4e21b29c2cb3442a887124ea65104
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency log