Add tests and check the twist on x25519c.lua

This commit is contained in:
Miguel Oliveira 2023-01-02 01:50:01 -03:00
parent 88a584b393
commit a7d98da04c
6 changed files with 259 additions and 43 deletions

View file

@ -143,9 +143,10 @@ end
--
-- @tparam point P The base point.
-- @tparam {{number...}, {number...}} The ruleset generated by scalars m, n.
-- @treturn point [8m]P
-- @treturn point [8n]P
-- @treturn point [8m]P - [8n]P
-- @treturn[1] point [8m]P
-- @treturn[1] point [8n]P
-- @treturn[1] point [8m]P - [8n]P
-- @treturn[2] nil If any of the three results is equal to O.
--
local function prac(P, ruleset)
-- Randomize.
@ -156,10 +157,10 @@ local function prac(P, ruleset)
local A = double(double(double(A)))
-- Throw away small order points.
if fp.eqz(fp.mul(A[1], A[2])) then return A end
if fp.eqz(A[2]) then return end
-- Now e = d = gcd(m, n) / 8.
-- Update A from [8]P to [8u]P.
-- Update A from [8]P to [8 * gcd(m, n)]P.
A = ladder(A, ruleset[1])
-- Reject rulesets where m = n.
@ -170,7 +171,12 @@ local function prac(P, ruleset)
-- Since e = d, this means A - B = C = O. Differential addition fails when
-- C = O, so we need to treat this case specially.
-- Note that rules 0 and 1 never happen last, since the algorithm would stop
-- one step earlier if they did.
-- one step earlier if they did:
-- - If after rule 0 we had e = d, then (d, e) → (e, d) would also mean that
-- e = d, so it stops one step earlier.
-- - If after rule 1 we had e = d, then (d, e) → ((2d - e)/3, (2e - d)/3)
-- would mean that (2d - e)/3 = (2e - d)/3, thus 2d - e = 2e - d, thus
-- 3d = 3e, thus d = e, so it stops one step earlier.
local B, C
local rule = rules[#rules]
if rule == 2 then
@ -203,7 +209,7 @@ local function prac(P, ruleset)
end
-- Evaluate the other rules.
-- Let's assume addition is undefined here, this happens when A - B = 0.
-- Let's assume addition is undefined here, this happens when A - B = O.
-- Since A = [d]P and B = [e]P, A = B happens when:
-- (1) P is on the large order base group and d ≡ e (mod q).
-- (2) P is on the large order twist group and d ≡ e (mod q').

View file

@ -247,10 +247,12 @@ end
--- Returns a scalar in binary.
--
-- @tparam {number...} a A number a < q as 11 limbs in [0..2²⁴).
-- @treturn {number...} 2⁻²⁶⁴ × a mod q as 265 bits.
-- @treturn {number...} 2⁻²⁶⁴ × a mod q as 253 bits.
--
local function bits(a)
return util.rebaseLE(demontgomery(a), 2 ^ 24, 2)
local out = util.rebaseLE(demontgomery(a), 2 ^ 24, 2)
for i = 254, 289 do out[i] = nil end
return out
end
--- Makes a PRAC ruleset from a pair of scalars.

View file

@ -52,7 +52,6 @@ local function bits(str)
out[255] = 1
out[256] = 0
-- We remove the 3 lowest bits since the ladder already multiplies by 8.
return out
end

View file

@ -1,48 +1,102 @@
local expect = require "cc.expect".expect
local fq = require "ccryptolib.internal.fq"
local util = require "ccryptolib.internal.util"
local fp = require "ccryptolib.internal.fp"
local c25 = require "ccryptolib.internal.curve25519"
local random = require "ccryptolib.random"
local mod = {}
function mod.keypair()
local x = random.random(32)
local r = random.random(32)
local X = c25.mulG(util.bits(x))
local x8 = fq.decodeClamped8(x)
local r8 = fq.decodeClamped8(r)
local xr8 = fq.sub(x8, r8)
return fq.encode(xr8), r, c25.encode(c25.scale(X))
end
function mod.remask(sk, ek)
local function mask(sk)
expect(1, sk, "string")
assert(#sk == 32, "secret key length must be 32")
expect(2, ek, "string")
assert(#ek == 32, "ephemeral secret key length must be 32")
local s = random.random(32)
local r8 = fq.decodeClamped8(ek)
local s8 = fq.decodeClamped8(s)
local xr8 = fq.decode(sk)
local xs8 = fq.add(xr8, fq.sub(r8, s8))
return fq.encode(xs8), s
local mask = random.random(32)
local x = fq.decodeClamped8(sk)
local r = fq.decodeClamped8(mask)
local xr = fq.sub(x, r)
return fq.encode(xr), mask
end
function mod.exchange(sk, ek, pk)
expect(1, sk, "string")
assert(#sk == 32, "secret key length must be 32")
expect(2, ek, "string")
assert(#ek == 32, "ephemeral secret key length must be 32")
local function remask(msk, oldMask)
expect(1, msk, "string")
assert(#msk == 32, "masked secret key length must be 32")
expect(2, oldMask, "string")
assert(#oldMask == 32, "old mask length must be 32")
local newMask = random.random(32)
local xr = fq.decode(msk)
local r = fq.decodeClamped8(oldMask)
local s = fq.decodeClamped8(newMask)
local xs = fq.add(xr, fq.sub(r, s))
return fq.encode(xs), newMask
end
--- Treats both shares as X25519 keys and performs a double key exchange.
--
-- Returns 0 if the input public key has small order or if it isn't in the base
-- curve. This is different from standard X25519, which performs the exchange
-- even on the twist.
--
-- May incorrectly return 0 with negligible chance if the mask happens to match
-- the masked key. I haven't checked if clamping prevents that from happening.
--
local function exchange(msk, mask, pk)
expect(1, msk, "string")
assert(#msk == 32, "masked secret key length must be 32")
expect(2, mask, "string")
assert(#mask == 32, "mask length must be 32")
expect(3, pk, "string")
assert(#pk == 32, "public key length must be 32")
local P = c25.decode(pk)
local r8 = fq.decodeClamped8(ek)
local xr8 = fq.decode(sk)
local ruleset = fq.makeRuleset(r8, xr8)
local rP, xrP, dP = c25.prac(P, ruleset)
local xr = fq.decode(msk)
local r = fq.decodeClamped8(mask)
local rP, xrP, dP = c25.prac(P, fq.makeRuleset(r, xr))
-- Return early if P has small order or if r = xr. (1)
if not rP then
local out = fp.encode(fp.num(0))
return out, out
end
local xP = c25.dadd(dP, rP, xrP)
return c25.encode(c25.scale(xP)), c25.encode(c25.scale(rP))
-- Extract coordinates for scaling.
local xPx, xPz = xP[1], xP[2]
local rPx, rPz = rP[1], rP[2]
-- We're splitting the secret x into (x - r (mod q), r). The multiplication
-- adds them back together, but this only works if P's order is q, which is
-- not the case on the twist.
-- As a result, we need to check if P is on the twist and return 0 so as to
-- not leak part of x. We do this by checking the curve equation against P.
-- The equation for Curve25519 is y² = x³ + 486662x² + x. Checking it
-- amounts to verifying that x³ + 486662x² + x is a quadratic residue.
local Px = P[1]
local Px2 = fp.square(Px)
local Px3 = fp.mul(Px2, Px)
local APx2 = fp.kmul(Px2, 486662)
local Py2 = fp.carry(fp.add(fp.carry(fp.add(Px3, APx2)), Px))
-- Square the Z coordinate on both products.
xPx, xPz = fp.mul(xPx, xPz), fp.square(xPz)
rPx, rPz = fp.mul(rPx, rPz), fp.square(rPz)
-- Find the square root of 1 / (Py2 * xPz * rPz).
-- Neither rPz, xPz, nor Py2 are 0:
-- - If Py2 was 0, then P would be low order, which would return at (1).
-- - Since P isn't low order, clamping prevents the ladder from returning O.
-- Since we've just squared both xPz and rPz, the root will exist iff Py2 is
-- a quadratic residue. This checks the curve equation, so we're done.
local root = fp.sqrtDiv(fp.num(1), fp.mul(fp.mul(xPz, rPz), Py2))
if not root then return fp.encode(fp.num(0)) end
-- Get the inverses of both Z values.
local xPzrPzInv = fp.mul(fp.square(root), Py2)
local xPzInv = fp.mul(xPzrPzInv, rPz)
local rPzInv = fp.mul(xPzrPzInv, xPz)
-- Finish scaling and encode the output.
return fp.encode(fp.mul(xPx, xPzInv)), fp.encode(fp.mul(rPx, rPzInv))
end
return mod
return {
mask = mask,
remask = remask,
exchange = exchange,
}

View file

@ -62,4 +62,46 @@ describe("x25519.exchange", function()
expect(k):eq(k1000)
end)
it("passes the appendix A.1 test vectors of CPace", function()
local sk = util.hexcat {
"af46e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449aff",
}
local ins = {
"0000000000000000000000000000000000000000000000000000000000000000",
"0100000000000000000000000000000000000000000000000000000000000000",
"e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800",
"5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157",
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
"edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
"cdeb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b880",
"4c9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f11d7",
"d9ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"daffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"dbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
}
local outs = {
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000000",
"e062dcd5376d58297be2618c7498f55baa07d7e03184e8aada20bca28888bf7a",
"993c6ad11c4c29da9a56f7691fd0ff8d732e49de6250b6c2e80003ff4629a175",
"db64dafa9b8fdd136914e61461935fe92aa372cb056314e1231bc4ec12417456",
"d8e2c776bbacd510d09fd9278b7edcd25fc5ae9adfba3b6e040e8d3b71b21806",
"c85c655ebe8be44ba9c0ffde69f2fe10194458d137f09bbff725ce58803cdb38",
}
for i = 1, #ins do
local input = util.hexcat { ins[i] }
local output = util.hexcat { outs[i] }
expect(x25519.exchange(sk, input)):eq(output)
end
end)
end)

113
spec/x25519c_spec.lua Normal file
View file

@ -0,0 +1,113 @@
--- Test vector specification for masked X25519.
--
-- Derived from RFC 7748.
--
local util = require "spec.util"
local x25519c = require "ccryptolib.x25519c"
local function exchange(sk, pk)
local sk, ek = x25519c.mask(sk)
sk, ek = x25519c.remask(sk, ek)
return (x25519c.exchange(sk, ek, pk))
end
describe("x25519c.exchange", function()
it("passes the section 5.2 test vector #1", function()
local x = util.hexcat {
"a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4",
}
local p = util.hexcat {
"e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c",
}
local q = util.hexcat {
"c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552",
}
expect(exchange(x, p)):eq(q)
end)
it("doesn't pass the section 5.2 test vector #2", function()
local x = util.hexcat {
"4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d",
}
local p = util.hexcat {
"e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493",
}
local q = util.hexcat {
"0000000000000000000000000000000000000000000000000000000000000000",
}
expect(exchange(x, p)):eq(q)
end)
it("passes the section 5.2 test vector #3 (1k iterations)", function()
local k = util.hexcat {
"0900000000000000000000000000000000000000000000000000000000000000",
}
local u = k
local u2 = util.hexcat {
"422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079",
}
expect(exchange(k, u)):eq(u2)
for _ = 1, 1000 do
k, u = exchange(k, u), k
sleep()
end
local k1000 = util.hexcat {
"684cf59ba83309552800ef566f2f4d3c1c3887c49360e3875f2eb94d99532c51",
}
expect(k):eq(k1000)
end)
it("passes the appendix A.1 test vectors of CPace, mostly", function()
local sk = util.hexcat {
"af46e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449aff",
}
local ins = {
"0000000000000000000000000000000000000000000000000000000000000000",
"0100000000000000000000000000000000000000000000000000000000000000",
"e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800",
"5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157",
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
"edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
"cdeb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b880",
"4c9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f11d7",
"d9ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"daffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"dbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
}
local outs = {
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000000",
"0000000000000000000000000000000000000000000000000000000000000000",
"993c6ad11c4c29da9a56f7691fd0ff8d732e49de6250b6c2e80003ff4629a175",
"0000000000000000000000000000000000000000000000000000000000000000",
"d8e2c776bbacd510d09fd9278b7edcd25fc5ae9adfba3b6e040e8d3b71b21806",
"0000000000000000000000000000000000000000000000000000000000000000",
}
for i = 1, #ins do
local input = util.hexcat { ins[i] }
local output = util.hexcat { outs[i] }
expect(exchange(sk, input)):eq(output)
end
end)
end)