ckeygen.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. # -*- test-case-name: twisted.conch.test.test_ckeygen -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Implementation module for the `ckeygen` command.
  6. """
  7. from __future__ import print_function
  8. import sys, os, getpass, socket
  9. from functools import wraps
  10. from imp import reload
  11. if getpass.getpass == getpass.unix_getpass:
  12. try:
  13. import termios # hack around broken termios
  14. termios.tcgetattr, termios.tcsetattr
  15. except (ImportError, AttributeError):
  16. sys.modules['termios'] = None
  17. reload(getpass)
  18. from twisted.conch.ssh import keys
  19. from twisted.python import failure, filepath, log, usage
  20. from twisted.python.compat import raw_input, _PY3
  21. supportedKeyTypes = dict()
  22. def _keyGenerator(keyType):
  23. def assignkeygenerator(keygenerator):
  24. @wraps(keygenerator)
  25. def wrapper(*args, **kwargs):
  26. return keygenerator(*args, **kwargs)
  27. supportedKeyTypes[keyType] = wrapper
  28. return wrapper
  29. return assignkeygenerator
  30. class GeneralOptions(usage.Options):
  31. synopsis = """Usage: ckeygen [options]
  32. """
  33. longdesc = "ckeygen manipulates public/private keys in various ways."
  34. optParameters = [['bits', 'b', None, 'Number of bits in the key to create.'],
  35. ['filename', 'f', None, 'Filename of the key file.'],
  36. ['type', 't', None, 'Specify type of key to create.'],
  37. ['comment', 'C', None, 'Provide new comment.'],
  38. ['newpass', 'N', None, 'Provide new passphrase.'],
  39. ['pass', 'P', None, 'Provide old passphrase.'],
  40. ['format', 'o', 'sha256-base64',
  41. 'Fingerprint format of key file.'],
  42. ['private-key-subtype', None, 'PEM',
  43. 'OpenSSH private key subtype to write ("PEM" or "v1").']]
  44. optFlags = [['fingerprint', 'l', 'Show fingerprint of key file.'],
  45. ['changepass', 'p', 'Change passphrase of private key file.'],
  46. ['quiet', 'q', 'Quiet.'],
  47. ['no-passphrase', None, "Create the key with no passphrase."],
  48. ['showpub', 'y',
  49. 'Read private key file and print public key.']]
  50. compData = usage.Completions(
  51. optActions={
  52. "type": usage.CompleteList(list(supportedKeyTypes.keys())),
  53. "private-key-subtype": usage.CompleteList(["PEM", "v1"]),
  54. })
  55. def run():
  56. options = GeneralOptions()
  57. try:
  58. options.parseOptions(sys.argv[1:])
  59. except usage.UsageError as u:
  60. print('ERROR: %s' % u)
  61. options.opt_help()
  62. sys.exit(1)
  63. log.discardLogs()
  64. log.deferr = handleError # HACK
  65. if options['type']:
  66. if options['type'].lower() in supportedKeyTypes:
  67. print('Generating public/private %s key pair.' % (options['type']))
  68. supportedKeyTypes[options['type'].lower()](options)
  69. else:
  70. sys.exit(
  71. 'Key type was %s, must be one of %s'
  72. % (options['type'], ', '.join(supportedKeyTypes.keys())))
  73. elif options['fingerprint']:
  74. printFingerprint(options)
  75. elif options['changepass']:
  76. changePassPhrase(options)
  77. elif options['showpub']:
  78. displayPublicKey(options)
  79. else:
  80. options.opt_help()
  81. sys.exit(1)
  82. def enumrepresentation(options):
  83. if options['format'] == 'md5-hex':
  84. options['format'] = keys.FingerprintFormats.MD5_HEX
  85. return options
  86. elif options['format'] == 'sha256-base64':
  87. options['format'] = keys.FingerprintFormats.SHA256_BASE64
  88. return options
  89. else:
  90. raise keys.BadFingerPrintFormat(
  91. 'Unsupported fingerprint format: %s' % (options['format'],))
  92. def handleError():
  93. global exitStatus
  94. exitStatus = 2
  95. log.err(failure.Failure())
  96. raise
  97. @_keyGenerator('rsa')
  98. def generateRSAkey(options):
  99. from cryptography.hazmat.backends import default_backend
  100. from cryptography.hazmat.primitives.asymmetric import rsa
  101. if not options['bits']:
  102. options['bits'] = 1024
  103. keyPrimitive = rsa.generate_private_key(
  104. key_size=int(options['bits']),
  105. public_exponent=65537,
  106. backend=default_backend(),
  107. )
  108. key = keys.Key(keyPrimitive)
  109. _saveKey(key, options)
  110. @_keyGenerator('dsa')
  111. def generateDSAkey(options):
  112. from cryptography.hazmat.backends import default_backend
  113. from cryptography.hazmat.primitives.asymmetric import dsa
  114. if not options['bits']:
  115. options['bits'] = 1024
  116. keyPrimitive = dsa.generate_private_key(
  117. key_size=int(options['bits']),
  118. backend=default_backend(),
  119. )
  120. key = keys.Key(keyPrimitive)
  121. _saveKey(key, options)
  122. @_keyGenerator('ecdsa')
  123. def generateECDSAkey(options):
  124. from cryptography.hazmat.backends import default_backend
  125. from cryptography.hazmat.primitives.asymmetric import ec
  126. if not options['bits']:
  127. options['bits'] = 256
  128. # OpenSSH supports only mandatory sections of RFC5656.
  129. # See https://www.openssh.com/txt/release-5.7
  130. curve = b'ecdsa-sha2-nistp' + str(options['bits']).encode('ascii')
  131. keyPrimitive = ec.generate_private_key(
  132. curve=keys._curveTable[curve],
  133. backend=default_backend()
  134. )
  135. key = keys.Key(keyPrimitive)
  136. _saveKey(key, options)
  137. def printFingerprint(options):
  138. if not options['filename']:
  139. filename = os.path.expanduser('~/.ssh/id_rsa')
  140. options['filename'] = raw_input('Enter file in which the key is (%s): ' % filename)
  141. if os.path.exists(options['filename']+'.pub'):
  142. options['filename'] += '.pub'
  143. options = enumrepresentation(options)
  144. try:
  145. key = keys.Key.fromFile(options['filename'])
  146. print('%s %s %s' % (
  147. key.size(),
  148. key.fingerprint(options['format']),
  149. os.path.basename(options['filename'])))
  150. except keys.BadKeyError:
  151. sys.exit('bad key')
  152. def changePassPhrase(options):
  153. if not options['filename']:
  154. filename = os.path.expanduser('~/.ssh/id_rsa')
  155. options['filename'] = raw_input(
  156. 'Enter file in which the key is (%s): ' % filename)
  157. try:
  158. key = keys.Key.fromFile(options['filename'])
  159. except keys.EncryptedKeyError:
  160. # Raised if password not supplied for an encrypted key
  161. if not options.get('pass'):
  162. options['pass'] = getpass.getpass('Enter old passphrase: ')
  163. try:
  164. key = keys.Key.fromFile(
  165. options['filename'], passphrase=options['pass'])
  166. except keys.BadKeyError:
  167. sys.exit('Could not change passphrase: old passphrase error')
  168. except keys.EncryptedKeyError as e:
  169. sys.exit('Could not change passphrase: %s' % (e,))
  170. except keys.BadKeyError as e:
  171. sys.exit('Could not change passphrase: %s' % (e,))
  172. if not options.get('newpass'):
  173. while 1:
  174. p1 = getpass.getpass(
  175. 'Enter new passphrase (empty for no passphrase): ')
  176. p2 = getpass.getpass('Enter same passphrase again: ')
  177. if p1 == p2:
  178. break
  179. print('Passphrases do not match. Try again.')
  180. options['newpass'] = p1
  181. try:
  182. newkeydata = key.toString(
  183. 'openssh', subtype=options.get('private-key-subtype'),
  184. passphrase=options['newpass'])
  185. except Exception as e:
  186. sys.exit('Could not change passphrase: %s' % (e,))
  187. try:
  188. keys.Key.fromString(newkeydata, passphrase=options['newpass'])
  189. except (keys.EncryptedKeyError, keys.BadKeyError) as e:
  190. sys.exit('Could not change passphrase: %s' % (e,))
  191. with open(options['filename'], 'wb') as fd:
  192. fd.write(newkeydata)
  193. print('Your identification has been saved with the new passphrase.')
  194. def displayPublicKey(options):
  195. if not options['filename']:
  196. filename = os.path.expanduser('~/.ssh/id_rsa')
  197. options['filename'] = raw_input('Enter file in which the key is (%s): ' % filename)
  198. try:
  199. key = keys.Key.fromFile(options['filename'])
  200. except keys.EncryptedKeyError:
  201. if not options.get('pass'):
  202. options['pass'] = getpass.getpass('Enter passphrase: ')
  203. key = keys.Key.fromFile(
  204. options['filename'], passphrase = options['pass'])
  205. displayKey = key.public().toString('openssh')
  206. if _PY3:
  207. displayKey = displayKey.decode("ascii")
  208. print(displayKey)
  209. def _saveKey(key, options):
  210. """
  211. Persist a SSH key on local filesystem.
  212. @param key: Key which is persisted on local filesystem.
  213. @type key: C{keys.Key} implementation.
  214. @param options:
  215. @type options: L{dict}
  216. """
  217. KeyTypeMapping = {'EC': 'ecdsa', 'RSA': 'rsa', 'DSA': 'dsa'}
  218. keyTypeName = KeyTypeMapping[key.type()]
  219. if not options['filename']:
  220. defaultPath = os.path.expanduser(u'~/.ssh/id_%s' % (keyTypeName,))
  221. newPath = raw_input(
  222. 'Enter file in which to save the key (%s): ' % (defaultPath,))
  223. options['filename'] = newPath.strip() or defaultPath
  224. if os.path.exists(options['filename']):
  225. print('%s already exists.' % (options['filename'],))
  226. yn = raw_input('Overwrite (y/n)? ')
  227. if yn[0].lower() != 'y':
  228. sys.exit()
  229. if options.get('no-passphrase'):
  230. options['pass'] = b''
  231. elif not options['pass']:
  232. while 1:
  233. p1 = getpass.getpass(
  234. 'Enter passphrase (empty for no passphrase): ')
  235. p2 = getpass.getpass('Enter same passphrase again: ')
  236. if p1 == p2:
  237. break
  238. print('Passphrases do not match. Try again.')
  239. options['pass'] = p1
  240. comment = '%s@%s' % (getpass.getuser(), socket.gethostname())
  241. filepath.FilePath(options['filename']).setContent(
  242. key.toString(
  243. 'openssh', subtype=options.get('private-key-subtype'),
  244. passphrase=options['pass']))
  245. os.chmod(options['filename'], 33152)
  246. filepath.FilePath(options['filename'] + '.pub').setContent(
  247. key.public().toString('openssh', comment=comment))
  248. options = enumrepresentation(options)
  249. print('Your identification has been saved in %s' % (options['filename'],))
  250. print('Your public key has been saved in %s.pub' % (options['filename'],))
  251. print('The key fingerprint in %s is:' % (options['format'],))
  252. print(key.fingerprint(options['format']))
  253. if __name__ == '__main__':
  254. run()