<?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://l3th4l.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://l3th4l.github.io/" rel="alternate" type="text/html" /><updated>2026-08-21T07:34:23+00:00</updated><id>https://l3th4l.github.io/feed.xml</id><title type="html">Safalya Pal</title><subtitle>Data Scientist, passionate about model-based Reinforcement Learning, AI-Security and Cryptography</subtitle><author><name>Safalya Pal</name></author><entry><title type="html">Exploring Complete Key Recovery in Provably Weak Instances of Poly-LWE</title><link href="https://l3th4l.github.io/blog/weak-poly-lwe/" rel="alternate" type="text/html" title="Exploring Complete Key Recovery in Provably Weak Instances of Poly-LWE" /><published>2026-08-18T19:34:30+00:00</published><updated>2026-08-18T19:34:30+00:00</updated><id>https://l3th4l.github.io/blog/weak-poly-lwe</id><content type="html" xml:base="https://l3th4l.github.io/blog/weak-poly-lwe/"><![CDATA[<p>One of the fundamental hard problems in modern Post-Quantum Cryptography is Learning With Errors (LWE). Poly-LWE adds algebraic structure to the LWE problem by substituting the vectors in LWE with polynomials. However, this also induces the potential risk of being vulnerable to attacks exploiting its algebraic structure.</p>

<p><em>Elias et al.</em> <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> and <em>Eisentraeger et al.</em> <sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> explore weak instances of Poly-LWE, which are susceptible to such algebraic attacks. In this article, I will be focusing on Poly-LWE instances from <em>Elias et al.</em> <sup id="fnref:1:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>, satisfying certain properties which allow us to recover the secret polynomial.</p>

<p>More specifically, I’d describe <code class="language-plaintext highlighter-rouge">Algorithm 1</code> from the paper, with accompanying code in <code class="language-plaintext highlighter-rouge">python / sagemath</code>, along with a demo of the best-case scenario where it allows for the full secret recovery in Poly-LWE.</p>

<p class="notice--warning"><strong>Practicality Note:</strong> The best case scenario results in a toy problem, with the secret polynomial being of degree 4. Any higher order polynomial (i.e. for most practical applications), would render complete secret recovery impractical with the current approach.</p>
<h1 id="background--poly-lwe-and-weak-quotient-rings">Background : Poly-LWE and Weak Quotient Rings</h1>

<p>In this section, I give a brief introduction to the Poly-LWE problem, and then describe instances of polynomial rings which make Poly-LWE vulnerable.</p>
<h2 id="poly-lwe">Poly-LWE</h2>

<p>Let $f(x)$ be an $n$-degree <a href="https://en.wikipedia.org/wiki/Monic_polynomial">monic</a> <a href="https://en.wikipedia.org/wiki/Irreducible_polynomial">irriducible</a> polynomial in $\mathbb{Z}\left[x\right]$, and let $q$ be a prime number. In Poly-LWE, all polynomials are computed in the <a href="https://courses.csail.mit.edu/6.440/spring08/scribe/lec5.pdf">polynomial</a> <a href="https://en.wikipedia.org/wiki/Quotient_ring">quotient ring</a> $R_q = \mathbb{Z}_q / f(x)$</p>

<p>We first sample a random secret polynomial $s(x)$ uniformly from $R_q$. The Poly-LWE system in our setup, hence consists of the ciphertext tuple $\left(a(x), b(x)\right)$ such that $b(x) = a(x)s(x) + e(x) \;\in R_q$, where each time we generate a sample for our Poly-LWE system, $e(x)$ is the error drawn from a <a href="https://di-mgt.com.au/discrete_gaussian.html">discrete Gaussian distribution</a> with mean of $0$ and variance $\sigma^2$, and $a(x)$ is a random public polynomial, sampled uniformly from $R_q$.</p>

<h2 id="weak-rings">Weak Rings</h2>

<p>There are specific properties that the ring $R_q$ must satisfy to be vulnerable to the attacks in the following sections. Those are as follows:</p>

<ol>
  <li>
    <p>$q$ must be a prime such that $f(x)$ factorizes completely modulo $q$.</p>

    <blockquote class="notice--info">
      <p><strong>Note:</strong> This might be counterintuitive to the fact that $f(x)$ is an irreducible polynomial in $\mathbb{Z}[x]$, so let’s demonstrate what it means with the example below.</p>

      <p><strong>Example 1:</strong> Let’s take $f(x) =  x^2 + 603$ and the prime $q =  151$. By itself, $f(x)$ cannot be reduced into factors $c(x), d(x)$ such that $f(x) = c(x)d(x)$. 
But when we take $f(x) \pmod{151}$, we get</p>

\[f(x) \equiv x^2 + 150 \pmod{151}\]

      <p>Which factorizes into</p>

\[f(x) \equiv (x + 1) (x + 150) \pmod{151}\]

      <p>Let’s verify this.</p>

\[\begin{aligned} f(x) &amp;\equiv x^2 + x + 150x + 150 \pmod{151} \\ &amp;\equiv x^2 + 151x + 150   \pmod{151} \\ &amp;\equiv x^2 + 150 \pmod{151} \end{aligned}\]
    </blockquote>
  </li>
  <li>
    <p>All theh roots of $f(x)$ must be of small <a href="https://crypto.stanford.edu/pbc/notes/numbertheory/order.html">order</a> or $\pm 1$.</p>

    <p class="notice--info"><strong>Note:</strong> The original paper only requires $f(x)$ to have a single root of small order. This is because the original paper only recovers a single homomorphic image of the secret polynomial $s(x)$ and does not attempt to recover the whole of $s(x)$</p>
  </li>
</ol>

<p>The method fo generating such polynomial rings along with the prime $q$ is described later in the section: <a href="#polygen">Generating Weak Rings</a></p>
<h1 id="poly-lwe-in-sagemath">Poly-LWE in Sagemath</h1>

<p>While <code class="language-plaintext highlighter-rouge">sagemath</code> doesn’t provide us with an implementation of Poly-LWE, it does provide us with a <a href="https://doc.sagemath.org/html/en/reference/cryptography/sage/crypto/lwe.html#sage.crypto.lwe.RingLWE">Ring-LWE Oracle Generator</a> with the option to provide our own  polynomial for calculating the Quotient Group $R_q$, which effectively turns it into a Poly-LWE Oracle.</p>

