Add masked signing

This commit is contained in:
Miguel Oliveira 2023-01-02 16:02:03 -03:00
parent 77892f3a76
commit 73e0df0b5e
5 changed files with 128 additions and 72 deletions

View file

@ -11,11 +11,6 @@ local random = require "ccryptolib.random"
local mod = {} local mod = {}
--- Converts a signing key to an equivalent exchange key.
function mod.exchangeKey(sk)
return sha512.digest(sk):sub(1, 32)
end
--- Computes a public key from a secret key. --- Computes a public key from a secret key.
-- --
-- @tparam string sk A random 32-byte secret key. -- @tparam string sk A random 32-byte secret key.
@ -59,7 +54,7 @@ function mod.sign(sk, pk, msg)
-- Response. -- Response.
local m = fq.decodeWide(random.random(64)) local m = fq.decodeWide(random.random(64))
local s = fq.add(fq.add(k, fq.mul(fq.add(x, m), e)), fq.mul(m, e)) local s = fq.sub(fq.add(k, fq.mul(fq.add(x, m), e)), fq.mul(m, e))
local sStr = fq.encode(s) local sStr = fq.encode(s)
return rStr .. sStr return rStr .. sStr

View file

@ -276,6 +276,7 @@ return {
add = add, add = add,
sub = sub, sub = sub,
niels = niels, niels = niels,
scale = scale,
encode = encode, encode = encode,
decode = decode, decode = decode,
mulG = mulG, mulG = mulG,

View file

@ -20,7 +20,7 @@ local ufq = packing.compileUnpack(fmtfq)
local ufql, fmtfql = packing.compileUnpack("<I3I3I3I3I3I3I3I3I3I3I3") local ufql, fmtfql = packing.compileUnpack("<I3I3I3I3I3I3I3I3I3I3I3")
local ufqh, fmtfqh = packing.compileUnpack("<I3I3I3I3I3I3I3I3I3I3I1") local ufqh, fmtfqh = packing.compileUnpack("<I3I3I3I3I3I3I3I3I3I3I1")
--- The scalar field's order, q. --- The scalar field's order, q = 2²⁵² + 27742317777372353535851937790883648493.
local Q = { local Q = {
16110573, 16110573,
06494812, 06494812,
@ -66,17 +66,17 @@ local T1 = {
} }
local T8 = { local T8 = {
01130678, 5110253,
05563041, 3039345,
03870191, 2503500,
01622646, 11779568,
01247520, 15416472,
12151703, 16766550,
16693196, 16777215,
09337410, 16777215,
04700637, 16777215,
07308819, 16777215,
00002083, 4095,
} }
local ZERO = mp.num(0) local ZERO = mp.num(0)
@ -139,7 +139,7 @@ end
-- --
-- @tparam {number...} a A number a as 11 limbs in [0..2²⁴). -- @tparam {number...} a A number a as 11 limbs in [0..2²⁴).
-- @tparam {number...} b A number b < q as 11 limbs in [0..2²⁴). -- @tparam {number...} b A number b < q as 11 limbs in [0..2²⁴).
-- @treturn 2⁻²⁶⁴ × a × b mod q as 11 limbs in [0..2²⁴). -- @treturn {number...} 2⁻²⁶⁴ × a × b mod q as 11 limbs in [0..2²⁴).
-- --
local function mul(a, b) local function mul(a, b)
local t0, t1 = mp.mul(a, b) local t0, t1 = mp.mul(a, b)
@ -226,22 +226,12 @@ local function decodeClamped(str)
return montgomery(words) return montgomery(words)
end end
--- Decodes a scalar using the X25519/Ed25519 bit clamping scheme and division --- Divides a scalar by 8.
-- by 8.
-- --
-- @tparam string str A 32-byte string encoding some little-endian number a. -- @tparam {number...} 2²⁶⁴ × a mod q as 11 limbs in [0..2²⁴).
-- @treturn {number...} 2²⁶⁴ × clamp(a) ÷ 8 mod q as 11 limbs in [0..2²⁴). -- @treturn {number...} 2²⁶⁵ × a ÷ 8 mod q as 11 limbs in [0..2²⁴).
-- local function eighth(a)
local function decodeClamped8(str) return mul(a, T8)
-- Decode.
local words = {ufq(fmtfq, str, 1)} words[12] = nil
-- Clamp.
words[1] = bit32.band(words[1], 0xfffff8)
words[11] = bit32.band(words[11], 0x7fff)
words[11] = bit32.bor(words[11], 0x4000)
return mul(words, T8)
end end
--- Returns a scalar in binary. --- Returns a scalar in binary.
@ -398,8 +388,8 @@ return {
encode = encode, encode = encode,
decode = decode, decode = decode,
decodeWide = decodeWide, decodeWide = decodeWide,
decodeClamped8 = decodeClamped8,
decodeClamped = decodeClamped, decodeClamped = decodeClamped,
eighth = eighth,
bits = bits, bits = bits,
makeRuleset = makeRuleset, makeRuleset = makeRuleset,
} }

View file

@ -2,35 +2,56 @@ local expect = require "cc.expect".expect
local fq = require "ccryptolib.internal.fq" local fq = require "ccryptolib.internal.fq"
local fp = require "ccryptolib.internal.fp" local fp = require "ccryptolib.internal.fp"
local c25 = require "ccryptolib.internal.curve25519" local c25 = require "ccryptolib.internal.curve25519"
local ed = require "ccryptolib.internal.edwards25519"
local sha512 = require "ccryptolib.internal.sha512"
local random = require "ccryptolib.random" local random = require "ccryptolib.random"
local function mask(sk) --- Transforms an X25519 secret key into a masked key.
local function maskExchangeSk(sk)
expect(1, sk, "string") expect(1, sk, "string")
assert(#sk == 32, "secret key length must be 32") assert(#sk == 32, "secret key length must be 32")
local mask = random.random(32) local mask = random.random(32)
local x = fq.decodeClamped8(sk) local x = fq.decodeClamped(sk)
local r = fq.decodeClamped8(mask) local r = fq.decodeClamped(mask)
local xr = fq.sub(x, r) local xr = fq.sub(x, r)
return fq.encode(xr), mask return fq.encode(xr) .. mask
end end
local function remask(msk, oldMask) --- Transforms an Ed25519 secret key into a masked key.
expect(1, msk, "string") function maskSignatureSk(sk)
assert(#msk == 32, "masked secret key length must be 32") expect(1, sk, "string")
expect(2, oldMask, "string") assert(#sk == 32, "secret key length must be 32")
assert(#oldMask == 32, "old mask length must be 32") return maskExchangeSk(sha512.digest(sk):sub(1, 32))
end
--- Rerandomizes the masking on a masked key.
local function remask(sk)
expect(1, sk, "string")
assert(#sk == 64, "masked secret key length must be 64")
local newMask = random.random(32) local newMask = random.random(32)
local xr = fq.decode(msk) local xr = fq.decode(sk:sub(1, 32))
local r = fq.decodeClamped8(oldMask) local r = fq.decodeClamped(sk:sub(33))
local s = fq.decodeClamped8(newMask) local s = fq.decodeClamped(newMask)
local xs = fq.add(xr, fq.sub(r, s)) local xs = fq.add(xr, fq.sub(r, s))
return fq.encode(xs), newMask return fq.encode(xs) .. newMask
end end
local function exchangeOnPoint(msk, mask, P) --- Returns the ephemeral exchange secret key of this masked key.
local xr = fq.decode(msk) --
local r = fq.decodeClamped8(mask) -- This is the second secret key in the "double key exchange" in @{exchange},
local rP, xrP, dP = c25.prac(P, fq.makeRuleset(r, xr)) -- the first being the key that has been masked. The ephemeral key changes every
-- time @{remask} is called.
--
local function exchangeEsk(sk)
expect(1, sk, "string")
assert(#sk == 64, "masked secret key length must be 64")
return sk:sub(33)
end
local function exchangeOnPoint(sk, P)
local xr = fq.decode(sk:sub(1, 32))
local r = fq.decodeClamped(sk:sub(33))
local rP, xrP, dP = c25.prac(P, fq.makeRuleset(fq.eighth(r), fq.eighth(xr)))
-- Return early if P has small order or if r = xr. (1) -- Return early if P has small order or if r = xr. (1)
if not rP then if not rP then
@ -79,7 +100,24 @@ local function exchangeOnPoint(msk, mask, P)
return fp.encode(fp.mul(xPx, xPzInv)), fp.encode(fp.mul(rPx, rPzInv)) return fp.encode(fp.mul(xPx, xPzInv)), fp.encode(fp.mul(rPx, rPzInv))
end end
--- Treats both shares as X25519 keys and performs a double key exchange. --- Returns the X25519 public key of this masked key.
local function exchangePk(sk)
expect(1, sk, "string")
assert(#sk == 64, "masked secret key length must be 64")
return (exchangeOnPoint(sk, c25.G))
end
--- Returns the Ed25519 public key of this masked key.
local function signaturePk(sk)
expect(1, sk, "string")
assert(#sk == 64, "masked secret key length must be 64")
local xr = fq.decode(sk:sub(1, 32))
local r = fq.decodeClamped(sk:sub(33))
local y = ed.add(ed.mulG(fq.bits(xr)), ed.niels(ed.mulG(fq.bits(r))))
return ed.encode(ed.scale(y))
end
--- Performs a double key exchange.
-- --
-- Returns 0 if the input public key has small order or if it isn't in the base -- 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 -- curve. This is different from standard X25519, which performs the exchange
@ -88,30 +126,62 @@ end
-- May incorrectly return 0 with negligible chance if the mask happens to match -- 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. -- the masked key. I haven't checked if clamping prevents that from happening.
-- --
local function exchange(msk, mask, pk) local function exchange(sk, pk)
expect(1, msk, "string") expect(1, sk, "string")
assert(#msk == 32, "masked secret key length must be 32") assert(#sk == 64, "masked secret key length must be 64")
expect(2, mask, "string") expect(2, pk, "string")
assert(#mask == 32, "mask length must be 32")
expect(3, pk, "string")
assert(#pk == 32, "public key length must be 32") assert(#pk == 32, "public key length must be 32")
return exchangeOnPoint(msk, mask, c25.decode(pk)) return exchangeOnPoint(sk, c25.decode(pk))
end end
--- Same as @{exchange}, but decodes the public key as an Edwards25519 point. --- Performs an exchange against an Ed25519 key.
local function exchangeEd(msk, mask, pk) --
expect(1, msk, "string") -- This is done by converting the key into X25519 before passing it to the
assert(#msk == 32, "masked secret key length must be 32") -- regular exchange. Using this function on the result of @{signaturePk} leads
expect(2, mask, "string") -- to the same value as using @{exchange} on the result of @{exchangePk}.
assert(#mask == 32, "mask length must be 32") --
expect(3, pk, "string") local function exchangeEd(sk, pk)
expect(1, sk, "string")
assert(#sk == 64, "masked secret key length must be 64")
expect(2, pk, "string")
assert(#pk == 32, "public key length must be 32") assert(#pk == 32, "public key length must be 32")
return exchangeOnPoint(msk, mask, c25.decodeEd(pk)) return exchangeOnPoint(sk, c25.decodeEd(pk))
end
--- Signs a message using Ed25519.
local function sign(sk, pk, msg)
expect(1, sk, "string")
assert(#sk == 64, "masked secret key length must be 64")
expect(2, pk, "string")
assert(#pk == 32, "public key length must be 32")
expect(3, msg, "string")
-- Secret key.
local xr = fq.decode(sk:sub(1, 32))
local r = fq.decodeClamped(sk:sub(33))
-- Commitment.
local k = fq.decodeWide(random.random(64))
local rStr = ed.encode(ed.mulG(fq.bits(k)))
-- Challenge.
local e = fq.decodeWide(sha512.digest(rStr .. pk .. msg))
-- Response.
local s = fq.add(fq.add(k, fq.mul(xr, e)), fq.mul(r, e))
local sStr = fq.encode(s)
return rStr .. sStr
end end
return { return {
mask = mask, maskExchangeSk = maskExchangeSk,
maskSignatureSk = maskSignatureSk,
remask = remask, remask = remask,
exchangePk = exchangePk,
exchangeEsk = exchangeEsk,
signaturePk = signaturePk,
exchange = exchange, exchange = exchange,
exchangeEd = exchangeEd, exchangeEd = exchangeEd,
sign = sign,
} }

View file

@ -7,9 +7,9 @@ local util = require "spec.util"
local x25519c = require "ccryptolib.x25519c" local x25519c = require "ccryptolib.x25519c"
local function exchange(sk, pk) local function exchange(sk, pk)
local sk, ek = x25519c.mask(sk) local sk = x25519c.maskExchangeSk(sk)
sk, ek = x25519c.remask(sk, ek) sk = x25519c.remask(sk)
return (x25519c.exchange(sk, ek, pk)) return (x25519c.exchange(sk, pk))
end end
describe("x25519c.exchange", function() describe("x25519c.exchange", function()