constantine/constantine/math/io/io_bigints.nim
Mamy Ratsimbazafy 65eedd1cf7
Hash-to-Curve BLS12-381 G1 (#189)
* Skeleton of hash to curve for BLS12-381 G1

* Remove isodegree parameter

* Fix polynomial evaluation of hashToG1

* Optimize hash_to_curve and add bench for hash to G1

* slight optim of jacobian isomap + v7 test vectors
2022-04-11 00:57:16 +02:00

663 lines
20 KiB
Nim
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Constantine
# Copyright (c) 2018-2019 Status Research & Development GmbH
# Copyright (c) 2020-Present Mamy André-Ratsimbazafy
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
# TODO ⚠️:
# - Constant-time validation for parsing secret keys
# - Burning memory to ensure secrets are not left after dealloc.
import
../../platforms/[abstractions, endians],
../arithmetic/bigints,
../config/type_bigint
export BigInt, wordsRequired
# ############################################################
#
# Parsing from canonical inputs to internal representation
#
# ############################################################
# No exceptions for the byte API
{.push raises: [].}
# Note: the parsing/serialization routines were initially developed
# with an internal representation that used 31 bits out of a uint32
# or 63-bits out of an uint64
# TODO: the in-place API should return a bool
# to indicate success.
# the out-of place API are for configuration,
# prototyping, research and debugging purposes,
# and can use exceptions.
func unmarshalLE(
dst: var BigInt,
src: openarray[byte]) =
## Parse an unsigned integer from its canonical
## little-endian unsigned representation
## and store it into a BigInt
##
## Constant-Time:
## - no leaks
##
## Can work at compile-time
# TODO: error on destination to small
var
dst_idx = 0
acc = Zero
acc_len = 0
for src_idx in 0 ..< src.len:
let src_byte = SecretWord(src[src_idx])
# buffer reads
acc = acc or (src_byte shl acc_len)
acc_len += 8 # We count bit by bit
# if full, dump
if acc_len >= WordBitWidth:
dst.limbs[dst_idx] = acc
inc dst_idx
acc_len -= WordBitWidth
acc = src_byte shr (8 - acc_len)
if dst_idx < dst.limbs.len:
dst.limbs[dst_idx] = acc
for i in dst_idx + 1 ..< dst.limbs.len:
dst.limbs[i] = Zero
func unmarshalBE(
dst: var BigInt,
src: openarray[byte]) =
## Parse an unsigned integer from its canonical
## big-endian unsigned representation (octet string)
## and store it into a BigInt.
##
## In cryptography specifications, this is often called
## "Octet string to Integer"
##
## Constant-Time:
## - no leaks
##
## Can work at compile-time
var
dst_idx = 0
acc = Zero
acc_len = 0
for src_idx in countdown(src.len-1, 0):
let src_byte = SecretWord(src[src_idx])
# buffer reads
acc = acc or (src_byte shl acc_len)
acc_len += 8 # We count bit by bit
# if full, dump
if acc_len >= WordBitWidth:
dst.limbs[dst_idx] = acc
inc dst_idx
acc_len -= WordBitWidth
acc = src_byte shr (8 - acc_len)
if dst_idx < dst.limbs.len:
dst.limbs[dst_idx] = acc
for i in dst_idx + 1 ..< dst.limbs.len:
dst.limbs[i] = Zero
func unmarshal*(
dst: var BigInt,
src: openarray[byte],
srcEndianness: static Endianness) =
## Parse an unsigned integer from its canonical
## big-endian or little-endian unsigned representation
## And store it into a BigInt of size `bits`
##
## Constant-Time:
## - no leaks
##
## Can work at compile-time to embed curve moduli
## from a canonical integer representation
when srcEndianness == littleEndian:
dst.unmarshalLE(src)
else:
dst.unmarshalBE(src)
func unmarshal*(
T: type BigInt,
src: openarray[byte],
srcEndianness: static Endianness): T {.inline.}=
## Parse an unsigned integer from its canonical
## big-endian or little-endian unsigned representation
## And store it into a BigInt of size `bits`
##
## Constant-Time:
## - no leaks
##
## Can work at compile-time to embed curve moduli
## from a canonical integer representation
result.unmarshal(src, srcEndianness)
func fromUint*(
T: type BigInt,
src: SomeUnsignedInt): T {.inline.}=
## Parse a regular unsigned integer
## and store it into a BigInt of size `bits`
result.unmarshal(cast[array[sizeof(src), byte]](src), cpuEndian)
func fromUint*(
dst: var BigInt,
src: SomeUnsignedInt) {.inline.}=
## Parse a regular unsigned integer
## and store it into a BigInt of size `bits`
dst.unmarshal(cast[array[sizeof(src), byte]](src), cpuEndian)
# ############################################################
#
# Serialising from internal representation to canonical format
#
# ############################################################
template blobFrom(dst: var openArray[byte], src: SomeUnsignedInt, startIdx: int, endian: static Endianness) =
## Write an integer into a raw binary blob
## Swapping endianness if needed
## startidx is the first written array item if littleEndian is requested
## or the last if bigEndian is requested
when endian == cpuEndian:
for i in 0 ..< sizeof(src):
dst[startIdx+i] = toByte(src shr (i * 8))
else:
for i in 0 ..< sizeof(src):
dst[startIdx+sizeof(src)-1-i] = toByte(src shr (i * 8))
func marshalLE(
dst: var openarray[byte],
src: BigInt) =
## Serialize a bigint into its canonical little-endian representation
## I.e least significant bit first
var
src_idx, dst_idx = 0
acc: BaseType = 0
acc_len = 0
var tail = dst.len
while tail > 0:
let w = if src_idx < src.limbs.len: BaseType(src.limbs[src_idx])
else: 0
inc src_idx
if acc_len == 0:
# We need to refill the buffer to output 64-bit
acc = w
acc_len = WordBitWidth
else:
when WordBitWidth == sizeof(SecretWord) * 8:
let lo = acc
acc = w
else: # If using 63-bit (or less) out of uint64
let lo = (w shl acc_len) or acc
dec acc_len
acc = w shr (WordBitWidth - acc_len)
if tail >= sizeof(SecretWord):
# Unrolled copy
dst.blobFrom(src = lo, dst_idx, littleEndian)
dst_idx += sizeof(SecretWord)
tail -= sizeof(SecretWord)
else:
# Process the tail and exit
when cpuEndian == littleEndian:
# When requesting little-endian on little-endian platform
# we can just copy each byte
# tail is inclusive
for i in 0 ..< tail:
dst[dst_idx+i] = toByte(lo shr (i*8))
else: # TODO check this
# We need to copy from the end
for i in 0 ..< tail:
dst[dst_idx+i] = toByte(lo shr ((tail-i)*8))
return
func marshalBE(
dst: var openarray[byte],
src: BigInt) =
## Serialize a bigint into its canonical big-endian representation
## (octet string)
## I.e most significant bit first
##
## In cryptography specifications, this is often called
## "Octet string to Integer"
var
src_idx = 0
acc: BaseType = 0
acc_len = 0
var tail = dst.len
while tail > 0:
let w = if src_idx < src.limbs.len: BaseType(src.limbs[src_idx])
else: 0
inc src_idx
if acc_len == 0:
# We need to refill the buffer to output 64-bit
acc = w
acc_len = WordBitWidth
else:
when WordBitWidth == sizeof(SecretWord) * 8:
let lo = acc
acc = w
else: # If using 63-bit (or less) out of uint64
let lo = (w shl acc_len) or acc
dec acc_len
acc = w shr (WordBitWidth - acc_len)
if tail >= sizeof(SecretWord):
# Unrolled copy
tail -= sizeof(SecretWord)
dst.blobFrom(src = lo, tail, bigEndian)
else:
# Process the tail and exit
when cpuEndian == littleEndian:
# When requesting little-endian on little-endian platform
# we can just copy each byte
# tail is inclusive
for i in 0 ..< tail:
dst[tail-1-i] = toByte(lo shr (i*8))
else: # TODO check this
# We need to copy from the end
for i in 0 ..< tail:
dst[tail-1-i] = toByte(lo shr ((tail-i)*8))
return
func marshal*(
dst: var openarray[byte],
src: BigInt,
dstEndianness: static Endianness) =
## Serialize a bigint into its canonical big-endian or little endian
## representation.
## A destination buffer of size "(BigInt.bits + 7) div 8" at minimum is needed,
## i.e. bits -> byte conversion rounded up
##
## If the buffer is bigger, output will be zero-padded left for big-endian
## or zero-padded right for little-endian.
## I.e least significant bit is aligned to buffer boundary
debug:
doAssert dst.len >= (BigInt.bits + 7) div 8, "BigInt -> Raw int conversion: destination buffer is too small"
when BigInt.bits == 0:
zeroMem(dst, dst.len)
when dstEndianness == littleEndian:
marshalLE(dst, src)
else:
marshalBE(dst, src)
{.pop.} # {.push raises: [].}
# ############################################################
#
# Conversion helpers
#
# ############################################################
func readHexChar(c: char): SecretWord {.inline.}=
## Converts an hex char to an int
template sw(a: char or int): SecretWord = SecretWord(a)
const k = WordBitWidth - 1
let c = sw(c)
let lowercaseMask = not -(((c - sw'a') or (sw('f') - c)) shr k)
let uppercaseMask = not -(((c - sw'A') or (sw('F') - c)) shr k)
var val = c - sw'0'
val = val xor ((val xor (c - sw('a') + sw(10))) and lowercaseMask)
val = val xor ((val xor (c - sw('A') + sw(10))) and uppercaseMask)
val = val and sw(0xF) # Prevent overflow of invalid inputs
return val
func hexToPaddedByteArray*(hexStr: string, output: var openArray[byte], order: static[Endianness]) =
## Read a hex string and store it in a byte array `output`.
## The string may be shorter than the byte array.
##
## The source string must be hex big-endian.
## The destination array can be big or little endian
##
## Only characters accepted are 0x or 0X prefix
## and 0-9,a-f,A-F in particular spaces and _ are not valid.
##
## Procedure is constant-time except for the presence (or absence) of the 0x prefix.
##
## This procedure is intended for configuration, prototyping, research and debugging purposes.
## You MUST NOT use it for production.
template sw(a: bool or int): SecretWord = SecretWord(a)
var
skip = Zero
dstIdx: int
shift = 4
if hexStr.len >= 2:
skip = sw(2)*(
sw(hexStr[0] == '0') and
(sw(hexStr[1] == 'x') or sw(hexStr[1] == 'X'))
)
let maxStrSize = output.len * 2
let size = hexStr.len - skip.int
doAssert size <= maxStrSize, "size: " & $size & ", maxSize: " & $maxStrSize
if size < maxStrSize:
# include extra byte if odd length
dstIdx = output.len - (size + 1) shr 1
# start with shl of 4 if length is even
shift = 4 - (size and 1) * 4
for srcIdx in skip.int ..< hexStr.len:
let c = hexStr[srcIdx]
let nibble = byte(c.readHexChar() shl shift)
when order == bigEndian:
output[dstIdx] = output[dstIdx] or nibble
else:
output[output.high - dstIdx] = output[output.high - dstIdx] or nibble
shift = (shift + 4) and 4
dstIdx += shift shr 2
func nativeEndianToHex(bytes: openarray[byte], order: static[Endianness]): string =
## Convert a byte-array to its hex representation
## Output is in lowercase and not prefixed.
## This assumes that input is in platform native endianness
const hexChars = "0123456789abcdef"
result = newString(2 + 2 * bytes.len)
result[0] = '0'
result[1] = 'x'
for i in 0 ..< bytes.len:
when order == system.cpuEndian:
let bi = bytes[i]
result[2 + 2*i] = hexChars.secretLookup(SecretWord bi shr 4 and 0xF)
result[2 + 2*i+1] = hexChars.secretLookup(SecretWord bi and 0xF)
else:
let bmi = bytes[bytes.high - i]
result[2 + 2*i] = hexChars.secretLookup(SecretWord bmi shr 4 and 0xF)
result[2 + 2*i+1] = hexChars.secretLookup(SecretWord bmi and 0xF)
# ############################################################
#
# Hex conversion
#
# ############################################################
func fromHex*(a: var BigInt, s: string) =
## Convert a hex string to BigInt that can hold
## the specified number of bits
##
## For example `fromHex(BigInt[256], "0x123456")`
##
## Hex string is assumed big-endian
##
## Procedure is constant-time except for the presence (or absence) of the 0x prefix.
##
## This procedure is intended for configuration, prototyping, research and debugging purposes.
## You MUST NOT use it for production.
##
## Can work at compile-time to declare curve moduli from their hex strings
# 1. Convert to canonical uint
const canonLen = (BigInt.bits + 8 - 1) div 8
var bytes: array[canonLen, byte]
hexToPaddedByteArray(s, bytes, bigEndian)
# 2. Convert canonical uint to Big Int
a.unmarshal(bytes, bigEndian)
func fromHex*(T: type BigInt, s: string): T {.noInit.} =
## Convert a hex string to BigInt that can hold
## the specified number of bits
##
## For example `fromHex(BigInt[256], "0x123456")`
##
## Hex string is assumed big-endian
##
## Procedure is constant-time except for the presence (or absence) of the 0x prefix.
##
## This procedure is intended for configuration, prototyping, research and debugging purposes.
## You MUST NOT use it for production.
##
## Can work at compile-time to declare curve moduli from their hex strings
result.fromHex(s)
func appendHex*(dst: var string, big: BigInt, order: static Endianness = bigEndian) =
## Append the BigInt hex into an accumulator
## Note. Leading zeros are not removed.
## Result is prefixed with 0x
##
## Output will be padded with 0s to maintain constant-time.
##
## CT:
## - no leaks
##
## This is useful to reduce the number of allocations when serializing
## Fp towers
##
## This function may allocate.
# 1. Convert Big Int to canonical uint
const canonLen = (big.bits + 8 - 1) div 8
var bytes: array[canonLen, byte]
marshal(bytes, big, cpuEndian)
# 2 Convert canonical uint to hex
dst.add bytes.nativeEndianToHex(order)
func toHex*(a: openArray[byte]): string =
nativeEndianToHex(a, system.cpuEndian)
func toHex*(big: BigInt, order: static Endianness = bigEndian): string =
## Stringify an int to hex.
## Note. Leading zeros are not removed.
## Result is prefixed with 0x
##
## Output will be padded with 0s to maintain constant-time.
##
## CT:
## - no leaks
result.appendHex(big, order)
# ############################################################
#
# Decimal conversion
#
# ############################################################
#
# We need to convert between the size in binary
# and the size in decimal. Unlike for the hexadecimal case
# this is not trivial. We find the following relation.
#
# binary_length = log₂(value)
# decimal_length = log₁₀(value)
#
# Hence we have:
# binary_length = (log₂(value) / log₁₀(value)) * decimal_length
#
# the log change of base formula allow us to express
# log₁₀(value) = log₂(value) / log₂(10)
#
# Hence
# binary_length = log₂(10) * decimal_length
#
# Now we need to approximate log₂(10), we can have a best approximation
# using continued factions:
# In Sagemath: "continued_fraction(log(10,2)).convergents()[0:10].list()"
# [3,
# 10/3,
# 93/28,
# 196/59,
# 485/146,
# 2136/643,
# 13301/4004,
# 28738/8651,
# 42039/12655,
# 70777/21306]
#
# According to http://www.maths.surrey.ac.uk/hosted-sites/R.Knott/Fibonacci/cfCALC.html
# we have
# 42039/12655 = [3;3,9,2,2,4,6,2,1] = 3.321928091663374 error -3.223988631617658×10-9 (9.705×10-8%)
# 70777/21306 = [3;3,9,2,2,4,6,2,1,1] = 3.3219280953721957 error +4.848330625861763×10-10 (1.459×10-8%
# as lower and upper bound.
const log2_10_Num = 42039
const log2_10_Denom = 12655
# No exceptions for the in-place API
{.push raises: [].}
func hasEnoughBitsForDecimal(bits: uint, decimalLength: uint): bool =
## Check if the decimalLength would fit in a big int of size bits.
## This assumes that bits and decimal length are **public.**
##
## The check uses continued fraction approximation
## In Sagemath: "continued_fraction(log(10,2)).convergents()[0:10].list()"
## which gives 70777/21306
##
## The check might be too lenient by 1 bit.
if bits >= high(uint) div log2_10_Num:
# The next multiplication would overflow
return false
# Compute the expected length
let maxExpectedBitlength = ((decimalLength * log2_10_Num) div log2_10_Denom)
# A big int "400....." might take 381 bits and "500....." might take 382
let lenientBitlength = maxExpectedBitlength - 1
result = bits >= lenientBitlength
func fromDecimal*[aBits: static int](a: var BigInt[aBits], s: string): SecretBool =
## Convert a decimal string. The input must be packed
## with no spaces or underscores.
## This assumes that bits and decimal length are **public.**
##
## This function does approximate validation that the BigInt
## can hold the input string.
##
## It is intended for configuration, prototyping, research and debugging purposes.
## You MUST NOT use it for production.
##
## Return true if conversion is successful
##
## Return false if an error occured:
## - There is not enough space in the BigInt
## - An invalid character was found
if not aBits.hasEnoughBitsForDecimal(s.len.uint):
return CtFalse
a.setZero()
result = CtTrue
for i in 0 ..< s.len:
let c = SecretWord(ord(s[i]))
result = result and (SecretWord(ord('0')) <= c and c <= SecretWord(ord('9')))
a += c - SecretWord(ord('0'))
if i != s.len - 1:
a *= 10
func fromDecimal*(T: type BigInt, s: string): T {.raises: [ValueError].}=
## Convert a decimal string. The input must be packed
## with no spaces or underscores.
## This assumes that bits and decimal length are **public.**
##
## This function does approximate validation that the BigInt
## can hold the input string.
##
## It is intended for configuration, prototyping, research and debugging purposes.
## You MUST NOT use it for production.
##
## This function may raise an exception if input is incorrect
## - There is not enough space in the BigInt
## - An invalid character was found
let status = result.fromDecimal(s)
if not status.bool:
raise newException(ValueError,
"BigInt could not be parsed from decimal string." &
" <Potentially secret input withheld>")
# Conversion to decimal
# ----------------------------------------------------------------
#
# The first problem to solve is precomputing the final size
# We also use continued fractions to approximate log₁₀(2)
#
# decimal_length = log₁₀(2) * binary_length
#
# sage: [(frac, numerical_approx(frac - log(2,10))) for frac in continued_fraction(log(2,10)).convergents()[0:10]]
# [(0, -0.301029995663981),
# (1/3, 0.0323033376693522),
# (3/10, -0.00102999566398115),
# (28/93, 0.0000452731532231687),
# (59/196, -9.58750071583525e-6),
# (146/485, 9.32171070389121e-7),
# (643/2136, -3.31171646772432e-8),
# (4004/13301, 2.08054934391910e-9),
# (8651/28738, -5.35579747218407e-10),
# (12655/42039, 2.92154800352051e-10)]
const log10_2_Num = 12655
const log10_2_Denom = 42039
# Then the naive way to serialize is to repeatedly do
# const intToCharMap = "0123456789"
# rest = number
# while rest != 0:
# digitToPrint = rest mod 10
# result.add intToCharMap[digitToPrint]
# rest /= 10
#
# For constant-time we:
# 1. can't compare with 0 as a stopping condition
# 2. repeatedly add to a buffer
# 3. can't use naive indexing (cache timing attacks though very unlikely given the small size)
# 4. need (fast) constant-time division
#
# 1 and 2 is solved by precomputing the length and make the number of add be fixed.
# 3 is easily solved by doing "digitToPrint + ord('0')" instead
func decimalLength(bits: static int): int =
doAssert bits < (high(uint) div log10_2_Num),
"Constantine does not support that many bits to convert to a decimal string: " & $bits
# The next multiplication would overflow
result = 1 + ((bits * log10_2_Num) div log10_2_Denom)
func toDecimal*(a: BigInt): string =
## Convert to a decimal string.
##
## This procedure is intended for configuration, prototyping, research and debugging purposes.
## You MUST NOT use it for production.
##
## This function is constant-time.
## This function does heap-allocation.
const len = decimalLength(BigInt.bits)
result = newString(len)
var a = a
for i in countdown(len-1, 0):
let c = ord('0') + a.div10().int
result[i] = char(c)