replaced pytcryptopp rsa with our own wrapper

This commit is contained in:
heartsucker 2019-05-24 14:40:10 +02:00 committed by meejah
parent 9e31bfe2f4
commit 8063d93c6d
13 changed files with 226 additions and 41 deletions

View File

@ -9,10 +9,10 @@ from twisted.application import service
from twisted.application.internet import TimerService
from twisted.python.filepath import FilePath
from twisted.python.failure import Failure
from pycryptopp.publickey import rsa
import allmydata
from allmydata.crypto.ed25519 import SigningKey
from allmydata.crypto.rsa import PrivateKey
from allmydata.storage.server import StorageServer
from allmydata import storage_client
from allmydata.immutable.upload import Uploader
@ -156,8 +156,8 @@ class KeyGenerator(object):
keysize = keysize or self.default_keysize
# RSA key generation for a 2048 bit key takes between 0.8 and 3.2
# secs
signer = rsa.generate(keysize)
verifier = signer.get_verifying_key()
signer = PrivateKey.generate(keysize)
verifier = signer.public_key()
return defer.succeed( (verifier, signer) )
class Terminator(service.Service):

View File

@ -1,3 +1,7 @@
class BadSignature(Exception):
pass
class BadPrefixError(Exception):
pass

View File

@ -17,18 +17,12 @@ base32.
import six
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption, \
PublicFormat
# When we were still using `pycryptopp`, BadSignatureError was the name of the exception, and now
# that we've switched to `cryptography`, we are importing and "re-exporting" this to keep the name
# the same (for now).
from cryptography.exceptions import InvalidSignature
BadSignatureError = InvalidSignature
del InvalidSignature
from allmydata.crypto import remove_prefix
from allmydata.crypto import remove_prefix, BadSignature
from allmydata.util.base32 import a2b, b2a
_PRIV_PREFIX = 'priv-v0-'
@ -109,7 +103,10 @@ class VerifyingKey:
if not isinstance(data, six.binary_type):
raise ValueError('data must be bytes')
self._pub_key.verify(signature, data)
try:
self._pub_key.verify(signature, data)
except InvalidSignature:
raise BadSignature
@classmethod
def parse_encoded_key(cls, pub_str):

117
src/allmydata/crypto/rsa.py Normal file
View File

@ -0,0 +1,117 @@
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives.serialization import load_der_private_key, load_der_public_key, \
Encoding, PrivateFormat, PublicFormat, NoEncryption
from allmydata.crypto import BadSignature
class RsaMixin(object):
'''
This is the value that was used by `pycryptopp`, and we must continue to use it for
both backwards compatibility and interoperability.
The docs for `cryptography` suggest to use the constant defined at
`cryptography.hazmat.primitives.asymmetric.padding.PSS.MAX_LENGTH`, but this causes old
signatures to fail to validate.
'''
RSA_PSS_SALT_LENGTH = 32
class PrivateKey(RsaMixin):
def __init__(self, priv_key):
self._priv_key = priv_key
@classmethod
def generate(cls, key_size):
priv_key = rsa.generate_private_key(
public_exponent=65537, # serisously don't change this value
key_size=key_size,
backend=default_backend()
)
return cls(priv_key)
@classmethod
def parse_string(cls, priv_key_str):
priv_key = load_der_private_key(
priv_key_str,
password=None,
backend=default_backend(),
)
return cls(priv_key)
def public_key(self):
return PublicKey(self._priv_key.public_key())
def serialize(self):
return self._priv_key.private_bytes(
encoding=Encoding.DER,
format=PrivateFormat.PKCS8,
encryption_algorithm=NoEncryption(),
)
def sign(self, data):
return self._priv_key.sign(
data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=self.RSA_PSS_SALT_LENGTH,
),
hashes.SHA256(),
)
def __eq__(self, other):
if isinstance(other, type(self)):
return self.serialize() == other.serialize()
else:
return False
def __ne__(self, other):
return not self.__eq__(other)
class PublicKey(RsaMixin):
def __init__(self, pub_key):
self._pub_key = pub_key
@classmethod
def parse_string(cls, pub_key_str):
pub_key = load_der_public_key(
pub_key_str,
backend=default_backend(),
)
return cls(pub_key)
def serialize(self):
return self._pub_key.public_bytes(
encoding=Encoding.DER,
format=PublicFormat.SubjectPublicKeyInfo,
)
def verify(self, signature, data):
try:
self._pub_key.verify(
signature,
data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=self.RSA_PSS_SALT_LENGTH,
),
hashes.SHA256(),
)
except InvalidSignature:
raise BadSignature
def __eq__(self, other):
if isinstance(other, type(self)):
return self.serialize() == other.serialize()
else:
return False
def __ne__(self, other):
return not self.__eq__(other)

