create-gpg-shadow-key-from-x509-cert-req.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. #!/usr/bin/env python3
  2. """
  3. create a shadowed-private-key in sexp format
  4. for gnupg's private-keys-v1.d folder
  5. containing the public key of a
  6. X.509 certificate signing request or a public key
  7. """
  8. import ctypes
  9. import math
  10. import binascii
  11. import cryptography.hazmat.backends
  12. import cryptography.hazmat.primitives.serialization
  13. import cryptography.x509
  14. DEFAULT_KEY_OUTPUT_PATH_PATTERN = '{keygrip_hex}.key'
  15. DEFAULT_SMARTCARD_APP_ID_HEX = 'D2760001240102010001234567890000'
  16. gcrypt = ctypes.cdll.LoadLibrary('libgcrypt.so')
  17. gcrypt.gcry_pk_get_keygrip.restype = ctypes.POINTER(ctypes.c_char)
  18. def convert_to_sexp(data):
  19. if isinstance(data, int):
  20. return convert_to_sexp(data.to_bytes(
  21. math.ceil(data.bit_length() / 8),
  22. 'big',
  23. ))
  24. elif isinstance(data, str):
  25. return convert_to_sexp(data.encode())
  26. elif isinstance(data, bytes):
  27. return str(len(data)).encode() + b':' + data
  28. else:
  29. return b'(' + b''.join(convert_to_sexp(i) for i in data) + b')'
  30. def keygrip_from_key_sexp(key_sexp_data):
  31. sexp = ctypes.c_void_p()
  32. assert not gcrypt.gcry_sexp_new(
  33. ctypes.pointer(sexp),
  34. key_sexp_data,
  35. len(key_sexp_data),
  36. 0,
  37. )
  38. keygrip = gcrypt.gcry_pk_get_keygrip(sexp, None)[:20]
  39. gcrypt.gcry_sexp_release(sexp)
  40. return keygrip
  41. def load_public_key(input_path):
  42. backend = cryptography.hazmat.backends.default_backend()
  43. with open(input_path, 'rb') as input_file:
  44. input_data = input_file.read()
  45. try:
  46. return cryptography.hazmat.primitives.serialization.load_pem_public_key(
  47. input_data,
  48. backend,
  49. )
  50. except ValueError:
  51. req = cryptography.x509.load_pem_x509_csr(input_data, backend)
  52. assert req.is_signature_valid
  53. return req.public_key()
  54. def create_gpg_key(input_path, gpg_key_output_path_pattern,
  55. smartcard_app_id_hex, keygrip_hex_output_path):
  56. pubnums = load_public_key(input_path).public_numbers()
  57. key_data = ['shadowed-private-key', [
  58. 'rsa',
  59. ['n', pubnums.n],
  60. ['e', pubnums.e],
  61. ['shadowed', 't1-v1', [int(smartcard_app_id_hex, 16), 'OPENPGP.1']],
  62. ]]
  63. key_sexp_data = convert_to_sexp(key_data)
  64. keygrip = keygrip_from_key_sexp(key_sexp_data)
  65. keygrip_hex = binascii.hexlify(keygrip).upper().decode()
  66. if keygrip_hex_output_path:
  67. with open(keygrip_hex_output_path, 'w') as keygrip_file:
  68. keygrip_file.write(keygrip_hex + '\n')
  69. with open(gpg_key_output_path_pattern.format(keygrip_hex=keygrip_hex), 'wb') as sexp_file:
  70. sexp_file.write(key_sexp_data)
  71. def _init_argparser():
  72. import argparse
  73. argparser = argparse.ArgumentParser(
  74. description='create a shadowed-private-key in sexp format'
  75. ' for gnupg\'s private-keys-v1.d folder'
  76. ' containing the public key of a'
  77. ' PEM-encoded X.509 certificate signing request (CSR)',
  78. )
  79. argparser.add_argument(
  80. 'input_path',
  81. help='path to PEM-encoded X.509 signing request or public key',
  82. )
  83. argparser.add_argument(
  84. '--gpg-key-output-path',
  85. dest='gpg_key_output_path_pattern',
  86. metavar='path-pattern',
  87. default=DEFAULT_KEY_OUTPUT_PATH_PATTERN,
  88. help='path to sexp-encoded shadowed-private-key to be created.'
  89. ' if specified, {keygrip_hex} will be substituted.'
  90. ' (default: "%(default)s")',
  91. )
  92. argparser.add_argument(
  93. '--smartcard-app-id',
  94. dest='smartcard_app_id_hex',
  95. metavar='hex-string',
  96. default=DEFAULT_SMARTCARD_APP_ID_HEX,
  97. help='default: %(default)s',
  98. )
  99. argparser.add_argument(
  100. '--keygrip-hex-output-path',
  101. metavar='path',
  102. default=None,
  103. help='path where to save keygrip in hex format'
  104. ' (default: no output)',
  105. )
  106. return argparser
  107. def main(argv):
  108. argparser = _init_argparser()
  109. args = argparser.parse_args(argv[1:])
  110. create_gpg_key(**vars(args))
  111. return 0
  112. if __name__ == '__main__':
  113. import sys
  114. sys.exit(main(sys.argv))