For Speed, Python is Not The Best

To me, if you are into cybersecurity, you should not be locked into a specific programming language or environment for analysis, testing…

For Speed, Python is Not The Best

To me, if you are into cybersecurity, you should not be locked into a specific programming language or environment for analysis, testing and tool creation. Basically, you should know all of the common languages and how they work. A great cybersecurity analyst should know Golang, Rust, Python, Node.js, Java, .NET/C# and .NET/Powershell, and should be continually prototyping in these languages.

Speed comparisons

The measurement of how fast something is, it is often relative to what you are measuring. Two cars might have a top speed that ranges from 100mph to 200mph, while two people cycling may have a top speed of between 10 mph and 30 mph. While the measurement of speed is absolute, the comparison is relative.

As you may know, Python is the most popular programming language, but it has four major weaknesses:

  • It is interpreted and thus much slower than a compiled language (such as C++, Golang and Rust).
  • As it is a script, it cannot be fully checked that it will not create an exception before it is run.
  • It has weak version control for library integration.
  • It can break easily.

Overall, I tend to prototype in Golang as it is a compiled language, and where I can compile to executable code. So let’s put Python head-to-head with Golang on a taxing problem. For this, we will generate RSA keys using safe primes, and where a safe prime is where p and 2p+1 are primes.

And, so the results are given in the following sections, but here is a compilation:

We can see at the more challenging tasks (such as RSA-512 and above) that Golang is running at least 10 times faster, and as the challenge gets tougher, the margin between the two languages increases — and with a nearly 40 times improvement in speed for Golang and Python for RSA-1024. The RSA-2048 challenge takes a rather long time, so entered as N/A.

RSA key generation with safe primes in Python

First, we start with the Python code [1][here]:

import gmpy2
import Crypto.Util.number as number
import sys
from timeit import default_timer
import gmpy2
import Crypto.Util.number as number
import sys
from timeit import default_timer

def probable_safe_prime(bits, randfunc=None):
r = gmpy2.mpz(number.getRandomNBitInteger(bits - 1, randfunc))
q = gmpy2.next_prime(r)
p = 2 * q + 1
if gmpy2.is_prime(p):
return p

def gen_safe_prime(bits, randfunc=None):
p = probable_safe_prime(num_bits)
while p is None:
p = probable_safe_prime(num_bits)
return p

def encode_int(n):
v = "%x" % n
if len(v) % 2 == 0:
return v
return "0" + v

while True:
num_bits = int(sys.argv[1])

start = default_timer()
p = gen_safe_prime(num_bits)
q = gen_safe_prime(num_bits)
while p == q:
q = gen_safe_prime(num_bits)
N = p * q
if N.bit_length() == (num_bits * 2):
phi = (p - 1) * (q - 1)
e = 65537
d = gmpy2.invert(e, phi)
duration = default_timer() - start


assert((e * d) % phi == 1)


break

print(f"RSA-{num_bits}")
print(f"Duration: {duration} seconds\n")
print(f"p={encode_int(p)}")
print(f"q={encode_int(q)}")
print(f"PHI={encode_int(phi)}")
print(f"N={encode_int(N)}")
print(f"e={encode_int(e)}")
print(f"d={encode_int(d)}")

A sample run for RSA-128 is [here]:

RSA-128
Duration: 0.09402079999999999 seconds
p=89ac9a3a1b3710220766c55d53928b43
q=ee30a7f82151d28511bebe7d94749d5b
PHI=8018a213f9eedc1362c04d5abe94a915c6b6dc2f16ae311152ab313382d46f34
N=8018a213f9eedc1362c04d5abe94a9173e941e61533713b86bd0b50e6adb97d1
e=010001
d=5bd84dade11080bf6d98d05df634052056009ad33081e930b0838a9d927adad9

A sample run for RSA-256 is [here]:

RSA-256
Duration: 0.5379435 seconds
p=c3e42d9cd4200df1f57d9e808efff1d3389f6ebf40512a6285bf66f86ba2b543
q=f674944db4caa0bfce93fda6204c23d05a1b6d1a6124cb083211dae950663133
PHI=bc9678b4bf3ecbdacdf6d97043bf5b065e6304ab5a87c9a2ef06ddb85b336e26aaddacfd6379c9026ad9a2d0fc62b05e46487903bc866e4fdd8891aa02c508e4
N=bc9678b4bf3ecbdacdf6d97043bf5b065e6304ab5a87c9a2ef06ddb85b336e2865366ee7ec6477b42eeb3ef7abaec601d90354dd5dfc63ba9559d38bbecdef59
e=010001
d=0608c689fa08975b43f5df154185b0f2ee383ae104bf8d8b221cd5215e01b2479876095ffe3bda5bb2718e4d239640d0736d443a036145cc56695547dc58d3a5

A run with RSA-512 is [here]:

