You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

220 lines
7.7 KiB

7 years ago
  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 distrubuted 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. # 59 Temple Place, Suite 330, Boston, MA 02111-1307 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. import six
  21. from paramiko.message import Message
  22. from paramiko.pkey import PKey
  23. from paramiko.ssh_exception import SSHException, PasswordRequiredException
  24. OPENSSH_AUTH_MAGIC = b"openssh-key-v1\x00"
  25. def unpad(data):
  26. # At the moment, this is only used for unpadding private keys on disk. This
  27. # really ought to be made constant time (possibly by upstreaming this logic
  28. # into pyca/cryptography).
  29. padding_length = six.indexbytes(data, -1)
  30. if padding_length > 16:
  31. raise SSHException("Invalid key")
  32. for i in range(1, padding_length + 1):
  33. if six.indexbytes(data, -i) != (padding_length - i + 1):
  34. raise SSHException("Invalid key")
  35. return data[:-padding_length]
  36. class Ed25519Key(PKey):
  37. """
  38. Representation of an `Ed25519 <https://ed25519.cr.yp.to/>`_ key.
  39. .. note::
  40. Ed25519 key support was added to OpenSSH in version 6.5.
  41. .. versionadded:: 2.2
  42. .. versionchanged:: 2.3
  43. Added a ``file_obj`` parameter to match other key classes.
  44. """
  45. def __init__(self, msg=None, data=None, filename=None, password=None,
  46. file_obj=None):
  47. self.public_blob = None
  48. verifying_key = signing_key = None
  49. if msg is None and data is not None:
  50. msg = Message(data)
  51. if msg is not None:
  52. self._check_type_and_load_cert(
  53. msg=msg,
  54. key_type="ssh-ed25519",
  55. cert_type="ssh-ed25519-cert-v01@openssh.com",
  56. )
  57. verifying_key = nacl.signing.VerifyKey(msg.get_binary())
  58. elif filename is not None:
  59. with open(filename, "r") as f:
  60. data = self._read_private_key("OPENSSH", f)
  61. elif file_obj is not None:
  62. data = self._read_private_key("OPENSSH", file_obj)
  63. if filename or file_obj:
  64. signing_key = self._parse_signing_key_data(data, password)
  65. if signing_key is None and verifying_key is None:
  66. raise ValueError("need a key")
  67. self._signing_key = signing_key
  68. self._verifying_key = verifying_key
  69. def _parse_signing_key_data(self, data, password):
  70. from paramiko.transport import Transport
  71. # We may eventually want this to be usable for other key types, as
  72. # OpenSSH moves to it, but for now this is just for Ed25519 keys.
  73. # This format is described here:
  74. # https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
  75. # The description isn't totally complete, and I had to refer to the
  76. # source for a full implementation.
  77. message = Message(data)
  78. if message.get_bytes(len(OPENSSH_AUTH_MAGIC)) != OPENSSH_AUTH_MAGIC:
  79. raise SSHException("Invalid key")
  80. ciphername = message.get_text()
  81. kdfname = message.get_text()
  82. kdfoptions = message.get_binary()
  83. num_keys = message.get_int()
  84. if kdfname == "none":
  85. # kdfname of "none" must have an empty kdfoptions, the ciphername
  86. # must be "none"
  87. if kdfoptions or ciphername != "none":
  88. raise SSHException("Invalid key")
  89. elif kdfname == "bcrypt":
  90. if not password:
  91. raise PasswordRequiredException(
  92. "Private key file is encrypted"
  93. )
  94. kdf = Message(kdfoptions)
  95. bcrypt_salt = kdf.get_binary()
  96. bcrypt_rounds = kdf.get_int()
  97. else:
  98. raise SSHException("Invalid key")
  99. if ciphername != "none" and ciphername not in Transport._cipher_info:
  100. raise SSHException("Invalid key")
  101. public_keys = []
  102. for _ in range(num_keys):
  103. pubkey = Message(message.get_binary())
  104. if pubkey.get_text() != "ssh-ed25519":
  105. raise SSHException("Invalid key")
  106. public_keys.append(pubkey.get_binary())
  107. private_ciphertext = message.get_binary()
  108. if ciphername == "none":
  109. private_data = private_ciphertext
  110. else:
  111. cipher = Transport._cipher_info[ciphername]
  112. key = bcrypt.kdf(
  113. password=password,
  114. salt=bcrypt_salt,
  115. desired_key_bytes=cipher["key-size"] + cipher["block-size"],
  116. rounds=bcrypt_rounds,
  117. # We can't control how many rounds are on disk, so no sense
  118. # warning about it.
  119. ignore_few_rounds=True,
  120. )
  121. decryptor = Cipher(
  122. cipher["class"](key[:cipher["key-size"]]),
  123. cipher["mode"](key[cipher["key-size"]:]),
  124. backend=default_backend()
  125. ).decryptor()
  126. private_data = (
  127. decryptor.update(private_ciphertext) + decryptor.finalize()
  128. )
  129. message = Message(unpad(private_data))
  130. if message.get_int() != message.get_int():
  131. raise SSHException("Invalid key")
  132. signing_keys = []
  133. for i in range(num_keys):
  134. if message.get_text() != "ssh-ed25519":
  135. raise SSHException("Invalid key")
  136. # A copy of the public key, again, ignore.
  137. public = message.get_binary()
  138. key_data = message.get_binary()
  139. # The second half of the key data is yet another copy of the public
  140. # key...
  141. signing_key = nacl.signing.SigningKey(key_data[:32])
  142. # Verify that all the public keys are the same...
  143. assert (
  144. signing_key.verify_key.encode() == public == public_keys[i] ==
  145. key_data[32:]
  146. )
  147. signing_keys.append(signing_key)
  148. # Comment, ignore.
  149. message.get_binary()
  150. if len(signing_keys) != 1:
  151. raise SSHException("Invalid key")
  152. return signing_keys[0]
  153. def asbytes(self):
  154. if self.can_sign():
  155. v = self._signing_key.verify_key
  156. else:
  157. v = self._verifying_key
  158. m = Message()
  159. m.add_string("ssh-ed25519")
  160. m.add_string(v.encode())
  161. return m.asbytes()
  162. def __hash__(self):
  163. if self.can_sign():
  164. v = self._signing_key.verify_key
  165. else:
  166. v = self._verifying_key
  167. return hash((self.get_name(), v))
  168. def get_name(self):
  169. return "ssh-ed25519"
  170. def get_bits(self):
  171. return 256
  172. def can_sign(self):
  173. return self._signing_key is not None
  174. def sign_ssh_data(self, data):
  175. m = Message()
  176. m.add_string("ssh-ed25519")
  177. m.add_string(self._signing_key.sign(data).signature)
  178. return m
  179. def verify_ssh_sig(self, data, msg):
  180. if msg.get_text() != "ssh-ed25519":
  181. return False
  182. try:
  183. self._verifying_key.verify(data, msg.get_binary())
  184. except nacl.exceptions.BadSignatureError:
  185. return False
  186. else:
  187. return True

Powered by TurnKey Linux.