From cadd81b978b398cf08aee0bcbc7a4cb186e99bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mamy=20Andr=C3=A9-Ratsimbazafy?= Date: Sun, 28 Apr 2019 17:42:30 +0200 Subject: [PATCH] Add hex conversion --- constantine/field_fp.nim | 2 +- constantine/io.nim | 360 ++++++++++++++++++++++++--------------- tests/all_tests.nim | 3 +- tests/test_io.nim | 22 +++ 4 files changed, 252 insertions(+), 135 deletions(-) diff --git a/constantine/field_fp.nim b/constantine/field_fp.nim index 9400cec..d5d05cc 100644 --- a/constantine/field_fp.nim +++ b/constantine/field_fp.nim @@ -155,7 +155,7 @@ template shiftAddImpl(a: var Fp, c: Word) = # if carry > hi (negative result) we must do "a+= p" let neg = carry < hi - let tooBig = not over and (over_p or (carry < hi)) + let tooBig = not neg and (over_p or (carry < hi)) add(a, Fp.P, neg) sub(a, Fp.P, tooBig) diff --git a/constantine/io.nim b/constantine/io.nim index faf1a9f..d048b56 100644 --- a/constantine/io.nim +++ b/constantine/io.nim @@ -15,7 +15,151 @@ import # ############################################################ # -# Constant-time hex to byte conversion +# Parsing from canonical inputs to internal representation +# +# ############################################################ + +func parseRawUintLE( + src: openarray[byte], + bits: static int): BigInt[bits] {.inline.}= + ## Parse an unsigned integer from its canonical + ## little-endian unsigned representation + ## And store it into a BigInt of size bits + ## + ## CT: + ## - no leaks + + var + dst_idx = 0 + acc = Word(0) + acc_len = 0 + + for src_idx in 0 ..< src.len: + let src_byte = Word(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 >= WordBitSize: + result[dst_idx] = acc and MaxWord + inc dst_idx + acc_len -= WordBitSize + acc = src_byte shr (8 - acc_len) + + if acc_len != 0: + result[dst_idx] = acc + +func parseRawUint*( + src: openarray[byte], + bits: static int, + srcEndianness: static Endianness): BigInt[bits] = + ## Parse an unsigned integer from its canonical + ## big-endian or little-endian unsigned representation + ## And store it into a BigInt of size bits + ## + ## CT: + ## - no leaks + + when srcEndianness == littleEndian: + parseRawUintLE(src, bits) + else: + {.error: "Not implemented at the moment".} + +# ############################################################ +# +# Serialising from internal representation to canonical format +# +# ############################################################ + +template bigEndianXX[T: uint16 or uint32 or uint64](outp: pointer, inp: ptr T) = + when T is uint64: + bigEndian64(outp, inp) + elif T is uint32: + bigEndian32(outp, inp) + elif T is uint16: + bigEndian16(outp, inp) + +template littleEndianXX[T: uint16 or uint32 or uint64](outp: pointer, inp: ptr T) = + when T is uint64: + littleEndian64(outp, inp) + elif T is uint32: + littleEndian32(outp, inp) + elif T is uint16: + littleEndian16(outp, inp) + +func dumpRawUintLE( + dst: var openarray[byte], + src: BigInt) {.inline.}= + ## Serialize a bigint into its canonical little-endian representation + ## I.e least significant bit is aligned to buffer boundary + + 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: src[src_idx].BaseType + else: 0 + inc src_idx + + if acc_len == 0: + # Edge case, we need to refill the buffer to output 64-bit + # as we can only read 63-bit per word + acc = w + acc_len = WordBitSize + else: + let lo = (w shl acc_len) or acc + dec acc_len + acc = w shr (WordBitSize - acc_len) + + if tail >= sizeof(Word): + # Unrolled copy + # debugecho src.repr + littleEndianXX(dst[dst_idx].addr, lo.unsafeAddr) + dst_idx += sizeof(Word) + tail -= sizeof(Word) + else: + # Process the tail + when cpuEndian == littleEndian: + # When requesting little-endian on little-endian platform + # we can just copy each byte + for i in dst_idx ..< tail: + dst[dst_idx] = byte(lo shr (i-dst_idx)) + else: + # We need to copy from the end + for i in 0 ..< tail: + dst[dst_idx] = byte(lo shr (tail-i)) + +func dumpRawUint*( + 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 div 8" at minimum is needed. + ## + ## 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 + + if dst.len < static(BigInt.bits div 8): + raise newException(ValueError, "BigInt -> Raw int conversion: destination buffer is too small") + + when BigInt.bits == 0: + zeroMem(dst, dst.len) + + when dstEndianness == littleEndian: + dumpRawUintLE(dst, src) + else: + {.error: "Not implemented at the moment".} + +# ############################################################ +# +# Conversion helpers # # ############################################################ @@ -69,146 +213,96 @@ func readDecChar(c: range['0'..'9']): int {.inline.}= # specialization without branching for base <= 10. ord(c) - ord('0') -# ############################################################ -# -# Parsing from canonical inputs to internal representation -# -# ############################################################ - -func parseRawUintLE( - src: openarray[byte], - bits: static int): BigInt[bits] {.inline.}= - ## Parse an unsigned integer from its canonical - ## little-endian unsigned representation - ## And store it into a BigInt of size bits +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. ## - ## CT: - ## - no leaks - + ## The source string must be hex big-endian. + ## The destination array can beb ig or little endian + let maxStrSize = output.len * 2 var - dst_idx = 0 - acc = Word(0) - acc_len = 0 + skip = 0 + dstIdx: int + shift = 4 + skipPrefixes(skip, hexStr, 16) + let size = hexStr.len - skip - for src_idx in 0 ..< src.len: - let src_byte = Word(src[src_idx]) + doAssert size <= maxStrSize + + if size < maxStrSize: + # include extra byte if odd length + dstIdx = output.len - (size + 1) div 2 + # start with shl of 4 if length is even + shift = 4 - size mod 2 * 4 - # buffer reads - acc = acc or (src_byte shl acc_len) - acc_len += 8 # We count bit by bit - - # if full, dump - if acc_len >= WordBitSize: - result[dst_idx] = acc and MaxWord - inc dst_idx - acc_len -= WordBitSize - acc = src_byte shr (8 - acc_len) - - if acc_len != 0: - result[dst_idx] = acc - -func parseRawUint*( - src: openarray[byte], - bits: static int, - order: static Endianness): BigInt[bits] = - ## Parse an unsigned integer from its canonical - ## big-endian or little-endian unsigned representation - ## And store it into a BigInt of size bits - ## - ## CT: - ## - no leaks - - when order == littleEndian: - parseRawUintLE(src, bits) - else: - {.error: "Not implemented at the moment".} - -# ############################################################ -# -# Serialising from internal representation to canonical format -# -# ############################################################ - -template bigEndianXX[T: uint16 or uint32 or uint64](outp: pointer, inp: ptr T) = - when T is uint64: - bigEndian64(outp, inp) - elif T is uint32: - bigEndian32(outp, inp) - elif T is uint16: - bigEndian16(outp, inp) - -template littleEndianXX[T: uint16 or uint32 or uint64](outp: pointer, inp: ptr T) = - when T is uint64: - littleEndian64(outp, inp) - elif T is uint32: - littleEndian32(outp, inp) - elif T is uint16: - littleEndian16(outp, inp) - -func dumpRawUintLE( - dst: var openarray[byte], - src: BigInt) {.inline.}= - ## Serialize a bigint into its canonical big-endian representation - ## I.e least significant bit is aligned to buffer boundary - - 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: src[src_idx].BaseType - else: 0 - inc src_idx - - if acc_len == 0: - # Edge case, we need to refill the buffer to output 64-bit - # as we can only read 63-bit per word - acc = w - acc_len = WordBitSize + for srcIdx in skip ..< hexStr.len: + let nibble = hexStr[srcIdx].readHexChar shl shift + when order == bigEndian: + output[dstIdx] = output[dstIdx] or nibble else: - let lo = (w shl acc_len) or acc - dec acc_len - acc = w shr (WordBitSize - acc_len) + output[output.high - dstIdx] = output[output.high - dstIdx] or nibble + shift = (shift + 4) and 4 + dstIdx += shift shr 2 - if tail >= sizeof(Word): - # Unrolled copy - # debugecho src.repr - littleEndianXX(dst[dst_idx].addr, lo.unsafeAddr) - dst_idx += sizeof(Word) - tail -= sizeof(Word) - else: - # Process the tail - when cpuEndian == littleEndian: - # When requesting little-endian on little-endian platform - # we can just copy each byte - for i in dst_idx ..< tail: - dst[dst_idx] = byte(lo shr (i-dst_idx)) - else: - # We need to copy from the end - for i in 0 ..< tail: - dst[dst_idx] = byte(lo shr (tail-i)) +func toHex(bytes: openarray[byte], order: static[Endianness]): string = + ## Convert a byte-array to its hex representation + ## Output is in lowercase and not prefixed. + const hexChars = "0123456789abcdef" -func dumpRawUint*( - dst: var openarray[byte], - src: BigInt, - order: static Endianness) = - ## Serialize a bigint into its canonical big-endian or little endian - ## representation. - ## A destination buffer of size "BigInt.bits div 8" at minimum is needed. + result = newString(2 * bytes.len) + for i in 0 ..< bytes.len: + when order == system.cpuEndian: + result[2*i] = hexChars[int bytes[i] shr 4 and 0xF] + result[2*i+1] = hexChars[int bytes[i] and 0xF] + else: + result[2*i] = hexChars[int bytes[bytes.high - i] shr 4 and 0xF] + result[2*i+1] = hexChars[int bytes[bytes.high - i] and 0xF] + + +# ############################################################ +# +# Hex conversion +# +# ############################################################ + +func fromHex*(T: type BigInt, s: string): T = + ## Convert a hex string to BigInt that can hold + ## the specified number of bits ## - ## 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 + ## For example `fromHex(BigInt[256], "0x123456")` + ## + ## Hex string is assumed big-endian - if dst.len < static(BigInt.bits div 8): - raise newException(ValueError, "BigInt -> Raw int conversion: destination buffer is too small") + # 1. Convert to canonical uint + const canonLen = (T.bits + 8 - 1) div 8 + var bytes: array[canonLen, byte] + hexToPaddedByteArray(s, bytes, littleEndian) - when BigInt.bits == 0: - zeroMem(dst, dst.len) + # 2. Convert canonical uint to Big Int + result = parseRawUint(bytes, T.bits, littleEndian) + +func dumpHex*(big: BigInt, order: static Endianness = bigEndian): string = + ## Stringify an int to hex. + ## Note. Leading zeros are not removed. + ## + ## This is a raw memory dump. Output will be padded with 0 + ## if the big int does not use the full memory allocated for it. + ## + ## Regardless of the machine endianness the output will be big-endian hex. + ## + ## For example a BigInt representing 10 will be + ## - 0x0A for BigInt[8] + ## - 0x000A for BigInt[16] + ## - 0x00000000_0000000A for BigInt[64] + ## + ## CT: + ## - no leaks - when order == littleEndian: - dumpRawUintLE(dst, src) - else: - {.error: "Not implemented at the moment".} + # 1. Convert Big Int to canonical uint + const canonLen = (big.bits + 8 - 1) div 8 + var bytes: array[canonLen, byte] + dumpRawUint(bytes, big, cpuEndian) + + # 2 Convert canonical uint to hex + result = bytes.toHex(order) + \ No newline at end of file diff --git a/tests/all_tests.nim b/tests/all_tests.nim index f766bdf..fe7ee43 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -6,4 +6,5 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - test_word_types + test_word_types, + test_io diff --git a/tests/test_io.nim b/tests/test_io.nim index b3508e0..fa16bb1 100644 --- a/tests/test_io.nim +++ b/tests/test_io.nim @@ -60,3 +60,25 @@ suite "IO": var r_bytes: array[8, byte] dumpRawUint(r_bytes, big, littleEndian) check: x_bytes == r_bytes + + test "Round trip on elliptic curve constants": + block: # Secp256k1 - https://en.bitcoin.it/wiki/Secp256k1 + const p = "fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f" + let x = fromHex(BigInt[256], p) + let hex = x.dumpHex(bigEndian) + + check: p == hex + + block: # alt-BN128 - https://github.com/ethereum/py_ecc/blob/master/py_ecc/fields/field_properties.py + const p = "30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47" + let x = fromHex(BigInt[254], p) + let hex = x.dumpHex(bigEndian) + + check: p == hex + + block: # BLS12-381 - https://github.com/ethereum/py_ecc/blob/master/py_ecc/fields/field_properties.py + const p = "1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab" + let x = fromHex(BigInt[381], p) + let hex = x.dumpHex(bigEndian) + + check: p == hex