Skip to content

ecc_hardware_accelerator_NIST-B233 computes k⋅P: a number k, a point P on an elliptic curve, and the point you get from adding P to itself k times. This is almost the entire cost of an elliptic curve cryptosystem. Everything below exists to make it fast: field multiplication, squaring, inversion, point addition, point doubling.

The field is GF(2^233). The curve is sect233r1, also called NIST B-233 [3, 4]. The coordinates are Modified López–Dahab [1]. This document builds the design bottom up: field, then field operations, then point operations, then the loop that ties them together.

Source: miesource/ecc_hardware_accelerator_NIST-B233.

Notation ​

SymbolMeaning
Wfield width, 233 bits
f(x)reduction polynomial, x233+x74+1
a,b,cfield elements, 233-bit words
(X,Y,Z)a point in projective coordinates
(x,y)the same point in affine coordinates: x=X/Z, y=Y/Z2
a2the curve's binary constant, 1 for this curve
kthe scalar
Pthe base point

The field: GF(2^233) ​

An element of GF(2^233) is a binary polynomial of degree less than 233, stored as a 233-bit word:

a(x)=a232x232+⋯+a1x+a0,ai∈0,1

Addition is XOR. There is no carry, so a + b and a - b are the same operation.

Multiplication is polynomial multiplication followed by reduction, using

f(x)=x233+x74+1

the standard reduction polynomial for B-233 [3]. Whenever a product's degree reaches 233 or higher, replace x233 with x74+1, since f(x)=0 in the field. Repeat until the degree drops below 233.

Almost every operation below depends on one function: multiply by x, then reduce if it overflowed.

vhdl
CONSTANT REDUCTION_LOW : UNSIGNED(W-1 DOWNTO 0) :=
    (74 => '1', 0 => '1', OTHERS => '0');

FUNCTION xtimes(p : UNSIGNED(W-1 DOWNTO 0)) RETURN UNSIGNED IS
    VARIABLE shifted : UNSIGNED(W DOWNTO 0);
BEGIN
    shifted := p & '0';
    IF shifted(W) = '1' THEN
        RETURN shifted(W-1 DOWNTO 0) XOR REDUCTION_LOW;
    ELSE
        RETURN shifted(W-1 DOWNTO 0);
    END IF;
END FUNCTION;

REDUCTION_LOW is x74+1. xtimes shifts left by one bit into a 234-bit register. If the bit that fell off the top is 1, it XORs in REDUCTION_LOW. This function lives in gf2m_pkg, and every module below calls it.

Multiplication ​

Multiply two field elements, a and b:

a⋅b=∑i=0232ai(b⋅ximodf(x))

Read one bit of a at a time. If it is 1, add the current shifted copy of b. Shift b by one more power of x.

c ← 0
for i in 0 to 232:
    if bit i of a = 1:
        c ← c XOR b
    b ← xtimes(b)
return c
vhdl
ENTITY gf2m_mult_serial IS
    PORT (
        clk     : IN  STD_LOGIC;
        rst_n   : IN  STD_LOGIC;
        start   : IN  STD_LOGIC;
        a_in    : IN  UNSIGNED(W-1 DOWNTO 0);
        b_in    : IN  UNSIGNED(W-1 DOWNTO 0);
        busy    : OUT STD_LOGIC;
        done    : OUT STD_LOGIC;
        product : OUT UNSIGNED(W-1 DOWNTO 0)
    );
END ENTITY gf2m_mult_serial;

ARCHITECTURE rtl OF gf2m_mult_serial IS

    TYPE state_t IS (IDLE, RUN, DONE_ST);
    SIGNAL state : state_t;

    SIGNAL a_r, b_r, c_r : UNSIGNED(W-1 DOWNTO 0);
    SIGNAL cnt : INTEGER RANGE 0 TO W-1;

BEGIN

    PROCESS (clk, rst_n)
    BEGIN
        IF rst_n = '0' THEN
            state   <= IDLE;
            a_r     <= (OTHERS => '0');
            b_r     <= (OTHERS => '0');
            c_r     <= (OTHERS => '0');
            cnt     <= 0;
            done    <= '0';
            product <= (OTHERS => '0');
        ELSIF RISING_EDGE(clk) THEN
            done <= '0';
            CASE state IS
                WHEN IDLE =>
                    IF start = '1' THEN
                        IF a_in(0) = '1' THEN
                            c_r <= b_in;
                        ELSE
                            c_r <= (OTHERS => '0');
                        END IF;
                        b_r   <= xtimes(b_in);
                        a_r   <= SHIFT_RIGHT(a_in, 1);
                        cnt   <= 1;
                        state <= RUN;
                    END IF;

                WHEN RUN =>
                    IF a_r(0) = '1' THEN
                        c_r <= c_r XOR b_r;
                    END IF;

                    IF cnt = W-1 THEN
                        state <= DONE_ST;
                    ELSE
                        b_r <= xtimes(b_r);
                        a_r <= SHIFT_RIGHT(a_r, 1);
                        cnt <= cnt + 1;
                    END IF;

                WHEN DONE_ST =>
                    product <= c_r;
                    done    <= '1';
                    state   <= IDLE;

            END CASE;
        END IF;
    END PROCESS;

    busy <= '0' WHEN state = IDLE ELSE '1';

