I’ve used hardware-backed OpenPGP keys since 2006 when I imported newly generated rsa1024 subkeys to a FSFE Fellowship card. This worked well for several years, and I recall buying more ZeitControl cards for multi-machine usage and backup purposes. As a side note, I recall being unsatisfied with the weak 1024-bit RSA subkeys at the time – my primary key was a somewhat stronger 1280-bit RSA key created back in 2002 — but OpenPGP cards at the time didn’t support more than 1024 bit RSA, and were (and still often are) also limited to power-of-two RSA key sizes which I dislike.
I had my master key on disk with a strong password for a while, mostly to refresh expiration time of the subkeys and to sign other’s OpenPGP keys. At some point I stopped carrying around encrypted copies of my master key. That was my main setup when I migrated to a new stronger RSA 3744 bit key with rsa2048 subkeys on a YubiKey NEO back in 2014. At that point, signing other’s OpenPGP keys was a rare enough occurrence that I settled with bringing out my offline machine to perform this operation, transferring the public key to sign on USB sticks. In 2019 I re-evaluated my OpenPGP setup and ended up creating a offline Ed25519 key with subkeys on a FST-01G running Gnuk. My approach for signing other’s OpenPGP keys were still to bring out my offline machine and sign things using the master secret using USB sticks for storage and transport. Which meant I almost never did that, because it took too much effort. So my 2019-era Ed25519 key still only has a handful of signatures on it, since I had essentially stopped signing other’s keys which is the traditional way of getting signatures in return.
None of this caused any critical problem for me because I continued to use my old 2014-era RSA3744 key in parallel with my new 2019-era Ed25519 key, since too many systems didn’t handle Ed25519. However, during 2022 this changed, and the only remaining environment that I still used my RSA3744 key for was in Debian — and they require OpenPGP signatures on the new key to allow it to replace an older key. I was in denial about this sub-optimal solution during 2022 and endured its practical consequences, having to use the YubiKey NEO (which I had replaced with a permanently inserted YubiKey Nano at some point) for Debian-related purposes alone.
In December 2022 I bought a new laptop and setup a FST-01SZ with my Ed25519 key, and while I have taken a vacation from Debian, I continue to extend the expiration period on the old RSA3744-key in case I will ever have to use it again, so the overall OpenPGP setup was still sub-optimal. Having two valid OpenPGP keys at the same time causes people to use both for email encryption (leading me to have to use both devices), and the WKD Key Discovery protocol doesn’t like two valid keys either. At FOSDEM’23 I ran into Andre Heinecke at GnuPG and I couldn’t help complain about how complex and unsatisfying all OpenPGP-related matters were, and he mildly ignored my rant and asked why I didn’t put the master key on another smartcard. The comment sunk in when I came home, and recently I connected all the dots and this post is a summary of what I did to move my offline OpenPGP master key to a Nitrokey Start.
First a word about device choice, I still prefer to use hardware devices that are as compatible with free software as possible, but the FST-01G or FST-01SZ are no longer easily available for purchase. I got a comment about Nitrokey start in my last post, and had two of them available to experiment with. There are things to dislike with the Nitrokey Start compared to the YubiKey (e.g., relative insecure chip architecture, the bulkier form factor and lack of FIDO/U2F/OATH support) but – as far as I know – there is no more widely available owner-controlled device that is manufactured for an intended purpose of implementing an OpenPGP card. Thus it hits the sweet spot for me.
The first step is to run latest firmware on the Nitrokey Start – for bug-fixes and important OpenSSH 9.0 compatibility – and there are reproducible-built firmware published that you can install using pynitrokey. I run Trisquel 11 aramo on my laptop, which does not include the Python Pip package (likely because it promotes installing non-free software) so that was a slight complication. Building the firmware locally may have worked, and I would like to do that eventually to confirm the published firmware, however to save time I settled with installing the Ubuntu 22.04 packages on my machine:
$ sha256sum python3-pip*
ded6b3867a4a4cbaff0940cab366975d6aeecc76b9f2d2efa3deceb062668b1c python3-pip_22.0.2+dfsg-1ubuntu0.2_all.deb
e1561575130c41dc3309023a345de337e84b4b04c21c74db57f599e267114325 python3-pip-whl_22.0.2+dfsg-1ubuntu0.2_all.deb
$ doas dpkg -i python3-pip*
...
$ doas apt install -f
...
$
Installing pynitrokey downloaded a bunch of dependencies, and it would be nice to audit the license and security vulnerabilities for each of them. (Verbose output below slightly redacted.)
jas@kaka:~$ pip3 install --user pynitrokey Collecting pynitrokey Downloading pynitrokey-0.4.34-py3-none-any.whl (572 kB) Collecting frozendict~=2.3.4 Downloading frozendict-2.3.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (113 kB) Requirement already satisfied: click<9,>=8.0.0 in /usr/lib/python3/dist-packages (from pynitrokey) (8.0.3) Collecting ecdsa Downloading ecdsa-0.18.0-py2.py3-none-any.whl (142 kB) Collecting python-dateutil~=2.7.0 Downloading python_dateutil-2.7.5-py2.py3-none-any.whl (225 kB) Collecting fido2<2,>=1.1.0 Downloading fido2-1.1.0-py3-none-any.whl (201 kB) Collecting tlv8 Downloading tlv8-0.10.0.tar.gz (16 kB) Preparing metadata (setup.py) ... done Requirement already satisfied: certifi>=14.5.14 in /usr/lib/python3/dist-packages (from pynitrokey) (2020.6.20) Requirement already satisfied: pyusb in /usr/lib/python3/dist-packages (from pynitrokey) (1.2.1.post1) Collecting urllib3~=1.26.7 Downloading urllib3-1.26.15-py2.py3-none-any.whl (140 kB) Collecting spsdk<1.8.0,>=1.7.0 Downloading spsdk-1.7.1-py3-none-any.whl (684 kB) Collecting typing_extensions~=4.3.0 Downloading typing_extensions-4.3.0-py3-none-any.whl (25 kB) Requirement already satisfied: cryptography<37,>=3.4.4 in /usr/lib/python3/dist-packages (from pynitrokey) (3.4.8) Collecting intelhex Downloading intelhex-2.3.0-py2.py3-none-any.whl (50 kB) Collecting nkdfu Downloading nkdfu-0.2-py3-none-any.whl (16 kB) Requirement already satisfied: requests in /usr/lib/python3/dist-packages (from pynitrokey) (2.25.1) Collecting tqdm Downloading tqdm-4.65.0-py3-none-any.whl (77 kB) Collecting nrfutil<7,>=6.1.4 Downloading nrfutil-6.1.7.tar.gz (845 kB) Preparing metadata (setup.py) ... done Requirement already satisfied: cffi in /usr/lib/python3/dist-packages (from pynitrokey) (1.15.0) Collecting crcmod Downloading crcmod-1.7.tar.gz (89 kB) Preparing metadata (setup.py) ... done Collecting libusb1==1.9.3 Downloading libusb1-1.9.3-py3-none-any.whl (60 kB) Collecting pc_ble_driver_py>=0.16.4 Downloading pc_ble_driver_py-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.9 MB) Collecting piccata Downloading piccata-2.0.3-py3-none-any.whl (21 kB) Collecting protobuf<4.0.0,>=3.17.3 Downloading protobuf-3.20.3-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (1.1 MB) Collecting pyserial Downloading pyserial-3.5-py2.py3-none-any.whl (90 kB) Collecting pyspinel>=1.0.0a3 Downloading pyspinel-1.0.3.tar.gz (58 kB) Preparing metadata (setup.py) ... done Requirement already satisfied: pyyaml in /usr/lib/python3/dist-packages (from nrfutil<7,>=6.1.4->pynitrokey) (5.4.1) Requirement already satisfied: six>=1.5 in /usr/lib/python3/dist-packages (from python-dateutil~=2.7.0->pynitrokey) (1.16.0) Collecting pylink-square<0.11.9,>=0.8.2 Downloading pylink_square-0.11.1-py2.py3-none-any.whl (78 kB) Collecting jinja2<3.1,>=2.11 Downloading Jinja2-3.0.3-py3-none-any.whl (133 kB) Collecting bincopy<17.11,>=17.10.2 Downloading bincopy-17.10.3-py3-none-any.whl (17 kB) Collecting fastjsonschema>=2.15.1 Downloading fastjsonschema-2.16.3-py3-none-any.whl (23 kB) Collecting astunparse<2,>=1.6 Downloading astunparse-1.6.3-py2.py3-none-any.whl (12 kB) Collecting oscrypto~=1.2 Downloading oscrypto-1.3.0-py2.py3-none-any.whl (194 kB) Collecting deepmerge==0.3.0 Downloading deepmerge-0.3.0-py2.py3-none-any.whl (7.6 kB) Collecting pyocd<=0.31.0,>=0.28.3 Downloading pyocd-0.31.0-py3-none-any.whl (12.5 MB) Collecting click-option-group<0.6,>=0.3.0 Downloading click_option_group-0.5.5-py3-none-any.whl (12 kB) Collecting pycryptodome<4,>=3.9.3 Downloading pycryptodome-3.17-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.1 MB) Collecting pyocd-pemicro<1.2.0,>=1.1.1 Downloading pyocd_pemicro-1.1.5-py3-none-any.whl (9.0 kB) Requirement already satisfied: colorama<1,>=0.4.4 in /usr/lib/python3/dist-packages (from spsdk<1.8.0,>=1.7.0->pynitrokey) (0.4.4) Collecting commentjson<1,>=0.9 Downloading commentjson-0.9.0.tar.gz (8.7 kB) Preparing metadata (setup.py) ... done Requirement already satisfied: asn1crypto<2,>=1.2 in /usr/lib/python3/dist-packages (from spsdk<1.8.0,>=1.7.0->pynitrokey) (1.4.0) Collecting pypemicro<0.2.0,>=0.1.9 Downloading pypemicro-0.1.11-py3-none-any.whl (5.7 MB) Collecting libusbsio>=2.1.11 Downloading libusbsio-2.1.11-py3-none-any.whl (247 kB) Collecting sly==0.4 Downloading sly-0.4.tar.gz (60 kB) Preparing metadata (setup.py) ... done Collecting ruamel.yaml<0.18.0,>=0.17 Downloading ruamel.yaml-0.17.21-py3-none-any.whl (109 kB) Collecting cmsis-pack-manager<0.3.0 Downloading cmsis_pack_manager-0.2.10-py2.py3-none-manylinux1_x86_64.whl (25.1 MB) Collecting click-command-tree==1.1.0 Downloading click_command_tree-1.1.0-py3-none-any.whl (3.6 kB) Requirement already satisfied: bitstring<3.2,>=3.1 in /usr/lib/python3/dist-packages (from spsdk<1.8.0,>=1.7.0->pynitrokey) (3.1.7) Collecting hexdump~=3.3 Downloading hexdump-3.3.zip (12 kB) Preparing metadata (setup.py) ... done Collecting fire Downloading fire-0.5.0.tar.gz (88 kB) Preparing metadata (setup.py) ... done Requirement already satisfied: wheel<1.0,>=0.23.0 in /usr/lib/python3/dist-packages (from astunparse<2,>=1.6->spsdk<1.8.0,>=1.7.0->pynitrokey) (0.37.1) Collecting humanfriendly Downloading humanfriendly-10.0-py2.py3-none-any.whl (86 kB) Collecting argparse-addons>=0.4.0 Downloading argparse_addons-0.12.0-py3-none-any.whl (3.3 kB) Collecting pyelftools Downloading pyelftools-0.29-py2.py3-none-any.whl (174 kB) Collecting milksnake>=0.1.2 Downloading milksnake-0.1.5-py2.py3-none-any.whl (9.6 kB) Requirement already satisfied: appdirs>=1.4 in /usr/lib/python3/dist-packages (from cmsis-pack-manager<0.3.0->spsdk<1.8.0,>=1.7.0->pynitrokey) (1.4.4) Collecting lark-parser<0.8.0,>=0.7.1 Downloading lark-parser-0.7.8.tar.gz (276 kB) Preparing metadata (setup.py) ... done Requirement already satisfied: MarkupSafe>=2.0 in /usr/lib/python3/dist-packages (from jinja2<3.1,>=2.11->spsdk<1.8.0,>=1.7.0->pynitrokey) (2.0.1) Collecting asn1crypto<2,>=1.2 Downloading asn1crypto-1.5.1-py2.py3-none-any.whl (105 kB) Collecting wrapt Downloading wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl (78 kB) Collecting future Downloading future-0.18.3.tar.gz (840 kB) Preparing metadata (setup.py) ... done Collecting psutil>=5.2.2 Downloading psutil-5.9.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl (280 kB) Collecting capstone<5.0,>=4.0 Downloading capstone-4.0.2-py2.py3-none-manylinux1_x86_64.whl (2.1 MB) Collecting naturalsort<2.0,>=1.5 Downloading naturalsort-1.5.1.tar.gz (7.4 kB) Preparing metadata (setup.py) ... done Collecting prettytable<3.0,>=2.0 Downloading prettytable-2.5.0-py3-none-any.whl (24 kB) Collecting intervaltree<4.0,>=3.0.2 Downloading intervaltree-3.1.0.tar.gz (32 kB) Preparing metadata (setup.py) ... done Collecting ruamel.yaml.clib>=0.2.6 Downloading ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl (485 kB) Collecting termcolor Downloading termcolor-2.2.0-py3-none-any.whl (6.6 kB) Collecting sortedcontainers<3.0,>=2.0 Downloading sortedcontainers-2.4.0-py2.py3-none-any.whl (29 kB) Requirement already satisfied: wcwidth in /usr/lib/python3/dist-packages (from prettytable<3.0,>=2.0->pyocd<=0.31.0,>=0.28.3->spsdk<1.8.0,>=1.7.0->pynitrokey) (0.2.5) Building wheels for collected packages: nrfutil, crcmod, sly, tlv8, commentjson, hexdump, pyspinel, fire, intervaltree, lark-parser, naturalsort, future Building wheel for nrfutil (setup.py) ... done Created wheel for nrfutil: filename=nrfutil-6.1.7-py3-none-any.whl size=898520 sha256=de6f8803f51d6c26d24dc7df6292064a468ff3f389d73370433fde5582b84a10 Stored in directory: /home/jas/.cache/pip/wheels/39/2b/9b/98ab2dd716da746290e6728bdb557b14c1c9a54cb9ed86e13b Building wheel for crcmod (setup.py) ... done Created wheel for crcmod: filename=crcmod-1.7-cp310-cp310-linux_x86_64.whl size=31422 sha256=5149ac56fcbfa0606760eef5220fcedc66be560adf68cf38c604af3ad0e4a8b0 Stored in directory: /home/jas/.cache/pip/wheels/85/4c/07/72215c529bd59d67e3dac29711d7aba1b692f543c808ba9e86 Building wheel for sly (setup.py) ... done Created wheel for sly: filename=sly-0.4-py3-none-any.whl size=27352 sha256=f614e413918de45c73d1e9a8dca61ca07dc760d9740553400efc234c891f7fde Stored in directory: /home/jas/.cache/pip/wheels/a2/23/4a/6a84282a0d2c29f003012dc565b3126e427972e8b8157ea51f Building wheel for tlv8 (setup.py) ... done Created wheel for tlv8: filename=tlv8-0.10.0-py3-none-any.whl size=11266 sha256=3ec8b3c45977a3addbc66b7b99e1d81b146607c3a269502b9b5651900a0e2d08 Stored in directory: /home/jas/.cache/pip/wheels/e9/35/86/66a473cc2abb0c7f21ed39c30a3b2219b16bd2cdb4b33cfc2c Building wheel for commentjson (setup.py) ... done Created wheel for commentjson: filename=commentjson-0.9.0-py3-none-any.whl size=12092 sha256=28b6413132d6d7798a18cf8c76885dc69f676ea763ffcb08775a3c2c43444f4a Stored in directory: /home/jas/.cache/pip/wheels/7d/90/23/6358a234ca5b4ec0866d447079b97fedf9883387d1d7d074e5 Building wheel for hexdump (setup.py) ... done Created wheel for hexdump: filename=hexdump-3.3-py3-none-any.whl size=8913 sha256=79dfadd42edbc9acaeac1987464f2df4053784fff18b96408c1309b74fd09f50 Stored in directory: /home/jas/.cache/pip/wheels/26/28/f7/f47d7ecd9ae44c4457e72c8bb617ef18ab332ee2b2a1047e87 Building wheel for pyspinel (setup.py) ... done Created wheel for pyspinel: filename=pyspinel-1.0.3-py3-none-any.whl size=65033 sha256=01dc27f81f28b4830a0cf2336dc737ef309a1287fcf33f57a8a4c5bed3b5f0a6 Stored in directory: /home/jas/.cache/pip/wheels/95/ec/4b/6e3e2ee18e7292d26a65659f75d07411a6e69158bb05507590 Building wheel for fire (setup.py) ... done Created wheel for fire: filename=fire-0.5.0-py2.py3-none-any.whl size=116951 sha256=3d288585478c91a6914629eb739ea789828eb2d0267febc7c5390cb24ba153e8 Stored in directory: /home/jas/.cache/pip/wheels/90/d4/f7/9404e5db0116bd4d43e5666eaa3e70ab53723e1e3ea40c9a95 Building wheel for intervaltree (setup.py) ... done Created wheel for intervaltree: filename=intervaltree-3.1.0-py2.py3-none-any.whl size=26119 sha256=5ff1def22ba883af25c90d90ef7c6518496fcd47dd2cbc53a57ec04cd60dc21d Stored in directory: /home/jas/.cache/pip/wheels/fa/80/8c/43488a924a046b733b64de3fac99252674c892a4c3801c0a61 Building wheel for lark-parser (setup.py) ... done Created wheel for lark-parser: filename=lark_parser-0.7.8-py2.py3-none-any.whl size=62527 sha256=3d2ec1d0f926fc2688d40777f7ef93c9986f874169132b1af590b6afc038f4be Stored in directory: /home/jas/.cache/pip/wheels/29/30/94/33e8b58318aa05cb1842b365843036e0280af5983abb966b83 Building wheel for naturalsort (setup.py) ... done Created wheel for naturalsort: filename=naturalsort-1.5.1-py3-none-any.whl size=7526 sha256=bdecac4a49f2416924548cae6c124c85d5333e9e61c563232678ed182969d453 Stored in directory: /home/jas/.cache/pip/wheels/a6/8e/c9/98cfa614fff2979b457fa2d9ad45ec85fa417e7e3e2e43be51 Building wheel for future (setup.py) ... done Created wheel for future: filename=future-0.18.3-py3-none-any.whl size=492037 sha256=57a01e68feca2b5563f5f624141267f399082d2f05f55886f71b5d6e6cf2b02c Stored in directory: /home/jas/.cache/pip/wheels/5e/a9/47/f118e66afd12240e4662752cc22cefae5d97275623aa8ef57d Successfully built nrfutil crcmod sly tlv8 commentjson hexdump pyspinel fire intervaltree lark-parser naturalsort future Installing collected packages: tlv8, sortedcontainers, sly, pyserial, pyelftools, piccata, naturalsort, libusb1, lark-parser, intelhex, hexdump, fastjsonschema, crcmod, asn1crypto, wrapt, urllib3, typing_extensions, tqdm, termcolor, ruamel.yaml.clib, python-dateutil, pyspinel, pypemicro, pycryptodome, psutil, protobuf, prettytable, oscrypto, milksnake, libusbsio, jinja2, intervaltree, humanfriendly, future, frozendict, fido2, ecdsa, deepmerge, commentjson, click-option-group, click-command-tree, capstone, astunparse, argparse-addons, ruamel.yaml, pyocd-pemicro, pylink-square, pc_ble_driver_py, fire, cmsis-pack-manager, bincopy, pyocd, nrfutil, nkdfu, spsdk, pynitrokey WARNING: The script nitropy is installed in '/home/jas/.local/bin' which is not on PATH. Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location. Successfully installed argparse-addons-0.12.0 asn1crypto-1.5.1 astunparse-1.6.3 bincopy-17.10.3 capstone-4.0.2 click-command-tree-1.1.0 click-option-group-0.5.5 cmsis-pack-manager-0.2.10 commentjson-0.9.0 crcmod-1.7 deepmerge-0.3.0 ecdsa-0.18.0 fastjsonschema-2.16.3 fido2-1.1.0 fire-0.5.0 frozendict-2.3.5 future-0.18.3 hexdump-3.3 humanfriendly-10.0 intelhex-2.3.0 intervaltree-3.1.0 jinja2-3.0.3 lark-parser-0.7.8 libusb1-1.9.3 libusbsio-2.1.11 milksnake-0.1.5 naturalsort-1.5.1 nkdfu-0.2 nrfutil-6.1.7 oscrypto-1.3.0 pc_ble_driver_py-0.17.0 piccata-2.0.3 prettytable-2.5.0 protobuf-3.20.3 psutil-5.9.4 pycryptodome-3.17 pyelftools-0.29 pylink-square-0.11.1 pynitrokey-0.4.34 pyocd-0.31.0 pyocd-pemicro-1.1.5 pypemicro-0.1.11 pyserial-3.5 pyspinel-1.0.3 python-dateutil-2.7.5 ruamel.yaml-0.17.21 ruamel.yaml.clib-0.2.7 sly-0.4 sortedcontainers-2.4.0 spsdk-1.7.1 termcolor-2.2.0 tlv8-0.10.0 tqdm-4.65.0 typing_extensions-4.3.0 urllib3-1.26.15 wrapt-1.15.0 jas@kaka:~$
Then upgrading the device worked remarkable well, although I wish that the tool would have printed URLs and checksums for the firmware files to allow easy confirmation.
jas@kaka:~$ PATH=$PATH:/home/jas/.local/bin jas@kaka:~$ nitropy start list Command line tool to interact with Nitrokey devices 0.4.34 :: 'Nitrokey Start' keys: FSIJ-1.2.15-5D271572: Nitrokey Nitrokey Start (RTM.12.1-RC2-modified) jas@kaka:~$ nitropy start update Command line tool to interact with Nitrokey devices 0.4.34 Nitrokey Start firmware update tool Platform: Linux-5.15.0-67-generic-x86_64-with-glibc2.35 System: Linux, is_linux: True Python: 3.10.6 Saving run log to: /tmp/nitropy.log.gc5753a8 Admin PIN: Firmware data to be used: - FirmwareType.REGNUAL: 4408, hash: ...b'72a30389' valid (from ...built/RTM.13/regnual.bin) - FirmwareType.GNUK: 129024, hash: ...b'25a4289b' valid (from ...prebuilt/RTM.13/gnuk.bin) Currently connected device strings: Device: Vendor: Nitrokey Product: Nitrokey Start Serial: FSIJ-1.2.15-5D271572 Revision: RTM.12.1-RC2-modified Config: *:*:8e82 Sys: 3.0 Board: NITROKEY-START-G initial device strings: [{'name': '', 'Vendor': 'Nitrokey', 'Product': 'Nitrokey Start', 'Serial': 'FSIJ-1.2.15-5D271572', 'Revision': 'RTM.12.1-RC2-modified', 'Config': '*:*:8e82', 'Sys': '3.0', 'Board': 'NITROKEY-START-G'}] Please note: - Latest firmware available is: RTM.13 (published: 2022-12-08T10:59:11Z) - provided firmware: None - all data will be removed from the device! - do not interrupt update process - the device may not run properly! - the process should not take more than 1 minute Do you want to continue? [yes/no]: yes ... Starting bootloader upload procedure Device: Nitrokey Start FSIJ-1.2.15-5D271572 Connected to the device Running update! Do NOT remove the device from the USB slot, until further notice Downloading flash upgrade program... Executing flash upgrade... Waiting for device to appear: Wait 20 seconds..... Downloading the program Protecting device Finish flashing Resetting device Update procedure finished. Device could be removed from USB slot. Currently connected device strings (after upgrade): Device: Vendor: Nitrokey Product: Nitrokey Start Serial: FSIJ-1.2.19-5D271572 Revision: RTM.13 Config: *:*:8e82 Sys: 3.0 Board: NITROKEY-START-G device can now be safely removed from the USB slot final device strings: [{'name': '', 'Vendor': 'Nitrokey', 'Product': 'Nitrokey Start', 'Serial': 'FSIJ-1.2.19-5D271572', 'Revision': 'RTM.13', 'Config': '*:*:8e82', 'Sys': '3.0', 'Board': 'NITROKEY-START-G'}] finishing session 2023-03-16 21:49:07.371291 Log saved to: /tmp/nitropy.log.gc5753a8 jas@kaka:~$ jas@kaka:~$ nitropy start list Command line tool to interact with Nitrokey devices 0.4.34 :: 'Nitrokey Start' keys: FSIJ-1.2.19-5D271572: Nitrokey Nitrokey Start (RTM.13) jas@kaka:~$
Before importing the master key to this device, it should be configured. Note the commands in the beginning to make sure scdaemon/pcscd is not running because they may have cached state from earlier cards. Change PIN code as you like after this, my experience with Gnuk was that the Admin PIN had to be changed first, then you import the key, and then you change the PIN.
jas@kaka:~$ gpg-connect-agent "SCD KILLSCD" "SCD BYE" /bye OK ERR 67125247 Slut på fil <GPG Agent> jas@kaka:~$ ps auxww|grep -e pcsc -e scd jas 11651 0.0 0.0 3468 1672 pts/0 R+ 21:54 0:00 grep --color=auto -e pcsc -e scd jas@kaka:~$ gpg --card-edit Reader ...........: 20A0:4211:FSIJ-1.2.19-5D271572:0 Application ID ...: D276000124010200FFFE5D2715720000 Application type .: OpenPGP Version ..........: 2.0 Manufacturer .....: unmanaged S/N range Serial number ....: 5D271572 Name of cardholder: [not set] Language prefs ...: [not set] Salutation .......: URL of public key : [not set] Login data .......: [not set] Signature PIN ....: forced Key attributes ...: rsa2048 rsa2048 rsa2048 Max. PIN lengths .: 127 127 127 PIN retry counter : 3 3 3 Signature counter : 0 KDF setting ......: off Signature key ....: [none] Encryption key....: [none] Authentication key: [none] General key info..: [none] gpg/card> admin Admin commands are allowed gpg/card> kdf-setup gpg/card> passwd gpg: OpenPGP card no. D276000124010200FFFE5D2715720000 detected 1 - change PIN 2 - unblock PIN 3 - change Admin PIN 4 - set the Reset Code Q - quit Your selection? 3 PIN changed. 1 - change PIN 2 - unblock PIN 3 - change Admin PIN 4 - set the Reset Code Q - quit Your selection? q gpg/card> name Cardholder's surname: Josefsson Cardholder's given name: Simon gpg/card> lang Language preferences: sv gpg/card> sex Salutation (M = Mr., F = Ms., or space): m gpg/card> login Login data (account name): jas gpg/card> url URL to retrieve public key: https://josefsson.org/key-20190320.txt gpg/card> forcesig gpg/card> key-attr Changing card key attribute for: Signature key Please select what kind of key you want: (1) RSA (2) ECC Your selection? 2 Please select which elliptic curve you want: (1) Curve 25519 (4) NIST P-384 Your selection? 1 The card will now be re-configured to generate a key of type: ed25519 Note: There is no guarantee that the card supports the requested size. If the key generation does not succeed, please check the documentation of your card to see what sizes are allowed. Changing card key attribute for: Encryption key Please select what kind of key you want: (1) RSA (2) ECC Your selection? 2 Please select which elliptic curve you want: (1) Curve 25519 (4) NIST P-384 Your selection? 1 The card will now be re-configured to generate a key of type: cv25519 Changing card key attribute for: Authentication key Please select what kind of key you want: (1) RSA (2) ECC Your selection? 2 Please select which elliptic curve you want: (1) Curve 25519 (4) NIST P-384 Your selection? 1 The card will now be re-configured to generate a key of type: ed25519 gpg/card> jas@kaka:~$ gpg --card-edit Reader ...........: 20A0:4211:FSIJ-1.2.19-5D271572:0 Application ID ...: D276000124010200FFFE5D2715720000 Application type .: OpenPGP Version ..........: 2.0 Manufacturer .....: unmanaged S/N range Serial number ....: 5D271572 Name of cardholder: Simon Josefsson Language prefs ...: sv Salutation .......: Mr. URL of public key : https://josefsson.org/key-20190320.txt Login data .......: jas Signature PIN ....: not forced Key attributes ...: ed25519 cv25519 ed25519 Max. PIN lengths .: 127 127 127 PIN retry counter : 3 3 3 Signature counter : 0 KDF setting ......: on Signature key ....: [none] Encryption key....: [none] Authentication key: [none] General key info..: [none] jas@kaka:~$
Once setup, bring out your offline machine and boot it and mount your USB stick with the offline key. The paths below will be different, and this is using a somewhat unorthodox approach of working with fresh GnuPG configuration paths that I chose for the USB stick.
jas@kaka:/media/jas/2c699cbd-b77e-4434-a0d6-0c4965864296$ cp -a gnupghome-backup-masterkey gnupghome-import-nitrokey-5D271572 jas@kaka:/media/jas/2c699cbd-b77e-4434-a0d6-0c4965864296$ gpg --homedir $PWD/gnupghome-import-nitrokey-5D271572 --edit-key B1D2BD1375BECB784CF4F8C4D73CF638C53C06BE gpg (GnuPG) 2.2.27; Copyright (C) 2021 Free Software Foundation, Inc. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Secret key is available. sec ed25519/D73CF638C53C06BE created: 2019-03-20 expired: 2019-10-22 usage: SC trust: ultimate validity: expired [ expired] (1). Simon Josefsson <simon@josefsson.org> gpg> keytocard Really move the primary key? (y/N) y Please select where to store the key: (1) Signature key (3) Authentication key Your selection? 1 sec ed25519/D73CF638C53C06BE created: 2019-03-20 expired: 2019-10-22 usage: SC trust: ultimate validity: expired [ expired] (1). Simon Josefsson <simon@josefsson.org> gpg> Save changes? (y/N) y jas@kaka:/media/jas/2c699cbd-b77e-4434-a0d6-0c4965864296$
Don’t forget to change the PIN at this point. At this point it is useful to confirm that the Nitrokey has the master key available and that is possible to sign statements with it, back on your regular machine:
jas@kaka:~$ gpg --card-status Reader ...........: 20A0:4211:FSIJ-1.2.19-5D271572:0 Application ID ...: D276000124010200FFFE5D2715720000 Application type .: OpenPGP Version ..........: 2.0 Manufacturer .....: unmanaged S/N range Serial number ....: 5D271572 Name of cardholder: Simon Josefsson Language prefs ...: sv Salutation .......: Mr. URL of public key : https://josefsson.org/key-20190320.txt Login data .......: jas Signature PIN ....: not forced Key attributes ...: ed25519 cv25519 ed25519 Max. PIN lengths .: 127 127 127 PIN retry counter : 3 3 3 Signature counter : 1 KDF setting ......: on Signature key ....: B1D2 BD13 75BE CB78 4CF4 F8C4 D73C F638 C53C 06BE created ....: 2019-03-20 23:37:24 Encryption key....: [none] Authentication key: [none] General key info..: pub ed25519/D73CF638C53C06BE 2019-03-20 Simon Josefsson <simon@josefsson.org> sec> ed25519/D73CF638C53C06BE created: 2019-03-20 expires: 2023-09-19 card-no: FFFE 5D271572 ssb> ed25519/80260EE8A9B92B2B created: 2019-03-20 expires: 2023-09-19 card-no: FFFE 42315277 ssb> ed25519/51722B08FE4745A2 created: 2019-03-20 expires: 2023-09-19 card-no: FFFE 42315277 ssb> cv25519/02923D7EE76EBD60 created: 2019-03-20 expires: 2023-09-19 card-no: FFFE 42315277 jas@kaka:~$ echo foo|gpg -a --sign|gpg --verify gpg: Signature made Thu Mar 16 22:11:02 2023 CET gpg: using EDDSA key B1D2BD1375BECB784CF4F8C4D73CF638C53C06BE gpg: Good signature from "Simon Josefsson <simon@josefsson.org>" [ultimate] jas@kaka:~$
Finally to retrieve and sign a key, for example Andre Heinecke’s that I could confirm the OpenPGP key identifier from his business card.
jas@kaka:~$ gpg --locate-external-keys aheinecke@gnupg.com gpg: key 1FDF723CF462B6B1: public key "Andre Heinecke <aheinecke@gnupg.com>" imported gpg: Total number processed: 1 gpg: imported: 1 gpg: marginals needed: 3 completes needed: 1 trust model: pgp gpg: depth: 0 valid: 2 signed: 7 trust: 0-, 0q, 0n, 0m, 0f, 2u gpg: depth: 1 valid: 7 signed: 64 trust: 7-, 0q, 0n, 0m, 0f, 0u gpg: next trustdb check due at 2023-05-26 pub rsa3072 2015-12-08 [SC] [expires: 2025-12-05] 94A5C9A03C2FE5CA3B095D8E1FDF723CF462B6B1 uid [ unknown] Andre Heinecke <aheinecke@gnupg.com> sub ed25519 2017-02-13 [S] sub ed25519 2017-02-13 [A] sub rsa3072 2015-12-08 [E] [expires: 2025-12-05] sub rsa3072 2015-12-08 [A] [expires: 2025-12-05] jas@kaka:~$ gpg --edit-key "94A5C9A03C2FE5CA3B095D8E1FDF723CF462B6B1" gpg (GnuPG) 2.2.27; Copyright (C) 2021 Free Software Foundation, Inc. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. pub rsa3072/1FDF723CF462B6B1 created: 2015-12-08 expires: 2025-12-05 usage: SC trust: unknown validity: unknown sub ed25519/2978E9D40CBABA5C created: 2017-02-13 expires: never usage: S sub ed25519/DC74D901C8E2DD47 created: 2017-02-13 expires: never usage: A The following key was revoked on 2017-02-23 by RSA key 1FDF723CF462B6B1 Andre Heinecke <aheinecke@gnupg.com> sub cv25519/1FFE3151683260AB created: 2017-02-13 revoked: 2017-02-23 usage: E sub rsa3072/8CC999BDAA45C71F created: 2015-12-08 expires: 2025-12-05 usage: E sub rsa3072/6304A4B539CE444A created: 2015-12-08 expires: 2025-12-05 usage: A [ unknown] (1). Andre Heinecke <aheinecke@gnupg.com> gpg> sign pub rsa3072/1FDF723CF462B6B1 created: 2015-12-08 expires: 2025-12-05 usage: SC trust: unknown validity: unknown Primary key fingerprint: 94A5 C9A0 3C2F E5CA 3B09 5D8E 1FDF 723C F462 B6B1 Andre Heinecke <aheinecke@gnupg.com> This key is due to expire on 2025-12-05. Are you sure that you want to sign this key with your key "Simon Josefsson <simon@josefsson.org>" (D73CF638C53C06BE) Really sign? (y/N) y gpg> quit Save changes? (y/N) y jas@kaka:~$
This is on my day-to-day machine, using the NitroKey Start with the offline key. No need to boot the old offline machine just to sign keys or extend expiry anymore! At FOSDEM’23 I managed to get at least one DD signature on my new key, and the Debian keyring maintainers accepted my Ed25519 key. Hopefully I can now finally let my 2014-era RSA3744 key expire in 2023-09-19 and not extend it any further. This should finish my transition to a simpler OpenPGP key setup, yay!