Add constant-time raw bytes/integers parsing

This commit is contained in:
mratsim 2018-12-02 20:57:32 +01:00
parent b496f57c68
commit 43ac4972a0
3 changed files with 153 additions and 1 deletions

View File

@ -43,7 +43,7 @@ type Word* = Ct[uint64]
const WordBitSize* = sizeof(Word) * 8 - 1 const WordBitSize* = sizeof(Word) * 8 - 1
## Limbs are 63-bit by default ## Limbs are 63-bit by default
func words_required(bits: static int): static int = func words_required(bits: int): int {.compileTime.}=
(bits + WordBitSize - 1) div WordBitSize (bits + WordBitSize - 1) div WordBitSize
type type
@ -59,6 +59,9 @@ const MaxWord* = (not Ct[uint64](0)) shr 1
template `[]`*(a: Bigint, idx: int): Word = template `[]`*(a: Bigint, idx: int): Word =
a.limbs[idx] a.limbs[idx]
template `[]=`*(a: var Bigint, idx: int, w: Word) =
a.limbs[idx] = w
# ############################################################ # ############################################################
# #
# BigInt primitives # BigInt primitives

118
constantine/io.nim Normal file
View File

@ -0,0 +1,118 @@
# Constantine
# Copyright (c) 2018 Status Research & Development GmbH
# 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 ./word_types, ./bigints
# ############################################################
#
# Constant-time hex to byte conversion
#
# ############################################################
func readHexChar(c: char): uint8 {.inline.}=
## Converts an hex char to an int
## CT: leaks position of invalid input if any.
case c
of '0'..'9': result = uint8 ord(c) - ord('0')
of 'a'..'f': result = uint8 ord(c) - ord('a') + 10
of 'A'..'F': result = uint8 ord(c) - ord('A') + 10
else:
raise newException(ValueError, $c & "is not a hexadecimal character")
func skipPrefixes(current_idx: var int, str: string, radix: static range[2..16]) {.inline.} =
## Returns the index of the first meaningful char in `hexStr` by skipping
## "0x" prefix
## CT:
## - leaks if input length < 2
## - leaks if input start with 0x, 0o or 0b prefix
if str.len < 2:
return
assert current_idx == 0, "skipPrefixes only works for prefixes (position 0 and 1 of the string)"
if str[0] == '0':
case str[1]
of {'x', 'X'}:
assert radix == 16, "Parsing mismatch, 0x prefix is only valid for a hexadecimal number (base 16)"
current_idx = 2
of {'o', 'O'}:
assert radix == 8, "Parsing mismatch, 0o prefix is only valid for an octal number (base 8)"
current_idx = 2
of {'b', 'B'}:
assert radix == 2, "Parsing mismatch, 0b prefix is only valid for a binary number (base 2)"
current_idx = 2
else: discard
func nextNonBlank(current_idx: var int, s: string) {.inline.} =
## Move the current index, skipping white spaces and "_" characters.
## CT:
## - Leaks white-spaces and non-white spaces position
const blanks = {' ', '_'}
inc current_idx
while current_idx < s.len and s[current_idx] in blanks:
inc current_idx
func readDecChar(c: range['0'..'9']): int {.inline.}=
## Converts a decimal char to an int
# specialization without branching for base <= 10.
ord(c) - ord('0')
# ############################################################
#
# Parsing from canonical inputs to internal representation
#
# ############################################################
func parseRawUint*(
input: openarray[byte],
bits: static int,
endian: 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
var
dst_idx = 0
acc = Word(0)
acc_len = 0
template body(){.dirty.} =
let src_byte = Word(input[src_idx])
acc = acc and (src_byte shl acc_len)
acc_len += 8 # We count bit by bit
if acc_len >= WordBitSize:
result[dst_idx] = acc and MaxWord
inc dst_idx
acc_len -= WordBitSize
acc = src_byte shr (8 - acc_len)
when endian == bigEndian:
for src_idx in countdown(input.high, 0):
body()
else:
for src_idx in 0 ..< input.len:
body()
if acc_len != 0:
result[dst_idx] = acc
# ############################################################
#
# Serialising from internal representation to canonical format
#
# ############################################################

31
tests/test_io.nim Normal file
View File

@ -0,0 +1,31 @@
# constantine
# Copyright (c) 2018 Status Research & Development GmbH
# 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.
import unittest,
../constantine/[io, bigints]
type T = uint64
suite "IO":
test "Parsing raw integers":
block: # Sanity check
let x = 0'u64
let x_bytes = cast[array[8, byte]](x)
let big = parseRawUint(x_bytes, 64, cpuEndian)
check:
T(big[0]) == 0
T(big[1]) == 0
block: # 2^63 is properly represented on 2 limbs
let x = 1'u64 shl 63
let x_bytes = cast[array[8, byte]](x)
let big = parseRawUint(x_bytes, 64, cpuEndian)
check:
T(big[0]) == 0
T(big[1]) == 1