def recover_key_from_nonce(msg, sig, k, q, hash=sha1): """Recovers a DSA private key (`x`) from a message-signature-pair given a known nonce Example: ```python >>> from bop.crypto_constructor import dsa >>> my_dsa = dsa() >>> msg = b"Hello kind stranger!" >>> sig = my_dsa.sign(msg) >>> leaked_nonce = sig._k >>> my_dsa.x == recover_key_from_nonce(msg, sig, leaked_nonce, my_dsa.q, hash=my_dsa.hash) True ``` Arguments: msg {bytes or int} -- The message which was signed sig {tuple or Signature} -- The signature of the given message in form `(r, s)` k {int} -- The leaked nonce q {int} -- The public parameter q """ if type(msg) != bytes: msg = i2b(msg) h = b2i(hash(msg)) r, s = sig return ((s * k - h) * invmod(r, q)) % q
def _dsa_sign(msg, p, q, g, x, k, hash=hash): if type(msg) != bytes: msg = i2b(msg) h = b2i(hash(msg)) r = pow(g, k, p) % q s = (invmod(k, q) * (h + r * x)) % q return r, s
def rsa(p=None, q=None, e=0x10001): if p is None or q is None: p = 0xfe8557c536be50a41a682a848731377fec2e7889a02b68697ba8a93dfd7aca45 q = 0xcc1215873a81091163885ebd390c0e3ae18c2e5c961cdaa5daa563cbd459685d p = 11657510804603879190708332092675312014343829286377852544041511516023862575778860458573625524357360917135073366324924898682703984245741417367112631371911313 q = 10089976740518582496356045969057602987024596248854888881417032743865018594042312715942618960983484996648974071339742122325713010480659249911953583913570703 n = p * q pq = (p - 1) * (q - 1) d = invmod(e, pq) key_size = bit_length_exp2(n) return SimpleRSAInterface(d, n, e, key_size=key_size)
def verify(self, msg, sig): r, s = sig if r <= 0 or r >= self.q or s <= 0 or s >= self.q: return False if type(msg) != bytes: msg = i2b(msg) h = b2i(self.hash(msg)) w = invmod(s, self.q) u1 = (w * h) % self.q u2 = (w * r) % self.q v = (pow(self.g, u1, self.p) * pow(self.y, u2, self.p)) % self.q return v == r
def broadcast_e3(messages, public_keys): """Perform a RSA broadcast attack for `e=3` For this to work the same plain text message has to be encrypted at least `e=3` times. This method will solve for the plain text using the chinese remainder theorem. This attack is well known as [Håstad's broadcast attack](https://en.wikipedia.org/wiki/Coppersmith%27s_attack#H.C3.A5stad.27s_broadcast_attack) Example: ```python >>> plain = 1818 >>> n0 = 179 * 181 >>> n1 = 191 * 193 >>> n2 = 197 * 199 >>> e = 3 >>> msg0 = pow(plain, e, n0) >>> msg1 = pow(plain, e, n1) >>> msg2 = pow(plain, e, n2) >>> broadcast_e3([msg0, msg1, msg2], [n0, n1, n2]) 1818 ``` Arguments: messages {list of int} -- The cipher texts captured public_keys {list of int} -- The public key parameters `N` for each message Returns: int -- The decrypted plain text """ e = 3 public_keys = public_keys[:e] messages = messages[:e] assert (len(set(public_keys)) == len(public_keys)) assert (len(public_keys) == e) assert (len(messages) == e) it = zip(messages, public_keys) x, N = next(it) for (c, n) in it: x += ((invmod(N, n) * (c - x)) % n) * N N *= n return cubic_root(x)
def sign(self, msg): if type(msg) != bytes: msg = i2b(msg) h = b2i(self.hash(msg)) s = 0 while s == 0: r = 0 while r == 0: if self.generate_nonce is not None: k = self.generate_nonce() else: k = secrets.randbelow(self.q - 2) + 2 r = pow(self.g, k, self.p) % self.q s = (invmod(k, self.q) * (h + r * self.x)) % self.q return self.Signature(r, s, k)
def recover_unpadded(oracle, ciphertext, e, n, s=2): """Recover the plaintext given an oracle which decrypts recently unique messages. This is useful if to bypass the "recently unique" property of the oracle. Example: ```python3 >>> from bop.oracles.message_recovery import MessageRecoveryOracle as Oracle >>> oracle = Oracle() >>> cipher = oracle.encrypt(1337) >>> oracle(cipher) 1337 >>> # We cannot recover the plaintext again: >>> assert(oracle(cipher) is None) >>> # Or can we? >>> recover_unpadded(oracle, cipher, oracle.rsa.e, oracle.rsa.n) 1337 ``` Arguments: oracle {callable} -- The oracle to use. It should decrypt some ciphertext with some fixed key. You are only allowed to decrypt a message once. ciphertext {int} -- [description] e {int} -- The RSA public key parameter e n {int} -- The RSA public key parameter n Keyword Arguments: s {int} -- A *random* parameter to use for key recovery in the range [2, n] (default: {2}) Returns: int -- The recovered plaintext """ new_ciphertext = (pow(s, e, n) * ciphertext) % n tmp = oracle(new_ciphertext) return (invmod(s, n) * tmp) % n
def decrypt_pkcs_padding_leak(oracle, msg, e, n): """Perform an adaptive chosen ciphertext against a weak RSA implementation leaking PKCS1v5 padding information. This attack was discovered by Daniel Bleichenbacher and is well described in the original [paper](http://archiv.infsec.ethz.ch/education/fs08/secsem/bleichenbacher98.pdf). Side note:\ This attack is generally faster if `msg` is already properly padded. Depending on the keysize the expected running time may be somewhere between a few seconds (key size <= 512 bits) or somewhere around one minute for a 1024 public modulus. I once tested using a 2048 bit key which ran about an hour..\ I am pretty sure this implementation is far from optimal, though I am not qualified to judge where I went wrong. Example: ```python3 >>> from bop.oracles.padding import PaddingRSAOracle as Oracle >>> import bop.utils as utils >>> # We will choose smaller params to keep the running time down a little bit >>> p, q = utils.gen_rsa_key_params(256) >>> o = Oracle(p, q) >>> e, n = o.public_key() >>> plain = b"Hey! This message is save!" >>> # Optional: Pad the message: >>> # plain = utils.pad_pkcs1v5(plain, n) >>> msg = o.encrypt(plain) >>> decrypt_pkcs_padding_leak(o, msg, e, n) b'Hey! This message is save!' ``` Args: oracle (callable): An oracle which leaks whether the decrypted message has a valid PKCS1v5 padding or not (i. e. the first byte is equal to 0 and the second byte is equal to 2) msg (bytes or int): The message to decrypt e (int): The public exponent n (int): The public modulus Returns: bytes or int: The decrypted message. Depending on the input type the output type is matched. """ was_bytes = False if type(msg) != int: msg = b2i(msg) was_bytes = True k = bit_length_exp2(n) assert k > 16 B = 1 << (k - 16) s0 = 1 c0 = msg first = True M = {(2 * B, 3 * B - 1)} if not oracle(msg): # Blinding # ensure we have a valid padding to work with while True: s0 = secrets.randbelow(n - 1) + 1 c0 = (msg * pow(s0, e, n)) % n if oracle(c0): break while True: if first: # Step 2a s = n // (3 * B) while oracle((c0 * pow(s, e, n)) % n) is False: s += 1 first = False elif len(M) > 1: # Step 2b s_ = s + 1 while not oracle((c0 * pow(s_, e, n)) % n): s_ += 1 s = s_ else: # Step 2c a, b = next(iter(M)) found = False r = 2 * (b * s - 2 * B) // n while not found: s_ = (2 * B + r * n) // b s_max = (3 * B + r * n) // a while s_ <= s_max: if oracle((c0 * pow(s_, e, n)) % n): found = True break s_ += 1 r += 1 s = s_ # Step 3 M_ = set() for a, b in M: r_low = (a * s - 3 * B + 1) // n r_high = (b * s - 2 * B) // n for r in range(r_low, r_high + 1): # note the + s - 1 in order to ensure rounding to the next integer low = max(a, (2 * B + r * n + s - 1) // s) high = min(b, (3 * B - 1 + r * n) // s) if low <= high and (low, high) not in M_: M_.add((low, high)) M = M_ # Step 4 if len(M) == 1: a, b = next(iter(M)) if a == b: plain = (a * invmod(s0, n)) % n if was_bytes: return i2b(plain) return plain
def recover_key_from_duplicate_nonce(gen, public_parameters, hash=sha1): """Attempt to recover a DSA private key (`x`) from a stream of messages by searching for a duplicate usage of a nonce. Note that this is a pretty naive implementation running in O(n^2) where n is the number of messages (signatures) checked. Example: ```python >>> from bop.crypto_constructor import dsa >>> import secrets >>> >>> # Setup a weak implementation >>> my_dsa = dsa() >>> def weak_nonce_generator(): ... return secrets.choice(range(2, 11)) ... >>> my_dsa.generate_nonce = weak_nonce_generator >>> def message_src(): ... while True: ... msg = secrets.token_bytes(16) ... yield msg, my_dsa.sign(msg) ... >>> x, leaked_nonce = recover_key_from_duplicate_nonce(message_src(), my_dsa.public_parameters, hash=my_dsa.hash) >>> my_dsa.x == x True ``` Arguments: gen {generator} -- A generator yielding message-signature-pairs `(msg, signature)`. It will be consumed until a duplicate nonce is found or it is empty. public_parameters {tuple} -- The public parameters of the DSA algorithm used Keyword Arguments: hash {callable} -- The hash function to use (default: {sha1}) Returns: (int, int) -- (x, leaked_nonce), i.e. the private key and the leaked nonce. Or (None, None) if no duplicate was found. """ p, q, g, y = public_parameters first_msg, first_sig = next(gen) if type(first_msg) != bytes: first_msg = i2b(first_msg) bases = [(first_msg, first_sig)] # we do a simple exhaustive search for a duplicated nonce for msg1, sig1 in gen: if type(msg1) != bytes: msg1 = i2b(msg1) h1 = b2i(hash(msg1)) r1, s1 = sig1 for msg2, sig2 in bases: h2 = b2i(hash(msg2)) r2, s2 = sig2 # Assume k was equal k = ((h2 - h1) * invmod(s2 - s1, q)) % q # Check if our assumption holds x = recover_key_from_nonce(msg1, sig1, k, q, hash=hash) if _dsa_sign(msg1, p, q, g, x, k, hash=hash) == (r1, s1): return x, k bases.append((msg1, sig1)) return None, None