Skip to content

vpelletier/python-smartcard-app-openpgp

Repository files navigation

OpenPGP smartcard application implementation.

It implements parts of the OpenPGP specification 3.4.1 .

Warning

THIS IS A WORK IN PROGRESS.

  • it may not be fully functional
  • future upgrades may bring changes incompatible with previous version's stored data
  • despite best attention, it may contain security holes:
    • it may allow access to unexpected pieces of data
    • cryptographic functions may contain bugs making decryption either impossible to one in posession of the decryption key, or trivial to one not in posession of the decryption key.
  • it may support weak cryptographic algorithms (weak hashes, weak elliptic curves, ...)

Fee free to play with it, review it and contribute. But DO NOT USE IT ON SENSIBLE OR VALUABLE DATA, and DO NOT IMPORT VALUABLE KEYS IN IT.

This code is in dire need of reviews and tests.

Installation

No extra hardware requirements

To get a standard card, with an executable setting up a gadget.

pip install smartcard-app-openpgp[ccid]

Then, you may set it up to automatically start on boot (assuming pip comes fom a virtualenv at /opt/smartcard-openpgp):

  • create a systemd service:

    [Unit]
    Description=Behave like a CCID + smartcard combo USB device
    
    [Service]
    ExecStart=/opt/smartcard-openpgp/bin/smartcard-openpgp-simple \
      --user smartcard-openpgp \
      --filestorage /srv/smartcard-openpgp/card.fs \
      --serial "%m"
    KillMode=mixed
    
    [Install]
    WantedBy=usb-gadget.target
  • create a system user, enable the systemd service, and start it:

    adduser --system --home /srv/smartcard-openpgp smartcard-openpgp
    chmod go= /srv/smartcard-openpgp
    systemctl enable smartcard-gadget.service
    systemctl start smartcard-gadget.service

USB-device-capable Raspberry Pi with IL3895/SSD1780-based ePaper displays

Tested on the raspi Debian port .

Expected screen resolution: 250x122, such as the WaveShare 2.13 inches e-Paper display V1, or the black-and-white Pimoroni Inky pHAT.

This extra hardware enables the use of a random PW1 PIN.

The e-Paper display presents a grid of random values. One cell (A1 by default) contains the valid PIN.

The cell containing the valid PIN can be changed by requesting a PW1 change, and providing a specially-formatted new password. For example: C30000 references cell C3, 2b0000 references cell B2. Trailing zeroes are ignored, but may be necessary to get the host's software to actually send the pin to the device.

The grid changes periodically, when the card is accessed but at most once every 30 seconds or when a verify command is run (whichever comes first), and both the currently-displayed (at the time the "verify" command runs ont the card) and the previous PIN are accepted as a correct PIN.

Key Derivation Function (KDF-DO) is not available in this mode.

usage is similar to the No extra hardware requirements variant, but starts with:

pip install smartcard-app-openpgp[ccid,randpin]

The executable is then called smartcard-openpgp-randpin-epaper rather than smartcard-openpgp-simple.

Screen layout

smartcard-openpgp-randpin-epaper screenshot

(support for data URLs is needed to see an image here - unfortunately github and pypi seem hostile to data URLs)

  • Top left corner: number of PW1 tries left. ○ are for tries left, ⨯ for tries used. Here, there are 2 tries left out of 3.
  • Left and top borders, white text on black background: row and column titles.
  • Main area: PIN grid. If this card uses the default pin cell, PW1 is 291413.
  • Top right corner (not present here): Battery level (from ■■■■ for a >90% charged battery to □□□□ for a <10% discharged battery). A lightning bolt logo is superimposed to the battery level when the battery is being charged.

External requirements

Beyond the installation/build requirements, the code expects the Noto Mono font to be located at /usr/share/fonts/truetype/noto/NotoMono-Regular.ttf:

apt-get install fonts-noto-mono

If you have a battery, it further expects systemd-logind to be reachable through DBus, where it will instruct it to not trigger the idle action while plugged to a host, allowing logind to be configured to automatically shut the system down independently from the battery level, thereby saving the battery from undergoing unnecessary charge/discharge cycles. For example, you may want to put the following in your `/etc/systemd/logind.conf`:

IdleAction=poweroff
IdleActionSec=15min

Limitations

