test_ckeygen.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Tests for L{twisted.conch.scripts.ckeygen}.
  5. """
  6. import getpass
  7. import sys
  8. import subprocess
  9. from io import BytesIO, StringIO
  10. from twisted.python.compat import unicode, _PY3
  11. from twisted.python.reflect import requireModule
  12. if requireModule('cryptography') and requireModule('pyasn1'):
  13. from twisted.conch.ssh.keys import (Key, BadKeyError,
  14. BadFingerPrintFormat, FingerprintFormats)
  15. from twisted.conch.scripts.ckeygen import (
  16. changePassPhrase, displayPublicKey, printFingerprint,
  17. _saveKey, enumrepresentation)
  18. else:
  19. skip = "cryptography and pyasn1 required for twisted.conch.scripts.ckeygen"
  20. from twisted.python.filepath import FilePath
  21. from twisted.trial.unittest import TestCase
  22. from twisted.conch.test.keydata import (
  23. publicRSA_openssh, privateRSA_openssh, privateRSA_openssh_encrypted, privateECDSA_openssh)
  24. def makeGetpass(*passphrases):
  25. """
  26. Return a callable to patch C{getpass.getpass}. Yields a passphrase each
  27. time called. Use case is to provide an old, then new passphrase(s) as if
  28. requested interactively.
  29. @param passphrases: The list of passphrases returned, one per each call.
  30. @return: A callable to patch C{getpass.getpass}.
  31. """
  32. passphrases = iter(passphrases)
  33. def fakeGetpass(_):
  34. return next(passphrases)
  35. return fakeGetpass
  36. class KeyGenTests(TestCase):
  37. """
  38. Tests for various functions used to implement the I{ckeygen} script.
  39. """
  40. def setUp(self):
  41. """
  42. Patch C{sys.stdout} so tests can make assertions about what's printed.
  43. """
  44. if _PY3:
  45. self.stdout = StringIO()
  46. else:
  47. self.stdout = BytesIO()
  48. self.patch(sys, 'stdout', self.stdout)
  49. def _testrun(self, keyType, keySize=None):
  50. filename = self.mktemp()
  51. if keySize is None:
  52. subprocess.call(['ckeygen', '-t', keyType, '-f', filename, '--no-passphrase'])
  53. else:
  54. subprocess.call(['ckeygen', '-t', keyType, '-f', filename, '--no-passphrase',
  55. '-b', keySize])
  56. privKey = Key.fromFile(filename)
  57. pubKey = Key.fromFile(filename + '.pub')
  58. if keyType == 'ecdsa':
  59. self.assertEqual(privKey.type(), 'EC')
  60. else:
  61. self.assertEqual(privKey.type(), keyType.upper())
  62. self.assertTrue(pubKey.isPublic())
  63. def test_keygeneration(self):
  64. self._testrun('ecdsa', '384')
  65. self._testrun('ecdsa')
  66. self._testrun('dsa', '2048')
  67. self._testrun('dsa')
  68. self._testrun('rsa', '2048')
  69. self._testrun('rsa')
  70. def test_runBadKeytype(self):
  71. filename = self.mktemp()
  72. with self.assertRaises(subprocess.CalledProcessError):
  73. subprocess.check_call(['ckeygen', '-t', 'foo', '-f', filename])
  74. def test_enumrepresentation(self):
  75. """
  76. L{enumrepresentation} takes a dictionary as input and returns a
  77. dictionary with its attributes changed to enum representation.
  78. """
  79. options = enumrepresentation({'format': 'md5-hex'})
  80. self.assertIs(options['format'],
  81. FingerprintFormats.MD5_HEX)
  82. def test_enumrepresentationsha256(self):
  83. """
  84. Test for format L{FingerprintFormats.SHA256-BASE64}.
  85. """
  86. options = enumrepresentation({'format': 'sha256-base64'})
  87. self.assertIs(options['format'],
  88. FingerprintFormats.SHA256_BASE64)
  89. def test_enumrepresentationBadFormat(self):
  90. """
  91. Test for unsupported fingerprint format
  92. """
  93. with self.assertRaises(BadFingerPrintFormat) as em:
  94. enumrepresentation({'format': 'sha-base64'})
  95. self.assertEqual('Unsupported fingerprint format: sha-base64',
  96. em.exception.args[0])
  97. def test_printFingerprint(self):
  98. """
  99. L{printFingerprint} writes a line to standard out giving the number of
  100. bits of the key, its fingerprint, and the basename of the file from it
  101. was read.
  102. """
  103. filename = self.mktemp()
  104. FilePath(filename).setContent(publicRSA_openssh)
  105. printFingerprint({'filename': filename,
  106. 'format': 'md5-hex'})
  107. self.assertEqual(
  108. self.stdout.getvalue(),
  109. '768 3d:13:5f:cb:c9:79:8a:93:06:27:65:bc:3d:0b:8f:af temp\n')
  110. def test_printFingerprintsha256(self):
  111. """
  112. L{printFigerprint} will print key fingerprint in
  113. L{FingerprintFormats.SHA256-BASE64} format if explicitly specified.
  114. """
  115. filename = self.mktemp()
  116. FilePath(filename).setContent(publicRSA_openssh)
  117. printFingerprint({'filename': filename,
  118. 'format': 'sha256-base64'})
  119. self.assertEqual(
  120. self.stdout.getvalue(),
  121. '768 ryaugIFT0B8ItuszldMEU7q14rG/wj9HkRosMeBWkts= temp\n')
  122. def test_printFingerprintBadFingerPrintFormat(self):
  123. """
  124. L{printFigerprint} raises C{keys.BadFingerprintFormat} when unsupported
  125. formats are requested.
  126. """
  127. filename = self.mktemp()
  128. FilePath(filename).setContent(publicRSA_openssh)
  129. with self.assertRaises(BadFingerPrintFormat) as em:
  130. printFingerprint({'filename': filename, 'format':'sha-base64'})
  131. self.assertEqual('Unsupported fingerprint format: sha-base64',
  132. em.exception.args[0])
  133. def test_saveKey(self):
  134. """
  135. L{_saveKey} writes the private and public parts of a key to two
  136. different files and writes a report of this to standard out.
  137. """
  138. base = FilePath(self.mktemp())
  139. base.makedirs()
  140. filename = base.child('id_rsa').path
  141. key = Key.fromString(privateRSA_openssh)
  142. _saveKey(key, {'filename': filename, 'pass': 'passphrase',
  143. 'format': 'md5-hex'})
  144. self.assertEqual(
  145. self.stdout.getvalue(),
  146. "Your identification has been saved in %s\n"
  147. "Your public key has been saved in %s.pub\n"
  148. "The key fingerprint in <FingerprintFormats=MD5_HEX> is:\n"
  149. "3d:13:5f:cb:c9:79:8a:93:06:27:65:bc:3d:0b:8f:af\n" % (
  150. filename,
  151. filename))
  152. self.assertEqual(
  153. key.fromString(
  154. base.child('id_rsa').getContent(), None, 'passphrase'),
  155. key)
  156. self.assertEqual(
  157. Key.fromString(base.child('id_rsa.pub').getContent()),
  158. key.public())
  159. def test_saveKeyECDSA(self):
  160. """
  161. L{_saveKey} writes the private and public parts of a key to two
  162. different files and writes a report of this to standard out.
  163. Test with ECDSA key.
  164. """
  165. base = FilePath(self.mktemp())
  166. base.makedirs()
  167. filename = base.child('id_ecdsa').path
  168. key = Key.fromString(privateECDSA_openssh)
  169. _saveKey(key, {'filename': filename, 'pass': 'passphrase',
  170. 'format': 'md5-hex'})
  171. self.assertEqual(
  172. self.stdout.getvalue(),
  173. "Your identification has been saved in %s\n"
  174. "Your public key has been saved in %s.pub\n"
  175. "The key fingerprint in <FingerprintFormats=MD5_HEX> is:\n"
  176. "1e:ab:83:a6:f2:04:22:99:7c:64:14:d2:ab:fa:f5:16\n" % (
  177. filename,
  178. filename))
  179. self.assertEqual(
  180. key.fromString(
  181. base.child('id_ecdsa').getContent(), None, 'passphrase'),
  182. key)
  183. self.assertEqual(
  184. Key.fromString(base.child('id_ecdsa.pub').getContent()),
  185. key.public())
  186. def test_saveKeysha256(self):
  187. """
  188. L{_saveKey} will generate key fingerprint in
  189. L{FingerprintFormats.SHA256-BASE64} format if explicitly specified.
  190. """
  191. base = FilePath(self.mktemp())
  192. base.makedirs()
  193. filename = base.child('id_rsa').path
  194. key = Key.fromString(privateRSA_openssh)
  195. _saveKey(key, {'filename': filename, 'pass': 'passphrase',
  196. 'format': 'sha256-base64'})
  197. self.assertEqual(
  198. self.stdout.getvalue(),
  199. "Your identification has been saved in %s\n"
  200. "Your public key has been saved in %s.pub\n"
  201. "The key fingerprint in <FingerprintFormats=SHA256_BASE64> is:\n"
  202. "ryaugIFT0B8ItuszldMEU7q14rG/wj9HkRosMeBWkts=\n" % (
  203. filename,
  204. filename))
  205. self.assertEqual(
  206. key.fromString(
  207. base.child('id_rsa').getContent(), None, 'passphrase'),
  208. key)
  209. self.assertEqual(
  210. Key.fromString(base.child('id_rsa.pub').getContent()),
  211. key.public())
  212. def test_saveKeyBadFingerPrintformat(self):
  213. """
  214. L{_saveKey} raises C{keys.BadFingerprintFormat} when unsupported
  215. formats are requested.
  216. """
  217. base = FilePath(self.mktemp())
  218. base.makedirs()
  219. filename = base.child('id_rsa').path
  220. key = Key.fromString(privateRSA_openssh)
  221. with self.assertRaises(BadFingerPrintFormat) as em:
  222. _saveKey(key, {'filename': filename, 'pass': 'passphrase',
  223. 'format': 'sha-base64'})
  224. self.assertEqual('Unsupported fingerprint format: sha-base64',
  225. em.exception.args[0])
  226. def test_saveKeyEmptyPassphrase(self):
  227. """
  228. L{_saveKey} will choose an empty string for the passphrase if
  229. no-passphrase is C{True}.
  230. """
  231. base = FilePath(self.mktemp())
  232. base.makedirs()
  233. filename = base.child('id_rsa').path
  234. key = Key.fromString(privateRSA_openssh)
  235. _saveKey(key, {'filename': filename, 'no-passphrase': True,
  236. 'format': 'md5-hex'})
  237. self.assertEqual(
  238. key.fromString(
  239. base.child('id_rsa').getContent(), None, b''),
  240. key)
  241. def test_saveKeyECDSAEmptyPassphrase(self):
  242. """
  243. L{_saveKey} will choose an empty string for the passphrase if
  244. no-passphrase is C{True}.
  245. """
  246. base = FilePath(self.mktemp())
  247. base.makedirs()
  248. filename = base.child('id_ecdsa').path
  249. key = Key.fromString(privateECDSA_openssh)
  250. _saveKey(key, {'filename': filename, 'no-passphrase': True,
  251. 'format': 'md5-hex'})
  252. self.assertEqual(
  253. key.fromString(
  254. base.child('id_ecdsa').getContent(), None),
  255. key)
  256. def test_saveKeyNoFilename(self):
  257. """
  258. When no path is specified, it will ask for the path used to store the
  259. key.
  260. """
  261. base = FilePath(self.mktemp())
  262. base.makedirs()
  263. keyPath = base.child('custom_key').path
  264. import twisted.conch.scripts.ckeygen
  265. self.patch(twisted.conch.scripts.ckeygen, 'raw_input', lambda _: keyPath)
  266. key = Key.fromString(privateRSA_openssh)
  267. _saveKey(key, {'filename': None, 'no-passphrase': True,
  268. 'format': 'md5-hex'})
  269. persistedKeyContent = base.child('custom_key').getContent()
  270. persistedKey = key.fromString(persistedKeyContent, None, b'')
  271. self.assertEqual(key, persistedKey)
  272. def test_displayPublicKey(self):
  273. """
  274. L{displayPublicKey} prints out the public key associated with a given
  275. private key.
  276. """
  277. filename = self.mktemp()
  278. pubKey = Key.fromString(publicRSA_openssh)
  279. FilePath(filename).setContent(privateRSA_openssh)
  280. displayPublicKey({'filename': filename})
  281. displayed = self.stdout.getvalue().strip('\n')
  282. if isinstance(displayed, unicode):
  283. displayed = displayed.encode("ascii")
  284. self.assertEqual(
  285. displayed,
  286. pubKey.toString('openssh'))
  287. def test_displayPublicKeyEncrypted(self):
  288. """
  289. L{displayPublicKey} prints out the public key associated with a given
  290. private key using the given passphrase when it's encrypted.
  291. """
  292. filename = self.mktemp()
  293. pubKey = Key.fromString(publicRSA_openssh)
  294. FilePath(filename).setContent(privateRSA_openssh_encrypted)
  295. displayPublicKey({'filename': filename, 'pass': 'encrypted'})
  296. displayed = self.stdout.getvalue().strip('\n')
  297. if isinstance(displayed, unicode):
  298. displayed = displayed.encode("ascii")
  299. self.assertEqual(
  300. displayed,
  301. pubKey.toString('openssh'))
  302. def test_displayPublicKeyEncryptedPassphrasePrompt(self):
  303. """
  304. L{displayPublicKey} prints out the public key associated with a given
  305. private key, asking for the passphrase when it's encrypted.
  306. """
  307. filename = self.mktemp()
  308. pubKey = Key.fromString(publicRSA_openssh)
  309. FilePath(filename).setContent(privateRSA_openssh_encrypted)
  310. self.patch(getpass, 'getpass', lambda x: 'encrypted')
  311. displayPublicKey({'filename': filename})
  312. displayed = self.stdout.getvalue().strip('\n')
  313. if isinstance(displayed, unicode):
  314. displayed = displayed.encode("ascii")
  315. self.assertEqual(
  316. displayed,
  317. pubKey.toString('openssh'))
  318. def test_displayPublicKeyWrongPassphrase(self):
  319. """
  320. L{displayPublicKey} fails with a L{BadKeyError} when trying to decrypt
  321. an encrypted key with the wrong password.
  322. """
  323. filename = self.mktemp()
  324. FilePath(filename).setContent(privateRSA_openssh_encrypted)
  325. self.assertRaises(
  326. BadKeyError, displayPublicKey,
  327. {'filename': filename, 'pass': 'wrong'})
  328. def test_changePassphrase(self):
  329. """
  330. L{changePassPhrase} allows a user to change the passphrase of a
  331. private key interactively.
  332. """
  333. oldNewConfirm = makeGetpass('encrypted', 'newpass', 'newpass')
  334. self.patch(getpass, 'getpass', oldNewConfirm)
  335. filename = self.mktemp()
  336. FilePath(filename).setContent(privateRSA_openssh_encrypted)
  337. changePassPhrase({'filename': filename})
  338. self.assertEqual(
  339. self.stdout.getvalue().strip('\n'),
  340. 'Your identification has been saved with the new passphrase.')
  341. self.assertNotEqual(privateRSA_openssh_encrypted,
  342. FilePath(filename).getContent())
  343. def test_changePassphraseWithOld(self):
  344. """
  345. L{changePassPhrase} allows a user to change the passphrase of a
  346. private key, providing the old passphrase and prompting for new one.
  347. """
  348. newConfirm = makeGetpass('newpass', 'newpass')
  349. self.patch(getpass, 'getpass', newConfirm)
  350. filename = self.mktemp()
  351. FilePath(filename).setContent(privateRSA_openssh_encrypted)
  352. changePassPhrase({'filename': filename, 'pass': 'encrypted'})
  353. self.assertEqual(
  354. self.stdout.getvalue().strip('\n'),
  355. 'Your identification has been saved with the new passphrase.')
  356. self.assertNotEqual(privateRSA_openssh_encrypted,
  357. FilePath(filename).getContent())
  358. def test_changePassphraseWithBoth(self):
  359. """
  360. L{changePassPhrase} allows a user to change the passphrase of a private
  361. key by providing both old and new passphrases without prompting.
  362. """
  363. filename = self.mktemp()
  364. FilePath(filename).setContent(privateRSA_openssh_encrypted)
  365. changePassPhrase(
  366. {'filename': filename, 'pass': 'encrypted',
  367. 'newpass': 'newencrypt'})
  368. self.assertEqual(
  369. self.stdout.getvalue().strip('\n'),
  370. 'Your identification has been saved with the new passphrase.')
  371. self.assertNotEqual(privateRSA_openssh_encrypted,
  372. FilePath(filename).getContent())
  373. def test_changePassphraseWrongPassphrase(self):
  374. """
  375. L{changePassPhrase} exits if passed an invalid old passphrase when
  376. trying to change the passphrase of a private key.
  377. """
  378. filename = self.mktemp()
  379. FilePath(filename).setContent(privateRSA_openssh_encrypted)
  380. error = self.assertRaises(
  381. SystemExit, changePassPhrase,
  382. {'filename': filename, 'pass': 'wrong'})
  383. self.assertEqual('Could not change passphrase: old passphrase error',
  384. str(error))
  385. self.assertEqual(privateRSA_openssh_encrypted,
  386. FilePath(filename).getContent())
  387. def test_changePassphraseEmptyGetPass(self):
  388. """
  389. L{changePassPhrase} exits if no passphrase is specified for the
  390. C{getpass} call and the key is encrypted.
  391. """
  392. self.patch(getpass, 'getpass', makeGetpass(''))
  393. filename = self.mktemp()
  394. FilePath(filename).setContent(privateRSA_openssh_encrypted)
  395. error = self.assertRaises(
  396. SystemExit, changePassPhrase, {'filename': filename})
  397. self.assertEqual(
  398. 'Could not change passphrase: Passphrase must be provided '
  399. 'for an encrypted key',
  400. str(error))
  401. self.assertEqual(privateRSA_openssh_encrypted,
  402. FilePath(filename).getContent())
  403. def test_changePassphraseBadKey(self):
  404. """
  405. L{changePassPhrase} exits if the file specified points to an invalid
  406. key.
  407. """
  408. filename = self.mktemp()
  409. FilePath(filename).setContent(b'foobar')
  410. error = self.assertRaises(
  411. SystemExit, changePassPhrase, {'filename': filename})
  412. if _PY3:
  413. expected = "Could not change passphrase: cannot guess the type of b'foobar'"
  414. else:
  415. expected = "Could not change passphrase: cannot guess the type of 'foobar'"
  416. self.assertEqual(expected, str(error))
  417. self.assertEqual(b'foobar', FilePath(filename).getContent())
  418. def test_changePassphraseCreateError(self):
  419. """
  420. L{changePassPhrase} doesn't modify the key file if an unexpected error
  421. happens when trying to create the key with the new passphrase.
  422. """
  423. filename = self.mktemp()
  424. FilePath(filename).setContent(privateRSA_openssh)
  425. def toString(*args, **kwargs):
  426. raise RuntimeError('oops')
  427. self.patch(Key, 'toString', toString)
  428. error = self.assertRaises(
  429. SystemExit, changePassPhrase,
  430. {'filename': filename,
  431. 'newpass': 'newencrypt'})
  432. self.assertEqual(
  433. 'Could not change passphrase: oops', str(error))
  434. self.assertEqual(privateRSA_openssh, FilePath(filename).getContent())
  435. def test_changePassphraseEmptyStringError(self):
  436. """
  437. L{changePassPhrase} doesn't modify the key file if C{toString} returns
  438. an empty string.
  439. """
  440. filename = self.mktemp()
  441. FilePath(filename).setContent(privateRSA_openssh)
  442. def toString(*args, **kwargs):
  443. return ''
  444. self.patch(Key, 'toString', toString)
  445. error = self.assertRaises(
  446. SystemExit, changePassPhrase,
  447. {'filename': filename, 'newpass': 'newencrypt'})
  448. if _PY3:
  449. expected = (
  450. "Could not change passphrase: cannot guess the type of b''")
  451. else:
  452. expected = (
  453. "Could not change passphrase: cannot guess the type of ''")
  454. self.assertEqual(expected, str(error))
  455. self.assertEqual(privateRSA_openssh, FilePath(filename).getContent())
  456. def test_changePassphrasePublicKey(self):
  457. """
  458. L{changePassPhrase} exits when trying to change the passphrase on a
  459. public key, and doesn't change the file.
  460. """
  461. filename = self.mktemp()
  462. FilePath(filename).setContent(publicRSA_openssh)
  463. error = self.assertRaises(
  464. SystemExit, changePassPhrase,
  465. {'filename': filename, 'newpass': 'pass'})
  466. self.assertEqual(
  467. 'Could not change passphrase: key not encrypted', str(error))
  468. self.assertEqual(publicRSA_openssh, FilePath(filename).getContent())