openpgp-card-tools 0.10.1

A tool for inspecting and configuring OpenPGP cards
#!/usr/bin/python3
#
# WARNING: This will wipe any information on a card. Do not use it unless
# you're very sure you don't mind.
#
# Prepare an OpenPGP card for use within a hypothetical organization:
#
# - factory reset the card
# - set card holder name, if desired
# - generate elliptic curve 25519 keys
# - write to stdout a JSON object with the card id, card holder, and
#   key fingerprints
#
# Usage: run with --help.
#
# SPDX-FileCopyrightText: 2022 Lars Wirzenius <liw@liw.fi>
# SPDX-FileCopyrightText: 2024 David Runge <dave@sleepmap.de>
# SPDX-License-Identifier: MIT OR Apache-2.0


import argparse
import json
import sys
from subprocess import run


tracing = False


def trace(msg):
    if tracing:
        sys.stderr.write(f"DEBUG: {msg}\n")
        sys.stderr.flush()


def oct_raw(args):
    argv = ["oct"] + args
    trace(f"running {argv}")
    p = run(argv, capture_output=True)
    if p.returncode != 0:
        raise Exception(f"oct failed:\n{p.stderr}")
    o = p.stdout
    trace(f"oct raw output: {o!r}")
    return o


def oct_json(args):
    o = json.loads(oct_raw(["--output-format=json"] + args))
    trace(f"oct JSON output: {o}")
    return o


def list_cards():
    return oct_json(["list"])["idents"]


def pick_card(card):
    cards = list_cards()
    if card is None:
        if not cards:
            raise Exception("No cards found")
        if len(cards) > 1:
            raise Exception(f"Can't pick card automatically: found {len(cards)} cards")
        return cards[0]
    elif card in cards:
        return card
    else:
        raise Exception(f"Can't find specified card {card}")


def factory_reset(card):
    oct_raw(["factory-reset", "--card", card])


def set_card_holder(card, admin_pin, name):
    trace(f"set card holder to {name!r}")
    oct_raw(["admin", "--card", card, "--admin-pin", admin_pin, "name", name])


def generate_key(card, admin_pin, user_pin):
    oct_raw(
        [
            "admin",
            f"--card={card}",
            f"--admin-pin={admin_pin}",
            "generate",
            f"--user-pin={user_pin}",
            "--output=/dev/null",
            "cv25519",
        ]
    )


def status(card):
    o = oct_json(["status", f"--card={card}"])
    return {
        "card_ident": o["ident"],
        "cardholder_name": o["cardholder_name"],
        "signature_key": o["signature_key"]["fingerprint"],
        "decryption_key": o["signature_key"]["fingerprint"],
        "authentication_key": o["signature_key"]["fingerprint"],
    }


def card_is_empty(card):
    o = status(card)
    del o["card_ident"]
    for key in o:
        if o[key]:
            return False
    return True


def main():
    p = argparse.ArgumentParser()
    p.add_argument("--force", action="store_true", help="prepare a card that has data")
    p.add_argument(
        "--verbose", action="store_true", help="produce debugging output to stderr"
    )
    p.add_argument("--card", help="card identifier, default is to pick the only one")
    p.add_argument("--card-holder", help="name of card holder", required=True)
    p.add_argument(
        "--admin-pin", action="store", help="set file with admin PIN", required=True
    )
    p.add_argument(
        "--user-pin", action="store", help="set file with user PIN", required=True
    )
    args = p.parse_args()

    if args.verbose:
        global tracing
        tracing = True

    trace(f"args: {args}")
    card = pick_card(args.card)
    if not args.force and not card_is_empty(card):
        raise Exception(f"card {card} has existing keys, not touching it")
    factory_reset(card)
    set_card_holder(card, args.admin_pin, args.card_holder)
    key = generate_key(card, args.admin_pin, args.user_pin)
    o = status(card)
    print(json.dumps(o, indent=4))


if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        sys.stderr.write(f"ERROR: {e}\n")
        sys.exit(1)