The Raspberry Pi Zero has the USB Vbus pins bridged to the 5v power rail, which prevents the UDC from detecting bus disconnection. As a result, the display does not change when the Pi is disconnected from the host, and refreshes twice when reconnected.

Getting access to the screen

To configure the 40-pins connector correctly, you need to apply the following devicetree overlay:

// Enable SPI0 interface (board pins 19, 21, 23) and its chip-enable lines
//   (board pins 24, 26)
// setup GPIO 25 as output (data/command, board pin 22)
// setup GPIO 17 as output (rst, board pin 11)
// setup GPIO 24 as input (busy, board pin 18)
/dts-v1/;
/plugin/;

&gpio { // @7e200000
    #gpio-cells = <2>;
    alt0 {
        brcm,pins = <4 5>; // removed 7, 8, 9, 10, 11
    };
    spi0_cs_pins: spi0_cs_pins {
        brcm,function = <1>; // out
        brcm,pins = <7 8>;
    };
    spi0_pins: spi0_pins {
        brcm,function = <4>; // alt0
        brcm,pins = <9 10 11>;
    };
    epaper_pins {
        brcm,function = <1 0 1>; // out in out
        brcm,pins = <17 24 25>;
        brcm,pull = <0 2 0>; // none pull-up none
    };
};

&spi { // @7e204000
    #address-cells = <1>;
    #size-cells = <0>;
    cs-gpios = <&gpio 8 0x01>, <&gpio 7 0x01>; // CE0 is gpio 8, CE1 is gpio 7, both active low
    status = "okay";
    pinctrl-0 = <&spi0_cs_pins &spi0_pins>;
    pinctrl-names = "default";
    spidev@0 {
        #address-cells = <1>;
        #size-cells = <0>;
        // "waveshare,epaper-display-v1": because that's what it really is.
        // "rohm,dh2228fv": hack to get a spidev to this device.
        compatible = "waveshare,epaper-display-v1", "rohm,dh2228fv";
        reg = <0>; // uses CS0
        spi-max-frequency = <4000000>; // 4MHz: tcycle >= 250ns
    };
};
  • Compile it with the dtc command, which may be available from the device-tree-compiler package:

    ${KERNEL_SOURCE}/scripts/dtc/dtc -I dts -O dtb -o epaper2.13in.dtbo epaper2.13in.dts
  • (optional) check that the overlay is consistent with kernel's dtb using fdtoverlay from the device-tree-compiler package:

    fdtoverlay -i /boot/firmware/bcm2835-rpi-zero-w.dtb -o /dev/null epaper2.13in.dtbo

    If this emits any error, then you pi may not boot with this overlay. If this happens, plug the micro-sd card on a computer and comment-out the correspondig dtoverlay line in config.txt.

  • install the devicetree overlay (as root):

    mkdir -p /boot/firmware/overlays/
    cp epaper2.13in.dtbo /boot/firmware/overlays/
  • tell the raspberry pi stage 2 bootloader about both files, by adding to /etc/default/raspi-firmware-custom:

    dtoverlay=epaper2.13in.dtbo

Battery (UPS-Lite)

Tested on the raspi Debian port .

If you have a screen, then there is also optional support for a UPS-Lite battery.

Getting access to the battery

To configure the 40-pins connector correctly, you need to apply the following devicetree overlay:

// setup i2c1 dev 0x36 for use with max17040 kernel driver
// setup GPIO 4 as input (power source detect, board pin 7)
/dts-v1/;
/plugin/;

&gpio { // @7e200000
    #gpio-cells = <2>;
    alt0 {
        brcm,pins = <5>; // removed 4, 7, 8, 9, 10, 11
    };
    external_power {
        brcm,function = <0>; // in
        brcm,pins = <4>;
        brcm,pull = <0>; // no bias
    };
};