END ARCHITECTURE rtl;

FSM: IDLE to RUN on start, RUN loops on itself 232 times testing a_r bit 0 and updating b_r via xtimes, then to DONE, then back to IDLE

Cost. 233 cycles per call: one bit of a per cycle, one cycle to publish the result. One multiplier exists in the whole design. Every module below shares it or instantiates its own copy.

Squaring ​

Square a field element. In characteristic 2, squaring is linear:

a(x)2=(∑iaixi)2=∑iai2x2i+∑i≠j2aiajxi+j=∑iaix2i

The cross terms carry a factor of 2, and 2=0 in this field, so they vanish. Squaring spreads each bit to twice its position, then reduces.

for each bit i of a:
    if a_i = 1: set bit 2i of a wide (465-bit) result
reduce the wide result mod f(x), from the top bit down

Precedence diagram: spread each bit i to position 2i, then reduce modulo f(x)

vhdl
FUNCTION gf_square(a : UNSIGNED(W-1 DOWNTO 0)) RETURN UNSIGNED IS
    VARIABLE wide : UNSIGNED(2*W-2 DOWNTO 0);
BEGIN
    wide := (OTHERS => '0');
    FOR i IN 0 TO W-1 LOOP
        wide(2*i) := a(i);
    END LOOP;
    FOR i IN 2*W-2 DOWNTO W LOOP
        IF wide(i) = '1' THEN
            wide(i)      := '0';
            wide(i-W)    := wide(i-W)    XOR '1';
            wide(i-W+74) := wide(i-W+74) XOR '1';
        END IF;
    END LOOP;
    RETURN wide(W-1 DOWNTO 0);
END FUNCTION;

Bit i≥233 stands for xi=xi−233⋅x233≡xi−233⋅(x74+1). The code clears that bit and XORs it into positions i-W and i-W+74. Same reduction constant as xtimes, applied at an arbitrary degree.

Cost. Zero cycles. gf_square is combinational. Point doubling and inversion both call it constantly for free.

Inversion ​

Every nonzero element of GF(2^233) has an inverse. The multiplicative group has order 2233−1, so

a2233−1=1⟹a−1=a2233−2

That exponent has 232 ones in it: 232 multiplications by naive repeated squaring.

The Itoh–Tsujii algorithm [2] does it in far fewer, because squaring is free here. Write a−1=(a2232−1)2, and define ek(a)=a2k−1. Two identities build ek quickly:

e2k(a)=ek(a)2k⋅ek(a),ek+1(a)=ek(a)2⋅a

