Try   HackMD

After the competition ended, I found out through the Discord channel that the code was an implementation of Eaglesign(seems not exactly same), and that there is a paper on attacks related to it.

Most of the solvers, aside from myself, seem to have either referenced the paper or figured out a similar method on their own to solve it.

Even though I only added a very short explanation along with my solver code on Discord channel, I sincerely thank @vishiswoz and @Neobeo for sharing their interpretations of my solution.

@vishiswoz's interpretation

@Neobeo's interpretation

Challenge

  • N = 1024, q = 12289
  • Key generation
    • Private keys (
      G,D
      ) are randomly generated polynomial on Quotient Ring
      Zq[x]/xN+1
      • coefficients must be {-1, 0, 1}.
      • G
        must be invertible.
    • Public key
      A
      is randomly generated polynomial on same Quotient Ring
    • Public key
      E=(A+D)G1
  • Sign
    • Generate
      Y1,Y2
      randomly
      • Y1
        has 140 nonzero coefficients, which are -1 or 1
      • Y2
        has bounded coefficients which absolute max are 64
    • P=AY1+Y2
      ,
      r=H(P)
    • Generate
      C
      with given message and
      r
      • C
        has 38 nonzero coefficients, which are -1 or 1
    • U=Y1+C
    • V=GU
      ,
      W=Y2DU
    • Signature is
      (r,V,W)
  • Verify
    • Generate
      C
      with given message and
      r
      (same as Sign)
    • Z=EVAC+W
      • EVAC+W=EGUAC+W=(A+D)UAC+Y2DU=A(UC)+Y2=P
    • Check
      r
      equals
      H(Z)
  • Keys are generated and public Keys are given when code starts.
  • We can sign for any message with no limit, and we need
    D
    for get the flag.

My approach

  • G1=(Y1+C)V1
    (
    V=GU=G(Y1+C)
    )
  • So,
    (Y1i+Ci)Vi1=(Y10+C0)V01
    for every signature.
  • We can express every
    Y1i
    as form
    Ai(Y10)+Bi
    • Ai=ViV01
    • Bi=AiC0Ci

Building equation

  • Let

    Ai=k=0N1ai,kxk,
    Bi=k=0N1bi,kxk
    ,
    Y1i=k=0N1ci,kxk

    • xjAi=k=0j1ai,Nj+kxk+k=jN1ai,kjxk
  • Y1i=AiY10+Bi=j=0N1c0,j(xjAi)+Bi

  • Therefore, we can express it as Matrix equation below:

    (ai,0ai,N1ai,N2ai,1ai,1ai,0ai,N1ai,2ai,2ai,1ai,0ai,3ai,N1ai,N2ai,N3ai,0)(c0,0c0,1c0,2c0,N1)+(b0,0b0,1b0,2b0,N1)=(ci,0ci,1ci,2ci,N1)

  • Let matrix

    Mi=(ai,0ai,N1ai,N2ai,1ai,1ai,0ai,N1ai,2ai,2ai,1ai,0ai,3ai,N1ai,N2ai,N3ai,0)=(||||vi,0vi,1vi,2vi,N1||||)

  • Let vectors

    yi=(ci,0ci,1ci,2ci,N1),
    bi=(b0,0b0,1b0,2b0,N1)

  • So we can express equation above as below:

    j=0N1c0,jvi,j+bi=yi

  • Since Y1 has 140 nonzero coefficients which are -1 or 1, sum of square of coefficients equals 140, so

    yiyi=140. It leads to equation below:
    yiyi=(j=0N1c0,jvi,j+bi)(j=0N1c0,jvi,j+bi)

    =0j,k<N(vi,jvi,k)c0,jc0,k+2j=0N1(vi,jbi)c0,j+(bibi)=140

Linearization & reduce unknown variables

  • By linearization, we can treat equation above as linear equation of
    ((N2)+N)+N=N(N+3)/2
    unknown variables.
  • But we can reduce
    (N2)+N
    unknown variables into
    N
    , because they shares same coefficients!
  • vi,k
    expresses coefficients of
    xjAi
    as we announced ealier.
  • Let
    R=(0001100001000010)
    then
    RTR=I
    ,
    vi,j=Rjvi,0
    • be used to express the multiplication of
      x
      .
  • vi,jvi,k=(Rjvi,0)T(Rjvi,kj)=vi,0T(Rj)T(Rj)vi,kj=vi,0vi,kj
  • So, a lot of terms sharing same coefficients
    (vi,0vi,j)
    .
  • Therefore, we can express equation above as below:
    yiyi=j=0N1(vi,0vi,j)c0,j+2j=0N1(vi,jbi)c0,j+(bibi)=140
  • We can treat equation above as linear equation of
    N+N=2N
    unknown variables

Efficient calculation

  • If
    f=i=0N1aixi
    , let
    f=i=0N1aN1ixi
  • Let
    f=i=0N1aixi
    ,
    g=i=0N1bixi
    . Then
    i=0N1aibi
    equals coefficient of term
    xN1
    in
    fg
  • vi,jvi,0
    equals coefficient of term
    xN1
    in
    xjAiAi
    , which is also coefficient of term
    xN1j
    in
    AiAi
  • As same reason,
    vi,jbi
    equals coefficient of term
    xN1j
    in
    AiBi
  • Therefore, we can
    2N
    coefficients for linear equation by calculating
    AiAi
    and
    AiBi
  • It reduces time for running solution
    O(N3)
    to
    O(N2logN)

