├── .gitignore ├── LICENSE ├── README.md ├── rsatool.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Python stuff 2 | *.pyc 3 | *.pyo 4 | 5 | # Setuptools build dir 6 | /build 7 | /dist 8 | *.egg-info 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Joerie de Gram 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Description 2 | ----------- 3 | rsatool calculates RSA (p, q, n, d, e) and RSA-CRT (dP, dQ, qInv) parameters given 4 | either two primes (p, q) or modulus and private exponent (n, d). 5 | 6 | Resulting parameters are displayed and can optionally be written as an OpenSSL compatible DER or PEM encoded RSA private key. 7 | 8 | Requirements 9 | ------------ 10 | 11 | * python v3.7+ 12 | * [pyasn1][1] 13 | * [gmpy2][2] 14 | 15 | Usage examples 16 | -------------- 17 | 18 | Supplying modulus and private exponent, PEM output to key.pem: 19 | 20 | python rsatool.py -f PEM -o key.pem -n 13826123222358393307 -d 9793706120266356337 21 | 22 | Supplying two primes, DER output to key.der: 23 | 24 | python rsatool.py -f DER -o key.der -p 4184799299 -q 3303891593 25 | 26 | [1]: http://pypi.python.org/pypi/pyasn1/ 27 | [2]: http://pypi.python.org/pypi/gmpy2/ 28 | -------------------------------------------------------------------------------- /rsatool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import base64 3 | import argparse 4 | import random 5 | import sys 6 | import textwrap 7 | 8 | import gmpy2 9 | 10 | from pyasn1.codec.der import encoder 11 | from pyasn1.type.univ import Sequence, Integer 12 | 13 | PEM_TEMPLATE = ( 14 | '-----BEGIN RSA PRIVATE KEY-----\n' 15 | '%s\n' 16 | '-----END RSA PRIVATE KEY-----\n' 17 | ) 18 | 19 | DEFAULT_EXP = 65537 20 | 21 | 22 | def factor_modulus(n, d, e): 23 | """ 24 | Efficiently recover non-trivial factors of n 25 | 26 | See: Handbook of Applied Cryptography 27 | 8.2.2 Security of RSA -> (i) Relation to factoring (p.287) 28 | 29 | http://www.cacr.math.uwaterloo.ca/hac/ 30 | """ 31 | t = e * d - 1 32 | s = 0 33 | 34 | if d <= 1 or e <= 1: 35 | raise ValueError("d, e can't be <=1") 36 | 37 | if 17 != gmpy2.powmod(17, e * d, n): 38 | raise ValueError("n, d, e don't match") 39 | 40 | while True: 41 | quotient, remainder = divmod(t, 2) 42 | 43 | if remainder != 0: 44 | break 45 | 46 | s += 1 47 | t = quotient 48 | 49 | found = False 50 | 51 | tries = 0 52 | while not found: 53 | tries += 1 54 | if tries >= 1000: 55 | raise ValueError("Factorization/d: no success after 1000 tries") 56 | i = 1 57 | a = random.randint(1, n - 1) 58 | 59 | while i <= s and not found: 60 | c1 = pow(a, pow(2, i - 1, n) * t, n) 61 | c2 = pow(a, pow(2, i, n) * t, n) 62 | 63 | found = c1 != 1 and c1 != (-1 % n) and c2 == 1 64 | 65 | i += 1 66 | 67 | p = gmpy2.gcd(c1 - 1, n) 68 | q = n // p 69 | 70 | return p, q 71 | 72 | 73 | def factor_dp(n, dp, e): 74 | # algorithm from https://eprint.iacr.org/2020/1506.pdf page 9 75 | p = 1 76 | v = 2 77 | while p == 1: 78 | a = gmpy2.mpz(v) 79 | t = gmpy2.powmod(a, e * dp - 1, n) - 1 80 | p = gmpy2.gcd(t, n) 81 | v += 1 82 | if v > 100: 83 | raise ValueError("Factorization/dp: no success after 100 tries") 84 | q = n // p 85 | if p * q != n: 86 | raise ValueError("Factorization with dp failed") 87 | return p, q 88 | 89 | 90 | class RSA: 91 | def __init__(self, p=None, q=None, n=None, d=None, dp=None, e=DEFAULT_EXP): 92 | """ 93 | Initialize RSA instance using primes (p, q) 94 | or modulus and private exponent (n, d) 95 | """ 96 | 97 | self.e = e 98 | 99 | if p and q: 100 | assert gmpy2.is_prime(p), 'p is not prime' 101 | assert gmpy2.is_prime(q), 'q is not prime' 102 | 103 | self.p = p 104 | self.q = q 105 | elif n and d: 106 | self.p, self.q = factor_modulus(n, d, e) 107 | elif n and dp: 108 | self.p, self.q = factor_dp(n, dp, e) 109 | else: 110 | raise ValueError('Either (p, q) or (n, d) must be provided') 111 | 112 | self._calc_values() 113 | 114 | def _calc_values(self): 115 | self.n = self.p * self.q 116 | 117 | if self.p != self.q: 118 | phi = (self.p - 1) * (self.q - 1) 119 | else: 120 | phi = (self.p ** 2) - self.p 121 | 122 | self.d = gmpy2.invert(self.e, phi) 123 | 124 | # CRT-RSA precomputation 125 | self.dP = self.d % (self.p - 1) 126 | self.dQ = self.d % (self.q - 1) 127 | self.qInv = gmpy2.invert(self.q, self.p) 128 | 129 | def to_pem(self): 130 | """ 131 | Return OpenSSL-compatible PEM encoded key 132 | """ 133 | b64 = base64.b64encode(self.to_der()).decode() 134 | b64w = "\n".join(textwrap.wrap(b64, 64)) 135 | return (PEM_TEMPLATE % b64w).encode() 136 | 137 | def to_der(self): 138 | """ 139 | Return parameters as OpenSSL compatible DER encoded key 140 | """ 141 | seq = Sequence() 142 | 143 | for idx, x in enumerate( 144 | [0, self.n, self.e, self.d, self.p, self.q, self.dP, self.dQ, self.qInv] 145 | ): 146 | seq.setComponentByPosition(idx, Integer(x)) 147 | 148 | return encoder.encode(seq) 149 | 150 | def dump(self, verbose): 151 | vars = ['n', 'e', 'd', 'p', 'q'] 152 | 153 | if verbose: 154 | vars += ['dP', 'dQ', 'qInv'] 155 | 156 | for v in vars: 157 | self._dumpvar(v) 158 | 159 | def _dumpvar(self, var): 160 | val = getattr(self, var) 161 | 162 | def parts(s, n): 163 | return '\n'.join([s[i:i + n] for i in range(0, len(s), n)]) 164 | 165 | if len(str(val)) <= 40: 166 | print('%s = %d (%#x)\n' % (var, val, val)) 167 | else: 168 | print('%s =' % var) 169 | print(parts('%x' % val, 80) + '\n') 170 | 171 | 172 | if __name__ == '__main__': 173 | parser = argparse.ArgumentParser() 174 | 175 | parser.add_argument('-n', type=lambda x: int(x, 0), 176 | help='modulus. format : int or 0xhex') 177 | parser.add_argument('-p', type=lambda x: int(x, 0), 178 | help='first prime number. format : int or 0xhex') 179 | parser.add_argument('-q', type=lambda x: int(x, 0), 180 | help='second prime number. format : int or 0xhex') 181 | parser.add_argument('-d', type=lambda x: int(x, 0), 182 | help='private exponent. format : int or 0xhex') 183 | parser.add_argument('-e', type=lambda x: int(x, 0), 184 | help='public exponent (default: %d). format : int or 0xhex' % 185 | DEFAULT_EXP, default=DEFAULT_EXP) 186 | parser.add_argument('--dp', type=lambda x: int(x, 0), 187 | help='d (mod p-1) or d (mod q-1) : int or 0xhex') 188 | parser.add_argument('-o', '--output', help='output filename') 189 | parser.add_argument('-f', '--format', choices=['DER', 'PEM'], default='PEM', 190 | help='output format (DER, PEM) (default: PEM)') 191 | parser.add_argument('-v', '--verbose', action='store_true', default=False, 192 | help='also display CRT-RSA representation') 193 | 194 | args = parser.parse_args() 195 | 196 | if args.p and args.q: 197 | print('Using (p, q) to calculate RSA paramaters\n') 198 | rsa = RSA(p=args.p, q=args.q, e=args.e) 199 | elif args.n and args.d: 200 | print('Using (n, d) to calculate RSA parameters\n') 201 | rsa = RSA(n=args.n, d=args.d, e=args.e) 202 | elif args.n and args.dp: 203 | print('Using (n, dp) to calculate RSA parameters\n') 204 | rsa = RSA(n=args.n, dp=args.dp, e=args.e) 205 | else: 206 | parser.print_help() 207 | parser.error('Either (p, q), (n, d) or (n, dp) needs to be specified') 208 | 209 | if args.format == 'DER' and not args.output: 210 | parser.error('Output filename (-o) required for DER output') 211 | 212 | rsa.dump(args.verbose) 213 | 214 | if args.format == 'PEM': 215 | data = rsa.to_pem() 216 | elif args.format == 'DER': 217 | data = rsa.to_der() 218 | 219 | if args.output: 220 | print('Saving %s as %s' % (args.format, args.output)) 221 | 222 | fp = open(args.output, 'wb') 223 | fp.write(data) 224 | fp.close() 225 | else: 226 | sys.stdout.buffer.write(data) 227 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from setuptools import setup 3 | 4 | setup( 5 | name='rsatool', 6 | version='1.0', 7 | description='rsatool calculates RSA and RSA-CRT parameters', 8 | author='Joerie de Gram', 9 | author_email='j.de.gram@gmail.com', 10 | url='https://github.com/ius/rsatool', 11 | install_requires=['gmpy2', 'pyasn1'], 12 | scripts=['rsatool.py'] 13 | ) 14 | --------------------------------------------------------------------------------