DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

100 Languages Speedrun: Episode 30: SageMath

There are two kinds of "mathematical" computations - numerical and symbolic.

Numerical ones (like Octave, Julia, NumPy etc.) take a lot of numbers, often whole vectors and matrices of them, and do some numerical computation.

Symbolic ones (like Z3, SageMath) take abstract mathematical expressions, and do transformations on them. In practice most systems are somewhat mixed, but it's still an useful distinction.

SageMath is a fairly weird language is that it's 99% Python, and it runs on Python engine, but it's not quite completely Python, and they introduced a bunch of "pre-parsing" changes to make it more math-friendly. It's sort of like relation between React's JSX or Babel and plain JavaScript.

There's no real point bothering with Hello World, FizzBuzzes, and such, as you can run Python code on SageMath, and they will generally just work unless you just so happen to hit one of the differences.

Syntax differences

You'd most commonly run SageMath from a Jupyter Notebook or similar, but you can run it from command line as well.

Two most obvious syntax differences are / and ^, but even plain numbers are not quite the same:

#!/usr/bin/env sage

# XOR in Python, ** in SageMath
print(3^4)

# Floats in Python, Rationals in SageMath
print(2/3)

# All numbers are wrapped in special classes
print(type(2))
print(type(2/3))
Enter fullscreen mode Exit fullscreen mode

Let's try to run it:

$ sage syntax.sage
81
2/3
<class 'sage.rings.integer.Integer'>
<class 'sage.rings.rational.Rational'>
$ python3 syntax.sage
7
0.6666666666666666
<class 'int'>
<class 'float'>
Enter fullscreen mode Exit fullscreen mode

But we can even see what Sage converted the code into (I adjusted spacing a little):

# This file was *autogenerated* from the file syntax.sage
from sage.all_cmdline import * # import sage library

_sage_const_3 = Integer(3)
_sage_const_4 = Integer(4)
_sage_const_2 = Integer(2)

#!/usr/bin/env sage

# XOR in Python, ** in SageMath
print(_sage_const_3 ** _sage_const_4)

# Floats in Python, Rationals in SageMath
print(_sage_const_2 / _sage_const_3)

# All numbers are wrapped in special classes
print(type(_sage_const_2))
print(type(_sage_const_2 / _sage_const_3))
Enter fullscreen mode Exit fullscreen mode

Limits

Let's start with some high school stuff, like limits:

#!/usr/bin/env sage

print("limit of sin(x) / x as x approaches 0")
print(limit(sin(x) / x, x=0))

print("limit of sin(x) / x as x approaches infinity")
print(limit(sin(x) / x, x=oo))

print("limit of (1+1/x)^x as x approaches infinity")
print(limit((1+1/x)^x, x=oo))
Enter fullscreen mode Exit fullscreen mode

When we run it:

$ ./limits.sage
limit of sin(x) / x as x approaches 0
1
limit of sin(x) / x as x approaches infinity
0
limit of (1+1/x)^x as x approaches infinity
e
Enter fullscreen mode Exit fullscreen mode

In sage a lot of mathematical symbols are pre-defined. x, sin, oo are all symbolic, not what you'd expect in plain Python.

The oo alias for Infinity is just two os, not the Unicode infinite character. It's probably best for ease of typing, but it's somewhat annoying that Python doesn't support ∞ in variable names, as it's technically not a "letter". Raku has ∞ out of the box, and Julia doesn't by default, but you can do ∞ = inf in Julia.

Differentiation

We can do some simple differentiation:

#!/usr/bin/env sage

print("diff of (x+2)*(x-2):")
print(diff((x+2)*(x-2), x))

print("diff of cos(x^3) / x^3:")
print(diff(cos(x^3) / x^3, x))
Enter fullscreen mode Exit fullscreen mode

Which outputs:

./diff.sage
diff of (x+2)*(x-2):
2*x
diff of cos(x^3) / x^3:
-3*sin(x^3)/x - 3*cos(x^3)/x^4
Enter fullscreen mode Exit fullscreen mode

Integrals

And of course integration. We can also get numerical approximation. Sage takes requested precision in binary, not decimal digits.

#!/usr/bin/env sage

f = 1/x

print("Integral of f(x) = 1/x from 5 to 10:")
print(f.integral(x, 5, 10))

print("Numerical approximation to 100 binary digits:")
print(f.integral(x, 5, 10).n(100))
Enter fullscreen mode Exit fullscreen mode

Which outputs:

$ ./integral.sage
Integral of f(x) = 1/x from 5 to 10:
log(10) - log(5)
Numerical approximation to 100 binary digits:
0.69314718055994530941723212146
Enter fullscreen mode Exit fullscreen mode

GF(p) Finite Fields

SageMath can do all the math on real or complex numbers just fine, but so can so many other systems.

SageMath's more unique when in comes to its strengths with discrete mathematics, which matters a lot when it comes to cryptography, or CTFs.

Let's start with the simplest example of GF(p) (also known as Zp and a few other things) fields - basically integers modulo p, so they wrap around when you add, subtract, or multiply them:

#!/usr/bin/env sage

p = 65537
F = GF(p)

print("Some addition in GF(65537):")
for i in range(10):
  a = F.random_element()
  b = F.random_element()
  print(f"{a} + {b} = {a + b}")