View File

@ -9,7 +9,7 @@ from allmydata.introducer.common import sign_to_foolscap, unsign_from_foolscap,\
get_tubid_string_from_ann
from allmydata.util import log, yamlutil, connection_status
from allmydata.util.rrefutil import add_version_to_remote_reference
from allmydata.crypto.ed25519 import BadSignatureError
from allmydata.crypto import BadSignature
from allmydata.util.assertutil import precondition
class InvalidCacheError(Exception):
@ -238,7 +238,7 @@ class IntroducerClient(service.Service, Referenceable):
ann, key_s = unsign_from_foolscap(ann_t)
# key is "v0-base32abc123"
precondition(isinstance(key_s, str), key_s)
except BadSignatureError:
except BadSignature:
self.log("bad signature on inbound announcement: %s" % (ann_t,),
parent=lp, level=log.WEIRD, umid="ZAU15Q")
# process other announcements that arrived with the bad one

View File

@ -229,7 +229,7 @@ class IntroducerService(service.MultiService, Referenceable):
self._debug_counts["inbound_message"] += 1
self.log("introducer: announcement published: %s" % (ann_t,),
umid="wKHgCw")
ann, key = unsign_from_foolscap(ann_t) # might raise BadSignatureError
ann, key = unsign_from_foolscap(ann_t) # might raise BadSignature
service_name = str(ann["service-name"])
index = (service_name, key)

View File

@ -9,6 +9,7 @@ from foolscap.api import eventually, fireEventually, DeadReferenceError, \
RemoteException
from allmydata.crypto.aes import AES
from allmydata.crypto.rsa import PrivateKey
from allmydata.interfaces import IRetrieveStatus, NotEnoughSharesError, \
DownloadStopped, MDMF_VERSION, SDMF_VERSION
from allmydata.util.assertutil import _assert, precondition
@ -17,8 +18,6 @@ from allmydata.util.dictutil import DictOfSets
from allmydata import hashtree, codec
from allmydata.storage.server import si_b2a
from pycryptopp.publickey import rsa
from allmydata.mutable.common import CorruptShareError, BadShareError, \
UncoordinatedWriteError
from allmydata.mutable.layout import MDMFSlotReadProxy
@ -936,13 +935,11 @@ class Retrieve(object):
# it's good
self.log("got valid privkey from shnum %d on reader %s" %
(reader.shnum, reader))
privkey = rsa.create_signing_key_from_string(alleged_privkey_s)
privkey = PrivateKey.parse_string(alleged_privkey_s)
self._node._populate_encprivkey(enc_privkey)
self._node._populate_privkey(privkey)
self._need_privkey = False
def _done(self):
"""
I am called by _download_current_segment when the download process
@ -973,7 +970,6 @@ class Retrieve(object):
self._consumer.unregisterProducer()
eventually(self._done_deferred.callback, ret)
def _raise_notenoughshareserror(self):
"""
I am called when there are not enough active servers left to complete

View File

