Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
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.

cryptosign.py 38KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022
  1. ###############################################################################
  2. #
  3. # The MIT License (MIT)
  4. #
  5. # Copyright (c) typedef int GmbH
  6. #
  7. # Permission is hereby granted, free of charge, to any person obtaining a copy
  8. # of this software and associated documentation files (the "Software"), to deal
  9. # in the Software without restriction, including without limitation the rights
  10. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. # copies of the Software, and to permit persons to whom the Software is
  12. # furnished to do so, subject to the following conditions:
  13. #
  14. # The above copyright notice and this permission notice shall be included in
  15. # all copies or substantial portions of the Software.
  16. #
  17. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. # THE SOFTWARE.
  24. #
  25. ###############################################################################
  26. import os
  27. import binascii
  28. from binascii import a2b_hex, b2a_hex
  29. import struct
  30. from typing import Callable, Optional, Union, Dict, Any
  31. import txaio
  32. from autobahn import util
  33. from autobahn.wamp.interfaces import ISecurityModule, ICryptosignKey
  34. from autobahn.wamp.types import Challenge
  35. from autobahn.wamp.message import _URI_PAT_REALM_NAME_ETH
  36. from autobahn.util import parse_keyfile
  37. __all__ = [
  38. 'HAS_CRYPTOSIGN',
  39. ]
  40. try:
  41. # try to import everything we need for WAMP-cryptosign
  42. from nacl import encoding, signing, bindings
  43. from nacl.signing import SignedMessage
  44. except ImportError:
  45. HAS_CRYPTOSIGN = False
  46. else:
  47. HAS_CRYPTOSIGN = True
  48. __all__.append('CryptosignKey')
  49. def _unpack(keydata):
  50. """
  51. Unpack a SSH agent key blob into parts.
  52. See: http://blog.oddbit.com/2011/05/08/converting-openssh-public-keys/
  53. """
  54. parts = []
  55. while keydata:
  56. # read the length of the data
  57. dlen = struct.unpack('>I', keydata[:4])[0]
  58. # read in <length> bytes
  59. data, keydata = keydata[4:dlen + 4], keydata[4 + dlen:]
  60. parts.append(data)
  61. return parts
  62. def _pack(keyparts):
  63. """
  64. Pack parts into a SSH key blob.
  65. """
  66. parts = []
  67. for part in keyparts:
  68. parts.append(struct.pack('>I', len(part)))
  69. parts.append(part)
  70. return b''.join(parts)
  71. def _read_ssh_ed25519_pubkey(keydata):
  72. """
  73. Parse an OpenSSH Ed25519 public key from a string into a raw public key.
  74. Example input:
  75. ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJukDU5fqXv/yVhSirsDWsUFyOodZyCSLxyitPPzWJW9 oberstet@office-corei7
  76. :param keydata: The OpenSSH Ed25519 public key data to parse.
  77. :type keydata: str
  78. :returns: pair of raw public key (32 bytes) and comment
  79. :rtype: tuple
  80. """
  81. if type(keydata) != str:
  82. raise Exception("invalid type {} for keydata".format(type(keydata)))
  83. parts = keydata.strip().split()
  84. if len(parts) != 3:
  85. raise Exception('invalid SSH Ed25519 public key')
  86. algo, keydata, comment = parts
  87. if algo != 'ssh-ed25519':
  88. raise Exception('not a Ed25519 SSH public key (but {})'.format(algo))
  89. blob = binascii.a2b_base64(keydata)
  90. try:
  91. key = _unpack(blob)[1]
  92. except Exception as e:
  93. raise Exception('could not parse key ({})'.format(e))
  94. if len(key) != 32:
  95. raise Exception('invalid length {} for embedded raw key (must be 32 bytes)'.format(len(key)))
  96. return key, comment
  97. class _SSHPacketReader:
  98. """
  99. Read OpenSSH packet format which is used for key material.
  100. """
  101. def __init__(self, packet):
  102. self._packet = packet
  103. self._idx = 0
  104. self._len = len(packet)
  105. def get_remaining_payload(self):
  106. return self._packet[self._idx:]
  107. def get_bytes(self, size):
  108. if self._idx + size > self._len:
  109. raise Exception('incomplete packet')
  110. value = self._packet[self._idx:self._idx + size]
  111. self._idx += size
  112. return value
  113. def get_uint32(self):
  114. return struct.unpack('>I', self.get_bytes(4))[0]
  115. def get_string(self):
  116. return self.get_bytes(self.get_uint32())
  117. def _makepad(size: int) -> bytes:
  118. assert 0 <= size < 255
  119. return b''.join(x.to_bytes(1, byteorder='big') for x in range(1, size + 1))
  120. def _read_ssh_ed25519_privkey(keydata):
  121. """
  122. Parse an OpenSSH Ed25519 private key from a string into a raw private key.
  123. Example input:
  124. -----BEGIN OPENSSH PRIVATE KEY-----
  125. b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
  126. QyNTUxOQAAACCbpA1OX6l7/8lYUoq7A1rFBcjqHWcgki8corTz81iVvQAAAKDWjZ0Y1o2d
  127. GAAAAAtzc2gtZWQyNTUxOQAAACCbpA1OX6l7/8lYUoq7A1rFBcjqHWcgki8corTz81iVvQ
  128. AAAEArodzIMjH9MOBz0X+HDvL06rEJOMYFhzGQ5zXPM7b7fZukDU5fqXv/yVhSirsDWsUF
  129. yOodZyCSLxyitPPzWJW9AAAAFm9iZXJzdGV0QG9mZmljZS1jb3JlaTcBAgMEBQYH
  130. -----END OPENSSH PRIVATE KEY-----
  131. :param keydata: The OpenSSH Ed25519 private key data to parse.
  132. :type keydata: str
  133. :returns: pair of raw private key (32 bytes) and comment
  134. :rtype: tuple
  135. """
  136. # Some pointers:
  137. # https://github.com/ronf/asyncssh/blob/master/asyncssh/public_key.py
  138. # https://github.com/ronf/asyncssh/blob/master/asyncssh/ed25519.py
  139. # crypto_sign_ed25519_sk_to_seed
  140. # https://github.com/jedisct1/libsodium/blob/master/src/libsodium/crypto_sign/ed25519/sign_ed25519_api.c#L27
  141. # https://tools.ietf.org/html/draft-bjh21-ssh-ed25519-02
  142. # http://blog.oddbit.com/2011/05/08/converting-openssh-public-keys/
  143. SSH_BEGIN = '-----BEGIN OPENSSH PRIVATE KEY-----'
  144. SSH_END = '-----END OPENSSH PRIVATE KEY-----'
  145. OPENSSH_KEY_V1 = b'openssh-key-v1\0'
  146. if not (keydata.startswith(SSH_BEGIN) and keydata.endswith(SSH_END)):
  147. raise Exception('invalid OpenSSH private key (does not start/end with OPENSSH preamble)')
  148. ssh_end = keydata.find(SSH_END)
  149. keydata = keydata[len(SSH_BEGIN):ssh_end]
  150. keydata = ''.join(x.strip() for x in keydata.split())
  151. blob = binascii.a2b_base64(keydata)
  152. blob = blob[len(OPENSSH_KEY_V1):]
  153. packet = _SSHPacketReader(blob)
  154. cipher_name = packet.get_string()
  155. kdf = packet.get_string()
  156. packet.get_string() # kdf_data
  157. nkeys = packet.get_uint32()
  158. packet.get_string() # public_key
  159. key_data = packet.get_string()
  160. mac = packet.get_remaining_payload()
  161. block_size = 8
  162. if cipher_name != b'none':
  163. raise Exception('encrypted private keys not supported (please remove the passphrase from your private key or use SSH agent)')
  164. if kdf != b'none':
  165. raise Exception('passphrase encrypted private keys not supported')
  166. if nkeys != 1:
  167. raise Exception('multiple private keys in a key file not supported (found {} keys)'.format(nkeys))
  168. if mac:
  169. raise Exception('invalid OpenSSH private key (found remaining payload for mac)')
  170. packet = _SSHPacketReader(key_data)
  171. packet.get_uint32() # check1
  172. packet.get_uint32() # check2
  173. alg = packet.get_string()
  174. if alg != b'ssh-ed25519':
  175. raise Exception('invalid key type: we only support Ed25519 (found "{}")'.format(alg.decode('ascii')))
  176. vk = packet.get_string()
  177. sk = packet.get_string()
  178. if len(vk) != bindings.crypto_sign_PUBLICKEYBYTES:
  179. raise Exception('invalid public key length')
  180. if len(sk) != bindings.crypto_sign_SECRETKEYBYTES:
  181. raise Exception('invalid public key length')
  182. comment = packet.get_string() # comment
  183. pad = packet.get_remaining_payload()
  184. if len(pad) and (len(pad) >= block_size or pad != _makepad(len(pad))):
  185. raise Exception('invalid OpenSSH private key (padlen={}, actual_pad={}, expected_pad={})'.format(len(pad), pad, _makepad(len(pad))))
  186. # secret key (64 octets) = 32 octets seed || 32 octets secret key derived of seed
  187. seed = sk[:bindings.crypto_sign_SEEDBYTES]
  188. comment = comment.decode('ascii')
  189. return seed, comment
  190. def _read_signify_ed25519_signature(signature_file):
  191. """
  192. Read a Ed25519 signature file created with OpenBSD signify.
  193. http://man.openbsd.org/OpenBSD-current/man1/signify.1
  194. """
  195. with open(signature_file) as f:
  196. # signature file format: 2nd line is base64 of 'Ed' || 8 random octets || 64 octets Ed25519 signature
  197. sig = binascii.a2b_base64(f.read().splitlines()[1])[10:]
  198. if len(sig) != 64:
  199. raise Exception('bogus Ed25519 signature: raw signature length was {}, but expected 64'.format(len(sig)))
  200. return sig
  201. def _read_signify_ed25519_pubkey(pubkey_file):
  202. """
  203. Read a public key from a Ed25519 key pair created with OpenBSD signify.
  204. http://man.openbsd.org/OpenBSD-current/man1/signify.1
  205. """
  206. with open(pubkey_file) as f:
  207. # signature file format: 2nd line is base64 of 'Ed' || 8 random octets || 32 octets Ed25519 public key
  208. pubkey = binascii.a2b_base64(f.read().splitlines()[1])[10:]
  209. if len(pubkey) != 32:
  210. raise Exception('bogus Ed25519 public key: raw key length was {}, but expected 32'.format(len(pubkey)))
  211. return pubkey
  212. def _qrcode_from_signify_ed25519_pubkey(pubkey_file, mode='text'):
  213. """
  214. Usage:
  215. 1. Get the OpenBSD 5.7 release public key from here
  216. http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/etc/signify/Attic/openbsd-57-base.pub?rev=1.1
  217. 2. Generate QR Code and print to terminal
  218. print(cryptosign._qrcode_from_signify_ed25519_pubkey('openbsd-57-base.pub'))
  219. 3. Compare to (scroll down) QR code here
  220. https://www.openbsd.org/papers/bsdcan-signify.html
  221. """
  222. assert(mode in ['text', 'svg'])
  223. import qrcode
  224. with open(pubkey_file) as f:
  225. pubkey = f.read().splitlines()[1]
  226. qr = qrcode.QRCode(box_size=3,
  227. error_correction=qrcode.ERROR_CORRECT_L)
  228. qr.add_data(pubkey)
  229. if mode == 'text':
  230. import io
  231. with io.StringIO() as data_buffer:
  232. qr.print_ascii(out=data_buffer, invert=True)
  233. return data_buffer.getvalue()
  234. elif mode == 'svg':
  235. import qrcode.image.svg
  236. image = qr.make_image(image_factory=qrcode.image.svg.SvgImage)
  237. return image.to_string()
  238. else:
  239. raise Exception('logic error')
  240. def _verify_signify_ed25519_signature(pubkey_file, signature_file, message):
  241. """
  242. Verify a Ed25519 signature created with OpenBSD signify.
  243. This will raise a `nacl.exceptions.BadSignatureError` if the signature is bad
  244. and return silently when the signature is good.
  245. Usage:
  246. 1. Create a signature:
  247. signify-openbsd -S -s ~/.signify/crossbario-trustroot.sec -m .profile
  248. 2. Verify the signature
  249. from autobahn.wamp import cryptosign
  250. with open('.profile', 'rb') as f:
  251. message = f.read()
  252. cryptosign._verify_signify_ed25519_signature('.signify/crossbario-trustroot.pub', '.profile.sig', message)
  253. http://man.openbsd.org/OpenBSD-current/man1/signify.1
  254. """
  255. pubkey = _read_signify_ed25519_pubkey(pubkey_file)
  256. verify_key = signing.VerifyKey(pubkey)
  257. sig = _read_signify_ed25519_signature(signature_file)
  258. verify_key.verify(message, sig)
  259. # CryptosignKey from
  260. # - raw byte string or file with raw bytes
  261. # - SSH private key string or key file
  262. # - SSH agent proxy
  263. #
  264. # VerifyKey from
  265. # - raw byte string or file with raw bytes
  266. # - SSH public key string or key file
  267. if HAS_CRYPTOSIGN:
  268. def _format_challenge(challenge: Challenge, channel_id_raw: Optional[bytes], channel_id_type: Optional[str]) -> bytes:
  269. """
  270. Format the challenge based on provided parameters
  271. :param challenge: The WAMP-cryptosign challenge object for which a signature should be computed.
  272. :param channel_id_raw: The channel ID when channel_id_type is 'tls-unique'.
  273. :param channel_id_type: The type of the channel id, currently handles 'tls-unique' and
  274. ignores otherwise.
  275. """
  276. if not isinstance(challenge, Challenge):
  277. raise Exception(
  278. "challenge must be instance of autobahn.wamp.types.Challenge, not {}".format(type(challenge)))
  279. if 'challenge' not in challenge.extra:
  280. raise Exception("missing challenge value in challenge.extra")
  281. # the challenge sent by the router (a 32 bytes random value)
  282. challenge_hex = challenge.extra['challenge']
  283. if type(challenge_hex) != str:
  284. raise Exception("invalid type {} for challenge (expected a hex string)".format(type(challenge_hex)))
  285. if len(challenge_hex) != 64:
  286. raise Exception("unexpected challenge (hex) length: was {}, but expected 64".format(len(challenge_hex)))
  287. # the challenge for WAMP-cryptosign is a 32 bytes random value in Hex encoding (that is, a unicode string)
  288. challenge_raw = binascii.a2b_hex(challenge_hex)
  289. if channel_id_type == 'tls-unique':
  290. assert len(
  291. channel_id_raw) == 32, 'unexpected TLS transport channel ID length (was {}, but expected 32)'.format(
  292. len(channel_id_raw))
  293. # with TLS channel binding of type "tls-unique", the message to be signed by the client actually
  294. # is the XOR of the challenge and the TLS channel ID
  295. data = util.xor(challenge_raw, channel_id_raw)
  296. elif channel_id_type is None:
  297. # when no channel binding was requested, the message to be signed by the client is the challenge only
  298. data = challenge_raw
  299. else:
  300. assert False, 'invalid channel_id_type "{}"'.format(channel_id_type)
  301. return data
  302. def _sign_challenge(data: bytes, signer_func: Callable) -> bytes:
  303. """
  304. Sign the provided data using the provided signer.
  305. :param data: challenge to sign
  306. :param signer_func: The callable function to use for signing
  307. :returns: A Deferred/Future that resolves to the computed signature.
  308. :rtype: str
  309. """
  310. # a raw byte string is signed, and the signature is also a raw byte string
  311. d1 = signer_func(data)
  312. # asyncio lacks callback chaining (and we cannot use co-routines, since we want
  313. # to support older Pythons), hence we need d2
  314. d2 = txaio.create_future()
  315. def process(signature_raw):
  316. # convert the raw signature into a hex encode value (unicode string)
  317. signature_hex = binascii.b2a_hex(signature_raw).decode('ascii')
  318. # we return the concatenation of the signature and the message signed (96 bytes)
  319. data_hex = binascii.b2a_hex(data).decode('ascii')
  320. sig = signature_hex + data_hex
  321. txaio.resolve(d2, sig)
  322. txaio.add_callbacks(d1, process, None)
  323. return d2
  324. class CryptosignKey(object):
  325. """
  326. A cryptosign private key for signing, and hence usable for authentication or a
  327. public key usable for verification (but can't be used for signing).
  328. """
  329. def __init__(self, key, can_sign: bool, security_module: Optional[ISecurityModule] = None,
  330. key_no: Optional[int] = None, comment: Optional[str] = None) -> None:
  331. if not (isinstance(key, signing.VerifyKey) or isinstance(key, signing.SigningKey)):
  332. raise Exception("invalid type {} for key".format(type(key)))
  333. assert (can_sign and isinstance(key, signing.SigningKey)) or (not can_sign and isinstance(key, signing.VerifyKey))
  334. self._key = key
  335. self._can_sign = can_sign
  336. self._security_module = security_module
  337. self._key_no = key_no
  338. self._comment = comment
  339. @property
  340. def security_module(self) -> Optional['ISecurityModule']:
  341. """
  342. Implements :meth:`autobahn.wamp.interfaces.IKey.security_module`.
  343. """
  344. return self._security_module
  345. @property
  346. def key_no(self) -> Optional[int]:
  347. """
  348. Implements :meth:`autobahn.wamp.interfaces.IKey.key_no`.
  349. """
  350. return self._key_no
  351. @property
  352. def comment(self) -> Optional[str]:
  353. """
  354. Implements :meth:`autobahn.wamp.interfaces.IKey.comment`.
  355. """
  356. return self._comment
  357. @property
  358. def key_type(self) -> str:
  359. """
  360. Implements :meth:`autobahn.wamp.interfaces.IKey.key_type`.
  361. """
  362. return 'cryptosign'
  363. @property
  364. def can_sign(self) -> bool:
  365. """
  366. Implements :meth:`autobahn.wamp.interfaces.IKey.can_sign`.
  367. """
  368. return self._can_sign
  369. def sign(self, data: bytes) -> bytes:
  370. """
  371. Implements :meth:`autobahn.wamp.interfaces.IKey.sign`.
  372. """
  373. if not self._can_sign:
  374. raise Exception("a signing key required to sign")
  375. if type(data) != bytes:
  376. raise Exception("data to be signed must be binary")
  377. sig: SignedMessage = self._key.sign(data)
  378. # we only return the actual signature! if we return "sig",
  379. # it gets coerced into the concatenation of message + signature
  380. # not sure which order, but we don't want that. we only want
  381. # the signature
  382. return txaio.create_future_success(sig.signature)
  383. def sign_challenge(self, challenge: Challenge, channel_id: Optional[bytes] = None,
  384. channel_id_type: Optional[str] = None) -> bytes:
  385. """
  386. Implements :meth:`autobahn.wamp.interfaces.ICryptosignKey.sign_challenge`.
  387. """
  388. assert challenge.method in ['cryptosign', 'cryptosign-proxy'], \
  389. 'unexpected cryptosign challenge with method "{}"'.format(challenge.method)
  390. data = _format_challenge(challenge, channel_id, channel_id_type)
  391. return _sign_challenge(data, self.sign)
  392. def public_key(self, binary: bool = False) -> Union[str, bytes]:
  393. """
  394. Returns the public key part of a signing key or the (public) verification key.
  395. :returns: The public key in Hex encoding.
  396. :rtype: str or None
  397. """
  398. if isinstance(self._key, signing.SigningKey):
  399. key = self._key.verify_key
  400. else:
  401. key = self._key
  402. if binary:
  403. return key.encode()
  404. else:
  405. return key.encode(encoder=encoding.HexEncoder).decode('ascii')
  406. @classmethod
  407. def from_pubkey(cls, pubkey: bytes, comment: Optional[str] = None) -> 'CryptosignKey':
  408. if not (comment is None or type(comment) == str):
  409. raise ValueError("invalid type {} for comment".format(type(comment)))
  410. if type(pubkey) != bytes:
  411. raise ValueError("invalid key type {} (expected binary)".format(type(pubkey)))
  412. if len(pubkey) != 32:
  413. raise ValueError("invalid key length {} (expected 32)".format(len(pubkey)))
  414. return cls(key=signing.VerifyKey(pubkey), can_sign=False, comment=comment)
  415. @classmethod
  416. def from_bytes(cls, key: bytes, comment: Optional[str] = None) -> 'CryptosignKey':
  417. if not (comment is None or type(comment) == str):
  418. raise ValueError("invalid type {} for comment".format(type(comment)))
  419. if type(key) != bytes:
  420. raise ValueError("invalid key type {} (expected binary)".format(type(key)))
  421. if len(key) != 32:
  422. raise ValueError("invalid key length {} (expected 32)".format(len(key)))
  423. return cls(key=signing.SigningKey(key), can_sign=True, comment=comment)
  424. @classmethod
  425. def from_file(cls, filename: str, comment: Optional[str] = None) -> 'CryptosignKey':
  426. """
  427. Load an Ed25519 (private) signing key (actually, the seed for the key) from a raw file of 32 bytes length.
  428. This can be any random byte sequence, such as generated from Python code like
  429. os.urandom(32)
  430. or from the shell
  431. dd if=/dev/urandom of=client02.key bs=1 count=32
  432. :param filename: Filename of the key.
  433. :param comment: Comment for key (optional).
  434. """
  435. if not (comment is None or type(comment) == str):
  436. raise Exception("invalid type {} for comment".format(type(comment)))
  437. if type(filename) != str:
  438. raise Exception("invalid type {} for filename".format(filename))
  439. with open(filename, 'rb') as f:
  440. key_data = f.read()
  441. return cls.from_bytes(key_data, comment=comment)
  442. @classmethod
  443. def from_ssh_file(cls, filename: str) -> 'CryptosignKey':
  444. """
  445. Load an Ed25519 key from a SSH key file. The key file can be a (private) signing
  446. key (from a SSH private key file) or a (public) verification key (from a SSH
  447. public key file). A private key file must be passphrase-less.
  448. """
  449. with open(filename, 'rb') as f:
  450. key_data = f.read().decode('utf-8').strip()
  451. return cls.from_ssh_bytes(key_data)
  452. @classmethod
  453. def from_ssh_bytes(cls, key_data: str) -> 'CryptosignKey':
  454. """
  455. Load an Ed25519 key from SSH key file. The key file can be a (private) signing
  456. key (from a SSH private key file) or a (public) verification key (from a SSH
  457. public key file). A private key file must be passphrase-less.
  458. """
  459. SSH_BEGIN = '-----BEGIN OPENSSH PRIVATE KEY-----'
  460. if key_data.startswith(SSH_BEGIN):
  461. # OpenSSH private key
  462. key_data, comment = _read_ssh_ed25519_privkey(key_data)
  463. key = signing.SigningKey(key_data, encoder=encoding.RawEncoder)
  464. can_sign = True
  465. else:
  466. # OpenSSH public key
  467. key_data, comment = _read_ssh_ed25519_pubkey(key_data)
  468. key = signing.VerifyKey(key_data)
  469. can_sign = False
  470. return cls(key=key, can_sign=can_sign, comment=comment)
  471. @classmethod
  472. def from_seedphrase(cls, seedphrase: str, index: int = 0) -> 'CryptosignKey':
  473. """
  474. Create a private key from the given BIP-39 mnemonic seed phrase and index,
  475. which can be used to sign and create signatures.
  476. :param seedphrase: The BIP-39 seedphrase ("Mnemonic") from which to derive the account.
  477. :param index: The account index in account hierarchy defined by the seedphrase.
  478. :return: New instance of :class:`EthereumKey`
  479. """
  480. try:
  481. from autobahn.xbr._mnemonic import mnemonic_to_private_key
  482. except ImportError as e:
  483. raise RuntimeError('package autobahn[xbr] not installed ("{}")'.format(e))
  484. # BIP44 path for WAMP
  485. # https://github.com/wamp-proto/wamp-proto/issues/401
  486. # https://github.com/satoshilabs/slips/pull/1322
  487. derivation_path = "m/44'/655'/0'/0/{}".format(index)
  488. key_raw = mnemonic_to_private_key(seedphrase, derivation_path)
  489. assert type(key_raw) == bytes
  490. assert len(key_raw) == 32
  491. # create WAMP-Cryptosign key object from raw bytes
  492. key = cls.from_bytes(key_raw)
  493. return key
  494. @classmethod
  495. def from_keyfile(cls, keyfile: str) -> 'CryptosignKey':
  496. """
  497. Create a public or private key from reading the given public or private key file.
  498. Here is an example key file that includes an CryptosignKey private key ``private-key-ed25519``, which
  499. is loaded in this function, and other fields, which are ignored by this function:
  500. .. code-block::
  501. This is a comment (all lines until the first empty line are comments indeed).
  502. creator: oberstet@intel-nuci7
  503. created-at: 2022-07-05T12:29:48.832Z
  504. user-id: oberstet@intel-nuci7
  505. public-key-ed25519: 7326d9dc0307681cc6940fde0e60eb31a6e4d642a81e55c434462ce31f95deed
  506. public-adr-eth: 0x10848feBdf7f200Ba989CDf7E3eEB3EC03ae7768
  507. private-key-ed25519: f750f42b0430e28a2e272c3cedcae4dcc4a1cf33bc345c35099d3322626ab666
  508. private-key-eth: 4d787714dcb0ae52e1c5d2144648c255d660b9a55eac9deeb80d9f506f501025
  509. :param keyfile: Path (relative or absolute) to a public or private keys file.
  510. :return: New instance of :class:`CryptosignKey`
  511. """
  512. if not os.path.exists(keyfile) or not os.path.isfile(keyfile):
  513. raise RuntimeError('keyfile "{}" is not a file'.format(keyfile))
  514. # now load the private or public key file - this returns a dict which should
  515. # include (for a private key):
  516. #
  517. # private-key-ed25519: 20e8c05d0ede9506462bb049c4843032b18e8e75b314583d0c8d8a4942f9be40
  518. #
  519. # or (for a public key only):
  520. #
  521. # public-key-ed25519: 7326d9dc0307681cc6940fde0e60eb31a6e4d642a81e55c434462ce31f95deed
  522. #
  523. data = parse_keyfile(keyfile)
  524. privkey_ed25519_hex = data.get('private-key-ed25519', None)
  525. if privkey_ed25519_hex is None:
  526. pubkey_ed25519_hex = data.get('public-key-ed25519', None)
  527. if pubkey_ed25519_hex is None:
  528. raise RuntimeError('neither "private-key-ed25519" nor "public-key-ed25519" found in keyfile {}'.format(keyfile))
  529. else:
  530. return CryptosignKey.from_pubkey(binascii.a2b_hex(pubkey_ed25519_hex))
  531. else:
  532. return CryptosignKey.from_bytes(binascii.a2b_hex(privkey_ed25519_hex))
  533. ICryptosignKey.register(CryptosignKey)
  534. class CryptosignAuthextra(object):
  535. """
  536. WAMP-Cryptosign authextra object.
  537. """
  538. __slots__ = [
  539. '_pubkey',
  540. '_trustroot',
  541. '_challenge',
  542. '_channel_binding',
  543. '_channel_id',
  544. '_realm',
  545. '_chain_id',
  546. '_block_no',
  547. '_delegate',
  548. '_seeder',
  549. '_bandwidth',
  550. '_signature',
  551. ]
  552. def __init__(self,
  553. pubkey: Optional[bytes] = None,
  554. challenge: Optional[bytes] = None,
  555. channel_binding: Optional[str] = None,
  556. channel_id: Optional[bytes] = None,
  557. # domain address, certificates are verified against owner of the domain
  558. trustroot: Optional[bytes] = None,
  559. # FIXME: add delegate address
  560. # FIXME: add certificates
  561. # FIXME: remove reservation
  562. realm: Optional[bytes] = None,
  563. chain_id: Optional[int] = None,
  564. block_no: Optional[int] = None,
  565. delegate: Optional[bytes] = None,
  566. seeder: Optional[bytes] = None,
  567. bandwidth: Optional[int] = None,
  568. signature: Optional[bytes] = None,
  569. ):
  570. if pubkey:
  571. assert len(pubkey) == 32
  572. if trustroot:
  573. assert len(trustroot) == 20
  574. if challenge:
  575. assert len(challenge) == 32
  576. if channel_binding:
  577. assert channel_binding in ['tls-unique']
  578. if channel_id:
  579. assert len(channel_id) == 32
  580. if realm:
  581. assert len(realm) == 20
  582. if delegate:
  583. assert len(delegate) == 20
  584. if seeder:
  585. assert len(seeder) == 20
  586. if signature:
  587. assert len(signature) == 65
  588. self._pubkey = pubkey
  589. self._trustroot = trustroot
  590. self._challenge = challenge
  591. self._channel_binding = channel_binding
  592. self._channel_id = channel_id
  593. self._realm = realm
  594. self._chain_id = chain_id
  595. self._block_no = block_no
  596. self._delegate = delegate
  597. self._seeder = seeder
  598. self._bandwidth = bandwidth
  599. self._signature = signature
  600. @property
  601. def pubkey(self) -> Optional[bytes]:
  602. return self._pubkey
  603. @pubkey.setter
  604. def pubkey(self, value: Optional[bytes]):
  605. assert value is None or len(value) == 20
  606. self._pubkey = value
  607. @property
  608. def trustroot(self) -> Optional[bytes]:
  609. return self._trustroot
  610. @trustroot.setter
  611. def trustroot(self, value: Optional[bytes]):
  612. assert value is None or len(value) == 20
  613. self._trustroot = value
  614. @property
  615. def challenge(self) -> Optional[bytes]:
  616. return self._challenge
  617. @challenge.setter
  618. def challenge(self, value: Optional[bytes]):
  619. assert value is None or len(value) == 32
  620. self._challenge = value
  621. @property
  622. def channel_binding(self) -> Optional[str]:
  623. return self._channel_binding
  624. @channel_binding.setter
  625. def channel_binding(self, value: Optional[str]):
  626. assert value is None or value in ['tls-unique']
  627. self._channel_binding = value
  628. @property
  629. def channel_id(self) -> Optional[bytes]:
  630. return self._channel_id
  631. @channel_id.setter
  632. def channel_id(self, value: Optional[bytes]):
  633. assert value is None or len(value) == 32
  634. self._channel_id = value
  635. @property
  636. def realm(self) -> Optional[bytes]:
  637. return self._realm
  638. @realm.setter
  639. def realm(self, value: Optional[bytes]):
  640. assert value is None or len(value) == 20
  641. self._realm = value
  642. @property
  643. def chain_id(self) -> Optional[int]:
  644. return self._chain_id
  645. @chain_id.setter
  646. def chain_id(self, value: Optional[int]):
  647. assert value is None or value > 0
  648. self._chain_id = value
  649. @property
  650. def block_no(self) -> Optional[int]:
  651. return self._block_no
  652. @block_no.setter
  653. def block_no(self, value: Optional[int]):
  654. assert value is None or value > 0
  655. self._block_no = value
  656. @property
  657. def delegate(self) -> Optional[bytes]:
  658. return self._delegate
  659. @delegate.setter
  660. def delegate(self, value: Optional[bytes]):
  661. assert value is None or len(value) == 20
  662. self._delegate = value
  663. @property
  664. def seeder(self) -> Optional[bytes]:
  665. return self._seeder
  666. @seeder.setter
  667. def seeder(self, value: Optional[bytes]):
  668. assert value is None or len(value) == 20
  669. self._seeder = value
  670. @property
  671. def bandwidth(self) -> Optional[int]:
  672. return self._bandwidth
  673. @bandwidth.setter
  674. def bandwidth(self, value: Optional[int]):
  675. assert value is None or value > 0
  676. self._bandwidth = value
  677. @property
  678. def signature(self) -> Optional[bytes]:
  679. return self._signature
  680. @signature.setter
  681. def signature(self, value: Optional[bytes]):
  682. assert value is None or len(value) == 65
  683. self._signature = value
  684. @staticmethod
  685. def parse(data: Dict[str, Any]) -> 'CryptosignAuthextra':
  686. obj = CryptosignAuthextra()
  687. pubkey = data.get('pubkey', None)
  688. if pubkey is not None:
  689. if type(pubkey) != str:
  690. raise ValueError('invalid type {} for pubkey'.format(type(pubkey)))
  691. if len(pubkey) != 32 * 2:
  692. raise ValueError('invalid length {} of pubkey'.format(len(pubkey)))
  693. obj._pubkey = a2b_hex(pubkey)
  694. challenge = data.get('challenge', None)
  695. if challenge is not None:
  696. if type(challenge) != str:
  697. raise ValueError('invalid type {} for challenge'.format(type(challenge)))
  698. if len(challenge) != 32 * 2:
  699. raise ValueError('invalid length {} of challenge'.format(len(challenge)))
  700. obj._challenge = a2b_hex(challenge)
  701. channel_binding = data.get('channel_binding', None)
  702. if channel_binding is not None:
  703. if type(channel_binding) != str:
  704. raise ValueError('invalid type {} for channel_binding'.format(type(channel_binding)))
  705. if channel_binding not in ['tls-unique']:
  706. raise ValueError('invalid value "{}" for channel_binding'.format(channel_binding))
  707. obj._channel_binding = channel_binding
  708. channel_id = data.get('channel_id', None)
  709. if channel_id is not None:
  710. if type(channel_id) != str:
  711. raise ValueError('invalid type {} for channel_id'.format(type(channel_id)))
  712. if len(channel_id) != 32 * 2:
  713. raise ValueError('invalid length {} of channel_id'.format(len(channel_id)))
  714. obj._channel_id = a2b_hex(channel_id)
  715. trustroot = data.get('trustroot', None)
  716. if trustroot is not None:
  717. if type(trustroot) != str:
  718. raise ValueError('invalid type {} for trustroot - expected a string'.format(type(trustroot)))
  719. if not _URI_PAT_REALM_NAME_ETH.match(trustroot):
  720. raise ValueError('invalid value "{}" for trustroot - expected an Ethereum address'.format(type(trustroot)))
  721. obj._trustroot = a2b_hex(trustroot[2:])
  722. reservation = data.get('reservation', None)
  723. if reservation is not None:
  724. if type(reservation) != dict:
  725. raise ValueError('invalid type {} for reservation'.format(type(reservation)))
  726. chain_id = reservation.get('chain_id', None)
  727. if chain_id is not None:
  728. if type(chain_id) != int:
  729. raise ValueError('invalid type {} for reservation.chain_id - expected an integer'.format(type(chain_id)))
  730. obj._chain_id = chain_id
  731. block_no = reservation.get('block_no', None)
  732. if block_no is not None:
  733. if type(block_no) != int:
  734. raise ValueError('invalid type {} for reservation.block_no - expected an integer'.format(type(block_no)))
  735. obj._block_no = block_no
  736. realm = reservation.get('realm', None)
  737. if realm is not None:
  738. if type(realm) != str:
  739. raise ValueError('invalid type {} for reservation.realm - expected a string'.format(type(realm)))
  740. if not _URI_PAT_REALM_NAME_ETH.match(realm):
  741. raise ValueError('invalid value "{}" for reservation.realm - expected an Ethereum address'.format(type(realm)))
  742. obj._realm = a2b_hex(realm[2:])
  743. delegate = reservation.get('delegate', None)
  744. if delegate is not None:
  745. if type(delegate) != str:
  746. raise ValueError('invalid type {} for reservation.delegate - expected a string'.format(type(delegate)))
  747. if not _URI_PAT_REALM_NAME_ETH.match(delegate):
  748. raise ValueError('invalid value "{}" for reservation.delegate - expected an Ethereum address'.format(type(delegate)))
  749. obj._delegate = a2b_hex(delegate[2:])
  750. seeder = reservation.get('seeder', None)
  751. if seeder is not None:
  752. if type(seeder) != str:
  753. raise ValueError('invalid type {} for reservation.seeder - expected a string'.format(type(seeder)))
  754. if not _URI_PAT_REALM_NAME_ETH.match(seeder):
  755. raise ValueError('invalid value "{}" for reservation.seeder - expected an Ethereum address'.format(type(seeder)))
  756. obj._seeder = a2b_hex(seeder[2:])
  757. bandwidth = reservation.get('bandwidth', None)
  758. if bandwidth is not None:
  759. if type(bandwidth) != int:
  760. raise ValueError('invalid type {} for reservation.bandwidth - expected an integer'.format(type(bandwidth)))
  761. obj._bandwidth = bandwidth
  762. signature = data.get('signature', None)
  763. if signature is not None:
  764. if type(signature) != str:
  765. raise ValueError('invalid type {} for signature'.format(type(signature)))
  766. if len(signature) != 65 * 2:
  767. raise ValueError('invalid length {} of signature'.format(len(signature)))
  768. obj._signature = a2b_hex(signature)
  769. return obj
  770. def marshal(self) -> Dict[str, Any]:
  771. res = {}
  772. # FIXME: marshal check-summed eth addresses
  773. if self._pubkey is not None:
  774. res['pubkey'] = b2a_hex(self._pubkey).decode()
  775. if self._challenge is not None:
  776. res['challenge'] = b2a_hex(self._challenge).decode()
  777. if self._channel_binding is not None:
  778. res['channel_binding'] = self._channel_binding
  779. if self._channel_id is not None:
  780. res['channel_id'] = b2a_hex(self._channel_id).decode()
  781. if self._trustroot is not None:
  782. res['trustroot'] = '0x' + b2a_hex(self._trustroot).decode()
  783. reservation = {}
  784. if self._chain_id is not None:
  785. reservation['chain_id'] = self._chain_id
  786. if self._block_no is not None:
  787. reservation['block_no'] = self._block_no
  788. if self._realm is not None:
  789. reservation['realm'] = '0x' + b2a_hex(self._realm).decode()
  790. if self._delegate is not None:
  791. reservation['delegate'] = '0x' + b2a_hex(self._delegate).decode()
  792. if self._seeder is not None:
  793. reservation['seeder'] = '0x' + b2a_hex(self._seeder).decode()
  794. if self._bandwidth is not None:
  795. reservation['bandwidth'] = self._bandwidth
  796. if reservation:
  797. res['reservation'] = reservation
  798. if self._signature is not None:
  799. res['signature'] = b2a_hex(self._signature).decode()
  800. return res
  801. __all__.extend(['CryptosignKey', 'format_challenge', 'sign_challenge', 'CryptosignAuthextra'])