print("Some inverses in GF(65537):")
for i in range(10):
  a = F.random_element()
  b = a^-1
  print(f"{a} * {b} = {a * b}")
Enter fullscreen mode Exit fullscreen mode

Which randomly outputs:

./zp.sage
Some addition in GF(65537):
5993 + 52077 = 58070
34573 + 35447 = 4483
64465 + 9539 = 8467
23830 + 64214 = 22507
33632 + 5984 = 39616
32744 + 55138 = 22345
7174 + 12223 = 19397
12815 + 49721 = 62536
21107 + 51922 = 7492
32436 + 21708 = 54144
Some inverses in GF(65537):
4454 * 21880 = 1
8250 * 31990 = 1
2139 * 17403 = 1
34523 * 59411 = 1
44260 * 56663 = 1
3474 * 34655 = 1
53770 * 11490 = 1
12588 * 61575 = 1
34995 * 24902 = 1
2534 * 14354 = 1
Enter fullscreen mode Exit fullscreen mode

GF(p) addition and multiplication is a field that pretty much every programming language can deal with, as that's just doing whatever operation modulo p.

Things like GF(p) division, inverses, and so on - that's generally not coming out of the box.

Elliptic Curves

SageMath can deal with mathematical structures much more complex than GF(p) fields. One such notable structure are elliptic curves, which are one of the basic building blocks of modern cryptography.

#!/usr/bin/env sage

F = GF(101)
EC = EllipticCurve(F, [2, 3])

for i in range(10):
  a = EC.random_element()
  b = EC.random_element()
  print(f"{a} + {b} = {a + b}")
Enter fullscreen mode Exit fullscreen mode

Which outputs:

./ecc.sage
(84 : 56 : 1) + (41 : 15 : 1) = (40 : 94 : 1)
(41 : 15 : 1) + (50 : 41 : 1) = (57 : 51 : 1)
(0 : 1 : 0) + (90 : 93 : 1) = (90 : 93 : 1)
(27 : 67 : 1) + (56 : 30 : 1) = (35 : 86 : 1)
(50 : 41 : 1) + (11 : 89 : 1) = (20 : 93 : 1)
(30 : 55 : 1) + (30 : 55 : 1) = (35 : 15 : 1)
(9 : 12 : 1) + (92 : 93 : 1) = (96 : 26 : 1)
(49 : 40 : 1) + (40 : 94 : 1) = (48 : 55 : 1)
(81 : 89 : 1) + (35 : 15 : 1) = (21 : 69 : 1)
(99 : 71 : 1) + (0 : 1 : 0) = (99 : 71 : 1)
Enter fullscreen mode Exit fullscreen mode

Breaking Diffie-Hellman Key Exchange

I mostly use SageMath for CTFs, and this example is adapted from one of such challenges, with numbers changed.

It's a two step attack, but first part happened in challenge's backstory.

The first step is that we hacked parameter negotiations between Alice and Bob to make each of them think that the other side only supports using '90s "export grade" encryption with very short keys, which was strong enough to withstand casual attackers, but not really against NSA and such. Thanks to SageMath, we can do what NSA could back in the '90s, and break such relatively short keys.

#!/usr/bin/env sage

# "Export Grade" Diffie-Hellman Key Exchange
K = GF(57702167561280060733)
g = K(2)

# We don't know these numbers, CTF server does
secret_a = 21994334292003664313
secret_b = 5307079022839176516

# What we get from the challenge is these:
# Alice sent to Bob: g^a
# Bob sent to Alice: g^a
ga = K(20214349094702392183)
gb = K(36652172046887073403)

# This is fast only because SageMath knows all the fancy algorithms
# It would be completely non-viable to brute force
# and extremely tedious and slow to code yourself:
a = discrete_log(ga, g)
gab = gb ^ a
print(f"We hacked the key: {gab}")

# CTF server can verify our solution:
print(f"Alice shared key: {gb^secret_a}")
print(f"Bob shared key: {ga^secret_b}")
Enter fullscreen mode Exit fullscreen mode

Which outputs:

$ ./mitm.sage
We hacked the key: 44661687795688390997
Alice shared key: 44661687795688390997
Bob shared key: 44661687795688390997
Enter fullscreen mode Exit fullscreen mode

Should you use SageMath?

You already know the syntax, as it's 99% Python, so it has really friendly learning curve.

For numerical mathematics there's so many alternatives, including so many Python-based solutions, I don't think what SageMath is doing is all that special.

For symbolic mathematics, especially anything cryptography-related, it's really good, and I don't think anything else really matches its capabilities.

There are some pure-Python symbolic mathematics libraries like SymPy, but from a quick glance they seem to lack most of the discrete mathematics and crypto features. And of course there's the tradeoff - going 100% Python and dropping the SageMath pre-parsing means considerably more verbose code in some cases.

Overall, SageMath is a useful tool for anyone interested in CTFs, or understanding how modern cryptography and cryptocurrency works. It's really amazing in that limited niche.

If you're not interested in cryptography, and you just have regular mathematical needs, SageMath could work for you, but you can generally stick to the usual Python libraries like NumPy and Pandas, or use a numerical mathematics language like Julia.

Code

All code examples for the series will be in this repository.

Code for the SageMath episode is available here.

Top comments (0)