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 () are randomly generated polynomial on Quotient Ring
- coefficients must be {-1, 0, 1}.
- must be invertible.
- Public key is randomly generated polynomial on same Quotient Ring
- Public key
- Sign
- Generate randomly
- has 140 nonzero coefficients, which are -1 or 1
- has bounded coefficients which absolute max are 64
- ,
- Generate with given message and
- has 38 nonzero coefficients, which are -1 or 1
- ,
- Signature is
- Verify
- Generate with given message and (same as Sign)
-
- Check equals
- Keys are generated and public Keys are given when code starts.
- We can sign for any message with no limit, and we need for get the flag.
My approach
- ()
- So, for every signature.
- We can express every as form
Building equation
-
Let , ,
-
-
Therefore, we can express it as Matrix equation below:
-
Let matrix
-
Let vectors ,
-
So we can express equation above as below:
-
Since Y1 has 140 nonzero coefficients which are -1 or 1, sum of square of coefficients equals 140, so . It leads to equation below:
Linearization & reduce unknown variables
- By linearization, we can treat equation above as linear equation of unknown variables.
- But we can reduce unknown variables into , because they shares same coefficients!
- expresses coefficients of as we announced ealier.
- Let then ,
- be used to express the multiplication of .
- So, a lot of terms sharing same coefficients .
- Therefore, we can express equation above as below:
- We can treat equation above as linear equation of unknown variables
Efficient calculation
- If , let
- Let , . Then equals coefficient of term in
- equals coefficient of term in , which is also coefficient of term in
- As same reason, equals coefficient of term in
- Therefore, we can coefficients for linear equation by calculating and
- It reduces time for running solution to
Implementation
import os;os.environ["TERM"] = 'linux'
from pwn import *
from ntt import NTTDomain, NTTPoints
import hashlib
from tqdm import trange
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]
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 is a matrix not a ring element.)
At least, we should construct 2N equations , where is unknown, and the sum of the squared coefficients of is known.
(For example, we know number of nonzero coefficients and they must be -1 or 1)