Implementation

import os;os.environ["TERM"] = 'linux'
from pwn import *
from ntt import NTTDomain, NTTPoints
import hashlib
from tqdm import trange

#r = process(["python", "challenge.py"])
r = remote("54.78.163.105", "30859")

N, Q, W, P = 1024, 12289, 4324, 9389
PR.<x_> = PolynomialRing(GF(Q))
QR.<x> = PR.quotient(x_^N + 1)

def l2p(A):
    t = []
    while A:
        t.append(A % Q)
        A //= Q
    return QR(t)

def HashBall(m: bytes, tau: int) -> list:
    """ Deterministically generates sparse polynomials with weight tau. """
    if isinstance(m, str):
        m = m.encode()
    h = hashlib.sha256(m).digest()
    c = N * [0]
    for i in range(N - tau, N):
        hi = int(hashlib.sha256(h + i.to_bytes(2, 'big')).hexdigest(), 16)
        hi //= i; j = hi % i; hi //= i
        hi //= 2; k = hi % 2; hi //= 2
        c[i] = c[j]
        c[j] = (1 - 2 * k) % Q
    return c

def ntt(c):
    c = QR(c).lift()
    return QR([c(P * pow(W, i, Q)) for i in range(N)])

def intt(c):
    c = QR(c).lift()
    c = [c(pow(W, -i, Q)) * pow(N, -1, Q) for i in range(N)]
    c = [c[i] * pow(P, -i, Q) for i in range(N)]
    return QR(c)



r.recvuntil(b'= ')
data = eval(r.recvline().decode())
A = intt(l2p(int(data["A"], 16)))
E = intt(l2p(int(data["E"], 16)))


def get_sig(msg):
    r.recvuntil(b'|  Sig = ')
    sig = eval(r.recvline()[:-1].decode())
    sig_r = bytes.fromhex(sig["r"])
    sig_V = intt(l2p(int(sig["V"], 16)))
    sig_W = intt(l2p(int(sig["W"], 16)))
    sig_C = QR(HashBall(msg + sig_r, 38))
    sig_P = E * sig_V - A * sig_C + sig_W
    sig_Pint = sum([ZZ(j) * Q ** i for i,j in enumerate(ntt(sig_P).list())])
    sig_Pbyt = sig_Pint.to_bytes(-(-len(bin(sig_Pint)[2:]) // 8), 'big')
    assert sig_r == hashlib.sha256(sig_Pbyt.hex().encode()).digest()
    return sig_r, sig_V, sig_W, sig_C, sig_P

def send_n(msg, n):
    payload = b'S\n' + msg + b'\n'
    r.send(payload * n)

    

rs, Vs, Ws, Cs, Ps = [], [], [], [], []
m = 2 * N + 100
send_n(b'asdf', m)
for i in trange(m):
    r0, V0, W0, C0, P0 = get_sig(b'asdf')
    rs.append(r0)
    Vs.append(V0)
    Ws.append(W0)
    Cs.append(C0)
    Ps.append(P0)
As, Bs = [QR(1)], [QR(0)]
for i in trange(1, m):
    As.append(Vs[i] / Vs[0])
    Bs.append(Vs[i] / Vs[0] * Cs[0] - Cs[i])

M = []
for i in trange(m):
    Av = list(As[i]) + list(-As[i])
    Bv = list(Bs[i])
    coeff = list(As[i] * QR(list(As[i])[::-1]))[::-1]
    coeff += list(As[i] * QR(list(Bs[i])[::-1]))[::-1]
    #coeff.append(vector(Bs[i]) * vector(Bs[i]))
    M.append(coeff)

M = Matrix(GF(Q), M)
target_v = vector(GF(Q), [140 - vector(Bs[i]) * vector(Bs[i]) for i in trange(m)])
x = M.solve_right(target_v)
Y1_0 = QR(list(x[N:])) / 2
U_0 = Y1_0 + Cs[0]
G = Vs[0] / U_0
D = E * G - A

sig_Dint = sum([ZZ(j) * Q ** i for i,j in enumerate(ntt(D).list())])
sig_Dbyt = sig_Dint.to_bytes(-(-len(bin(sig_Dint)[2:]) // 8), 'big')
r.sendlineafter(b'> ', b'c')
r.sendlineafter(b') ', sig_Dbyt.hex().encode())
r.interactive()

Is it useful?

I'm personally curious whether this approach can be useful for any other signature/encryption schemes that operate over quotient rings as well.

(Although it doesn't seem to work with the original Eaglesign, where

G is a matrix not a ring element.)

At least, we should construct 2N equations

AiX+Bi=Yi, where
X
is unknown, and the sum of the squared coefficients of
Yi
is known.

(For example, we know number of nonzero coefficients and they must be -1 or 1)