<p>Throughout the article, we would work with the parameters $q =  13783771$, and</p>

\[\begin{aligned}
f(x) ={}&amp; x^4 - 13783770x^3 + 233945232486523x^2 \\
        &amp;- 605837133717152552775x \\
        &amp;+ 605836899771878714937.
\end{aligned}\]

<p>Which factorizes completely mod $q$ into:</p>

\[f(x) \equiv (x-13783770)(x-8774745)(x-5009025)(x-1) \pmod{q}\]

<p>It is clear from this expression that $13783770, 8774745, 5009025$ and $1$ are roots of $f(x)$. Let’s define this in our <code class="language-plaintext highlighter-rouge">sagemath</code> code:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">q</span> <span class="o">=</span> <span class="mi">13783771</span>
<span class="n">F</span> <span class="o">=</span> <span class="n">GF</span><span class="p">(</span><span class="n">q</span><span class="p">)</span>
<span class="n">R_q</span> <span class="o">=</span> <span class="n">PolynomialRing</span><span class="p">(</span><span class="n">F</span><span class="p">,</span> <span class="s">'x'</span><span class="p">)</span>

<span class="n">N</span> <span class="o">=</span> <span class="mi">4</span> <span class="c1">#degree of the polynomial
</span>
<span class="n">f</span> <span class="o">=</span> <span class="n">x</span><span class="o">^</span><span class="mi">4</span> <span class="o">-</span> <span class="mi">13783770</span><span class="o">*</span><span class="n">x</span><span class="o">^</span><span class="mi">3</span> <span class="o">+</span> <span class="mi">233945232486523</span><span class="o">*</span><span class="n">x</span><span class="o">^</span><span class="mi">2</span> <span class="o">-</span> <span class="mi">605837133717152552775</span><span class="o">*</span><span class="n">x</span> <span class="o">+</span> <span class="mi">605836899771878714937</span>
</code></pre></div></div>

<p>For the noise sampler, we would be going with a Discrete Gaussian Sampler with mean $0$, and the variance $\sigma = 3$. Let us use $\mathcal{N}_\sigma$ to denote this distribution. With the parameters now defined, we could create the Poly-LWE instance as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">sage.crypto.lwe</span> <span class="kn">import</span> <span class="n">RingLWE</span><span class="p">,</span> <span class="n">DiscreteGaussianDistributionPolynomialSampler</span>

<span class="n">sigma</span> <span class="o">=</span> <span class="mf">3.0</span>

<span class="c1">#the noise distribution
</span><span class="n">D</span> <span class="o">=</span> <span class="n">DiscreteGaussianDistributionPolynomialSampler</span><span class="p">(</span><span class="n">ZZ</span><span class="p">[</span><span class="s">'x'</span><span class="p">],</span> <span class="n">n</span><span class="o">=</span><span class="n">euler_phi</span><span class="p">(</span><span class="n">N</span><span class="p">),</span> <span class="n">sigma</span><span class="o">=</span><span class="n">sigma</span><span class="p">)</span>

<span class="n">PolyLWEInstance</span> <span class="o">=</span> <span class="n">RingLWE</span><span class="p">(</span><span class="n">N</span><span class="p">,</span> <span class="n">q</span><span class="p">,</span> <span class="n">D</span><span class="p">,</span> <span class="n">poly</span> <span class="o">=</span> <span class="n">f</span><span class="p">)</span>
</code></pre></div></div>

<p>For our next stages, we would need to calculate the roots of this polynomial. Although we had already talked about the factorization of $f(x)$ and the roots in the beginning of this section, we now show how we could calculate this for ourselves.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">f_q</span> <span class="o">=</span> <span class="n">R_q</span><span class="p">(</span><span class="n">f</span><span class="p">)</span> <span class="c1">#taking f(x) mod q
</span>
<span class="n">roots</span> <span class="o">=</span> <span class="p">[</span><span class="n">r</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">f_q</span><span class="p">.</span><span class="n">roots</span><span class="p">()]</span>
</code></pre></div></div>

<p>Let us also get the order of all the roots (this would help us later). We could also verify for ourselves that all the roots of $f(x)\pmod{q}$ are low-order (i.e. $« q$) as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">root_orders</span> <span class="o">=</span> <span class="p">[</span><span class="n">r</span><span class="p">.</span><span class="n">multiplicative_order</span><span class="p">()</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">roots</span><span class="p">]</span>

<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"order of the roots: </span><span class="si">{</span><span class="n">root_orders</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>Output:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>order of the roots: [2, 3, 3, 1]
</code></pre></div></div>

<h1 id="decision-and-search-poly-lwe-problem">Decision and Search Poly-LWE problem</h1>
<p>In cryptography, the security of LWE and its polynomial variant relies on two core problems:</p>

<ul>
  <li>
    <p><strong>Search Poly-LWE:</strong> Given a collection of public samples $(a(x), b(x))$, your goal is to find the hidden secret polynomial $s(x)$. This is the ultimate objective of our attack.</p>
  </li>
  <li>
    <p><strong>Decision Poly-LWE:</strong> Given a collection of samples, your goal is to distinguish whether they are valid Poly-LWE samples (generated via $b(x) = a(x)s(x) + e(x)$) or purely random pairs uniformly sampled from $R_q \times R_q$.</p>
  </li>
</ul>

<p class="notice--info"><strong>Note:</strong> In this article, we effectively turn the decision problem into a search problem. By running a decision test as a filter for each possible guess of the secret’s evaluation, we can validate which guess is correct and ultimately solve the search problem.</p>
<h1 id="attack-setup">Attack Setup</h1>

<p>Before we proceed with our attack, we need to draw $m$ samples of the pairs $(a_j(x), b_j(x))$ from our oracle. To generate a single sample we can simply call our <code class="language-plaintext highlighter-rouge">PolyLWEInstance</code> :</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">m</span> <span class="o">=</span> <span class="mi">20</span>

