I might as well publish my notes.
First, about RSA:
- You need three numbers and calculate the rest.
- p and q must be (large) primes. They need to be secret.
- e needs to be chosen so that a certain function (that indirectly depends on p and q) evaluates to 1. e becomes public.
- You calculate n := p * q. n also becomes public. (Obviously it has to be hard to factor large numbers, if p and q need to remain secret.)
- The number of digits in n is the length of the key.
- You calculate d as a function of the other numbers. d remains private.
- Together, n, and e are your public key.
- In addition to the public key, you need d to make a private key.
- To encrypt a (padded) message M, you calculate M ** e (mod n).
- To decrypt a cyphertext C, you calculate C ** d (mod n).
Now, about the RSA implementation Python’s cryptography module (docs are here):
- There are classes RSAPrivateKey and RSAPublicKey, which are what you think they are.
- There are convenience methods to convert them to and from common serialization formats.
- There are companion classes RSAPrivateNumbers and RSAPublicNumbers, which have conversion functions to and from RSAPrivateKey and RSAPublicKey. They are used to access the components of the private and public keys: to get at them, if the Python module generated them, or to initialize the RSAPrivateKey and RSAPublicKey objects with certain numbers.
- Python cryptography (well, probably the underlying OpenSSL) insists that RSAPrivateKey not only contains the necessary values as described above, but also some intermediate values that make calculations faster. The RSAPrivateNumbers object, for example, wants not n and d, as you would expect, but p, q (from which it can calculate n), d, dmp1, dmq1, and iqmp. If you don’t have those, you can calculate those first: how is described in Handling Partial RSA Private Keys.
Took me a bit of digging to figure those out, and the help of Stackoverflow.