RSA-512
Duration: 6.9823463 seconds
p=dcc86eab27fa65ebc7b7ea44b925836678d6989d4e5f383654812a3b1c68165a9182d35adc8710d69dc912aa81ad147587c0303e8a7ff610480c20dff502c73f
q=ec05cc961e3d55af7ce370f6e9aa123b9f046f7d22d9dd285c8346d38820ce19d7f64dd5051d587891a9d162f3cb7aa573fc3d38bfa17f18ac619b0dca4223d3
PHI=cb8dc66121e83c9ea297a48090bd645a34d8e3d4810165a1b2be3dee2c62091f709519a60e36e3b3f4335c17df2250eec3c9dd183f8a95b7e378806086502befca89e81f3b842287ee4fb7ccedbbd649cc755e3ff79d574a3565b8d3a73bcfdd25da1e538a06d04358a05200ace3b96ce1ed4f073bbfa0b939c79a4aa780eadc
N=cb8dc66121e83c9ea297a48090bd645a34d8e3d4810165a1b2be3dee2c62091f709519a60e36e3b3f4335c17df2250eec3c9dd183f8a95b7e378806086502bf19358236081bbde2332eb1308908b6bebe450665a68d66ca8e66a29e24bc4b4518f533f836bab39928813360e225c4887dda9bc7e85e115e22e35563866c5d5ed
e=010001
d=6ba60fcae0a6b42064f6736df69de28b7e3551cfd965c6bccc0da62ce655f6632524050b77aaf9a2fabb8ab1a934b9be7bd6aed19b110c7ad296b0b5db725cba14cff7ed44896dc59b7797498e546f505a0b9c0b9245289bbb3f8f8e877baf435238571f70f8877d864a4af3c67b2c5df3e18f90d0bd581f7cfa1d93eb9fd715

A run with RSA-1024 is [here]:

RSA-1024
Duration: 373.3933188 seconds
p=ee38a98945501f0c1688cf7a0bdd42b304a36862cafc4bde664241488b83bbf2f5f230867c4452194259f61d2f965144df5897fa08d09b30b4c838182be7adf0bfb0c692476f77cd19592543840429bea54911ed42d84687e5348b30d5b28213de5d1ed9d2eadb6f56caea1ed13dd84c1859727d5fbe5e27c5f1d43403ec204b
q=c7c0ad21cdc2895c32a0d5e5eaf00f76b4651af94c4f1320dd42d13f6e50cb9b04f68edafbeaf12c1f8cb913c4cf32a248668b8a2c539058c65864c425bc6bbdce3c09d177646f2d4db4c2d97d4c2f6675b462bb90ccb56267f6394c30e4c8466af8bde3c571d8cd87077d780582031a55f3a5f4fd0a7dd0cc8ef208ae792a5b
PHI=b9e157649b2d494847a03a3fb2ea64d92314174920989e1ec4f940fcde3422c95175ad5e66e0976e7d1fc074f39ff7132333822b972b66a525eeb456045cc1b3f9e536189cf8f440bfcd9bf54dfff30f106ca4ee3596a68579cb420b8979c991f360120d49ac50b37b190e508456db7cb5ac4414dad3f0f71f27d7d55982e1dd746134433020a1f5af4e0ec59528c366050d6e4109bcdc682961273e05db4a3311d0822c9bcdd3ef665d9398b404949b78319a32db8cf3a1ab414592ef2c6938e6ff6fd0b0dbf625aea95c105959bdf73cab67289d416c1553e58de622fb3700abe1e157436079b926437dcf5c506f8030704e34405b1028220871cbad497e04
N=b9e157649b2d494847a03a3fb2ea64d92314174920989e1ec4f940fcde3422c95175ad5e66e0976e7d1fc074f39ff7132333822b972b66a525eeb456045cc1b3f9e536189cf8f440bfcd9bf54dfff30f106ca4ee3596a68579cb420b8979c991f360120d49ac50b37b190e508456db7cb5ac4414dad3f0f71f27d7d55982e1df2a5a8aee43334a5df877b4258bf6158fbe15f19d21083b676ce639c5ffafd1c10cb9418e13fd1734c84442c9a86a18829ff0bdb710b11f2b2661e26f40d082e774ec40346fafdd2015b7442d5aaa171c57a8dbd170e667ffa11052632992815af537be14dbbd2df60415e56633104ae69ebd66a69d23ec20b48938085faec8a9
e=010001
d=b423bf873f9fc6ca66a7d06b951ff5763db9a5cb77ec9028afaa03a24c51e878841a7056f18aad1725e9d4a142cd7f2c2bcc2a55c65e084efa90f871f82e5086f4227fa7a0d0e01db4043625a85ee9166b4b6f177015bcf1d413f8c0a78dc09c526983751f3600cbbffb8b54dabc366008cc96fa430c6d15813b22bf9fcce8c9ae94d98a57a4f870da60e28df73d1708d2d2ae4c177cd1799cc88bb85f02a34914f5a68246c855a147ea5e34fa190a50154eb4ea9ebd5e0b0aa5ea2038ec28593dfcc547516f0a8080619f5834cf74fb9aef5dbd0d91ae8688d50a0cc350048101129a48f08219d52650f8273286bd58551931a10fd170ba04297ce6efdc2e65

