From 58eb9faa84fe3ccc3464052bad3eaad79a2da33a Mon Sep 17 00:00:00 2001 From: Philippe Teuwen Date: Sat, 26 Sep 2020 17:18:21 +0200 Subject: [PATCH] recover_pk: remove sslcrypto dep --- tools/recover_pk.py | 306 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 299 insertions(+), 7 deletions(-) diff --git a/tools/recover_pk.py b/tools/recover_pk.py index b2ff2e6c7..073a65575 100755 --- a/tools/recover_pk.py +++ b/tools/recover_pk.py @@ -1,26 +1,318 @@ #!/usr/bin/env python3 +# MIT License +# Copyright (c) 2020 @doegox -# @doegox -- 2020 - -import sslcrypto import binascii import sys debug = False +####################################################################### +# Using external sslcrypto library: +# import sslcrypto +# ... sslcrypto.ecc.get_curve() +# But to get this script autonomous, i.e. for CI, we embedded the +# code snippets we needed: +####################################################################### +# code snippets from JacobianCurve: +# This code is public domain. Everyone has the right to do whatever they want with it for any purpose. +# Copyright (c) 2013 Vitalik Buterin + +class JacobianCurve: + def __init__(self, p, n, a, b, g): + self.p = p + self.n = n + self.a = a + self.b = b + self.g = g + self.n_length = len(bin(self.n).replace("0b", "")) + + + def to_jacobian(self, p): + return p[0], p[1], 1 + + + def jacobian_double(self, p): + if not p[1]: + return 0, 0, 0 + ysq = (p[1] ** 2) % self.p + s = (4 * p[0] * ysq) % self.p + m = (3 * p[0] ** 2 + self.a * p[2] ** 4) % self.p + nx = (m ** 2 - 2 * s) % self.p + ny = (m * (s - nx) - 8 * ysq ** 2) % self.p + nz = (2 * p[1] * p[2]) % self.p + return nx, ny, nz + + + def jacobian_add(self, p, q): + if not p[1]: + return q + if not q[1]: + return p + u1 = (p[0] * q[2] ** 2) % self.p + u2 = (q[0] * p[2] ** 2) % self.p + s1 = (p[1] * q[2] ** 3) % self.p + s2 = (q[1] * p[2] ** 3) % self.p + if u1 == u2: + if s1 != s2: + return (0, 0, 1) + return self.jacobian_double(p) + h = u2 - u1 + r = s2 - s1 + h2 = (h * h) % self.p + h3 = (h * h2) % self.p + u1h2 = (u1 * h2) % self.p + nx = (r ** 2 - h3 - 2 * u1h2) % self.p + ny = (r * (u1h2 - nx) - s1 * h3) % self.p + nz = (h * p[2] * q[2]) % self.p + return (nx, ny, nz) + + + def from_jacobian(self, p): + z = inverse(p[2], self.p) + return (p[0] * z ** 2) % self.p, (p[1] * z ** 3) % self.p + + + def jacobian_shamir(self, a, n, b, m): + ab = self.jacobian_add(a, b) + if n < 0 or n >= self.n: + n %= self.n + if m < 0 or m >= self.n: + m %= self.n + res = 0, 0, 1 # point on infinity + for i in range(self.n_length - 1, -1, -1): + res = self.jacobian_double(res) + has_n = n & (1 << i) + has_m = m & (1 << i) + if has_n: + if has_m == 0: + res = self.jacobian_add(res, a) + if has_m != 0: + res = self.jacobian_add(res, ab) + else: + if has_m == 0: + res = self.jacobian_add(res, (0, 0, 1)) # Try not to leak + if has_m != 0: + res = self.jacobian_add(res, b) + return res + + def fast_shamir(self, a, n, b, m): + return self.from_jacobian(self.jacobian_shamir(self.to_jacobian(a), n, self.to_jacobian(b), m)) + +####################################################################### +# code snippets from sslcrypto +# MIT License +# Copyright (c) 2019 Ivan Machugovskiy + +import hmac +import os +import hashlib +import struct + +def int_to_bytes(raw, length): + data = [] + for _ in range(length): + data.append(raw % 256) + raw //= 256 + return bytes(data[::-1]) + + +def bytes_to_int(data): + raw = 0 + for byte in data: + raw = raw * 256 + byte + return raw + +def legendre(a, p): + res = pow(a, (p - 1) // 2, p) + if res == p - 1: + return -1 + else: + return res + +def inverse(a, n): + if a == 0: + return 0 + lm, hm = 1, 0 + low, high = a % n, n + while low > 1: + r = high // low + nm, new = hm - lm * r, high - low * r + lm, low, hm, high = nm, new, lm, low + return lm % n + +def square_root_mod_prime(n, p): + if n == 0: + return 0 + if p == 2: + return n # We should never get here but it might be useful + if legendre(n, p) != 1: + raise ValueError("No square root") + # Optimizations + if p % 4 == 3: + return pow(n, (p + 1) // 4, p) + # 1. By factoring out powers of 2, find Q and S such that p - 1 = + # Q * 2 ** S with Q odd + q = p - 1 + s = 0 + while q % 2 == 0: + q //= 2 + s += 1 + # 2. Search for z in Z/pZ which is a quadratic non-residue + z = 1 + while legendre(z, p) != -1: + z += 1 + m, c, t, r = s, pow(z, q, p), pow(n, q, p), pow(n, (q + 1) // 2, p) + while True: + if t == 0: + return 0 + elif t == 1: + return r + # Use repeated squaring to find the least i, 0 < i < M, such + # that t ** (2 ** i) = 1 + t_sq = t + i = 0 + for i in range(1, m): + t_sq = t_sq * t_sq % p + if t_sq == 1: + break + else: + raise ValueError("Should never get here") + # Let b = c ** (2 ** (m - i - 1)) + b = pow(c, 2 ** (m - i - 1), p) + m = i + c = b * b % p + t = t * b * b % p + r = r * b % p + return r + +# name: (nid, p, n, a, b, (Gx, Gy)), +CURVES = { + "secp128r1": ( + 706, + 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF, + 0xFFFFFFFE0000000075A30D1B9038A115, + 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFC, + 0xE87579C11079F43DD824993C2CEE5ED3, + ( + 0x161FF7528B899B2D0C28607CA52C5B86, + 0xCF5AC8395BAFEB13C02DA292DDED7A83 + ) + ), + "secp224r1": ( + 713, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000001, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFE, + 0xB4050A850C04B3ABF54132565044B0B7D7BFD8BA270B39432355FFB4, + ( + 0xB70E0CBD6BB4BF7F321390B94A03C1D356C21122343280D6115C1D21, + 0xBD376388B5F723FB4C22DFE6CD4375A05A07476444D5819985007E34 + ) + ), +} + +def get_curve(name): + if name not in CURVES: + raise ValueError("Unknown curve {}".format(name)) + nid, p, n, a, b, g = CURVES[name] + params = {"p": p, "n": n, "a": a, "b": b, "g": g} + return EllipticCurve(nid, p, n, a, b, g) + +class EllipticCurve: + def __init__(self, nid, p, n, a, b, g): + self.p, self.n, self.a, self.b, self.g = p, n, a, b, g + self.jacobian = JacobianCurve(self.p, self.n, self.a, self.b, self.g) + self.public_key_length = (len(bin(p).replace("0b", "")) + 7) // 8 + self.order_bitlength = len(bin(n).replace("0b", "")) + + + def _int_to_bytes(self, raw, len=None): + return int_to_bytes(raw, len or self.public_key_length) + + + def _subject_to_int(self, subject): + return bytes_to_int(subject[:(self.order_bitlength + 7) // 8]) + + + def recover(self, signature, data, hash="sha256"): + # Sanity check: is this signature recoverable? + if len(signature) != 1 + 2 * self.public_key_length: + raise ValueError("Cannot recover an unrecoverable signature") + subject = self._digest(data, hash) + z = self._subject_to_int(subject) + + recid = signature[0] - 27 if signature[0] < 31 else signature[0] - 31 + r = bytes_to_int(signature[1:self.public_key_length + 1]) + s = bytes_to_int(signature[self.public_key_length + 1:]) + + # Verify bounds + if not 0 <= recid < 2 * (self.p // self.n + 1): + raise ValueError("Invalid recovery ID") + if r >= self.n: + raise ValueError("r is out of bounds") + if s >= self.n: + raise ValueError("s is out of bounds") + + rinv = inverse(r, self.n) + u1 = (-z * rinv) % self.n + u2 = (s * rinv) % self.n + + # Recover R + rx = r + (recid // 2) * self.n + if rx >= self.p: + raise ValueError("Rx is out of bounds") + + # Almost copied from decompress_point + ry_square = (pow(rx, 3, self.p) + self.a * rx + self.b) % self.p + try: + ry = square_root_mod_prime(ry_square, self.p) + except Exception: + raise ValueError("Invalid recovered public key") from None + + # Ensure the point is correct + if ry % 2 != recid % 2: + # Fix Ry sign + ry = self.p - ry + + x, y = self.jacobian.fast_shamir(self.g, u1, (rx, ry), u2) + x, y = self._int_to_bytes(x), self._int_to_bytes(y) + + is_compressed = signature[0] >= 31 + if is_compressed: + return bytes([0x02 + (y[-1] % 2)]) + x + else: + return bytes([0x04]) + x + y + + def _digest(self, data, hash): + if hash is None: + return data + elif callable(hash): + return hash(data) + elif hash == "sha1": + return hashlib.sha1(data).digest() + elif hash == "sha256": + return hashlib.sha256(data).digest() + elif hash == "sha512": + return hashlib.sha512(data).digest() + else: + raise ValueError("Unknown hash/derivation method") + +####################################################################### + def recover(data, signature, alghash=None): recovered = set() if len(signature) == 32: - curve = sslcrypto.ecc.get_curve("secp128r1") + curve = get_curve("secp128r1") recoverable = False elif len(signature) == 33: - curve = sslcrypto.ecc.get_curve("secp128r1") + curve = get_curve("secp128r1") recoverable = True elif len(signature) == 56: - curve = sslcrypto.ecc.get_curve("secp224r1") + curve = get_curve("secp224r1") recoverable = False elif len(signature) == 57: - curve = sslcrypto.ecc.get_curve("secp224r1") + curve = get_curve("secp224r1") recoverable = True else: print("Unsupported signature size %i" % len(signature))