The first doubles k: one multiplication, k free squarings (the field's Frobenius map, applied k times). The second increments k by one: one more multiplication. Chaining these by the binary digits of 232 reaches e232(a) in about as many multiplications as 232 has bits. One final squaring gives a−1.

232 in binary is 11101000. After the leading 1, each bit is a doubling step, plus an increment step where the bit is 1:

s ← a,  k ← 1
for each bit after the leading 1 of (m-1):
    s ← frobenius(s, k) · s        # e_k → e_2k
    k ← 2k
    if bit = 1:
        s ← frobenius(s, 1) · a    # e_k → e_{k+1}
        k ← k + 1
return frobenius(s, 1)             # e_{m-1}(a)^2 = a^-1

Seven doubling steps, three increment steps: ten multiplications effectively reducing 232 to just 10.

vhdl
ENTITY gf2m_inverse IS
    PORT (
        clk     : IN  STD_LOGIC;
        rst_n   : IN  STD_LOGIC;
        start   : IN  STD_LOGIC;
        a_in    : IN  UNSIGNED(W-1 DOWNTO 0);
        busy    : OUT STD_LOGIC;
        done    : OUT STD_LOGIC;
        inv_out : OUT UNSIGNED(W-1 DOWNTO 0)
    );
END ENTITY gf2m_inverse;

ARCHITECTURE rtl OF gf2m_inverse IS

    CONSTANT M_MINUS_1 : INTEGER := W - 1;
    CONSTANT NBITS      : INTEGER := 8;
    CONSTANT BITS_C : UNSIGNED(NBITS-1 DOWNTO 0) := TO_UNSIGNED(M_MINUS_1, NBITS);

    TYPE state_t IS (
        IDLE,
        DBL_FROB_START, DBL_FROB_WAIT,
        DBL_MUL_START,  DBL_MUL_WAIT,
        INCR_SQ,
        INCR_MUL_START, INCR_MUL_WAIT,
        FINAL_SQ,
        DONE_ST
    );
    SIGNAL state : state_t;

    SIGNAL a_r      : UNSIGNED(W-1 DOWNTO 0);
    SIGNAL s_r       : UNSIGNED(W-1 DOWNTO 0);
    SIGNAL s_frob_r  : UNSIGNED(W-1 DOWNTO 0);
    SIGNAL k_r       : UNSIGNED(9 DOWNTO 0);
    SIGNAL frob_cnt   : UNSIGNED(9 DOWNTO 0);
    SIGNAL bit_idx   : INTEGER RANGE 0 TO NBITS-1;

    SIGNAL m_start : STD_LOGIC;
    SIGNAL mul_a, mul_b, mul_product : UNSIGNED(W-1 DOWNTO 0);
    SIGNAL mul_busy, mul_done : STD_LOGIC;

BEGIN

    MULT : ENTITY work.gf2m_mult_serial
        PORT MAP (
            clk => clk, rst_n => rst_n,
            start => m_start, a_in => mul_a, b_in => mul_b,
            busy => mul_busy, done => mul_done, product => mul_product
        );

    PROCESS (clk, rst_n)
    BEGIN
        IF rst_n = '0' THEN
            state     <= IDLE;
            a_r       <= (OTHERS => '0');
            s_r       <= (OTHERS => '0');
            s_frob_r  <= (OTHERS => '0');
            k_r       <= (OTHERS => '0');
            frob_cnt  <= (OTHERS => '0');
            bit_idx   <= 0;
            m_start   <= '0';
            mul_a     <= (OTHERS => '0');
            mul_b     <= (OTHERS => '0');
            done      <= '0';
            inv_out   <= (OTHERS => '0');
        ELSIF RISING_EDGE(clk) THEN
            m_start <= '0';
            done    <= '0';

            CASE state IS
                WHEN IDLE =>
                    IF start = '1' THEN
                        a_r     <= a_in;
                        s_r     <= a_in;
                        k_r     <= TO_UNSIGNED(1, 10);
                        bit_idx <= NBITS - 2;
                        state   <= DBL_FROB_START;
                    END IF;

                WHEN DBL_FROB_START =>
                    s_frob_r <= s_r;
                    frob_cnt <= k_r;
                    state    <= DBL_FROB_WAIT;

                WHEN DBL_FROB_WAIT =>
                    IF frob_cnt = 0 THEN
                        mul_a   <= s_frob_r;
                        mul_b   <= s_r;
                        m_start <= '1';
                        state   <= DBL_MUL_START;
                    ELSE
                        s_frob_r <= gf_square(s_frob_r);
                        frob_cnt <= frob_cnt - 1;
                    END IF;

                WHEN DBL_MUL_START =>
                    IF mul_done = '1' THEN
                        s_r <= mul_product;
                        k_r <= SHIFT_LEFT(k_r, 1);
                        IF BITS_C(bit_idx) = '1' THEN
                            state <= INCR_SQ;
                        ELSE
                            state <= DBL_MUL_WAIT;
                        END IF;
                    END IF;

                WHEN DBL_MUL_WAIT =>
                    IF bit_idx = 0 THEN
                        state <= FINAL_SQ;
                    ELSE
                        bit_idx <= bit_idx - 1;
                        state   <= DBL_FROB_START;
                    END IF;

                WHEN INCR_SQ =>
                    mul_a   <= gf_square(s_r);
                    mul_b   <= a_r;
                    m_start <= '1';
                    state   <= INCR_MUL_START;

                WHEN INCR_MUL_START =>
                    IF mul_done = '1' THEN
                        s_r <= mul_product;
                        k_r <= k_r + 1;
                        state <= INCR_MUL_WAIT;
                    END IF;

                WHEN INCR_MUL_WAIT =>
                    IF bit_idx = 0 THEN
                        state <= FINAL_SQ;
                    ELSE
                        bit_idx <= bit_idx - 1;
                        state   <= DBL_FROB_START;
                    END IF;

                WHEN FINAL_SQ =>
                    inv_out <= gf_square(s_r);
                    state   <= DONE_ST;

                WHEN DONE_ST =>
                    done  <= '1';
                    state <= IDLE;

            END CASE;
        END IF;
    END PROCESS;

    busy <= '0' WHEN state = IDLE ELSE '1';

END ARCHITECTURE rtl;

s_r holds ek(a). k_r tracks k. frob_cnt counts down while DBL_FROB_WAIT applies gf_square k times, the Frobenius map, one free squaring per cycle. BITS_C is 232. bit_idx walks its bits below the leading one.

FSM: IDLE to DOUBLE on start, DOUBLE loops on itself when the bit is 0, or goes to INCREMENT and back when the bit is 1, then to FINAL SQUARE, then DONE, then back to IDLE

Cost. Ten multiplier calls instead of 232: roughly 10 × 233 cycles plus free squaring cycles for the Frobenius steps. Inversion is still the single most expensive operation here, but Itoh–Tsujii keeps it from dominating everything else.

Coordinates: why not affine ​

Affine addition divides by x2−x1: one inversion per point addition. At ten multiplications per inversion, that would dominate a 233-bit scalar multiplication loop.

López–Dahab projective coordinates [1] carry an extra coordinate Z and defer the division:

x=XZ,y=YZ2

Addition and doubling use only multiplication, squaring, and addition. No division inside the loop. One inversion happens once, at the end, converting the final point back to affine.

The "modified" part removes four multiplications from addition, for the case where one point has Z=1 [1]. That is exactly the case for a fixed base point P added repeatedly into a moving accumulator: P never leaves affine form, so its Z stays 1 for the whole computation.

Point doubling ​

Double (X1,Y1,Z1). No assumption on Z1: it must work on any point.

S=X12U=S+Y1T=X1⋅Z1Z3=T2T′=U⋅TX3=U2+T′+a2Z3Y3=(Z3+T′)⋅X3+S2⋅Z3
S  ← square(X1)
U  ← S + Y1
T  ← mult(X1, Z1)
Z3 ← square(T)
T  ← mult(U, T)
X3 ← square(U) + T + a2·Z3
Y3 ← mult(Z3 + T, X3) + square(S)·Z3

Precedence graph for point doubling: S and T computed in parallel from the inputs, then U from S, Z3 from T, then T' from U and T, then X3 from U, T', Z3, then Y3 from S, Z3, T', X3

vhdl
ENTITY ld_point_double IS
    PORT (
        clk    : IN  STD_LOGIC;
        rst_n  : IN  STD_LOGIC;
        start  : IN  STD_LOGIC;
        x1_in  : IN  UNSIGNED(W-1 DOWNTO 0);
        y1_in  : IN  UNSIGNED(W-1 DOWNTO 0);
        z1_in  : IN  UNSIGNED(W-1 DOWNTO 0);
        busy   : OUT STD_LOGIC;
        done   : OUT STD_LOGIC;
        x3_out : OUT UNSIGNED(W-1 DOWNTO 0);
        y3_out : OUT UNSIGNED(W-1 DOWNTO 0);
        z3_out : OUT UNSIGNED(W-1 DOWNTO 0)
    );
END ENTITY ld_point_double;

ARCHITECTURE rtl OF ld_point_double IS

    TYPE state_t IS (
        IDLE,
        ST_S_U,
        ST_MUL1_START,
        ST_Z3,
        ST_MUL2_START, ST_MUL2_WAIT,
        ST_U2_X3,
        ST_S2,
        ST_MUL3_START, ST_MUL3_WAIT,
        ST_MUL4_START, ST_MUL4_WAIT,
        ST_Y3,
        DONE_ST
    );
    SIGNAL state : state_t;

    SIGNAL x1_r, z1_r : UNSIGNED(W-1 DOWNTO 0);
    SIGNAL s_r, u_r, t_r, z3_r, u2_r, s2_r, x3_r, term1_r, y3_r : UNSIGNED(W-1 DOWNTO 0);

    SIGNAL mul_start : STD_LOGIC;
    SIGNAL mul_busy, mul_done : STD_LOGIC;
    SIGNAL mul_a, mul_b, mul_product : UNSIGNED(W-1 DOWNTO 0);

BEGIN

    MULT : ENTITY work.gf2m_mult_serial
        PORT MAP (clk, rst_n, mul_start, mul_a, mul_b, mul_busy, mul_done, mul_product);

    PROCESS (clk, rst_n)
    BEGIN
        IF rst_n = '0' THEN
            state     <= IDLE;
            mul_start <= '0';
            done      <= '0';
            x1_r <= (OTHERS => '0'); z1_r <= (OTHERS => '0');
            s_r <= (OTHERS => '0'); u_r <= (OTHERS => '0'); t_r <= (OTHERS => '0');
            z3_r <= (OTHERS => '0');
            u2_r <= (OTHERS => '0'); s2_r <= (OTHERS => '0'); x3_r <= (OTHERS => '0');
            term1_r <= (OTHERS => '0'); y3_r <= (OTHERS => '0');
            x3_out <= (OTHERS => '0'); y3_out <= (OTHERS => '0'); z3_out <= (OTHERS => '0');
        ELSIF RISING_EDGE(clk) THEN
            mul_start <= '0';
            done      <= '0';

            CASE state IS
                WHEN IDLE =>
                    IF start = '1' THEN
                        x1_r  <= x1_in;
                        z1_r  <= z1_in;
                        s_r   <= gf_square(x1_in);
                        u_r   <= gf_square(x1_in) XOR y1_in;
                        state <= ST_S_U;
                    END IF;

                WHEN ST_S_U =>
                    mul_a     <= x1_r;
                    mul_b     <= z1_r;
                    mul_start <= '1';
                    state     <= ST_MUL1_START;

                WHEN ST_MUL1_START =>
                    IF mul_done = '1' THEN
                        t_r   <= mul_product;
                        state <= ST_Z3;
                    END IF;

                WHEN ST_Z3 =>
                    z3_r  <= gf_square(t_r);
                    state <= ST_MUL2_START;

                WHEN ST_MUL2_START =>
                    mul_a     <= u_r;
                    mul_b     <= t_r;
                    mul_start <= '1';
                    state     <= ST_MUL2_WAIT;

                WHEN ST_MUL2_WAIT =>
                    IF mul_done = '1' THEN
                        t_r   <= mul_product;
                        state <= ST_U2_X3;
                    END IF;

                WHEN ST_U2_X3 =>
                    u2_r  <= gf_square(u_r);
                    x3_r  <= gf_square(u_r) XOR t_r XOR mul_a2(z3_r);
                    state <= ST_S2;

                WHEN ST_S2 =>
                    s2_r  <= gf_square(s_r);
                    state <= ST_MUL3_START;

                WHEN ST_MUL3_START =>
                    mul_a     <= z3_r XOR t_r;
                    mul_b     <= x3_r;
                    mul_start <= '1';
                    state     <= ST_MUL3_WAIT;

                WHEN ST_MUL3_WAIT =>
                    IF mul_done = '1' THEN
                        term1_r <= mul_product;
                        state   <= ST_MUL4_START;
                    END IF;

                WHEN ST_MUL4_START =>
                    mul_a     <= s2_r;
                    mul_b     <= z3_r;
                    mul_start <= '1';
                    state     <= ST_MUL4_WAIT;

                WHEN ST_MUL4_WAIT =>
                    IF mul_done = '1' THEN
                        y3_r  <= term1_r XOR mul_product;
                        state <= ST_Y3;
                    END IF;

                WHEN ST_Y3 =>
                    x3_out <= x3_r;
                    y3_out <= y3_r;
                    z3_out <= z3_r;
                    state  <= DONE_ST;

                WHEN DONE_ST =>
                    done  <= '1';
                    state <= IDLE;

            END CASE;
        END IF;
    END PROCESS;

    busy <= '0' WHEN state = IDLE ELSE '1';

END ARCHITECTURE rtl;

t_r holds two different values in sequence: first T=X1Z1, later T′=U⋅T. The first is consumed by ST_Z3 before the second is written. mul_a2 applies the a2Z3 term generically, so the same code handles a2∈{0,1}.

FSM: IDLE to BUSY on start, BUSY loops on itself for four multiplier calls in the order of the precedence graph, then DONE, then back to IDLE

Cost. Four multiplier calls, roughly 4 × 233 cycles. No division and no restriction on Z1.

Point addition ​

Add a fixed point P1=(X1,Y1,1) to a general point Q=(X2,Y2,Z2). Fixing Z1=1 removes several multiplications a general addition would need [1]:

U=Z22⋅Y1+Y2S=Z2⋅X1+X2T=Z2⋅SZ3=T2V=Z3⋅X1C=X1+Y1X3=U2+T⋅(U+S2+a2T)Y3=(V+X3)⋅(T⋅U+Z3)+Z32⋅C
U  ← mult(square(Z2), Y1) + Y2
S  ← mult(Z2, X1) + X2
T  ← mult(Z2, S)
Z3 ← square(T)
V  ← mult(Z3, X1)
C  ← X1 + Y1
X3 ← square(U) + mult(T, U + square(S) + a2·T)
TU ← mult(T, U)
Y3 ← mult(V + X3, TU + Z3) + mult(square(Z3), C)

Eight multiplications, twice a doubling: more field elements in play and less symmetry to exploit.

Precedence graph for point addition: U, S, C computed in parallel from the inputs, then T from S, then Z3 from T, then V from Z3, TU from T and U, X3 from T, U, S, then Y3 from X3, V, TU, Z3, C

vhdl
ENTITY ld_point_add IS
    PORT (
        clk    : IN  STD_LOGIC;
        rst_n  : IN  STD_LOGIC;
        start  : IN  STD_LOGIC;
        x1_in  : IN  UNSIGNED(W-1 DOWNTO 0);
        y1_in  : IN  UNSIGNED(W-1 DOWNTO 0);
        x2_in  : IN  UNSIGNED(W-1 DOWNTO 0);
        y2_in  : IN  UNSIGNED(W-1 DOWNTO 0);
        z2_in  : IN  UNSIGNED(W-1 DOWNTO 0);
        busy   : OUT STD_LOGIC;
        done   : OUT STD_LOGIC;
        x3_out : OUT UNSIGNED(W-1 DOWNTO 0);
        y3_out : OUT UNSIGNED(W-1 DOWNTO 0);
        z3_out : OUT UNSIGNED(W-1 DOWNTO 0)
    );
END ENTITY ld_point_add;

No z1_in port. The formulas above require Z1=1; the entity does not expose a signal that could violate it. x1_in and y1_in are wired in directly, as if Z1 were already 1.

vhdl
ARCHITECTURE rtl OF ld_point_add IS

    TYPE state_t IS (
        IDLE,
        ST_MUL1_START,
        ST_MUL2_START,
        ST_MUL3_START, ST_MUL3_WAIT,
        ST_Z3,
        ST_MUL4_START, ST_MUL4_WAIT,
        ST_MUL5_START, ST_MUL5_WAIT,
        ST_MUL6_START,
        ST_X3,
        ST_MUL7_START, ST_MUL7_WAIT,
        ST_MUL8_START,
        ST_Y3,
        DONE_ST
    );
    SIGNAL state : state_t;

    SIGNAL x1_r, y1_r, x2_r, y2_r, z2_r : UNSIGNED(W-1 DOWNTO 0);
    SIGNAL u_r, s_r, t_r, z3_r, v_r, c_r, x3_r, tu_r, y3_r : UNSIGNED(W-1 DOWNTO 0);

    SIGNAL mul_start : STD_LOGIC;
    SIGNAL mul_busy, mul_done : STD_LOGIC;
    SIGNAL mul_a, mul_b, mul_product : UNSIGNED(W-1 DOWNTO 0);

BEGIN

    MULT : ENTITY work.gf2m_mult_serial
        PORT MAP (clk, rst_n, mul_start, mul_a, mul_b, mul_busy, mul_done, mul_product);

    PROCESS (clk, rst_n)
    BEGIN
        IF rst_n = '0' THEN
            state <= IDLE;
            mul_start <= '0';
            done <= '0';
            x1_r <= (OTHERS => '0'); y1_r <= (OTHERS => '0');
            x2_r <= (OTHERS => '0'); y2_r <= (OTHERS => '0'); z2_r <= (OTHERS => '0');
            u_r <= (OTHERS => '0'); s_r <= (OTHERS => '0'); t_r <= (OTHERS => '0');
            z3_r <= (OTHERS => '0'); v_r <= (OTHERS => '0'); c_r <= (OTHERS => '0');
            x3_r <= (OTHERS => '0'); tu_r <= (OTHERS => '0'); y3_r <= (OTHERS => '0');
            x3_out <= (OTHERS => '0'); y3_out <= (OTHERS => '0'); z3_out <= (OTHERS => '0');
        ELSIF RISING_EDGE(clk) THEN
            mul_start <= '0';
            done      <= '0';

            CASE state IS
                WHEN IDLE =>
                    IF start = '1' THEN
                        x1_r <= x1_in; y1_r <= y1_in;
                        x2_r <= x2_in; y2_r <= y2_in; z2_r <= z2_in;
                        c_r  <= x1_in XOR y1_in;
                        mul_a <= gf_square(z2_in);
                        mul_b <= y1_in;
                        mul_start <= '1';
                        state <= ST_MUL1_START;
                    END IF;

                WHEN ST_MUL1_START =>
                    IF mul_done = '1' THEN
                        u_r   <= mul_product XOR y2_r;
                        mul_a <= z2_r;
                        mul_b <= x1_r;
                        mul_start <= '1';
                        state <= ST_MUL2_START;
                    END IF;

                WHEN ST_MUL2_START =>
                    IF mul_done = '1' THEN
                        s_r   <= mul_product XOR x2_r;
                        mul_a <= z2_r;
                        state <= ST_MUL3_START;
                    END IF;

                WHEN ST_MUL3_START =>
                    mul_b     <= s_r;
                    mul_start <= '1';
                    state     <= ST_MUL3_WAIT;

                WHEN ST_MUL3_WAIT =>
                    IF mul_done = '1' THEN
                        t_r   <= mul_product;
                        state <= ST_Z3;
                    END IF;

                WHEN ST_Z3 =>
                    z3_r  <= gf_square(t_r);
                    state <= ST_MUL4_START;

                WHEN ST_MUL4_START =>
                    mul_a     <= z3_r;
                    mul_b     <= x1_r;
                    mul_start <= '1';
                    state     <= ST_MUL4_WAIT;

                WHEN ST_MUL4_WAIT =>
                    IF mul_done = '1' THEN
                        v_r   <= mul_product;
                        state <= ST_MUL5_START;
                    END IF;

                WHEN ST_MUL5_START =>
                    mul_a     <= t_r;
                    mul_b     <= u_r XOR gf_square(s_r) XOR mul_a2(t_r);
                    mul_start <= '1';
                    state     <= ST_MUL5_WAIT;

                WHEN ST_MUL5_WAIT =>
                    IF mul_done = '1' THEN
                        x3_r  <= gf_square(u_r) XOR mul_product;
                        mul_a <= t_r;
                        mul_b <= u_r;
                        mul_start <= '1';
                        state <= ST_MUL6_START;
                    END IF;

                WHEN ST_MUL6_START =>
                    IF mul_done = '1' THEN
                        tu_r  <= mul_product;
                        state <= ST_X3;
                    END IF;

                WHEN ST_X3 =>
                    state <= ST_MUL7_START;

                WHEN ST_MUL7_START =>
                    mul_a     <= v_r XOR x3_r;
                    mul_b     <= tu_r XOR z3_r;
                    mul_start <= '1';
                    state     <= ST_MUL7_WAIT;

                WHEN ST_MUL7_WAIT =>
                    IF mul_done = '1' THEN
                        y3_r  <= mul_product;
                        mul_a <= gf_square(z3_r);
                        mul_b <= c_r;
                        mul_start <= '1';
                        state <= ST_MUL8_START;
                    END IF;

                WHEN ST_MUL8_START =>
                    IF mul_done = '1' THEN
                        y3_r  <= y3_r XOR mul_product;
                        state <= ST_Y3;
                    END IF;

                WHEN ST_Y3 =>
                    x3_out <= x3_r;
                    y3_out <= y3_r;
                    z3_out <= z3_r;
                    state  <= DONE_ST;

                WHEN DONE_ST =>
                    done  <= '1';
                    state <= IDLE;

            END CASE;
        END IF;
    END PROCESS;

    busy <= '0' WHEN state = IDLE ELSE '1';

END ARCHITECTURE rtl;

FSM: IDLE to BUSY on start, BUSY loops on itself for eight multiplier calls in the order of the precedence graph, then DONE, then back to IDLE

Cost. Eight multiplier calls, roughly 8 × 233 cycles. This module cannot add two points that both have Z=1 and share the same X; that is the doubling case, and the caller must avoid it.

Scalar multiplication ​

Compute k⋅P by walking the bits of k: double the accumulator each step, add P where the bit is 1.

kP=∑i=0n−1ki2iP

A right-to-left walk would double the base point itself; after the first doubling it would no longer have Z=1, breaking the addition formula's one requirement. Walking left to right instead, doubling the accumulator and adding the fixed base point, keeps Z1=1 true on every call:

acc ← point at infinity
for i from (n-1) downto 0:
    acc ← double(acc)
    if bit i of k = 1:
        acc ← add(P, acc)          # P always has Z = 1
return to_affine(acc)

double on infinity, and add when the accumulator is still infinity, are not cases the formulas above handle. The VHDL treats both explicitly.

vhdl
ENTITY ecc_hardware_accelerator_NIST-B233 IS
    GENERIC (
        KBITS : INTEGER := W
    );
    PORT (
        clk    : IN  STD_LOGIC;
        rst_n  : IN  STD_LOGIC;
        start  : IN  STD_LOGIC;
        k_in   : IN  UNSIGNED(KBITS-1 DOWNTO 0);
        k_len  : IN  INTEGER RANGE 0 TO KBITS;
        px_in  : IN  UNSIGNED(W-1 DOWNTO 0);
        py_in  : IN  UNSIGNED(W-1 DOWNTO 0);
        busy   : OUT STD_LOGIC;
        done   : OUT STD_LOGIC;
        result_is_infinity : OUT STD_LOGIC;
        qx_out : OUT UNSIGNED(W-1 DOWNTO 0);
        qy_out : OUT UNSIGNED(W-1 DOWNTO 0)
    );
END ENTITY ecc_hardware_accelerator_NIST-B233;

k_len = 0 is a shortcut for k=0: reported as infinity, no loop run. The doubler, adder, inverter, and multiplier are each instantiated once:

vhdl
ARCHITECTURE rtl OF ecc_hardware_accelerator_NIST-B233 IS
    ...
BEGIN

    PD : ENTITY work.ld_point_double
        PORT MAP (
            clk => clk, rst_n => rst_n,
            start => pd_start,
            x1_in => xacc, y1_in => yacc, z1_in => zacc,
            busy => pd_busy, done => pd_done,
            x3_out => pd_x3, y3_out => pd_y3, z3_out => pd_z3
        );

    PA : ENTITY work.ld_point_add
        PORT MAP (
            clk => clk, rst_n => rst_n,
            start => pa_start,
            x1_in => px_r, y1_in => py_r,
            x2_in => xacc, y2_in => yacc, z2_in => zacc,
            busy => pa_busy, done => pa_done,
            x3_out => pa_x3, y3_out => pa_y3, z3_out => pa_z3
        );

PD always doubles the accumulator. PA always adds the fixed base point into it. The bit loop:

vhdl
                WHEN BIT_LOOP_DBL_START =>
                    pd_start <= '1';
                    state    <= BIT_LOOP_DBL_WAIT;

                WHEN BIT_LOOP_DBL_WAIT =>
                    IF pd_done = '1' THEN
                        xacc <= pd_x3;
                        yacc <= pd_y3;
                        zacc <= pd_z3;
                        IF k_r(bit_idx) = '1' THEN
                            IF pd_z3 = TO_UNSIGNED(0, W) THEN
                                xacc  <= px_r;
                                yacc  <= py_r;
                                zacc  <= TO_UNSIGNED(1, W);
                                state <= BIT_LOOP_NEXT;
                            ELSE
                                state <= BIT_LOOP_ADD_START;
                            END IF;
                        ELSE
                            state <= BIT_LOOP_NEXT;
                        END IF;
                    END IF;

                WHEN BIT_LOOP_ADD_START =>
                    pa_start <= '1';
                    state    <= BIT_LOOP_ADD_WAIT;

                WHEN BIT_LOOP_ADD_WAIT =>
                    IF pa_done = '1' THEN
                        xacc  <= pa_x3;
                        yacc  <= pa_y3;
                        zacc  <= pa_z3;
                        state <= BIT_LOOP_NEXT;
                    END IF;

                WHEN BIT_LOOP_NEXT =>
                    IF bit_idx = 0 THEN
                        state <= CONVERT_ZINV_START;
                    ELSE
                        bit_idx <= bit_idx - 1;
                        state   <= BIT_LOOP_DBL_START;
                    END IF;

The accumulator starts at (0,0,0), treated as infinity. Doubling infinity keeps Z=0: T=X1Z1=0 when Z1=0, so Z3=T2=0. The check pd_z3 = 0 catches the moment a bit says "add a point" but the accumulator is still infinity; the state machine loads the base point directly instead of calling ld_point_add with a Z2=0 operand.

Converting back to affine uses the same relation as before. Note y=Y/Z2, not y=Y: that shortcut only holds when Z=1, which after a real scalar multiplication it essentially never the case.

vhdl
                WHEN CONVERT_ZINV_START =>
                    IF zacc = TO_UNSIGNED(0, W) THEN
                        result_is_infinity <= '1';
                        state <= DONE_ST;
                    ELSE
                        inv_start <= '1';
                        state     <= CONVERT_ZINV_WAIT;
                    END IF;

                WHEN CONVERT_ZINV_WAIT =>
                    IF inv_done = '1' THEN
                        zinv_r <= inv_out;
                        state  <= CONVERT_ZINV2;
                    END IF;

                WHEN CONVERT_ZINV2 =>
                    zinv2_r   <= gf_square(zinv_r);
                    mul_a     <= xacc;
                    mul_b     <= zinv_r;
                    mul_start <= '1';
                    state     <= CONVERT_MULX_START;

                WHEN CONVERT_MULX_START =>
                    IF mul_done = '1' THEN
                        qx_out <= mul_product;
                        mul_a  <= yacc;
                        mul_b  <= zinv2_r;
                        mul_start <= '1';
                        state  <= CONVERT_MULY_START;
                    END IF;

                WHEN CONVERT_MULY_START =>
                    IF mul_done = '1' THEN
                        qy_out <= mul_product;
                        state  <= DONE_ST;
                    END IF;

zinv2_r is computed with gf_square at no extra cost, alongside starting the multiplier on xacc * zinv_r.

FSM: IDLE to DOUBLE on start, DOUBLE goes to NEXT BIT on bit 0, to ADD or LOAD BASE POINT on bit 1 depending on whether the accumulator is still infinity, both returning to NEXT BIT, which either loops back to DOUBLE for more bits or moves on to CONVERT TO AFFINE, then DONE, then back to IDLE

Cost. One inversion (about ten multiplier calls), two more calls for the coordinate multiplications paid once. On top of that: up to 233 doublings, and one addition for every set bit of k.

Reaching real hardware ​

Simulation checks the formulas. It does not check a bitstream on a real FPGA, over real wires. ecc_uart_top wraps the scalar multiplier in a UART protocol.

NBYTES=⌈2338⌉=30

A scalar arrives as 30 bytes, big-endian. The reply is 61 bytes: one status byte, then Qx and Qy, 30 bytes each, fixed length whether the result is a real point or infinity.

vhdl
CONSTANT GX : UNSIGNED(W-1 DOWNTO 0) :=
    233X"0fac9dfcbac8313bb2139f1bb755fef65bc391f8b36f8f8eb7371fd558b";
CONSTANT GY : UNSIGNED(W-1 DOWNTO 0) :=
    233X"1006a08a41903350678e58528bebf8a0beff867a7ca36716f7e01f81052";

CONSTANT NBYTES   : INTEGER := (W + 7) / 8;
CONSTANT PAD_BITS : INTEGER := 8*NBYTES - W;

GX, GY are sect233r1's generator [3], wired into px_in/py_in, so the host only sends k.

vhdl
                WHEN LOAD_RESPONSE =>
                    IF ecc_inf = '1' THEN
                        tx_shift <= X"01" &
                                    STD_LOGIC_VECTOR(TO_UNSIGNED(0, 8*NBYTES)) &
                                    STD_LOGIC_VECTOR(TO_UNSIGNED(0, 8*NBYTES));
                    ELSE
                        tx_shift <= X"00" &
                                    STD_LOGIC_VECTOR(TO_UNSIGNED(0, PAD_BITS)) & STD_LOGIC_VECTOR(ecc_qx) &
                                    STD_LOGIC_VECTOR(TO_UNSIGNED(0, PAD_BITS)) & STD_LOGIC_VECTOR(ecc_qy);
                    END IF;
                    tx_byte_cnt <= 0;
                    state       <= TX_BYTE_START;

uart_rx and uart_tx handle ordinary 8N1 framing at 115200 baud, not shown here.

FSM: WAIT FOR K loops on itself receiving one byte at a time, then to RUN ECC CORE on 30 bytes received, then to SEND RESPONSE on scalar multiplication done, looping on itself sending one byte at a time, then back to WAIT FOR K on 61 bytes sent

Cost. One 30-byte receive, one scalar multiplication, one 61-byte send per request.

Cost summary ​

OperationMultiplier callsCyclesNotes
Addition00XOR, combinational
Squaring00combinational
Multiplication1233shift-and-add
Inversion10≈ 10 × 233Itoh–Tsujii [2]
Point doubling4≈ 4 × 233any Z1
Point addition8≈ 8 × 233requires Z1=1
Scalar multiplication≤ 233 doublings, ≤ 233 additions, 1 inversion, 2variesleft-to-right double-and-add

References ​

[1] J. López and R. Dahab, "Improved Algorithms for Elliptic Curve Arithmetic in GF(2^n)," in Selected Areas in Cryptography (SAC 1998), LNCS 1556, Springer, 1999, pp. 201–212.

[2] T. Itoh and S. Tsujii, "A Fast Algorithm for Computing Multiplicative Inverses in GF(2^m) Using Normal Bases," Information and Computation, vol. 78, no. 3, pp. 171–177, 1988.

[3] Standards for Efficient Cryptography Group, SEC 2: Recommended Elliptic Curve Domain Parameters, Version 1.0, 2000. (sect233r1 / NIST B-233)

[4] National Institute of Standards and Technology, FIPS PUB 186-4: Digital Signature Standard (DSS), 2013. Appendix D.

Released under the Apache 2.0 License.