ed25519key.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. # This file is part of paramiko.
  2. #
  3. # Paramiko is free software; you can redistribute it and/or modify it under the
  4. # terms of the GNU Lesser General Public License as published by the Free
  5. # Software Foundation; either version 2.1 of the License, or (at your option)
  6. # any later version.
  7. #
  8. # Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
  9. # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  10. # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  11. # details.
  12. #
  13. # You should have received a copy of the GNU Lesser General Public License
  14. # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
  15. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  16. import bcrypt
  17. from cryptography.hazmat.backends import default_backend
  18. from cryptography.hazmat.primitives.ciphers import Cipher
  19. import nacl.signing
  20. from paramiko.message import Message
  21. from paramiko.pkey import PKey, OPENSSH_AUTH_MAGIC, _unpad_openssh
  22. from paramiko.py3compat import b
  23. from paramiko.ssh_exception import SSHException, PasswordRequiredException
  24. class Ed25519Key(PKey):
  25. """
  26. Representation of an `Ed25519 <https://ed25519.cr.yp.to/>`_ key.
  27. .. note::
  28. Ed25519 key support was added to OpenSSH in version 6.5.
  29. .. versionadded:: 2.2
  30. .. versionchanged:: 2.3
  31. Added a ``file_obj`` parameter to match other key classes.
  32. """
  33. def __init__(
  34. self, msg=None, data=None, filename=None, password=None, file_obj=None
  35. ):
  36. self.public_blob = None
  37. verifying_key = signing_key = None
  38. if msg is None and data is not None:
  39. msg = Message(data)
  40. if msg is not None:
  41. self._check_type_and_load_cert(
  42. msg=msg,
  43. key_type="ssh-ed25519",
  44. cert_type="ssh-ed25519-cert-v01@openssh.com",
  45. )
  46. verifying_key = nacl.signing.VerifyKey(msg.get_binary())
  47. elif filename is not None:
  48. with open(filename, "r") as f:
  49. pkformat, data = self._read_private_key("OPENSSH", f)
  50. elif file_obj is not None:
  51. pkformat, data = self._read_private_key("OPENSSH", file_obj)
  52. if filename or file_obj:
  53. signing_key = self._parse_signing_key_data(data, password)
  54. if signing_key is None and verifying_key is None:
  55. raise ValueError("need a key")
  56. self._signing_key = signing_key
  57. self._verifying_key = verifying_key
  58. def _parse_signing_key_data(self, data, password):
  59. from paramiko.transport import Transport
  60. # We may eventually want this to be usable for other key types, as
  61. # OpenSSH moves to it, but for now this is just for Ed25519 keys.
  62. # This format is described here:
  63. # https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
  64. # The description isn't totally complete, and I had to refer to the
  65. # source for a full implementation.
  66. message = Message(data)
  67. if message.get_bytes(len(OPENSSH_AUTH_MAGIC)) != OPENSSH_AUTH_MAGIC:
  68. raise SSHException("Invalid key")
  69. ciphername = message.get_text()
  70. kdfname = message.get_text()
  71. kdfoptions = message.get_binary()
  72. num_keys = message.get_int()
  73. if kdfname == "none":
  74. # kdfname of "none" must have an empty kdfoptions, the ciphername
  75. # must be "none"
  76. if kdfoptions or ciphername != "none":
  77. raise SSHException("Invalid key")
  78. elif kdfname == "bcrypt":
  79. if not password:
  80. raise PasswordRequiredException(
  81. "Private key file is encrypted"
  82. )
  83. kdf = Message(kdfoptions)
  84. bcrypt_salt = kdf.get_binary()
  85. bcrypt_rounds = kdf.get_int()
  86. else:
  87. raise SSHException("Invalid key")
  88. if ciphername != "none" and ciphername not in Transport._cipher_info:
  89. raise SSHException("Invalid key")
  90. public_keys = []
  91. for _ in range(num_keys):
  92. pubkey = Message(message.get_binary())
  93. if pubkey.get_text() != "ssh-ed25519":
  94. raise SSHException("Invalid key")
  95. public_keys.append(pubkey.get_binary())
  96. private_ciphertext = message.get_binary()
  97. if ciphername == "none":
  98. private_data = private_ciphertext
  99. else:
  100. cipher = Transport._cipher_info[ciphername]
  101. key = bcrypt.kdf(
  102. password=b(password),
  103. salt=bcrypt_salt,
  104. desired_key_bytes=cipher["key-size"] + cipher["block-size"],
  105. rounds=bcrypt_rounds,
  106. # We can't control how many rounds are on disk, so no sense
  107. # warning about it.
  108. ignore_few_rounds=True,
  109. )
  110. decryptor = Cipher(
  111. cipher["class"](key[: cipher["key-size"]]),
  112. cipher["mode"](key[cipher["key-size"] :]),
  113. backend=default_backend(),
  114. ).decryptor()
  115. private_data = (
  116. decryptor.update(private_ciphertext) + decryptor.finalize()
  117. )
  118. message = Message(_unpad_openssh(private_data))
  119. if message.get_int() != message.get_int():
  120. raise SSHException("Invalid key")
  121. signing_keys = []
  122. for i in range(num_keys):
  123. if message.get_text() != "ssh-ed25519":
  124. raise SSHException("Invalid key")
  125. # A copy of the public key, again, ignore.
  126. public = message.get_binary()
  127. key_data = message.get_binary()
  128. # The second half of the key data is yet another copy of the public
  129. # key...
  130. signing_key = nacl.signing.SigningKey(key_data[:32])
  131. # Verify that all the public keys are the same...
  132. assert (
  133. signing_key.verify_key.encode()
  134. == public
  135. == public_keys[i]
  136. == key_data[32:]
  137. )
  138. signing_keys.append(signing_key)
  139. # Comment, ignore.
  140. message.get_binary()
  141. if len(signing_keys) != 1:
  142. raise SSHException("Invalid key")
  143. return signing_keys[0]
  144. def asbytes(self):
  145. if self.can_sign():
  146. v = self._signing_key.verify_key
  147. else:
  148. v = self._verifying_key
  149. m = Message()
  150. m.add_string("ssh-ed25519")
  151. m.add_string(v.encode())
  152. return m.asbytes()
  153. @property
  154. def _fields(self):
  155. if self.can_sign():
  156. v = self._signing_key.verify_key
  157. else:
  158. v = self._verifying_key
  159. return (self.get_name(), v)
  160. def get_name(self):
  161. return "ssh-ed25519"
  162. def get_bits(self):
  163. return 256
  164. def can_sign(self):
  165. return self._signing_key is not None
  166. def sign_ssh_data(self, data, algorithm=None):
  167. m = Message()
  168. m.add_string("ssh-ed25519")
  169. m.add_string(self._signing_key.sign(data).signature)
  170. return m
  171. def verify_ssh_sig(self, data, msg):
  172. if msg.get_text() != "ssh-ed25519":
  173. return False
  174. try:
  175. self._verifying_key.verify(data, msg.get_binary())
  176. except nacl.exceptions.BadSignatureError:
  177. return False
  178. else:
  179. return True