&i2c1 { // @7e804000
    #address-cells = <1>;
    #size-cells = <0>;
    battery@36 {
        compatible = "maxim,max17040";
        reg = <0x36>;
    };
};
  • Compile it with the dtc command, which may be available from the device-tree-compiler package:

    ${KERNEL_SOURCE}/scripts/dtc/dtc -I dts -O dtb -o zero_ups_lite.dtbo zero_ups_lite.dts
  • (optional) check that the overlay is consistent with kernel's dtb using fdtoverlay from the device-tree-compiler package:

    fdtoverlay -i /boot/firmware/bcm2835-rpi-zero-w.dtb -o /dev/null zero_ups_lite.dtbo

    If this emits any error, then you pi may not boot with this overlay. If this happens, plug the micro-sd card on a computer and comment-out the correspondig dtoverlay line in config.txt.

  • install the devicetree overlay (as root):

    mkdir -p /boot/firmware/overlays/
    cp zero_ups_lite.dtbo /boot/firmware/overlays/
  • tell the raspberry pi stage 2 bootloader about both files, by adding to /etc/default/raspi-firmware-custom:

    dtoverlay=zero_ups_lite.dtbo
  • check that you have the driver for the max17040_battery:

    grep CONFIG_BATTERY_MAX17040 "/boot/config-$(uname -r)"

    If you do not have this module, you can build it off-tree with dkms and a recent copy of the kernel source:

    mkdir /usr/src/max17040-0.1/
    echo 'obj-m := max17040_battery.o' > /usr/src/max17040-0.1/Makefile
    cat > /usr/src/max17040-0.1/dkms.conf <<EOF
    PACKAGE_NAME="max17040"
    PACKAGE_VERSION="0.1"
    BUILT_MODULE_NAME[0]="max17040_battery"
    MAKE[0]="make -C ${kernel_source_dir} M=${dkms_tree}/${PACKAGE_NAME}/${PACKAGE_VERSION}/build"
    CLEAN="make -C ${kernel_source_dir} M=${dkms_tree}/${PACKAGE_NAME}/${PACKAGE_VERSION}/build clean"
    DEST_MODULE_LOCATION[0]="/kernel/drivers/power/supply"
    REMAKE_INITRD=no
    AUTOINSTALL=yes
    EOF
    cp "${KERNEL_SOURCE}/drivers/power/supply/max17040_battery.c" /usr/src/max17040-0.1/
    dkms install max17040/0.1

For use as a module

Without optional dependencies (to use as a python module in your own projects, for example to assemble more complex gadgets).

pip install smartcard-app-openpgp

Usage

Initial PIN values:

  • PW1 (aka user PIN): 123456
  • PW3 (aka admin PIN): 12345678
  • Reset Code: (not set)

Initial key format:

  • sign, authenticate: RSA2048
  • decrypt: X25519

Threat model

In a nutshell:

  • the system administrator of the device running this code is considered to be benevolent and competent
  • the host accessing this device through the smartcard API (typically, via USB) is considered hostile
  • the close-range physical world surrounding the device is considered to be under control of the device owner

In more details:

This code is intended to be used on general-purpose computing modules, unlike traditional smartcard implementations. They cannot be assumed to have any hardening against physical access to their persistent (or even volatile) memory:

  • it is trivially easy to pull the micro SD card from a Raspberry Pi Zero {,W}
  • it is easy to solder wires on test-points between the CPU and the micros card on a Raspberry Pi Zero {,W} and capture traffic
  • on an Intel Edison u-boot may be configured with DFU enabled, which, once triggered, allows convenient read access to the content of any partition it is configured to access
  • electronic noise (including actual noise: coil whine) will leak information about what the CPU is doing
  • they have communication channels dedicated smartcard hardware does not have: WiFi, Bluetooth, TTY on serial (possibly via USB), JTAG...

So if an attacker gets physical access to them, their secrets should be considered fully compromised.

Further, some of these interfaces allow wide-range networking, which further opens the device to remote attackers.

The system configuration of the device on which this code runs is outside of the area of responsibility of this project.

Just like any general-purpose computer on which you would store PGP/GPG keys.

Origin story

To do my daily job I rely on the same cryptographic operations as any other sysadmin: ssh key-based authentication, mail signature and decryption. When faced with the perspective of having to use a machine I do not trust enough to give it access to the machines my ssh key has access to, nor to give it access to the private key associated with my email address, I started looking for alternatives.

So suddenly I needed another computer I trusted to hold those secrets, and go through it from the machine I was told to use. Which is cumbersome, both in volume (who wants to carry around two laptops ?) and in usage (one extra hop for all accesses). All the while potentially leaking some credentials to the untrusted machine (the credentials I need to present to the trusted machine to get into my account and unlock my keys).

