Skip to main content

License Python codecov PyPI version Build status Documentation stars lastcommit numcontributors

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.

Documentation · API reference

Requirements

  • Python 3.10 or higher; tested on 3.10 to 3.14
  • A PC/SC smart card reader. On Linux, run the pcscd service; building pyscard from source also needs libpcsclite-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 .sw
  • InvalidStateError: a precondition isn't met, such as no secure channel or PIN not verified
  • UntrustedCardError: the card's Identity Certificate isn't signed by a trusted CA
  • InvalidResponseError: the card's response is malformed
  • TransportError: 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)

Source distribution for keycard 0.4.0
File Size Uploaded
keycard-0.4.0.tar.gz 38.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for keycard 0.4.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page