<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://pierrechr.github.io/pierre.c/feed.xml" rel="self" type="application/atom+xml" /><link href="https://pierrechr.github.io/pierre.c/" rel="alternate" type="text/html" /><updated>2026-04-01T18:11:06+00:00</updated><id>https://pierrechr.github.io/pierre.c/feed.xml</id><title type="html">Pierre Chrétien</title><subtitle>Mathematics and Cryptography</subtitle><author><name>Pierre Chrétien</name></author><entry><title type="html">Naive Quadratic Sieve</title><link href="https://pierrechr.github.io/pierre.c/2026/04/01/QS.html" rel="alternate" type="text/html" title="Naive Quadratic Sieve" /><published>2026-04-01T00:00:00+00:00</published><updated>2026-04-01T00:00:00+00:00</updated><id>https://pierrechr.github.io/pierre.c/2026/04/01/QS</id><content type="html" xml:base="https://pierrechr.github.io/pierre.c/2026/04/01/QS.html"><![CDATA[<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>

<p>The Quadratic Sieve is a factorization algorithm surpassed only by the General Number Field Sieve.</p>

<p>Here is a naive implementation in SageMath based on <a href="https://www.cs.virginia.edu/crab/QFS_Simple.pdf">Eric Landquist’s lecture notes</a>.
It is the most basic version possible I could realize, it is actually painfully slow but easy to read and understand. 
Below are some improvements that should be considered as exercices by a student :</p>
<ul>
  <li>Avoid exact arithmetic.</li>
  <li>Stop collecting relations when enough are found.</li>
  <li>Split the sieving interval.</li>
  <li>Implement Gaussian elminiation.</li>
  <li>Implement Tonelli-Shanks algorithm.</li>
</ul>

<pre><code>from tqdm import tqdm

def get_factor_base(n: int, bound: int) -&gt; list[int]:
    """
    Compute a factor base for `n` bounded by `bound`

    Args:
        n : Integer
        bound : Integer
    Returns:
        The list of primes less than or equal to bound subject to n is a square mod p
    """

    primes = Primes()
    return [2] + [p for i in range(1, bound) if legendre_symbol(n, p := primes.unrank(i)) == 1]

def L(n: int)-&gt; float:
    """
    Compute the complexity function L[1/2,1] for `n` in L-notation
    """

    return exp(sqrt(ln(n) * ln(ln(n))))

def smoothness_bound(n: int, scale: float = 0.5) -&gt; int:
    """
    Compute a smoothness bound for `n`, `scale` should me fine tuned to fit your memory constraints.
    """

    return int(pow(L(n), scale))

def interval_bound(n: int, scale: float = 3 * sqrt(2) / 4) -&gt; int:
    """
    Compute an interval size for `n`, `scale` should be fine tuned to fit your memory constraints.
    """

    return int(pow(L(n), scale))

def residues(x: int, p: int) -&gt; list[int]:
    """
    Compute quadratic residues of `x` mod `p`
    """
    s = mod(x, p).sqrt()
    return list(map(int,[s, p-s]))


def matrix_relations(smooth_numbers: list[int], factor_base: list[int]) -&gt; sage.matrix.matrix_mod2_dense.Matrix_mod2_dense:
    """
    Constructs a binary matrix representing factorization relations between a list of smooth numbers and a factor base.

    This function generates a matrix where each row corresponds to a number in the `smooth_numbers` list,
    and each column corresponds to a factor in the `factor_base` list. The entry M[i, j] is set to 1 if the
    factor `factor_base[j]` divides `smooth_numbers[i]`, modulo 2.

    Args:
        smooth_numbers: List of integers whose factorizations are to be analyzed.
        factor_base: List of prime factors serving as the reference factor base.

    Returns:
        The matrix where each row represents exponents mod 2 in the factorization of a number in `smooth_numbers` in terms of the `factor_base`.
    """

    M = matrix(ZZ, len(smooth_numbers), len(factor_base))
    for i, x in enumerate(smooth_numbers):
        factorization = list(factor(x))
        for p, np in factorization:
            M[i, factor_base.index(p)] = np
    return M.change_ring(GF(2))

def candidates(v, smooth_numbers: list[int], xis: list[int], s: int) -&gt; list[int]:
    """
    Computes candidate values (Px, Py) based on a binary vector and given lists of smooth numbers and xis.

    Args:
        v: A binary vector indicating which elements to include in the product.
        smooth_numbers: List of integers representing the smooth numbers.
        xis: List of integers representing the xis values.
        s: The ceiling of square root of n.

    Returns:
        A list containing two integers: [isqrt(Px), Py], where:
        - Px is the product of selected smooth numbers.
        - Py is the product of (xis[i] + s) for selected indices.
        - isqrt(Px) is the integer square root of Px.
    """

    Px , Py = 1, 1
    for i, bit in enumerate(v):
        if bit:
            Px *= smooth_numbers[i]
            Py *= xis[i] + s
    return [isqrt(Px), Py]