@ -8,11 +8,12 @@ from twisted.internet import defer
from twisted.python import failure
from foolscap.api import DeadReferenceError, RemoteException, eventually, \
fireEventually
from allmydata.crypto import BadSignature
from allmydata.crypto.rsa import PublicKey, PrivateKey
from allmydata.util import base32, hashutil, log, deferredutil
from allmydata.util.dictutil import DictOfSets
from allmydata.storage.server import si_b2a
from allmydata.interfaces import IServermapUpdaterStatus
from pycryptopp.publickey import rsa
from allmydata.mutable.common import MODE_CHECK, MODE_ANYTHING, MODE_WRITE, \
MODE_READ, MODE_REPAIR, CorruptShareError
@ -843,8 +844,9 @@ class ServermapUpdater(object):
# This is a new version tuple, and we need to validate it
# against the public key before keeping track of it.
assert self._node.get_pubkey()
valid = self._node.get_pubkey().verify(prefix, signature[1])
if not valid:
try:
self._node.get_pubkey().verify(prefix, signature[1])
except BadSignature:
raise CorruptShareError(server, shnum,
"signature is invalid")
@ -913,12 +915,10 @@ class ServermapUpdater(object):
verinfo,
update_data)
def _deserialize_pubkey(self, pubkey_s):
verifier = rsa.create_verifying_key_from_string(pubkey_s)
verifier = PublicKey.parse_string(pubkey_s)
return verifier
def _try_to_validate_privkey(self, enc_privkey, server, shnum, lp):
"""
Given a writekey from a remote server, I validate it against the
@ -937,7 +937,7 @@ class ServermapUpdater(object):
self.log("got valid privkey from shnum %d on serverid %s" %
(shnum, server.get_name()),
parent=lp)
privkey = rsa.create_signing_key_from_string(alleged_privkey_s)
privkey = PrivateKey.parse_string(alleged_privkey_s)
self._node._populate_encprivkey(enc_privkey)
self._node._populate_privkey(privkey)
self._need_privkey = False

View File

@ -0,0 +1 @@
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC0JwgBbVsI+XlOopqjvBatKkQbJPXuap7Psbe5i4EoMfiYI2PC2UB7GuYeTdE79TvDtmfjFD/RVWA3Y/RTQYQz/lKyCFS4w3wa/TPkZwF1r3OjIMSsCYe2J3W9NV3cK+PVw2A8D2y5DvUIAdO+Mi6aH26p2UV8FTnPqHWvJubrcLQt6979/BQnqKCFJ+SPx4se5XsMZ3vrbs6MCqM2qS9RnNEhexlNrJd1wXezILKsmQdf/QiZiY7LXjEdD6BNG8OYQ2iSbCa8aGEoSPQfdnZZxcTFE02QwKcScZKhU9fRv0Ttqr3i8xiliw9gn4UzptEZO6MVO2BrptS30SjJDXC7AgERAoIBADpI3PFnJPtfxV00m3E1UqFvjoFAqetAnMq5fzR/9RSIo0BHr1Wgo+uXwuuvw7GEC85gqSPR2GlfYuS+dLGGIz3/dRt7KngDAoEzzQYhU0u4w4eZqQp7jcn9tSagUxKGq5f7cfVQSNJ1x77TaibyHiLN7xjVWj67krQf6dbI0j0cYvnxu+4EZbzNdvFw93ddoOZB/dFjLu0kVKVl/mWyCX9GNr2nCSHe9wYipOz5b9WkdD0J2Oy0v8Wkn4y3yOOvo/EgrNYfo4IVslsDo9Yw3Yk32Eml0ZsdwSqu+wM4c+jRbTJ+sBGqci4etPpMhcsH0Vt9+97Lnuan2Jza9xjrL2ECgYEA8wj+/bfjTCXsu22f8V7Z40vJUyM7j4WvUoA9khAQ7qAlnFdqdzq5a7ArA9vRjeN6ya16j36IXCkpT+FGe6YWCsZCKd1ZVy7sZ1Uh7X2hRqf0vxJsSJvG/OmofFUfuwCgLFLKI4SDhHaB+pWAdkAIL4MkJQADg/qVlAdrWoPsfhECgYEAvcNHhSCW010SRudwmTRX5QtndHk/LM6VAgyR0ElarvmG6K5pbpL8MD5CpJ3AhUwKp96SlMsBEG3a9BR5zv6Jvhc/KHxT7W/EjLnV9PSD90+BgHHsTonjg6TayJ9XE6RpO3MqeifVG/2S5WhhFFGGd5KSFnvZwr9ni+LYRuDVpgsCgYEAgKpo4KylgqqqgVgnf8jNtJGIs4sfiDe3K61NxcxFMwl9UsTeAuLaomxTAgr2eEtBAVvXeSTex2EV3v7K9irAYA6bf5NNamQizUswFFGRneByg0X9F2GHdtYN53hcF7UJgOCJIdy+GPNx/SH4txLXKDZebfDyzWaLbHxmAr5QBoECgYBC+aDFkwgOXRWCb81jP6aNExV0Zwc8/Z4AuSRnoWtM0In3xRYnBrNcUjWjgvinhD//A0LLGnjYnz44BzoM0k67j7vwK+Fi3CdAug9HZVvAsqYtVWJ2EoyI0MWwODzZwY6Nc/Df0dK+lbtgBrjZ/qft937awkzbUp0EMfH65fENbQKBgQCSVWXy+WLQXeHtx/+nNv9HyjQnowalp3SwWRf0YoK/xa526xg+ixViVZvT6e2KTcJGdHFQ+cbCsc1Vx6E13n3Mu9y0N3a4WRQkZHPgnsNouPLaKn0SmVY7RX/I/Rz2r0hRE+gDM6+1/99zPuwP3FW5eLoTBX021Y35kBFHbZ4r+w==

View File

@ -0,0 +1 @@
MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEAtCcIAW1bCPl5TqKao7wWrSpEGyT17mqez7G3uYuBKDH4mCNjwtlAexrmHk3RO/U7w7Zn4xQ/0VVgN2P0U0GEM/5SsghUuMN8Gv0z5GcBda9zoyDErAmHtid1vTVd3Cvj1cNgPA9suQ71CAHTvjIumh9uqdlFfBU5z6h1rybm63C0Leve/fwUJ6ighSfkj8eLHuV7DGd7627OjAqjNqkvUZzRIXsZTayXdcF3syCyrJkHX/0ImYmOy14xHQ+gTRvDmENokmwmvGhhKEj0H3Z2WcXExRNNkMCnEnGSoVPX0b9E7aq94vMYpYsPYJ+FM6bRGTujFTtga6bUt9EoyQ1wuwIBEQ==

View File

@ -0,0 +1 @@
ItsyW1XTOIvet6WsS68AJ/ernMG62aoeJKzyBBZ9fdeB2mVzURCBmgX5P0hTPgxHa1sEI6oIbREv4lIQnWHcPgjvz5qBkDtbOp1YHkkFAFOh533dH4s2MiRECIzHh19sBsqTGe0w/pRTHhwV+nStFqZ0IMsdxv0Qsgk5IClIY/WgBSnHQZpVbxyfL7qwvm1JK2GRuygRRsrSsxLiSnA5RWlOsDkDikVu5nhZI31K+PWa9v1i6U7ZkV4uD9triJkHW2XBIRkCyqT6wgM4KBN6V4H9nqlxZhJSQoSn1U5Rh3pL+XG6yevaZq7+pwOnRUcFkEwiJ2wT/NIK0Bjng8Szmw==

View File

@ -1,10 +1,14 @@
import six
import unittest
from base64 import b64decode
from binascii import a2b_hex, b2a_hex
from os import path
from allmydata.crypto.aes import AES
from allmydata.crypto.ed25519 import SigningKey, VerifyingKey
from allmydata.crypto import ed25519, rsa
RESOURCE_DIR = path.join(path.dirname(__file__), 'data')
class TestRegression(unittest.TestCase):
@ -14,9 +18,35 @@ class TestRegression(unittest.TestCase):
keys.
'''
KEY = b'My\x9c\xc0f\xd3\x03\x9a1\x8f\xbd\x17W_\x1f2'
AES_KEY = b'My\x9c\xc0f\xd3\x03\x9a1\x8f\xbd\x17W_\x1f2'
IV = b'\x96\x1c\xa0\xbcUj\x89\xc1\x85J\x1f\xeb=\x17\x04\xca'
with open(path.join(RESOURCE_DIR, 'pycryptopp-rsa-2048-priv.txt')) as f:
'''
Created using `pycryptopp`:
from base64 import b64encode
from pycryptopp.publickey import rsa
priv = rsa.generate(2048)
priv_str = b64encode(priv.serialize())
pub_str = b64encode(priv.get_verifying_key().serialize())
'''
RSA_2048_PRIV_KEY = six.b(b64decode(f.read().strip()))
with open(path.join(RESOURCE_DIR, 'pycryptopp-rsa-2048-sig.txt')) as f:
'''
Signature created using `RSA_2048_PRIV_KEY` via:
sig = priv.sign(b'test')
'''
RSA_2048_SIG = six.b(b64decode(f.read().strip()))
with open(path.join(RESOURCE_DIR, 'pycryptopp-rsa-2048-pub.txt')) as f:
'''
The public key corresponding to `RSA_2048_PRIV_KEY`.
'''
RSA_2048_PUB_KEY = six.b(b64decode(f.read().strip()))
def test_old_start_up_test(self):
"""
This was the old startup test run at import time in `pycryptopp.cipher.aes`.
@ -74,7 +104,7 @@ class TestRegression(unittest.TestCase):
plaintext = b'test'
expected_ciphertext = b'\x7fEK\\'
aes = AES(self.KEY)
aes = AES(self.AES_KEY)
ciphertext = aes.process(plaintext)
self.failUnlessEqual(ciphertext, expected_ciphertext)
@ -96,7 +126,7 @@ class TestRegression(unittest.TestCase):
b'\x1f\xa1|\xd2$E\xb5\xe7\x9d\xae\xd1\x1f)\xe4\xc7\x83\xb8\xd5|dHhU\xc8\x9a\xb1\x10\xed'
b'\xd1\xe7|\xd1')
aes = AES(self.KEY)
aes = AES(self.AES_KEY)
ciphertext = aes.process(plaintext)
self.failUnlessEqual(ciphertext, expected_ciphertext)
@ -115,7 +145,7 @@ class TestRegression(unittest.TestCase):
plaintext = b'test'
expected_ciphertext = b'\x82\x0e\rt'
aes = AES(self.KEY, iv=self.IV)
aes = AES(self.AES_KEY, iv=self.IV)
ciphertext = aes.process(plaintext)
self.failUnlessEqual(ciphertext, expected_ciphertext)
@ -137,8 +167,7 @@ class TestRegression(unittest.TestCase):
b'\x97a\xdc\x100?\xf5L\x9f\xd9\xeeO\x98\xda\xf5g\x93\xa7q\xe1\xb1~\xf8\x1b\xe8[\\s'
b'\x144$\x86\xeaC^f')
aes = AES(self.KEY, iv=self.IV)
aes = AES(self.AES_KEY, iv=self.IV)
ciphertext = aes.process(plaintext)
self.failUnlessEqual(ciphertext, expected_ciphertext)
@ -165,8 +194,8 @@ class TestRegression(unittest.TestCase):
b'\x86 ier!\xe8\xe5#*\x9d\x8c\x0bI\x02\xd90\x0e7\xbeW\xbf\xa3\xfe\xc1\x1c\xf5+\xe9)'
b'\xa3\xde\xc9\xc6s\xc9\x90\xf7x\x08')
priv_key = SigningKey.parse_encoded_key(priv_str)
pub_key = VerifyingKey.parse_encoded_key(pub_str)
priv_key = ed25519.SigningKey.parse_encoded_key(priv_str)
pub_key = ed25519.VerifyingKey.parse_encoded_key(pub_str)
self.failUnlessEqual(priv_key.public_key(), pub_key)
@ -175,17 +204,30 @@ class TestRegression(unittest.TestCase):
pub_key.verify(new_sig, test_data)
def test_decode_rsa_keypair(self):
'''
This simply checks that keys and signatures generated using the old code are still valid
using the new code.
'''
priv_key = rsa.PrivateKey.parse_string(self.RSA_2048_PRIV_KEY)
pub_key = priv_key.public_key()
parsed_pub_key = rsa.PublicKey.parse_string(self.RSA_2048_PUB_KEY)
self.failUnlessEqual(pub_key, parsed_pub_key)
pub_key.verify(self.RSA_2048_SIG, b'test')
class TestEd25519(unittest.TestCase):
def test_keys(self):
priv_key = SigningKey.generate()
priv_key = ed25519.SigningKey.generate()
priv_key_str = priv_key.encoded_key()
self.assertIsInstance(priv_key_str, six.string_types)
self.assertIsInstance(priv_key.private_bytes(), six.binary_type)
priv_key2 = SigningKey.parse_encoded_key(priv_key_str)
priv_key2 = ed25519.SigningKey.parse_encoded_key(priv_key_str)
self.failUnlessEqual(priv_key, priv_key2)
@ -199,6 +241,32 @@ class TestEd25519(unittest.TestCase):
self.assertIsInstance(pub_key_str, six.string_types)
self.assertIsInstance(pub_key.public_bytes(), six.binary_type)
pub_key2 = VerifyingKey.parse_encoded_key(pub_key_str)
pub_key2 = ed25519.VerifyingKey.parse_encoded_key(pub_key_str)
self.failUnlessEqual(pub_key, pub_key2)
class TestRsa(unittest.TestCase):
def test_keys(self):
priv_key = rsa.PrivateKey.generate(2048)
priv_key_str = priv_key.serialize()
self.assertIsInstance(priv_key_str, six.string_types)
priv_key2 = rsa.PrivateKey.parse_string(priv_key_str)
self.failUnlessEqual(priv_key, priv_key2)
pub_key = priv_key.public_key()
pub_key2 = priv_key2.public_key()
self.failUnlessEqual(pub_key, pub_key2)
pub_key_str = pub_key.serialize()
self.assertIsInstance(pub_key_str, six.string_types)
pub_key2 = rsa.PublicKey.parse_string(pub_key_str)
self.failUnlessEqual(pub_key, pub_key2)

View File

@ -1006,7 +1006,7 @@ class Signatures(SyncTestCase):
# bad signature
bad_ann = {"key1": "value2"}
bad_msg = json.dumps(bad_ann).encode("utf-8")
self.failUnlessRaises(ed25519.BadSignatureError,
self.failUnlessRaises(crypto.BadSignature,
unsign_from_foolscap, (bad_msg, sig, key))
# unrecognized signatures