<span class="n">samples</span> <span class="o">=</span> <span class="p">[</span><span class="n">PolyLWEInstance</span><span class="p">()</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">m</span><span class="p">)]</span>
</code></pre></div></div>

<p>The attack then proceeds in the following four stages:</p>

<ol>
  <li>Transfer the problem from $R_q$ to $\mathbb{Z}_q$ via a ring homomorphism $\phi:R_q \rightarrow \mathbb{Z}_q$.</li>
  <li>Loop through the guesses for the possible images $\phi(s(x))$ of the secret.</li>
  <li>Assuming that the guess at hand is correct, compute image of the error polynomials $\phi(e_j(x))$.</li>
  <li>Examime the distribution of $\phi(e_j(x))$ to determine if it matches the distribution the errors are sampled from or not.</li>
</ol>

<p>In the next section (<a href="#recover">Recovering the Complete Secret with Lagrange Interpolation</a>), we extend this further to enable recovery of the complete secret.</p>

<h2 id="transferring-to-mathbbz_q">Transferring to $\mathbb{Z}_q$</h2>

<p>The first step of our attack requires us to find a ring homomorphism from $R_q$ to $\mathbb{Z}_q$, which makes the problem of guessing the secret, and validating the distribution of the errors tractable, given a root of small order.</p>

<p>Given $f(x)$ has no double roots, we specify a root $\alpha = \alpha_i$, where $\alpha_i$ for $i=0, 1, …, n-1$ are roots of $f(x)$. We define the evaluation homomorphism as :</p>

\[\phi_{\alpha} : R_q \rightarrow \mathbb{Z}_q \; \; ,  \phi_\alpha(g) = g(\alpha)\]

<p>We then apply this homomorphism to the coordinates of the $m$ samples $(a_j(x),b_j(x))$, giving us $(a_j(\alpha),b_j(\alpha))_{j=1,…,m}$.</p>

<h2 id="looping-through-the-guesses-of-the-secret-and-computing-the-image-of-the-error">Looping Through the Guesses of the Secret and Computing the Image of the Error</h2>

<p>We combine step 2. and 3. since every guess for the secret’s image yields a corresponding image of the error in a very straightforward manner.</p>

<p>We loop through all the possible values of $g\in\mathbb{Z}_q$, where each $g$ is considered to be a guess for the image of the secret $s(\alpha)$, i.e., $g=s(\alpha)$</p>

<p>For each guess $g$, we assume that it is a correct guess for $s(\alpha)$ and we compute the image of the errors $e_j(\alpha)_{j=1,…,m}$ as</p>

\[\begin{aligned} e_j(\alpha) &amp; = b_j(\alpha) - a_j(\alpha)g \\
&amp;=b_j(\alpha) - a_j(\alpha)s(\alpha)\end{aligned}\]

<p>If our guess $g$, is correct, then the collection of error images ${e_j(\alpha)}$ follow the distribution $\phi_\alpha(\mathcal{N}_\sigma)$</p>

<h2 id="verifying-membership-of-the-error-images-in-the-error-distribution">Verifying Membership of the Error Images in the Error Distribution</h2>

<p>Now that we have the images of our errors ${e_j(\alpha)}$, we must determine if the errors belong to the distribution our errors were originally sampled from, i.e. $\mathcal{N}_\sigma$. This gives us a way to determine if the pairs $(a_j(x), b_j(x))$ were generated from a Poly-LWE instance or are uniformly sampled from $R_q \times R_q$, which is only possible if our guess $g$ matches the image of the actual secret $s(\alpha)$.</p>

<p>To do this, we first compute the set $S$ of all possible values of $e(\alpha)$. Since $e(\alpha)$ is defined as</p>

\[e(\alpha) = e_0 + \alpha e_1 + \alpha^2 e_2 +...+\alpha^{n-1} e_{n-1}\]

<p>Since $\alpha$ is a root of small order $r$ modulo $q$ by assumption, i.e., $\alpha^r\equiv 1 \pmod{q}$, we can simplify this sum in the following 2 cases:</p>

<h3 id="case-1-n-is-divisible-by-r">Case 1: $n$ is divisible by $r$</h3>

<p>Here, calculating $e(\alpha)$ is pretty straightforward.</p>

\[\begin{aligned}e(\alpha) ={}&amp; (e_0 + e_r + e_{2r}+...) + \alpha (e_1 + e_{r+1} + e_{2r+1}+...) \\&amp;+ ...+ \alpha^{r-1}(e_{r-1} + e_{2r-1} + e_{3r-1}+...)\end{aligned}\]

<p>where each of the coeffecients of $\alpha ^ i$ is $(e_i + e_{i+r}+e_{2i+r}+e_{3i+r}+…e_{n-r+j})$</p>
<h3 id="case-2-n-is-not-divisible-by-r">Case 2: $n$ is not divisible by $r$</h3>

<p>When $r$ does not divide $n$, then the first $k$ coefficients of $\alpha^i$ have one extra term than the last $r-k$ coefficients.</p>

<p>We can find $k$ by taking $n \pmod{r}$ and let $q = \lfloor n/r \rfloor$ be the quotient. Then we can compute $e(\alpha)$ as:</p>

\[\begin{aligned} e(\alpha) ={}&amp; \sum_{i=0}^{k-1} \alpha^i \left( \sum_{j=0}^{q} e_{i + j \cdot r} \right) + \sum_{i=k}^{r-1} \alpha^i \left( \sum_{j=0}^{q-1} e_{i + j \cdot r} \right) \end{aligned}\]

<p><strong>Explanation of the terms:</strong></p>

<ul>
  <li>
    <p><strong>For the first $k$ groups</strong> (corresponding to powers $\alpha^0, \alpha^1, \dots, \alpha^{k-1}$), the index goes up to $q$, meaning each coefficient contains $q + 1$ terms:</p>

\[e_i + e_{i+r} + e_{i+2r} + \dots + e_{i + q \cdot r}\]
  </li>
  <li>
    <p><strong>For the remaining $r-k$ groups</strong> (corresponding to powers $\alpha^k, \dots, \alpha^{r-1}$), the index stops at $q-1$, meaning each coefficient contains $q$ terms:</p>

\[e_i + e_{i+r} + e_{i+2r} + \dots + e_{i + (q-1) \cdot r}\]
  </li>
</ul>

<p>We can write a function to compute the set $S$ as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">math</span>
<span class="kn">from</span> <span class="nn">itertools</span> <span class="kn">import</span> <span class="n">product</span> <span class="k">as</span> <span class="n">cart</span> 

<span class="k">def</span> <span class="nf">generate_error_sums</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">sigma</span><span class="p">,</span> <span class="n">truncate_limit</span><span class="p">,</span> <span class="n">q</span><span class="p">,</span> <span class="n">r</span><span class="p">):</span>
    <span class="n">max_val</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">math</span><span class="p">.</span><span class="n">ceil</span><span class="p">(</span><span class="n">truncate_limit</span> <span class="o">*</span> <span class="n">sigma</span><span class="p">))</span>
    <span class="n">possible_values</span> <span class="o">=</span> <span class="nb">range</span><span class="p">(</span><span class="o">-</span><span class="n">max_val</span><span class="p">,</span> <span class="n">max_val</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>

    <span class="n">q_quotient</span> <span class="o">=</span> <span class="n">n</span> <span class="o">//</span> <span class="n">r</span>
    <span class="n">k</span> <span class="o">=</span> <span class="n">n</span> <span class="o">%</span> <span class="n">r</span>
    
    <span class="c1"># Case 1: If n is divisible by r, all groups have length q_quotient
</span>    <span class="c1"># Case 2: If not divisible, the 'long' groups have length q_quotient + 1
</span>    <span class="n">long_len</span> <span class="o">=</span> <span class="n">q_quotient</span> <span class="o">+</span> <span class="mi">1</span> <span class="k">if</span> <span class="n">k</span> <span class="o">!=</span> <span class="mi">0</span> <span class="k">else</span> <span class="n">q_quotient</span>

    <span class="c1"># Using set comprehensions makes this faster and more Pythonic
</span>    <span class="n">sums_long</span> <span class="o">=</span> <span class="nb">list</span><span class="p">({</span>
        <span class="nb">sum</span><span class="p">(</span><span class="n">vector</span><span class="p">(</span><span class="n">Zmod</span><span class="p">(</span><span class="n">q</span><span class="p">),</span> <span class="n">vec</span><span class="p">))</span> 
        <span class="k">for</span> <span class="n">vec</span> <span class="ow">in</span> <span class="n">cart</span><span class="p">(</span><span class="n">possible_values</span><span class="p">,</span> <span class="n">repeat</span><span class="o">=</span><span class="n">long_len</span><span class="p">)</span>
    <span class="p">})</span>

    <span class="n">sums_short</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">if</span> <span class="n">k</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span>
        <span class="n">sums_short</span> <span class="o">=</span> <span class="nb">list</span><span class="p">({</span>
            <span class="nb">sum</span><span class="p">(</span><span class="n">vector</span><span class="p">(</span><span class="n">Zmod</span><span class="p">(</span><span class="n">q</span><span class="p">),</span> <span class="n">vec</span><span class="p">))</span> 
            <span class="k">for</span> <span class="n">vec</span> <span class="ow">in</span> <span class="n">cart</span><span class="p">(</span><span class="n">possible_values</span><span class="p">,</span> <span class="n">repeat</span><span class="o">=</span><span class="n">q_quotient</span><span class="p">)</span>
        <span class="p">})</span>

    <span class="k">return</span> <span class="n">sums_long</span><span class="p">,</span> <span class="n">sums_short</span>

<span class="k">def</span> <span class="nf">generate_error_set</span><span class="p">(</span><span class="n">alpha</span><span class="p">,</span> <span class="n">q</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">sigma</span><span class="p">,</span> <span class="n">r</span><span class="p">):</span>
    <span class="c1"># Generate [1, alpha, alpha^2, ..., alpha^{min(n,r)-1}]
</span>    <span class="n">alpha_is</span> <span class="o">=</span> <span class="n">vector</span><span class="p">(</span><span class="n">Zmod</span><span class="p">(</span><span class="n">q</span><span class="p">),</span> <span class="p">[</span><span class="nb">pow</span><span class="p">(</span><span class="n">alpha</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="n">q</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">min</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">r</span><span class="p">))])</span>

    <span class="n">k</span> <span class="o">=</span> <span class="n">n</span> <span class="o">%</span> <span class="n">r</span>

    <span class="c1"># s1 corresponds to the groups with an extra term, s2 to the rest
</span>    <span class="n">s1</span><span class="p">,</span> <span class="n">s2</span> <span class="o">=</span> <span class="n">generate_error_sums</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="n">sigma</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="n">q</span><span class="p">,</span> <span class="n">r</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">k</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
        <span class="c1"># Case 1: n is divisible by r. All r groups are identical in length.
</span>        <span class="n">possible_coefficients</span> <span class="o">=</span> <span class="p">[</span><span class="n">s1</span><span class="p">]</span> <span class="o">*</span> <span class="n">r</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="c1"># Case 2: n is not divisible by r. 
</span>        <span class="c1"># The first k coefficients have the extra term (s1).
</span>        <span class="c1"># The remaining r-k coefficients have one fewer term (s2).
</span>        <span class="n">possible_coefficients</span> <span class="o">=</span> <span class="p">[</span><span class="n">s1</span><span class="p">]</span> <span class="o">*</span> <span class="n">k</span> <span class="o">+</span> <span class="p">[</span><span class="n">s2</span><span class="p">]</span> <span class="o">*</span> <span class="p">(</span><span class="n">r</span> <span class="o">-</span> <span class="n">k</span><span class="p">)</span>

    <span class="c1"># Compute all possible values of e(alpha)
</span>    <span class="k">return</span> <span class="p">{</span>
        <span class="n">alpha_is</span> <span class="o">*</span> <span class="n">vector</span><span class="p">(</span><span class="n">coeffs</span><span class="p">)</span> 
        <span class="k">for</span> <span class="n">coeffs</span> <span class="ow">in</span> <span class="n">cart</span><span class="p">(</span><span class="o">*</span><span class="n">possible_coefficients</span><span class="p">)</span>
    <span class="p">}</span>
</code></pre></div></div>

<p class="notice--warning"><strong>Note:</strong> While the attack works on paper, pushing the order past $r &gt; 5$ makes generating the group sums using the Cartesian product (<code class="language-plaintext highlighter-rouge">cart</code>) computationally impossible. The sheer volume of combinations causes your script to instantly run out of memory or hang indefinitely. This is the primary bottleneck of this attack.</p>

<h2 id="putting-it-all-together">Putting it All Together</h2>

<p>Now that we have a rough idea of how the attack unfolds, let’s put it all together into pseudocode and then write a function in sage.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Input: A collection of l Poly-LWE samples (a_j, b_j), a root alpha, and valid error set S
Output: The secret guess g for s(alpha), or "NOT PLWE"

1. Initialize an empty list of surviving guesses: G = []

2. For each possible guess g in Z_q (from 0 to q-1):
   a. Assume the guess is valid: potential = True
   
   b. For each sample (a_j, b_j) in our collection:
      - Compute the error image: err = b_j(alpha) - g * a_j(alpha)
      - Check if err belongs to our expected error distribution/set S
      - If err NOT in S:
         - potential = False
         - Break (stop checking remaining samples for this guess)
         
   c. If potential is still True after checking all samples:
      - Append g to G (it survived the filter!)

3. Evaluate final results:
   - If G is empty: Return "NOT PLWE" (the samples are random, not Poly-LWE)
   - If G has a single element: Return g (Success! Secret image recovered)
   - If G has multiple elements: Return "INSUFFICIENT SAMPLES" (need more samples to narrow it down)
</code></pre></div></div>

<p>We want to define this as a function in sage, so that we can try repeating it with all the roots of $f(x)$</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">attack</span><span class="p">(</span><span class="n">samples</span><span class="p">,</span> <span class="n">q</span><span class="p">,</span> <span class="n">N</span><span class="p">,</span> <span class="n">sigma</span><span class="p">,</span> <span class="n">R_q</span><span class="p">,</span> <span class="n">alpha</span><span class="p">,</span> <span class="n">alpha_order</span><span class="p">,</span> <span class="n">D</span><span class="p">):</span>
    <span class="n">S</span> <span class="o">=</span> <span class="n">generate_error_set</span><span class="p">(</span><span class="n">alpha</span><span class="p">,</span> <span class="n">q</span><span class="p">,</span> <span class="n">N</span><span class="p">,</span> <span class="n">sigma</span><span class="p">,</span> <span class="n">alpha_order</span><span class="p">)</span>
    
    <span class="k">for</span> <span class="n">g</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">q</span><span class="p">):</span>
        <span class="n">valid</span> <span class="o">=</span> <span class="bp">True</span>
        <span class="k">for</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="ow">in</span> <span class="n">samples</span><span class="p">:</span>
            <span class="n">e1_alpha</span> <span class="o">=</span> <span class="n">R_q</span><span class="p">(</span><span class="nb">list</span><span class="p">(</span><span class="n">b</span> <span class="o">-</span> <span class="n">g</span> <span class="o">*</span> <span class="n">a</span><span class="p">))(</span><span class="n">alpha</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">e1_alpha</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">S</span><span class="p">:</span>
                <span class="n">valid</span> <span class="o">=</span> <span class="bp">False</span>
                <span class="k">break</span>  <span class="c1"># Early break: sample failed the error check
</span>                
        <span class="k">if</span> <span class="n">valid</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">g</span>  <span class="c1"># Early return: found the matching secret image
</span>            
    <span class="k">return</span> <span class="bp">None</span>
</code></pre></div></div>

<p class="notice--info">Performance Note: This implementation also uses an <strong>early return (<code class="language-plaintext highlighter-rouge">return g</code>)</strong> on the outer loop. Given a sufficient number of samples, the probability that an incorrect guess $g$ will accidentally satisfy the error distribution check across <em>every single sample</em> drops to virtually zero. However, do not that if your sample size is small, this would give you incorrect results.</p>
<h1 id="recover">Recovering the Complete Secret with Lagrange Interpolation</h1>

<p>Now that we can recover the image of the secret with respect to root $\alpha_i$, we do it for all the $n$ roots</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">secret_images</span> <span class="o">=</span> <span class="p">[]</span> 
<span class="k">for</span> <span class="n">root</span><span class="p">,</span> <span class="n">root_order</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">roots</span><span class="p">,</span> <span class="n">root_orders</span><span class="p">):</span>
    <span class="n">s_alpha</span> <span class="o">=</span> <span class="n">attack</span><span class="p">(</span><span class="n">samples</span><span class="p">,</span> <span class="n">q</span><span class="p">,</span> <span class="n">N</span><span class="p">,</span> <span class="n">sigma</span><span class="p">,</span> <span class="n">R_q</span><span class="p">,</span> <span class="n">root</span><span class="p">,</span> <span class="n">root_order</span><span class="p">,</span> <span class="n">D</span><span class="p">)</span>	
    <span class="n">secret_images</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">s_alpha</span><span class="p">)</span>

    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"secret found for root </span><span class="si">{</span><span class="n">root</span><span class="si">}</span><span class="s"> : </span><span class="si">{</span><span class="n">s_alpha</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>

</code></pre></div></div>

<p><strong>Output:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>secret found for root 13783770 : 12614025
secret found for root 8774745 : 4358721
secret found for root 5009025 : 1865108
secret found for root 1 : 1063299
</code></pre></div></div>

<p>We have successfully computed the evaluations of our secret polynomial $s(x)$ at $n$ distinct points (at the roots $\alpha_i$), giving us pairs of coordinates : $(\alpha_i, s(\alpha_i))$.</p>

<p>Because the degree of our secret polynomial $s(x)$ is strictly less than the number of roots we’ve evaluated, we can treat this as a standard curve-fitting problem. Lagrange Interpolation allows us to uniquely reconstruct the original secret polynomial coefficients from these evaluation points over $\mathbb{Z}_q$.</p>

<p class="notice--info"><strong>Note:</strong> We do not explain Lagrange Interpolation in this article, however, there is <a href="https://youtu.be/bzp_q7NDdd4?si=ir2xVZqejfW8k-PF">this</a> video on the YouTube, explaining it in a very intuitive manner</p>

<p>We first define a function to calculate the lagrange basis polynomial for the point $x_k = \alpha_i$:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">lagrange_polynomial</span><span class="p">(</span><span class="n">xs</span><span class="p">,</span> <span class="n">xk</span><span class="p">,</span> <span class="n">modulo</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="k">assert</span> <span class="n">modulo</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">,</span> <span class="s">"A modulus must be specified."</span>

    <span class="n">F</span> <span class="o">=</span> <span class="n">GF</span><span class="p">(</span><span class="n">modulo</span><span class="p">)</span>
    <span class="n">R</span> <span class="o">=</span> <span class="n">PolynomialRing</span><span class="p">(</span><span class="n">F</span><span class="p">,</span> <span class="s">'x'</span><span class="p">)</span>
    <span class="n">x</span> <span class="o">=</span> <span class="n">R</span><span class="p">.</span><span class="n">gen</span><span class="p">()</span>

    <span class="n">basis_poly</span> <span class="o">=</span> <span class="mi">1</span>
    <span class="k">for</span> <span class="n">xi</span> <span class="ow">in</span> <span class="n">xs</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">xi</span> <span class="o">!=</span> <span class="n">xk</span><span class="p">:</span>
            <span class="n">numerator</span> <span class="o">=</span> <span class="n">x</span> <span class="o">-</span> <span class="n">xi</span>
            <span class="n">denominator</span> <span class="o">=</span> <span class="n">F</span><span class="p">(</span><span class="n">xk</span> <span class="o">-</span> <span class="n">xi</span><span class="p">)</span>
            <span class="c1"># Sage automatically handles the modular inverse via division in GF(q)
</span>            <span class="n">basis_poly</span> <span class="o">*=</span> <span class="n">numerator</span> <span class="o">/</span> <span class="n">denominator</span>
            
    <span class="k">return</span> <span class="n">basis_poly</span>
</code></pre></div></div>

<p>The next step is to use this to compute our secret polynomial using Lagrange Interpolation. We define a function for that as</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">reconstruct_polynomial</span><span class="p">(</span><span class="n">xs</span><span class="p">,</span> <span class="n">ys</span><span class="p">,</span> <span class="n">degree</span><span class="p">,</span> <span class="n">modulo</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="k">assert</span> <span class="n">modulo</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">,</span> <span class="s">"A modulus must be specified."</span>
    <span class="k">assert</span> <span class="nb">len</span><span class="p">(</span><span class="n">xs</span><span class="p">)</span> <span class="o">==</span> <span class="nb">len</span><span class="p">(</span><span class="n">ys</span><span class="p">),</span> <span class="s">"Mismatch between number of x-coordinates and y-coordinates."</span>
    <span class="k">assert</span> <span class="nb">len</span><span class="p">(</span><span class="n">xs</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">degree</span><span class="p">,</span> <span class="s">"Not enough evaluation points for the given degree."</span>

    <span class="c1"># Sum up: y_i * l_i(x) across all points
</span>    <span class="k">return</span> <span class="nb">sum</span><span class="p">(</span><span class="n">yi</span> <span class="o">*</span> <span class="n">lagrange_polynomial</span><span class="p">(</span><span class="n">xs</span><span class="p">,</span> <span class="n">xi</span><span class="p">,</span> <span class="n">modulo</span><span class="o">=</span><span class="n">modulo</span><span class="p">)</span> <span class="k">for</span> <span class="n">xi</span><span class="p">,</span> <span class="n">yi</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">xs</span><span class="p">,</span> <span class="n">ys</span><span class="p">))</span>
</code></pre></div></div>

<p>We can now recover our secret polynomial with</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rec</span> <span class="o">=</span> <span class="n">reconstruct_polynomial</span><span class="p">(</span><span class="n">roots</span><span class="p">,</span> <span class="n">secret_images</span><span class="p">,</span> <span class="n">N</span><span class="p">,</span> <span class="n">modulo</span> <span class="o">=</span> <span class="n">q</span><span class="p">)</span>
<span class="k">print</span> <span class="p">(</span><span class="sa">f</span><span class="s">"reconstructed secret polynomial: </span><span class="si">{</span><span class="n">rec</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>Output:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>reonstructed secret polynomial: 1668982*x^3 + 1484011*x^2 + 6339426*x + 5354651
</code></pre></div></div>

<p>Voila! There we have it ! :)</p>

<p class="notice--info"><strong>Note:</strong> Since we’ve covered all the parts of our attack, the next two sections are completely optional, but it’s highly recommended to go through them, if you want to reproduce the attack with your own parameters.</p>
<h1 id="modi">Verifying the Secret</h1>

<p>Let’s take a look at the code for the <code class="language-plaintext highlighter-rouge">RingLWE</code> class from <a href="https://github.com/sagemath/sage/blob/develop/src/sage/crypto/lwe.py#L517">github.com/sagemath/sage/blob/develop/src/sage/crypto/lwe.py</a></p>

<p>The secret is generated by the lines</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>		<span class="p">...</span>
		
        <span class="bp">self</span><span class="p">.</span><span class="n">secret_dist</span> <span class="o">=</span> <span class="n">secret_dist</span>
        <span class="k">if</span> <span class="n">secret_dist</span> <span class="o">==</span> <span class="s">'uniform'</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">__s</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">R_q</span><span class="p">.</span><span class="n">random_element</span><span class="p">()</span>  <span class="c1"># uniform sampling of secret
</span>        <span class="k">elif</span> <span class="n">secret_dist</span> <span class="o">==</span> <span class="s">'noise'</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">__s</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">D</span><span class="p">()</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nb">TypeError</span><span class="p">(</span><span class="s">"Parameter secret_dist=%s not understood."</span> <span class="o">%</span> <span class="p">(</span><span class="n">secret_dist</span><span class="p">))</span>
            
		<span class="p">...</span>
</code></pre></div></div>

<p>So let’s try printing the polynomial <code class="language-plaintext highlighter-rouge">__s</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">print</span><span class="p">(</span><span class="n">PolyLWEInstance</span><span class="p">.</span><span class="n">__s</span><span class="p">)</span>
</code></pre></div></div>

<p>However, this results in the error:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    print(PolyLWEInstance.__s)
          ^^^^^^^^^^
AttributeError: 'RingLWE' object has no attribute '__s'
</code></pre></div></div>

<p>The polynomial <code class="language-plaintext highlighter-rouge">__s</code> has been <a href="https://en.wikipedia.org/wiki/Name_mangling"><em>name-mangled</em> </a> (<a href="https://www.youtube.com/watch?v=0hrEaA3N3lk&amp;t=13s">here</a> is a nice video on name mangling in python), hence it cannot be accessed simply by calling <code class="language-plaintext highlighter-rouge">PolyLWEInstance.__s</code>. But we can circumvent this by calling <code class="language-plaintext highlighter-rouge">PolyLWEInstance._RingLWE__s</code> instead, so let’s do that</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"secret polynomial: </span><span class="si">{</span><span class="n">PolyLWEInstance</span><span class="p">.</span><span class="n">_RingLWE__s</span><span class="p">.</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>Output:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>secret polynomial: 1668982*x^3 + 1484011*x^2 + 6339426*x + 5354651
</code></pre></div></div>

<p>That is is our actual secret <code class="language-plaintext highlighter-rouge">s(x)</code>, which also matches the secret polynomial we recovered with our attack.</p>

<h1 id="polygen">Generating Weak Rings</h1>

<p>In the previous sections, we looked at how an existing weak Poly-LWE instance can be completely broken using root evaluation and Lagrange interpolation. But how do we actually <em>find</em> or <em>generate</em> such weak rings in the first place?</p>

<p>As noted earlier, we can construct them by working backwards:</p>

<ol>
  <li>
    <p>First, we generate a prime $q$ whose multiplicative group contains elements of small order.</p>
  </li>
  <li>
    <p>Next, we construct the target polynomial modulo $q$ using these small-order roots.</p>
  </li>
  <li>
    <p>Finally, we “lift” this polynomial into $\mathbb{Z}[x]$ by adding a random multiple (from the polynomial ring $\mathbb{Z}_q[x]$) of $q$  until we find an irreducible polynomial.</p>
  </li>
</ol>

<h2 id="step-1-generating-a-prime-with-small-order-elements">Step 1: Generating a Prime with Small-Order Elements</h2>

<p>To ensure our ring has roots of small order, we want $q - 1$ to be divisible by several small prime numbers. This guarantees that $\mathbb{Z}_q$ contains elements of small multiplicative order.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">sage.all</span> <span class="kn">import</span> <span class="o">*</span> 
<span class="kn">from</span> <span class="nn">sympy</span> <span class="kn">import</span> <span class="n">isprime</span>

<span class="k">def</span> <span class="nf">generate_prime</span><span class="p">(</span><span class="n">n</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span> <span class="n">max_k</span><span class="o">=</span><span class="mi">100</span><span class="p">):</span>
    <span class="n">M</span> <span class="o">=</span> <span class="n">prod</span><span class="p">(</span><span class="n">primes_first_n</span><span class="p">(</span><span class="n">n</span><span class="p">))</span>
    <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
        <span class="n">p</span> <span class="o">=</span> <span class="n">randint</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">max_k</span><span class="p">)</span> <span class="o">*</span> <span class="n">M</span> <span class="o">+</span> <span class="mi">1</span>
        <span class="k">if</span> <span class="n">isprime</span><span class="p">(</span><span class="n">p</span><span class="p">):</span>
            <span class="k">return</span> <span class="n">p</span> 
</code></pre></div></div>

<h2 id="step-2-constructing-and-lifting-the-polynomial">Step 2: Constructing and Lifting the Polynomial</h2>

<p>Once we have our prime $q$, we collect elements of small order from $\mathbb{Z}_q$ to serve as the roots of our polynomial. We then form $f_q(x)$ as a product of linear factors and search for a random perturbation $h(x)$ such that $f(x) = f_q(x) + q \cdot h(x)$ is irreducible in $\mathbb{Z}[x]$.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">irreducible_poly_mod_q</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">max_order</span><span class="p">,</span> <span class="n">n</span><span class="p">):</span>
    <span class="n">orders</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">dict</span><span class="p">.</span><span class="n">fromkeys</span><span class="p">([</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span><span class="p">,</span> <span class="n">_</span> <span class="ow">in</span> <span class="n">factor</span><span class="p">(</span><span class="n">q</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)]))</span>
    <span class="n">_n</span> <span class="o">=</span> <span class="n">n</span>
    <span class="n">_order_id</span> <span class="o">=</span> <span class="mi">0</span>

    <span class="c1"># Find elements with small order in Z_q
</span>    <span class="n">roots</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">]</span>
    <span class="k">while</span> <span class="n">_n</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">:</span>
        <span class="n">order</span> <span class="o">=</span> <span class="n">orders</span><span class="p">[</span><span class="n">_order_id</span><span class="p">]</span>
        <span class="k">assert</span> <span class="n">order</span> <span class="o">&lt;</span> <span class="n">max_order</span>
        <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="n">q</span><span class="p">):</span>
            <span class="k">if</span> <span class="nb">pow</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="n">order</span><span class="p">,</span> <span class="n">q</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
                <span class="k">break</span> 

        <span class="c1"># Get the subgroup generated by c
</span>        <span class="n">roots</span> <span class="o">+=</span> <span class="p">[</span><span class="nb">pow</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="n">q</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nb">min</span><span class="p">(</span><span class="n">order</span><span class="p">,</span> <span class="n">_n</span><span class="p">))]</span>

        <span class="n">_n</span> <span class="o">-=</span> <span class="p">(</span><span class="n">order</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
        <span class="n">_order_id</span> <span class="o">+=</span> <span class="mi">1</span> 

    <span class="n">R</span> <span class="o">=</span> <span class="n">PolynomialRing</span><span class="p">(</span><span class="n">ZZ</span><span class="p">,</span> <span class="s">'x'</span><span class="p">)</span>
    <span class="n">R_q</span> <span class="o">=</span> <span class="n">PolynomialRing</span><span class="p">(</span><span class="n">Zmod</span><span class="p">(</span><span class="n">q</span><span class="p">),</span> <span class="s">'x'</span><span class="p">)</span>
    <span class="n">x</span> <span class="o">=</span> <span class="n">R</span><span class="p">.</span><span class="n">gen</span><span class="p">()</span>
    <span class="n">f_q</span> <span class="o">=</span> <span class="n">prod</span><span class="p">([(</span><span class="n">x</span> <span class="o">-</span> <span class="n">a</span><span class="p">)</span> <span class="k">for</span> <span class="n">a</span> <span class="ow">in</span> <span class="n">roots</span><span class="p">])</span>

    <span class="c1"># Find an irreducible polynomial in Z[x] which is equivalent to f_q in Z_q[x]
</span>    <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
        <span class="n">h</span> <span class="o">=</span> <span class="n">R</span><span class="p">.</span><span class="n">random_element</span><span class="p">(</span><span class="n">degree</span><span class="o">=</span><span class="n">n</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
        <span class="n">f</span> <span class="o">=</span> <span class="n">f_q</span> <span class="o">+</span> <span class="n">q</span> <span class="o">*</span> <span class="n">h</span>

        <span class="k">if</span> <span class="n">f</span><span class="p">.</span><span class="n">is_irreducible</span><span class="p">():</span>
            <span class="k">assert</span><span class="p">(</span><span class="n">R_q</span><span class="p">(</span><span class="n">f</span><span class="p">)</span> <span class="o">==</span> <span class="n">f_q</span><span class="p">)</span> 
            <span class="k">return</span> <span class="n">f</span> 
</code></pre></div></div>

<h2 id="putting-it-together-in-a-test-script">Putting It Together in a Test Script</h2>

<p>We can tie everything together with a quick execution script to generate a weak ring, print out its factorization modulo $q$, and verify the root orders:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">q</span> <span class="o">=</span> <span class="n">generate_prime</span><span class="p">(</span><span class="n">n</span><span class="o">=</span><span class="mi">7</span><span class="p">,</span> <span class="n">max_k</span><span class="o">=</span><span class="mi">50</span><span class="p">)</span>
    <span class="n">R_q</span> <span class="o">=</span> <span class="n">PolynomialRing</span><span class="p">(</span><span class="n">Zmod</span><span class="p">(</span><span class="n">q</span><span class="p">),</span> <span class="s">'x'</span><span class="p">)</span>

    <span class="k">print</span><span class="p">(</span><span class="s">"q = "</span><span class="p">,</span> <span class="n">q</span><span class="p">)</span> 

    <span class="c1"># Find a polynomial of degree 4 with max root order 4
</span>    <span class="n">f</span> <span class="o">=</span> <span class="n">irreducible_poly_mod_q</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">max_order</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">n</span><span class="o">=</span><span class="mi">4</span><span class="p">)</span>

    <span class="k">print</span><span class="p">(</span><span class="s">"f(x) = "</span><span class="p">,</span> <span class="n">f</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"f(x) = </span><span class="si">{</span><span class="n">R_q</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="si">}</span><span class="s"> (mod </span><span class="si">{</span><span class="n">q</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"[factorized] f(x) = </span><span class="si">{</span><span class="n">R_q</span><span class="p">(</span><span class="n">f</span><span class="p">).</span><span class="n">factor</span><span class="p">()</span><span class="si">}</span><span class="s"> (mod </span><span class="si">{</span><span class="n">q</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>

    <span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n\n</span><span class="s">=== Orders of roots ===</span><span class="se">\n\n</span><span class="s">"</span><span class="p">)</span>

    <span class="k">for</span> <span class="n">root</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="ow">in</span> <span class="n">R_q</span><span class="p">(</span><span class="n">f</span><span class="p">).</span><span class="n">roots</span><span class="p">():</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"root : </span><span class="si">{</span><span class="n">root</span><span class="si">}</span><span class="s">, order : </span><span class="si">{</span><span class="n">Zmod</span><span class="p">(</span><span class="n">q</span><span class="p">)(</span><span class="n">root</span><span class="p">).</span><span class="n">multiplicative_order</span><span class="p">()</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>Output:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>q =  13273261
f(x) =  x^4 - 39819782*x^3 + 220080781604882*x^2 - 582713379746921086858*x + 582713159666431493716
f(x) = x^4 + x^3 + 13273260*x + 13273260 (mod 13273261)
[factorized] f(x) = (x + 1) * (x + 6257720) * (x + 7015542) * (x + 13273260) (mod 13273261)


=== Orders of roots ===


root : 13273260, order : 2
root : 7015541, order : 3
root : 6257719, order : 3
root : 1, order : 1
</code></pre></div></div>

<p>And there you have it! With this script, you can programmatically mint your own custom weak Poly-LWE instances, test the attack script we built earlier, and experiment with different degrees and orders.</p>

<h1 id="references">References</h1>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Y. Elias, K. E. Lauter, E. Ozman, and K. E. Stange, <em>Provably Weak Instances of Ring-LWE,</em> 2015. <a href="https://arxiv.org/abs/1502.03708">arXiv:1502.03708</a>. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:1:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>K. Eisentraeger, S. Hallgren, and K. Lauter, <em>Weak Instances of PLWE</em>, Cryptology ePrint Archive, Paper 2014/784, 2014. <a href="https://eprint.iacr.org/2014/784">eprint.iacr.org/2014/784</a>. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Safalya Pal</name></author><category term="Blog" /><category term="Cryptography" /><category term="LWE" /><category term="FHE" /><summary type="html"><![CDATA[One of the fundamental hard problems in modern Post-Quantum Cryptography is Learning With Errors (LWE). Poly-LWE adds algebraic structure to the LWE problem by substituting the vectors in LWE with polynomials. However, this also induces the potential risk of being vulnerable to attacks exploiting its algebraic structure.]]></summary></entry></feed>