def check_candidates(test_x: int, test_y: int, n: int) -&gt; list[int]:
    """
    Checks if the candidate values (test_x, test_y) yield a non-trivial factorization of n.

    This function verifies whether the difference between `test_x` and `test_y` shares a common factor with `n`.
    If a non-trivial factorization of `n` is found, it returns the factors; otherwise, it returns an empty list.

    Args:
        test_x: An integer candidate value.
        test_y: An integer candidate value.
        n: The integer to be factored.

    Returns:
        A list containing two non-trivial factors of `n` if found, otherwise an empty list.
        The factors satisfy: n = f1 * f2, where neither f1 nor f2 is 1.
    """

    if test_x % n not in [test_y % n, -test_y % n]:
        f1 = gcd(test_x-test_y, n)
        f2 = n // f1
        if n == f1 * f2 and (1 not in [f1, f2]):
            return [f1, f2]
    return []

def sieve(factor_base, M, n, s) -&gt; list[int]:
    """
    Performs a quadratic sieve to find smooth values of the form |(x + s)^2 - n| for x in [-M, M].

    This function computes the values of |(x + s)^2 - n| for each x in the range [-M, M] and then
    removes all prime factors in the `factor_base` from these values. The result is a list of partially
    factored (smooth) values, which are candidates for further factorization of `n`.

    Args:
        factor_base: A list of prime numbers used as the factor base for sieving.
        M: The range parameter, defining the interval [-M, M] for x values.
        n: The integer to be factored.
        s: The offset used in the quadratic expression (x + s)^2 - n.

    Returns:
        A list of integers representing the partially factored (smooth) values of |(x + s)^2 - n|
        for x in [-M, M], after removing all factors in the `factor_base`.
    """

    Qxis = [abs(pow(x + s, 2) - n)  for x in range(-M, M + 1)]
    for p in factor_base:
        for r in set(residues(n, p)):
            a = (r - s) % p
            while a &lt; M + 1:
                Qxis[M  + a] //=  p
                a += p
            a = (r - s) % p - p
            while a &gt; - M :
                Qxis[M + a] //=  p
                a -= p
    return Qxis

def quadratic_sieve(n: int) -&gt; list[int]:
        B = smoothness_bound(n)
        factor_base = get_factor_base(n, B)
        M = interval_bound(n)
        s = isqrt(n) + 1

        Qxis = sieve(factor_base, M, n, s)
        xis = [- M + i for i, Qx in enumerate(Qxis) if abs(Qx) == 1]
        smooth_numbers = [abs(pow(x + s, 2) - n) for x in xis]

        relations =  matrix_relations(smooth_numbers, factor_base)
        ker = relations.kernel()

        for v in ker.basis():
            test_x, test_y = candidates(v, smooth_numbers, xis, s)
            if f := check_candidates(test_x, test_y, n):
                return f

if __name__ == "__main__":
    count = 0
    BENCH_SIZE = 10**1
    BIT_SIZE = 25
    results = f""
    for _ in tqdm(range(BENCH_SIZE)):
        p = random_prime(2**BIT_SIZE, False, 2**(BIT_SIZE-1))
        q = random_prime(2**BIT_SIZE, False, 2**(BIT_SIZE-1))
        n = p * q
        f = quadratic_sieve(n)

        if f:
            results += f"{n} = {f[0]} * {f[1]}\n"
            count += 1

    print(f"Rate of success : {float(count / BENCH_SIZE) * 100:.2f} %")
    print("Factorisations found :\n", results, sep = "")

</code></pre>]]></content><author><name>Pierre Chrétien</name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Non commuting endomorphisms</title><link href="https://pierrechr.github.io/pierre.c/2026/03/28/problemset.html" rel="alternate" type="text/html" title="Non commuting endomorphisms" /><published>2026-03-28T00:00:00+00:00</published><updated>2026-03-28T00:00:00+00:00</updated><id>https://pierrechr.github.io/pierre.c/2026/03/28/problemset</id><content type="html" xml:base="https://pierrechr.github.io/pierre.c/2026/03/28/problemset.html"><![CDATA[<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>

<p>Andrew Sutherland’s <a href="https://ocw.mit.edu/courses/18-783-elliptic-curves-spring-2021/resources/mit18_783s21_ps6/">Problem Set 6</a> starts with a nice exercice.
Let \(k = \mathbb{F}_{7^2}\) and \(E/k : y^2 = x^3 + (1 + i)x\) with \(i^2 = -1\).
The goal is to give the explicit structure of \(\mathop{End}(E)\) as a quaternion algebra.</p>