So I went looking for:

  • A widely-compatible private key store protocol (so I do not have to start all over again the next time the policy changes).

    A smartcard and a smartcard reader seem a sensible choice: there are widespread standards describing their protocol and they have been around for long enough in professional settings to have reasonable level of support in a lot of operating systems.

  • Is easy to carry around.

    In my view, this eliminates card readers with a built-in PIN pad, which means the PIN must be input through the keyboard of the untrusted computer, which leads me to the next point.

  • Which would not rely on nearly-constant credentials, so I can keep the device plugged in for extended periods of time without having to worry about the untrusted machine using it behind my back.

    Smartcards rely on PINs, which, while they can be changed, I am sure nobody change after every single operation, much less from a trusted terminal. So once I have input my PIN on the untrusted computer, what's stopping it from reusing the PIN for further operations without my consent ?

    So I need some form of TOTP, but smartcards do not have an RTC (...that I know of), which means they are not aware of time, so they cannot internally produce something which can be both unpredictable to an attacker and predictable to a TOTP display where the user can tell what the current password is. But further than this: I would very much not rely on an RTC at all, so be resilient to NTP attacks.

    So I want a device which has a display capable of telling me what the PIN I need to use for the next operation is, and change this pin after every input. There exist high-end cards with build-in 7-segments displays, some even with a tactile pin pad, which leads to the next point.

  • Which uses commonly-available hardware.

    I do not want to rely on a specific model, which may or may not remain available for the duration of my career.

    Instead, there are now commonly available USB-capable general-purpose computers for very affordable prices and with extension capabilities. And if a specific model is not available in a few years, then there should be another, thank to the maker communities relying on these devices (robotics, home automation, ...). I want to use these.

General-purpose devices come with a drawback, of course: they are not physically hardened (see Threat model). But so would my second laptop, so I believe this is an improvement overall.

Final refinement: I want some resistance to casual misuse. With large-enough displays, this is easy: instead of displaying a single random PIN, display an array of random PINs, of which a single cell contains the correct PIN. The larger the display and the smaller the font, the better the added security. But as discussed above, the device should remain small, and this is only aimed at a casual attacker: anyone motivated and competent enough will find other ways to access the data.

Implementation principles

  • how to manage memory: do not manage memory

    This module is implemented in pure python, to try to achieve a lower maintenance burden against buffer overflows that manual memory allocation languages are generally more prone to. It does interface (indirectly) with C code though, so there is a thin layer at which more care is required.

  • how to implement good cryptography: do not implement cryptography

    This module does not implement cryptography itself. It uses the pyca/cryptography module for this, which itself typically relies on OpenSSL. Standing on the shoulders of these giants is mandatory.

    There are also places related to security but not related to cryptography which needs to be carefully implemented:

    • PIN checking. While this is ultra-low-level cryptography, manipulating PINs could leak timing information to the outside world, so it must be (and is) carefully done with time-constant functions.
    • random number generation (for GET_CHALLENGE method). The best source of system entropy must be used.

Features

Implemented: Supposed to work, may fail nevertheless.

Missing: Known to exist, not implemented (yet ?). Contribute or express interest.

Unlisted: Not known to exist. Contribute or report existence (with links to spec, existing implementations, ...).

Category Implemented Missing
high level features

-------------------passcodes

-----------------------PW1, PW3, RC


passcode format UTF-8, KDF PIN block format 2

cryptography

RSA: 2048, 3072, 4096

ECDH: SECP256R1, SECP384R1, SECP512R1, BRAINPOOL256R1, BRAINPOOL384R1, BRAINPOOL512R1, X25519

ECDSA: SECP256R1, SECP384R1, SECP512R1, BRAINPOOL256R1, BRAINPOOL384R1, BRAINPOOL512R1

3DES, Elgamal, RSA <=1024, cast5, idea, blowfish, twofish, camellia, EDDSA ED25519

operations

key generation, key import, signature, decryption, authentication, key role swapping

encryption (AES), get challenge, attestation

hash support

I/O

private DOs

MD5, SHA1, SHA224, SHA256, SHA384, SHA512

0101, 0102, 0103, 0104

RipeMD160

display, biometric, button, keypad, LED, loudspeaker, microphone, touchscreen

key role selection low level features

simple format

extended format

-------------------serial number

lifecycle

-----------------------random in unmanaged space blank-on-terminate


protocol plain Secure Messaging

file selection

full DF, partial DF, path, file identifier, record identifier

short file identifier

About

Pure-python implementation of smartcard OpenPGP specification

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages