#!/usr/bin/env python3 """ create a shadowed-private-key in sexp format for gnupg's private-keys-v1.d folder containing the public key of a X.509 certificate signing request or a public key """ import ctypes import math import binascii import cryptography.hazmat.backends import cryptography.hazmat.primitives.serialization import cryptography.x509 DEFAULT_KEY_OUTPUT_PATH_PATTERN = '{keygrip_hex}.key' DEFAULT_SMARTCARD_APP_ID_HEX = 'D2760001240102010001234567890000' gcrypt = ctypes.cdll.LoadLibrary('libgcrypt.so') gcrypt.gcry_pk_get_keygrip.restype = ctypes.POINTER(ctypes.c_char) def convert_to_sexp(data): if isinstance(data, int): return convert_to_sexp(data.to_bytes( math.ceil(data.bit_length() / 8), 'big', )) elif isinstance(data, str): return convert_to_sexp(data.encode()) elif isinstance(data, bytes): return str(len(data)).encode() + b':' + data else: return b'(' + b''.join(convert_to_sexp(i) for i in data) + b')' def keygrip_from_key_sexp(key_sexp_data): sexp = ctypes.c_void_p() assert not gcrypt.gcry_sexp_new( ctypes.pointer(sexp), key_sexp_data, len(key_sexp_data), 0, ) keygrip = gcrypt.gcry_pk_get_keygrip(sexp, None)[:20] gcrypt.gcry_sexp_release(sexp) return keygrip def load_public_key(input_path): backend = cryptography.hazmat.backends.default_backend() with open(input_path, 'rb') as input_file: input_data = input_file.read() try: return cryptography.hazmat.primitives.serialization.load_pem_public_key( input_data, backend, ) except ValueError: req = cryptography.x509.load_pem_x509_csr(input_data, backend) assert req.is_signature_valid return req.public_key() def create_gpg_key(input_path, gpg_key_output_path_pattern, smartcard_app_id_hex, keygrip_hex_output_path): pubnums = load_public_key(input_path).public_numbers() key_data = ['shadowed-private-key', [ 'rsa', ['n', pubnums.n], ['e', pubnums.e], ['shadowed', 't1-v1', [int(smartcard_app_id_hex, 16), 'OPENPGP.1']], ]] key_sexp_data = convert_to_sexp(key_data) keygrip = keygrip_from_key_sexp(key_sexp_data) keygrip_hex = binascii.hexlify(keygrip).upper().decode() if keygrip_hex_output_path: with open(keygrip_hex_output_path, 'w') as keygrip_file: keygrip_file.write(keygrip_hex + '\n') with open(gpg_key_output_path_pattern.format(keygrip_hex=keygrip_hex), 'wb') as sexp_file: sexp_file.write(key_sexp_data) def _init_argparser(): import argparse argparser = argparse.ArgumentParser( description='create a shadowed-private-key in sexp format' ' for gnupg\'s private-keys-v1.d folder' ' containing the public key of a' ' PEM-encoded X.509 certificate signing request (CSR)', ) argparser.add_argument( 'input_path', help='path to PEM-encoded X.509 signing request or public key', ) argparser.add_argument( '--gpg-key-output-path', dest='gpg_key_output_path_pattern', metavar='path-pattern', default=DEFAULT_KEY_OUTPUT_PATH_PATTERN, help='path to sexp-encoded shadowed-private-key to be created.' ' if specified, {keygrip_hex} will be substituted.' ' (default: "%(default)s")', ) argparser.add_argument( '--smartcard-app-id', dest='smartcard_app_id_hex', metavar='hex-string', default=DEFAULT_SMARTCARD_APP_ID_HEX, help='default: %(default)s', ) argparser.add_argument( '--keygrip-hex-output-path', metavar='path', default=None, help='path where to save keygrip in hex format' ' (default: no output)', ) return argparser def main(argv): argparser = _init_argparser() args = argparser.parse_args(argv[1:]) create_gpg_key(**vars(args)) return 0 if __name__ == '__main__': import sys sys.exit(main(sys.argv))