<h2 id="defining-finite-fields">Defining finite fields</h2>

<p>First of all we will need to define the base field and its quadratic extension (see below why one needs this extension).
Thus one constructs those fields, but we have no guarantee that such a naive method allows to coerce elements from one to another.
That’s why one constructs an isomorphism between fields of order \(7^2\).</p>

<pre><code>from sage.rings.finite_rings.hom_finite_field import FiniteFieldHomomorphism_generic

k.&lt;i&gt; = GF(7**2, modulus = x**2 + 1)
k2 = GF(7**4)
kp = k2.subfields()[1][0]

phi_kp_to_k =  FiniteFieldHomomorphism_generic(Hom(kp, k))
phi_k_to_kp =  FiniteFieldHomomorphism_generic(Hom(k, kp))
</code></pre>

<h2 id="first-question-warm-up">First question, warm up.</h2>

<pre><code>E0 = EllipticCurve(k, [1+i, 0])
E0.trace_of_frobenius()
</code></pre>

<p>This shows that the characteristic polynomial of the Frobenius is 
\(X^2 - 14 X + 49 = (X - 7)^2\) so \(\pi_{E} = [7]\).</p>

<h2 id="frobenius-isnt-it-">Frobenius, isn’t it ?</h2>

<p>The easiest path for this question is to take random points \(P\) on \(E\) and try to apply \(\pi_7\), the \(7\)-th power Frobenius.
If one fails, it means that \((x; y)\) is on \(E\) but not \((x^7; y^7)\) : \(\pi_7\) won’t be an endomorphism of \(E\).</p>

<pre><code>while True:
    P = E0.random_point()
    x, y, _= P
    try:
        E0.lift_x(x**7)
    except:
        print("pi_p not an endomorphism of E")
        break
</code></pre>

<h2 id="lets-twist-again">Let’s twist again</h2>

<p>Since the polynomial in x defining E has no constant term in short Weierstrass form, one has \(j(E) = 1728\) and it is isomorphic (over the algebraic closure thought) to \(E_2/k : y^2 = x^3 + x\).
Actually, \(E\) and \(E_2\) are quadratic twists, so isomorphic over a quadratic extension of the base field.</p>

<p>Thus, one has the following three curves</p>

<pre><code>E0 = EllipticCurve(k, [1+i, 0])
E = EllipticCurve(kp, [1+phi_k_to_kp(i), 0])
E2 = EllipticCurve(k2, [1, 0])

E.j_invariant()
E2.j_invariant()
</code></pre>

<p>Something nice with \(E_2\) is that it DOES have an isogeny of degree \(7\) : the \(7\)-th power Frobenius since \(E_2\) is defined over \(\mathbb{F}_7\).
This allows to get an endomorphism \(\tau\) of \(E\) such that \(\tau^2 = [-7]\)</p>

<pre><code>psi = E.base_extend(k2).isomorphism_to(E2)
piE2 = E2.frobenius_isogeny()

tau = psi**(-1)*piE2*psi

(tau**2).rational_maps()
E.multiplication_by_m(-7)
</code></pre>

<p>It remains to pull back these coefficients to the proper base field</p>
<pre><code>coef1 = tau.rational_maps()[0].subs(x=1)
assert coef1**49 == coef1
r1 = phi_kp_to_k(coef1)

coef2 = tau.rational_maps()[1].subs(y=1)
assert coef2**49 == coef2
r2 = phi_kp_to_k(coef2)
</code></pre>
<p>and give a quick sanity check</p>
<pre><code>r1**8
r2**8
</code></pre>

<p>The endomorphism \(\alpha\) of \(E\) is \(\alpha(x; y) = (r_1 x^7; r_2  y^7) = ((5i+5)x^7; (4i+5)y^7)\).
An explicit computation shows that \(\alpha\) is indeed an endomorphism of \(E\) (the key point is that \(r_1^2(1+i)^6 = 1\)).</p>

<h2 id="square-root-of-minus-one-as-endomorphism">Square root of minus one as endomorphism</h2>

<p>One easily checks that \(\beta(x; y) = (-x; iy)\) is in \(\mathop{End}(E)\) and satisfies \(\beta^2 = [-1]\).
The last question is a direct computation</p>

\[\alpha(\beta(x; y))= \alpha(-x; iy) = (-r_1x^7; -ir_2y^7)\]

<p>and</p>

\[\beta(\alpha(x; y))= \beta(r_1x^7; r_2y^7) = (-r_1x^7; ir_2y^7)\]]]></content><author><name>Pierre Chrétien</name></author><summary type="html"><![CDATA[]]></summary></entry></feed>