crypto.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. """ Generic functions for the cryptographic primitives used in this project. """
  2. import os
  3. import os.path
  4. import tempfile
  5. from binascii import hexlify, unhexlify
  6. from typing import Iterator, Iterable
  7. from Crypto.Signature import PKCS1_PSS
  8. from Crypto.Hash import SHA512
  9. from Crypto.PublicKey import RSA
  10. __all__ = ['get_hasher', 'Signing', 'MAX_HASH']
  11. def get_hasher():
  12. """ Returns a object that you can use for hashing, compatible to the `hashlib` interface. """
  13. return SHA512.new()
  14. MAX_HASH = (1 << 512) - 1 # the largest possible hash value
  15. class Signing:
  16. """
  17. Functionality for creating and verifying signatures, and their public/private keys.
  18. :param byte_repr: The bytes serialization of a public key.
  19. """
  20. def __init__(self, byte_repr: bytes):
  21. self.rsa = RSA.importKey(byte_repr)
  22. def verify_sign(self, hashed_value: bytes, signature: bytes) -> bool:
  23. """ Verify a signature for an already hashed value and a public key. """
  24. ver = PKCS1_PSS.new(self.rsa)
  25. h = get_hasher()
  26. h.update(hashed_value)
  27. return ver.verify(h, signature)
  28. def sign(self, hashed_value: bytes) -> bytes:
  29. """ Sign a hashed value with this private key. """
  30. signer = PKCS1_PSS.new(self.rsa)
  31. h = get_hasher()
  32. h.update(hashed_value)
  33. return signer.sign(h)
  34. @classmethod
  35. def generate_private_key(cls):
  36. """ Generate a new private key. """
  37. return Signing(RSA.generate(3072).exportKey())
  38. @classmethod
  39. def from_file(cls, path):
  40. """ Reads a private or public key from the file at `path`. """
  41. with open(path, 'rb') as f:
  42. return cls(f.read())
  43. def as_bytes(self, include_priv: bool=False) -> bytes:
  44. """ Serialize this key to a `bytes` value. """
  45. if include_priv:
  46. return self.rsa.exportKey()
  47. else:
  48. return self.rsa.publickey().exportKey()
  49. def to_json_compatible(self):
  50. """ Returns a JSON-serializable representation of this object. """
  51. return hexlify(self.as_bytes()).decode()
  52. @classmethod
  53. def from_json_compatible(cls, obj):
  54. """ Creates a new object of this class, from a JSON-serializable representation. """
  55. return cls(unhexlify(obj))
  56. def __eq__(self, other: 'Signing'):
  57. return self.rsa.e == other.rsa.e and self.rsa.n == other.rsa.n
  58. def __hash__(self):
  59. return hash((self.rsa.e, self.rsa.n))
  60. @property
  61. def has_private(self) -> bool:
  62. """
  63. Returns a bool value indicating whether this instance has a private key that can be used to
  64. sign things.
  65. """
  66. return self.rsa.has_private()
  67. @classmethod
  68. def read_many_private(cls, file_contents: bytes) -> 'Iterator[Signing]':
  69. """ Reads many private keys from the (binary) contents of a file written with `write_many_private`. """
  70. end = b"-----END RSA PRIVATE KEY-----"
  71. for key in file_contents.strip().split(end):
  72. if not key:
  73. continue
  74. key = key.lstrip() + end
  75. yield cls(key)
  76. @staticmethod
  77. def write_many_private(path: str, keys: 'Iterable[Signing]'):
  78. """ Writes the private keys in `keys` to the file at `path`. """
  79. dirname = os.path.dirname(path) or "."
  80. with tempfile.NamedTemporaryFile("wb", delete=False, dir=dirname) as fp:
  81. try:
  82. for key in keys:
  83. fp.write(key.as_bytes(include_priv=True) + b"\n")
  84. fp.flush()
  85. os.fsync(fp.fileno())
  86. os.rename(fp.name, path)
  87. except Exception as e:
  88. os.unlink(fp.name)
  89. raise e
  90. fd = os.open(dirname, os.O_DIRECTORY)
  91. try:
  92. os.fsync(fd)
  93. finally:
  94. os.close(fd)