RSA key generation with safe primes in Golang

Next, we implement with Golang code [here]:

package main
import (
"crypto/rand"
"fmt"
"math"
"math/big"
"os"
"strconv"
"time"
)
func SafePrime(bits int) *big.Int {
var p *big.Int
p1 := new(big.Int)
checks := int(math.Max(float64(bits)/16, 8))
for {
p, _ = rand.Prime(rand.Reader, int(bits)-1)
p1.Set(p)
p.Add(p.Lsh(p, 1), big.NewInt(1))
if p.ProbablyPrime(checks) {
return (p)
}
}
}
func main() {
bits := 512
argCount := len(os.Args[1:])
if argCount > 0 {
bits, _ = strconv.Atoi(os.Args[1])
}
start := time.Now()
p := SafePrime(bits / 2)
q := SafePrime(bits / 2)
n := new(big.Int).Mul(p, q)
pminus1 := new(big.Int).Sub(p, new(big.Int).SetInt64(1))
qminus1 := new(big.Int).Sub(q, new(big.Int).SetInt64(1))
PHI := new(big.Int).Mul(pminus1, qminus1)
e := new(big.Int).SetInt64(65537)
d := new(big.Int).ModInverse(e, PHI)
duration := time.Since(start)
fmt.Printf("Safe RSA-%d\n", bits)
fmt.Printf("Time taken: %s\n", duration)
fmt.Printf("\ne=%s\n", e)
fmt.Printf("\np=%s\n", p)
fmt.Printf("\nq=%s\n", q)
fmt.Printf("\nN=%s\n", n)
fmt.Printf("\nPHI=%s\n", PHI)
fmt.Printf("\nd=%s", d)
}

A sample run for RSA-128 is [here]:

Safe RSA-128
Time taken: 16.2722ms
e=65537
p=15228170064981101087
q=16231964723388571247
N=247183119296535079537558731381808645489
PHI=247183119296535079506098596593438973156
d=216930654278644302193764848584284236081

A sample run for RSA-256 is [here]:

Safe RSA-256
Time taken: 77.5712ms
e=65537
p=338187869335060692897323358711246100727
q=295254891585570801521780714639956263527
N=99851622696078529071258417935755636664002966509406867094396939813399198284129
PHI=99851622696078529071258417935755636663369523748486235599977835740047995919876
d=8827689731008117512838110891859074398089064507052950990528580500752827995785

A sample run for RSA-512 is [here]:

Safe RSA-512
Time taken: 497.8538ms
e=65537
p=94947328152980300043515956368130899237318865867488820648419189052086569271023
q=112047864082509786386114326965076284491013846000148221289405537579042497399323
N=10638645319882591617789773486692785298267824141774066441282046799575824392710040875926529560786456769672135496378031819214599509675075815053992765443717429
PHI=10638645319882591617789773486692785298267824141774066441282046799575824392709833880734294070700027139388802289194303486502731872633137990327361636377047084
d=9815955015456618895221475089900758872102126402321376072110168117606093126062696567345499918537008729293398929235246311630585678414532571480312354701674781

A sample run for RSA-1024 is [here]:

Safe RSA-1024
Time taken: 9.3573516s
e=65537
p=12638668685678692330052666707152582919860619998041179170524101635657958110608678095776402328828482098552124095932555113977001648710950344998082148902087203
q=12556841871217245861520817090119215624983685371497098899193846260059545844887078286599528063817149797932587739750511286755454344860274719920677101478993843
N=158701764148772440369628317476033664148714936502313359161190420045867920150878307861444639482624614848039712050425887879758037091917262513500946185227492492538885845253138261320241702854985706555215905787441175719441068581218044614653297697468239615305823819908024591353026279997284418152132039547483086091129
PHI=158701764148772440369628317476033664148714936502313359161190420045867920150878307861444639482624614848039712050425887879758037091917262513500946185227492467343375288357200069746757905583187161710910536249163106001493172863714089118896915321537846969673927335196188908286625547541290846927067120788232705010084
d=96327227002670535811273705553796223906674572518203810276225547690687397892515498244051548193834392084473987299600094511022704082874968117621406812978385078023896815471581878548854581781186232291656777415131287572415535092324683019677440141835207815534112874662697385945857723967301045209756366599556109870701

Conclusions

And, so, Python is great, but not perfect. For serious production versions of code, the compiled languages are still the most robust and generally much faster. With, Golang we can produce binary code which is matched to the hardware, whereas .NET, Java and Python will generally have to convert the code into the hardware requirement through a run time environment. Generally, .NET is more robust than Python, as is benchmarked to be faster, but they are both much slower than the compiled languages.

For me, I am comfortable in teaching cryptography with Python, but in production, it must be Golang or Rust.