Exploration of formulas in open-source ECC libraries¶
This notebook explores formulas found in open-source ECC libraries.
[ ]:
import itertools
import numpy as np
import inspect
import tempfile
import sys
import multiprocessing
from copy import deepcopy
from concurrent.futures import ProcessPoolExecutor, as_completed
from contextlib import contextmanager
from importlib import import_module, invalidate_caches
from pathlib import Path
from matplotlib import pyplot as plt
from tqdm.notebook import tqdm
from importlib_resources import files
import pyecsca.ec
from pyecsca.ec.model import ShortWeierstrassModel, TwistedEdwardsModel, MontgomeryModel
from pyecsca.ec.formula import AdditionEFDFormula, DoublingEFDFormula, LadderEFDFormula
from pyecsca.ec.params import get_params
from pyecsca.ec.formula.metrics import formula_similarity, formula_similarity_fuzz
# Allow to use "spawn" multiprocessing method for function defined in a Jupyter notebook.
# https://neuromancer.sk/article/35
@contextmanager
def enable_spawn(func):
invalidate_caches()
source = inspect.getsource(func)
with tempfile.NamedTemporaryFile(suffix=".py", mode="w") as f:
f.write(source)
f.flush()
path = Path(f.name)
directory = str(path.parent)
sys.path.append(directory)
module = import_module(str(path.stem))
yield getattr(module, func.__name__)
sys.path.remove(directory)
[ ]:
sw = ShortWeierstrassModel()
mont = MontgomeryModel()
te = TwistedEdwardsModel()
curve25519 = get_params("other", "Curve25519", "xz")
ed25519 = get_params("other", "Ed25519", "extended")
p256_jac3 = get_params("secg", "secp256r1", "jacobian-3")
p256_jac = get_params("secg", "secp256r1", "jacobian")
p256_mod = get_params("secg", "secp256r1", "modified")
p256_proj3 = get_params("secg", "secp256r1", "projective-3")
The formulas¶
The following formulas were collected from open-source cryptographic libraries and are stored in the core package of the pyecsca toolkit. For a full overview of implementation choices done by open-source ECC libraries, see this page.
[ ]:
lib_formula_defs = [
[
"add-bc-r1rv76-jac", #ok
ShortWeierstrassModel,
"jacobian",
("secg", "secp128r1"),
AdditionEFDFormula,
],
[
"add-bc-r1rv76-mod", #ok
ShortWeierstrassModel,
"modified",
("secg", "secp128r1"),
AdditionEFDFormula,
],
[
"dbl-bc-r1rv76-jac", #ok
ShortWeierstrassModel,
"jacobian",
("secg", "secp128r1"),
DoublingEFDFormula,
],
[
"dbl-bc-r1rv76-mod", #ok
ShortWeierstrassModel,
"modified",
("secg", "secp128r1"),
DoublingEFDFormula,
],
[
"dbl-bc-r1rv76-x25519", #ok
MontgomeryModel,
"xz",
("other", "Curve25519"),
DoublingEFDFormula,
],
[
"ladd-bc-r1rv76-x25519", #ok
MontgomeryModel,
"xz",
("other", "Curve25519"),
LadderEFDFormula,
],
[
"dbl-boringssl-p224", #ok
ShortWeierstrassModel,
"jacobian-3",
("secg", "secp224r1"),
DoublingEFDFormula,
],
[
"add-boringssl-p224", #ok
ShortWeierstrassModel,
"jacobian-3",
("secg", "secp224r1"),
AdditionEFDFormula,
],
[
"add-libressl-v382", #ok
ShortWeierstrassModel,
"jacobian",
("secg", "secp128r1"),
AdditionEFDFormula,
],
[
"dbl-libressl-v382", #ok
ShortWeierstrassModel,
"jacobian",
("secg", "secp128r1"),
DoublingEFDFormula,
],
[
"dbl-secp256k1-v040", #ok
ShortWeierstrassModel,
"jacobian",
("secg", "secp256k1"),
DoublingEFDFormula,
],
[
"add-openssl-z256", #ok
ShortWeierstrassModel,
"jacobian-3",
("secg", "secp256r1"),
AdditionEFDFormula,
],
[
"add-openssl-z256a", #ok
ShortWeierstrassModel,
"jacobian-3",
("secg", "secp256r1"),
AdditionEFDFormula,
],
[
"ladd-openssl-x25519", #ok
MontgomeryModel,
"xz",
("other", "Curve25519"),
LadderEFDFormula,
],
[
"ladd-hacl-x25519", #ok
MontgomeryModel,
"xz",
("other", "Curve25519"),
LadderEFDFormula,
],
[
"dbl-hacl-x25519", #ok
MontgomeryModel,
"xz",
("other", "Curve25519"),
DoublingEFDFormula,
],
[
"dbl-sunec-v21", #ok
ShortWeierstrassModel,
"projective-3",
("secg", "secp256r1"),
DoublingEFDFormula,
],
[
"add-sunec-v21", #ok
ShortWeierstrassModel,
"projective-3",
("secg", "secp256r1"),
AdditionEFDFormula,
],
[
"add-sunec-v21-ed25519", #ok
TwistedEdwardsModel,
"extended-1",
("other", "Ed25519"),
AdditionEFDFormula,
],
[
"dbl-sunec-v21-ed25519", #ok
TwistedEdwardsModel,
"extended-1",
("other", "Ed25519"),
DoublingEFDFormula,
],
[
"ladd-rfc7748", #ok
MontgomeryModel,
"xz",
("other", "Curve25519"),
LadderEFDFormula,
],
[
"add-bearssl-v06", #ok
ShortWeierstrassModel,
"jacobian",
("secg", "secp256r1"),
AdditionEFDFormula,
],
[
"dbl-bearssl-v06", #ok
ShortWeierstrassModel,
"jacobian",
("secg", "secp256r1"),
DoublingEFDFormula,
],
[
"add-libgcrypt-v1102", #ok
ShortWeierstrassModel,
"jacobian",
("secg", "secp256r1"),
AdditionEFDFormula,
],
[
"dbl-libgcrypt-v1102", #ok
ShortWeierstrassModel,
"jacobian",
("secg", "secp256r1"),
DoublingEFDFormula,
],
[
"ladd-go-1214", #ok
MontgomeryModel,
"xz",
("other", "Curve25519"),
LadderEFDFormula,
],
[
"add-gecc-322", #ok
ShortWeierstrassModel,
"jacobian-3",
("secg", "secp256r1"),
AdditionEFDFormula,
],
[
"dbl-gecc-321", #ok
ShortWeierstrassModel,
"jacobian-3",
("secg", "secp256r1"),
DoublingEFDFormula,
],
[
"ladd-boringssl-x25519", #ok
MontgomeryModel,
"xz",
("other", "Curve25519"),
LadderEFDFormula,
],
[
"dbl-ipp-x25519", #ok
MontgomeryModel,
"xz",
("other", "Curve25519"),
DoublingEFDFormula,
],
[
"ladd-botan-x25519", #ok
MontgomeryModel,
"xz",
("other", "Curve25519"),
LadderEFDFormula,
],
]
Let’s load the formulas now.
[ ]:
lib_formulas = {}
base_path = files(pyecsca.ec).joinpath("data", "formulas")
for formula_def in lib_formula_defs:
meta_path = base_path / formula_def[0]
op3_path = base_path / (formula_def[0] + ".op3")
model = formula_def[1]()
formula = formula_def[4](meta_path, op3_path, formula_def[0], model.coordinates[formula_def[2]]).to_code()
lib_formulas[formula_def[0]] = formula
[ ]:
len(lib_formulas)
Now we can set up some code for examining the similarities between the formulas. This is done in two ways:
Measure similarity between sets of intermediate polynomials of the formulas (like in the pyecsca paper),
Measure similarity between sets of intermediate values of the formulas over random points on some curve (the fuzz approach).
[ ]:
def compute_similarities(formulas, curve):
table = [["One", "Other", "Similarity (output)", "Similarity (IV)"]]
im_iv = np.zeros((len(formulas), len(formulas)))
im_out = np.zeros((len(formulas), len(formulas)))
for formula in formulas:
if formula.assumptions:
for assumption_str in formula.assumptions_str:
lhs, rhs = assumption_str.strip().split(" == ")
if lhs in formula.inputs:
print(f"Warning, formula {formula.name} has assumptions: {assumption_str}")
for one, other in itertools.product(formulas, formulas):
i = formulas.index(one)
j = formulas.index(other)
if curve is None:
sim = formula_similarity(one, other)
else:
sim = formula_similarity_fuzz(one, other, curve, 100)
im_iv[i, j] = sim["ivs"]
im_out[i, j] = sim["output"]
table.append([one.name, other.name, f"{sim['output']:.2}", f"{sim['ivs']:.2}"])
return table, im_iv, im_out
def plot_similarities(formulas, im_data, name):
fig, ax = plt.subplots()
im = ax.imshow(im_data, vmin=0)
cbar_ax = fig.add_axes((0.90, 0.11, 0.04, 0.76))
cbar = fig.colorbar(im, cax=cbar_ax)
cbar.ax.set_ylabel(f"Similarity ({name})", rotation=-90, va="bottom")
ax.set_xticks(np.arange(len(formulas)), labels=list(map(lambda f: f.name, formulas)), rotation=90)
ax.set_yticks(np.arange(len(formulas)), labels=list(map(lambda f: f.name, formulas)), rotation=0)
for i, one in enumerate(formulas):
for j, other in enumerate(formulas):
ax.text(j, i, f"{im_data[i, j]:.2}", ha="center", va="center", color="white")
plt.show()
def analyze_formulas(formulas, curve=None):
table, im_iv, im_out = compute_similarities(formulas, curve)
plot_similarities(formulas, im_iv, "IV")
plot_similarities(formulas, im_out, "output")
Analysis¶
Let’s now go through the library formulas grouped by their type and coordinate model.
[ ]:
xz_ladders = [formula for formula in mont.coordinates["xz"].formulas.values() if formula.name.startswith("ladd") or formula.name.startswith("mladd")] + [
lib_formulas["ladd-rfc7748"], lib_formulas["ladd-hacl-x25519"], lib_formulas["ladd-openssl-x25519"], lib_formulas["ladd-bc-r1rv76-x25519"],
lib_formulas["ladd-go-1214"], lib_formulas["ladd-boringssl-x25519"], lib_formulas["ladd-botan-x25519"]]
analyze_formulas(xz_ladders)
analyze_formulas(xz_ladders, curve25519.curve)
[ ]:
xz_dbls = [formula for formula in mont.coordinates["xz"].formulas.values() if formula.name.startswith("dbl")] + [
lib_formulas["dbl-bc-r1rv76-x25519"], lib_formulas["dbl-hacl-x25519"], lib_formulas["dbl-ipp-x25519"]]
analyze_formulas(xz_dbls)
analyze_formulas(xz_dbls, curve25519.curve)
[ ]:
jac3_adds = [formula for formula in sw.coordinates["jacobian-3"].formulas.values() if formula.name.startswith("add") or formula.name.startswith("madd")] + [
lib_formulas["add-boringssl-p224"], lib_formulas["add-openssl-z256"], lib_formulas["add-openssl-z256a"],
lib_formulas["add-gecc-322"]]
jac3_adds_fixed = []
for formula in jac3_adds:
if formula.coordinate_model != sw.coordinates["jacobian-3"]:
formula = deepcopy(formula)
formula.coordinate_model = sw.coordinates["jacobian-3"]
jac3_adds_fixed.append(formula)
analyze_formulas(jac3_adds_fixed)
analyze_formulas(jac3_adds_fixed, p256_jac3.curve)
[ ]:
jac3_dbls = [formula for formula in sw.coordinates["jacobian-3"].formulas.values() if formula.name.startswith("dbl")] + [
lib_formulas["dbl-boringssl-p224"], lib_formulas["dbl-gecc-321"]]
jac3_dbls_fixed = []
for formula in jac3_dbls:
if formula.coordinate_model != sw.coordinates["jacobian-3"]:
formula = deepcopy(formula)
formula.coordinate_model = sw.coordinates["jacobian-3"]
jac3_dbls_fixed.append(formula)
analyze_formulas(jac3_dbls_fixed)
analyze_formulas(jac3_dbls_fixed, p256_jac3.curve)
[ ]:
mod_adds = [formula for formula in sw.coordinates["modified"].formulas.values() if formula.name.startswith("add") or formula.name.startswith("madd")] + [
lib_formulas["add-bc-r1rv76-mod"]]
analyze_formulas(mod_adds)
analyze_formulas(mod_adds, p256_mod.curve)
[ ]:
mod_dbls = [formula for formula in sw.coordinates["modified"].formulas.values() if formula.name.startswith("dbl")] + [
lib_formulas["dbl-bc-r1rv76-mod"]]
analyze_formulas(mod_dbls)
analyze_formulas(mod_dbls, p256_mod.curve)
[ ]:
jac_adds = [formula for formula in sw.coordinates["jacobian"].formulas.values() if formula.name.startswith("add")] + [
lib_formulas["add-bc-r1rv76-jac"], lib_formulas["add-libressl-v382"], lib_formulas["add-bearssl-v06"], lib_formulas["add-libgcrypt-v1102"]]
analyze_formulas(jac_adds)
analyze_formulas(jac_adds, p256_jac.curve)
[ ]:
jac_dbls = [formula for formula in sw.coordinates["jacobian"].formulas.values() if formula.name.startswith("dbl")] + [
lib_formulas["dbl-secp256k1-v040"], lib_formulas["dbl-libressl-v382"], lib_formulas["dbl-bearssl-v06"], lib_formulas["dbl-libgcrypt-v1102"]]
analyze_formulas(jac_dbls)
analyze_formulas(jac_dbls, p256_jac.curve)
[ ]:
proj3_adds = [formula for formula in sw.coordinates["projective-3"].formulas.values() if formula.name.startswith("add") or formula.name.startswith("madd")] + [
lib_formulas["add-sunec-v21"]]
analyze_formulas(proj3_adds)
analyze_formulas(proj3_adds, p256_proj3.curve)
[ ]:
proj3_dbls = [formula for formula in sw.coordinates["projective-3"].formulas.values() if formula.name.startswith("dbl")] + [
lib_formulas["dbl-sunec-v21"]]
analyze_formulas(proj3_dbls)
analyze_formulas(proj3_dbls, p256_proj3.curve)
[ ]:
ext_adds = [formula for formula in te.coordinates["extended-1"].formulas.values() if formula.name.startswith("add") or formula.name.startswith("madd")] + [
lib_formulas["add-sunec-v21-ed25519"]]
analyze_formulas(ext_adds)
[ ]:
ext_dbls = [formula for formula in te.coordinates["extended-1"].formulas.values() if formula.name.startswith("dbl")] + [
lib_formulas["dbl-sunec-v21-ed25519"]]
analyze_formulas(ext_dbls)
Expand¶
We can also expand the set of formulas by applying various transformations (like swapping the order of commutative operations).
This is a resource intensive cell that uses parallelism. Set the num_workers
variable to something reasonable, like the number of cores of your machine minus two.
[ ]:
# 22 is really a maximum usable number here, as there are only 22 "jobs".
num_workers = 22
[ ]:
def expand_out(formulas, name):
from pyecsca.ec.formula.expand import expand_formula_set
import pickle
expanded = expand_formula_set(formulas)
with open(name, "wb") as f:
pickle.dump(expanded, f)
return name, len(expanded)
context = multiprocessing.get_context("spawn")
with ProcessPoolExecutor(max_workers=num_workers, mp_context=context) as pool, enable_spawn(expand_out) as expand_func:
futures = []
args = []
for coord_name, coords in tqdm(sw.coordinates.items(), desc="Submitting"):
adds = [formula for formula in coords.formulas.values() if formula.name.startswith("add")]
lib_adds = [formula for formula in lib_formulas.values() if formula.coordinate_model == coords and formula.name.startswith("add")]
dbls = [formula for formula in coords.formulas.values() if formula.name.startswith("dbl")]
lib_dbls = [formula for formula in lib_formulas.values() if formula.coordinate_model == coords and formula.name.startswith("dbl")]
futures.append(pool.submit(expand_func, adds + lib_adds, f"sw_{coord_name}_adds.pickle"))
args.append(f"sw_{coord_name}_adds.pickle")
futures.append(pool.submit(expand_func, dbls + lib_dbls, f"sw_{coord_name}_dbls.pickle"))
args.append(f"sw_{coord_name}_dbls.pickle")
for future in tqdm(as_completed(futures), total=len(futures), smoothing=0, desc="Computing"):
j = futures.index(future)
arg = args[j]
error = future.exception()
if error:
print(arg, error)
else:
res = future.result()
print(*res)
[ ]: