* create a codecs.nim file for hex/base64 and other encoding conversions

* improve maintenance/readability of hex conversion

* add skeleton of constant-time base64 decoding

* use raw casts

* use raw casts only for same size types
This commit is contained in:
Mamy Ratsimbazafy 2023-02-07 13:10:17 +01:00 committed by GitHub
parent 95114bf707
commit cbb454fff1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 420 additions and 317 deletions

View File

@ -7,7 +7,7 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms. # at your option. This file may not be copied, modified, or distributed except according to those terms.
import import
../../platforms/abstractions, ../../platforms/[abstractions, signed_secret_words],
./limbs, ./limbs_unsaturated ./limbs, ./limbs_unsaturated
# No exceptions allowed # No exceptions allowed

View File

@ -6,11 +6,9 @@
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0). # * 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. # at your option. This file may not be copied, modified, or distributed except according to those terms.
import ../../platforms/abstractions import ../../platforms/[abstractions, signed_secret_words]
type type
SignedSecretWord* = distinct SecretWord
LimbsUnsaturated*[N, Excess: static int] = object LimbsUnsaturated*[N, Excess: static int] = object
## An array of signed secret words ## An array of signed secret words
## with each word having their top Excess bits unused between function calls ## with each word having their top Excess bits unused between function calls
@ -191,141 +189,13 @@ func setOne*(a: var LimbsUnsaturated) =
for i in 1 ..< a.words.len: for i in 1 ..< a.words.len:
a[i] = SignedSecretWord(0) a[i] = SignedSecretWord(0)
# ############################################################
#
# Arithmetic
#
# ############################################################
# Workaround bug # Workaround bug
# --------------
func `xor`*(x,y: SecretWord): SecretWord {.inline.} = func `xor`*(x,y: SecretWord): SecretWord {.inline.} =
# For some reason the template defined in constant_time.nim isn't found # For some reason the template defined in constant_time.nim isn't found
SecretWord(x.BaseType xor y.BaseType) SecretWord(x.BaseType xor y.BaseType)
when sizeof(int) == 8 and not defined(Constantine32):
type
SignedBaseType* = int64
else:
type
SignedBaseType* = int32
template fmap(x: SignedSecretWord, op: untyped, y: SignedSecretWord): SignedSecretWord =
## Unwrap x and y from their distinct type
## Apply op, and rewrap them
SignedSecretWord(op(SecretWord(x), SecretWord(y)))
template fmapAsgn(x: SignedSecretWord, op: untyped, y: SignedSecretWord) =
## Unwrap x and y from their distinct type
## Apply assignment op, and rewrap them
op(SecretWord(x), SecretWord(y))
template `and`*(x, y: SignedSecretWord): SignedSecretWord = fmap(x, `and`, y)
template `or`*(x, y: SignedSecretWord): SignedSecretWord = fmap(x, `or`, y)
template `xor`*(x, y: SignedSecretWord): SignedSecretWord = SignedSecretWord(BaseType(x) xor BaseType(y))
template `not`*(x: SignedSecretWord): SignedSecretWord = SignedSecretWord(not SecretWord(x))
template `+`*(x, y: SignedSecretWord): SignedSecretWord = fmap(x, `+`, y)
template `+=`*(x: var SignedSecretWord, y: SignedSecretWord) = fmapAsgn(x, `+=`, y)
template `-`*(x, y: SignedSecretWord): SignedSecretWord = fmap(x, `-`, y)
template `-=`*(x: var SignedSecretWord, y: SignedSecretWord) = fmapAsgn(x, `-=`, y)
template `-`*(x: SignedSecretWord): SignedSecretWord =
# We don't use Nim signed integers to avoid range checks
SignedSecretWord(-SecretWord(x))
template `*`*(x, y: SignedSecretWord): SignedSecretWord =
# Warning ⚠️ : We assume that hardware multiplication is constant time
# but this is not always true. See https://www.bearssl.org/ctmul.html
fmap(x, `*`, y)
# shifts
template ashr*(x: SignedSecretWord, y: SomeNumber): SignedSecretWord =
## Arithmetic right shift
# We need to cast to Nim ints without Nim checks
SignedSecretWord(cast[SignedBaseType](x).ashr(y))
template lshr*(x: SignedSecretWord, y: SomeNumber): SignedSecretWord =
## Logical right shift
SignedSecretWord(SecretWord(x) shr y)
template lshl*(x: SignedSecretWord, y: SomeNumber): SignedSecretWord =
## Logical left shift
SignedSecretWord(SecretWord(x) shl y)
# ############################################################
#
# Hardened Boolean primitives
#
# ############################################################
template `==`*(x, y: SignedSecretWord): SecretBool =
SecretWord(x) == SecretWord(y)
# ############################################################
#
# Conditional arithmetic
#
# ############################################################
# SignedSecretWord
# ----------------
func isNeg*(a: SignedSecretWord): SignedSecretWord {.inline.} =
## Returns 1 if a is negative
## and 0 otherwise
a.lshr(WordBitWidth-1)
func isOdd*(a: SignedSecretWord): SignedSecretWord {.inline.} =
## Returns 1 if a is odd
## and 0 otherwise
a and SignedSecretWord(1)
func isZeroMask*(a: SignedSecretWord): SignedSecretWord {.inline.} =
## Produce the -1 mask if a is 0
## and 0 otherwise
# In x86 assembly, we can use "neg" + "sbb"
-SignedSecretWord(a.SecretWord().isZero())
func isNegMask*(a: SignedSecretWord): SignedSecretWord {.inline.} =
## Produce the -1 mask if a is negative
## and 0 otherwise
a.ashr(WordBitWidth-1)
func isOddMask*(a: SignedSecretWord): SignedSecretWord {.inline.} =
## Produce the -1 mask if a is odd
## and 0 otherwise
-(a and SignedSecretWord(1))
func csetZero*(a: var SignedSecretWord, mask: SignedSecretWord) {.inline.} =
## Conditionally set `a` to 0
## mask must be 0 (0x00000...0000) (kept as is)
## or -1 (0xFFFF...FFFF) (zeroed)
a = a and mask
func cneg*(
a: SignedSecretWord,
mask: SignedSecretWord): SignedSecretWord {.inline.} =
## Conditionally negate `a`
## mask must be 0 (0x00000...0000) (no negation)
## or -1 (0xFFFF...FFFF) (negation)
(a xor mask) - mask
func cadd*(
a: var SignedSecretWord,
b: SignedSecretWord,
mask: SignedSecretWord) {.inline.} =
## Conditionally add `b` to `a`
## mask must be 0 (0x00000...0000) (no addition)
## or -1 (0xFFFF...FFFF) (addition)
a = a + (b and mask)
func csub*(
a: var SignedSecretWord,
b: SignedSecretWord,
mask: SignedSecretWord) {.inline.} =
## Conditionally substract `b` from `a`
## mask must be 0 (0x00000...0000) (no substraction)
## or -1 (0xFFFF...FFFF) (substraction)
a = a - (b and mask)
# UnsaturatedLimbs # UnsaturatedLimbs
# ---------------- # ----------------
@ -371,49 +241,3 @@ func cadd*(
## Carry propagation is deferred ## Carry propagation is deferred
for i in 0 ..< a.words.len: for i in 0 ..< a.words.len:
a[i].cadd(b[i], mask) a[i].cadd(b[i], mask)
# ############################################################
#
# Double-Width signed arithmetic
#
# ############################################################
type DSWord* = object
lo*, hi*: SignedSecretWord
func smulAccNoCarry*(r: var DSWord, a, b: SignedSecretWord) {.inline.}=
## Signed accumulated multiplication
## (_, hi, lo) += a*b
## This assumes no overflowing
var UV: array[2, SecretWord]
var carry: Carry
smul(UV[1], UV[0], SecretWord a, SecretWord b)
addC(carry, UV[0], UV[0], SecretWord r.lo, Carry(0))
addC(carry, UV[1], UV[1], SecretWord r.hi, carry)
r.lo = SignedSecretWord UV[0]
r.hi = SignedSecretWord UV[1]
func ssumprodAccNoCarry*(r: var DSWord, a, u, b, v: SignedSecretWord) {.inline.}=
## Accumulated sum of products
## (_, hi, lo) += a*u + b*v
## This assumes no overflowing
var carry: Carry
var x1, x0, y1, y0: SecretWord
smul(x1, x0, SecretWord a, SecretWord u)
addC(carry, x0, x0, SecretWord r.lo, Carry(0))
addC(carry, x1, x1, SecretWord r.hi, carry)
smul(y1, y0, SecretWord b, SecretWord v)
addC(carry, x0, x0, y0, Carry(0))
addC(carry, x1, x1, y1, carry)
r.lo = SignedSecretWord x0
r.hi = SignedSecretWord x1
func ashr*(
r: var DSWord,
k: SomeInteger) {.inline.} =
## Arithmetic right-shift of a double-word
## This does not normalize the excess bits
r.lo = r.lo.lshr(k) or r.hi.lshl(WordBitWidth - k)
r.hi = r.hi.ashr(k)

View File

@ -11,7 +11,7 @@
# - Burning memory to ensure secrets are not left after dealloc. # - Burning memory to ensure secrets are not left after dealloc.
import import
../../platforms/[abstractions, endians], ../../platforms/[abstractions, endians, codecs],
../arithmetic/bigints, ../arithmetic/bigints,
../config/type_bigint ../config/type_bigint
@ -51,7 +51,7 @@ func unmarshalLE[T](
## - no leaks ## - no leaks
## ##
## Can work at compile-time ## Can work at compile-time
## ##
## It is possible to use a 63-bit representation out of a 64-bit words ## It is possible to use a 63-bit representation out of a 64-bit words
## by setting `wordBitWidth` to something different from sizeof(T) * 8 ## by setting `wordBitWidth` to something different from sizeof(T) * 8
## This might be useful for architectures with no add-with-carry instructions. ## This might be useful for architectures with no add-with-carry instructions.
@ -61,7 +61,7 @@ func unmarshalLE[T](
dst_idx = 0 dst_idx = 0
acc = T(0) acc = T(0)
acc_len = 0 acc_len = 0
for src_idx in 0 ..< src.len: for src_idx in 0 ..< src.len:
let src_byte = T(src[src_idx]) let src_byte = T(src[src_idx])
@ -98,7 +98,7 @@ func unmarshalBE[T](
## - no leaks ## - no leaks
## ##
## Can work at compile-time ## Can work at compile-time
## ##
## It is possible to use a 63-bit representation out of a 64-bit words ## It is possible to use a 63-bit representation out of a 64-bit words
## by setting `wordBitWidth` to something different from sizeof(T) * 8 ## by setting `wordBitWidth` to something different from sizeof(T) * 8
## This might be useful for architectures with no add-with-carry instructions. ## This might be useful for architectures with no add-with-carry instructions.
@ -205,7 +205,7 @@ func marshalLE[T](
wordBitWidth: static int) = wordBitWidth: static int) =
## Serialize a bigint into its canonical little-endian representation ## Serialize a bigint into its canonical little-endian representation
## I.e least significant bit first ## I.e least significant bit first
## ##
## It is possible to use a 63-bit representation out of a 64-bit words ## It is possible to use a 63-bit representation out of a 64-bit words
## by setting `wordBitWidth` to something different from sizeof(T) * 8 ## by setting `wordBitWidth` to something different from sizeof(T) * 8
## This might be useful for architectures with no add-with-carry instructions. ## This might be useful for architectures with no add-with-carry instructions.
@ -271,7 +271,7 @@ func marshalBE[T](
## ##
## In cryptography specifications, this is often called ## In cryptography specifications, this is often called
## "Octet string to Integer" ## "Octet string to Integer"
## ##
## It is possible to use a 63-bit representation out of a 64-bit words ## It is possible to use a 63-bit representation out of a 64-bit words
## by setting `wordBitWidth` to something different from sizeof(T) * 8 ## by setting `wordBitWidth` to something different from sizeof(T) * 8
## This might be useful for architectures with no add-with-carry instructions. ## This might be useful for architectures with no add-with-carry instructions.
@ -367,93 +367,6 @@ func marshal*(
{.pop.} # {.push raises: [].} {.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 toHex*(bytes: openarray[byte]): string =
## Convert a byte-array to its hex representation
## Output is in lowercase and prefixed with 0x
const hexChars = "0123456789abcdef"
result = newString(2 + 2 * bytes.len)
result[0] = '0'
result[1] = 'x'
for i in 0 ..< bytes.len:
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)
func fromHex*[N: static int](T: type array[N, byte], hex: string): T =
hexToPaddedByteArray(hex, result, bigEndian)
# ############################################################ # ############################################################
# #
# Hex conversion # Hex conversion
@ -469,7 +382,7 @@ func fromHex*(a: var BigInt, s: string) =
## Hex string is assumed big-endian ## Hex string is assumed big-endian
## ##
## Procedure is constant-time except for the presence (or absence) of the 0x prefix. ## 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. ## This procedure is intended for configuration, prototyping, research and debugging purposes.
## You MUST NOT use it for production. ## You MUST NOT use it for production.
## ##
@ -478,7 +391,7 @@ func fromHex*(a: var BigInt, s: string) =
# 1. Convert to canonical uint # 1. Convert to canonical uint
const canonLen = (BigInt.bits + 8 - 1) div 8 const canonLen = (BigInt.bits + 8 - 1) div 8
var bytes: array[canonLen, byte] var bytes: array[canonLen, byte]
hexToPaddedByteArray(s, bytes, bigEndian) bytes.paddedFromHex(s, bigEndian)
# 2. Convert canonical uint to Big Int # 2. Convert canonical uint to Big Int
a.unmarshal(bytes, bigEndian) a.unmarshal(bytes, bigEndian)
@ -492,7 +405,7 @@ func fromHex*(T: type BigInt, s: string): T {.noInit.} =
## Hex string is assumed big-endian ## Hex string is assumed big-endian
## ##
## Procedure is constant-time except for the presence (or absence) of the 0x prefix. ## 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. ## This procedure is intended for configuration, prototyping, research and debugging purposes.
## You MUST NOT use it for production. ## You MUST NOT use it for production.
## ##
@ -511,7 +424,7 @@ func appendHex*(dst: var string, big: BigInt, order: static Endianness = bigEndi
## ##
## This is useful to reduce the number of allocations when serializing ## This is useful to reduce the number of allocations when serializing
## Fp towers ## Fp towers
## ##
## This function may allocate. ## This function may allocate.
# 1. Convert Big Int to canonical uint # 1. Convert Big Int to canonical uint

View File

@ -0,0 +1,170 @@
# 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.
import ./abstractions, ./signed_secret_words
# ############################################################
#
# Codecs
#
# ############################################################
template sw(a: auto): SecretWord = SecretWord(a)
template ssw(a: auto): SignedSecretWord = SignedSecretWord(a)
# ############################################################
#
# Hexadecimal
#
# ############################################################
func readHexChar(c: char): SecretWord {.inline.} =
## Converts an hex char to an int
const OOR = ssw 256 # Push chars out-of-range
var c = ssw(c) + OOR
# '0' -> '9' maps to [0, 9]
c.csub(OOR + ssw('0') - ssw 0, c.isInRangeMask(ssw('0') + OOR, ssw('9') + OOR))
# 'A' -> 'Z' maps to [10, 16)
c.csub(OOR + ssw('A') - ssw 10, c.isInRangeMask(ssw('A') + OOR, ssw('Z') + OOR))
# 'a' -> 'z' maps to [10, 16)
c.csub(OOR + ssw('a') - ssw 10, c.isInRangeMask(ssw('a') + OOR, ssw('z') + OOR))
c = c and ssw(0xF) # Prevent overflow of invalid inputs
return sw(c)
func paddedFromHex*(output: var openArray[byte], hexStr: string, 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.
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 toHex*(bytes: openarray[byte]): string =
## Convert a byte-array to its hex representation
## Output is in lowercase and prefixed with 0x
const hexChars = "0123456789abcdef"
result = newString(2 + 2 * bytes.len)
result[0] = '0'
result[1] = 'x'
for i in 0 ..< bytes.len:
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)
func fromHex*[N: static int](T: type array[N, byte], hex: string): T =
result.paddedFromHex(hex, bigEndian)
# ############################################################
#
# Base64
#
# ############################################################
func base64_decode(
dst: var openArray[byte],
src: openArray[char]): int {.used.} =
## Decode a Base64 string/bytearray input into
## an octet string
## This procedure is constant-time, except for new lines, padding and invalid base64 characters
##
## Returns -1 if the buffer is too small
## or the number of bytes written.
## Bytes are written from the start of the buffer
# TODO: unexposed, missing comprehensive test suite.
var s, d = 0
var vals: array[4, SecretWord]
var bytes: array[3, byte]
while s < src.len and d < dst.len:
var padding = ssw 0
for i in 0 ..< 4:
const OOR = ssw 256 # Push chars out-of-range
var c = ssw(src[s]) + OOR
s += 1
# 'A' -> 'Z' maps to [0, 26)
c.csub(OOR + ssw('A'), c.isInRangeMask(ssw('A') + OOR, ssw('Z') + OOR))
# 'a' -> 'z' maps to [26, 52)
c.csub(OOR + ssw('a') - ssw 26, c.isInRangeMask(ssw('a') + OOR, ssw('z') + OOR))
# '0' -> '9' maps to [52, 61)
c.csub(OOR + ssw('0') - ssw 52, c.isInRangeMask(ssw('0') + OOR, ssw('9') + OOR))
# '+' maps to 62
c.csub(OOR + ssw('+') - ssw 62, c.isInRangeMask(ssw('+') + OOR, ssw('+') + OOR))
# '/' maps to 63
c.csub(OOR + ssw('/') - ssw 63, c.isInRangeMask(ssw('/') + OOR, ssw('/') + OOR))
# '=' is padding and everything else is ignored
padding.cadd(ssw 1, c.isInRangeMask(ssw('=') + OOR, ssw('=') + OOR))
# https://www.rfc-editor.org/rfc/rfc7468#section-2
# "Furthermore, parsers SHOULD ignore whitespace and other non-
# base64 characters and MUST handle different newline conventions."
#
# Unfortunately, there is no way to deal with newlines, padding and invalid characters
# without revealing that they exist when we do not increment the destination index
if c.int >= OOR.int:
continue
vals[i] = SecretWord(c)
bytes[0] = byte((vals[0] shl 2) or (vals[1] shr 4))
bytes[1] = byte((vals[1] shl 4) or (vals[2] shr 2))
bytes[2] = byte((vals[2] shl 6) or vals[3] )
for i in 0 ..< 3 - padding.int:
if d >= dst.len:
return -1
dst[d] = bytes[i]
d += 1
return d

View File

@ -90,7 +90,7 @@ type
DynWord = uint32 or uint64 DynWord = uint32 or uint64
BigNum[T: DynWord] = object BigNum[T: DynWord] = object
bits: uint32 bits: uint32
limbs: seq[T] limbs: seq[T]
# Serialization # Serialization
# ------------------------------------------------ # ------------------------------------------------
@ -102,14 +102,14 @@ func byteLen(bits: SomeInteger): SomeInteger {.inline.} =
func wordsRequiredForBits(bits, wordBitwidth: SomeInteger): SomeInteger {.inline.} = func wordsRequiredForBits(bits, wordBitwidth: SomeInteger): SomeInteger {.inline.} =
## Compute the number of limbs required ## Compute the number of limbs required
## from the announced bit length ## from the announced bit length
debug: doAssert wordBitwidth == 32 or wordBitwidth == 64 # Power of 2 debug: doAssert wordBitwidth == 32 or wordBitwidth == 64 # Power of 2
(bits + wordBitwidth - 1) shr log2_vartime(uint32 wordBitwidth) # 5x to 55x faster than dividing by wordBitwidth (bits + wordBitwidth - 1) shr log2_vartime(uint32 wordBitwidth) # 5x to 55x faster than dividing by wordBitwidth
func fromHex[T](a: var BigNum[T], s: string) = func fromHex[T](a: var BigNum[T], s: string) =
var bytes = newSeq[byte](a.bits.byteLen()) var bytes = newSeq[byte](a.bits.byteLen())
hexToPaddedByteArray(s, bytes, bigEndian) bytes.paddedFromHex(s, bigEndian)
# 2. Convert canonical uint to BigNum # 2. Convert canonical uint to BigNum
const wordBitwidth = sizeof(T) * 8 const wordBitwidth = sizeof(T) * 8
a.limbs.unmarshal(bytes, wordBitwidth, bigEndian) a.limbs.unmarshal(bytes, wordBitwidth, bigEndian)
@ -117,7 +117,7 @@ func fromHex[T](a: var BigNum[T], s: string) =
func fromHex[T](BN: type BigNum[T], bits: uint32, s: string): BN = func fromHex[T](BN: type BigNum[T], bits: uint32, s: string): BN =
const wordBitwidth = sizeof(T) * 8 const wordBitwidth = sizeof(T) * 8
let numWords = wordsRequiredForBits(bits, wordBitwidth) let numWords = wordsRequiredForBits(bits, wordBitwidth)
result.bits = bits result.bits = bits
result.limbs.setLen(numWords) result.limbs.setLen(numWords)
result.fromHex(s) result.fromHex(s)
@ -160,7 +160,7 @@ func negInvModWord[T](M: BigNum[T]): T =
## ##
## µ ≡ -1/M[0] (mod 2^64) ## µ ≡ -1/M[0] (mod 2^64)
checkValidModulus(M) checkValidModulus(M)
result = invModBitwidth(M.limbs[0]) result = invModBitwidth(M.limbs[0])
# negate to obtain the negative inverse # negate to obtain the negative inverse
result = not(result) + 1 result = not(result) + 1
@ -175,11 +175,11 @@ type
WordSize* = enum WordSize* = enum
size32 size32
size64 size64
Field* = enum Field* = enum
fp fp
fr fr
FieldConst* = object FieldConst* = object
wordTy: TypeRef wordTy: TypeRef
fieldTy: TypeRef fieldTy: TypeRef
@ -193,7 +193,7 @@ type
prefix*: string prefix*: string
wordSize*: WordSize wordSize*: WordSize
fp*: FieldConst fp*: FieldConst
fr*: FieldConst fr*: FieldConst
Opcode* = enum Opcode* = enum
opFpAdd = "fp_add" opFpAdd = "fp_add"
@ -203,7 +203,7 @@ proc setFieldConst(fc: var FieldConst, ctx: ContextRef, wordSize: WordSize, modB
let wordTy = case wordSize let wordTy = case wordSize
of size32: ctx.int32_t() of size32: ctx.int32_t()
of size64: ctx.int64_t() of size64: ctx.int64_t()
let wordBitwidth = case wordSize let wordBitwidth = case wordSize
of size32: 32'u32 of size32: 32'u32
of size64: 64'u32 of size64: 64'u32
@ -212,7 +212,7 @@ proc setFieldConst(fc: var FieldConst, ctx: ContextRef, wordSize: WordSize, modB
fc.wordTy = wordTy fc.wordTy = wordTy
fc.fieldTy = array_t(wordTy, numWords) fc.fieldTy = array_t(wordTy, numWords)
case wordSize case wordSize
of size32: of size32:
let m = BigNum[uint32].fromHex(modBits, modulus) let m = BigNum[uint32].fromHex(modBits, modulus)
@ -239,13 +239,13 @@ proc init*(
prefix: string, wordSize: WordSize, prefix: string, wordSize: WordSize,
fpBits: uint32, fpMod: string, fpBits: uint32, fpMod: string,
frBits: uint32, frMod: string): CurveMetadata = frBits: uint32, frMod: string): CurveMetadata =
result = C(prefix: prefix, wordSize: wordSize) result = C(prefix: prefix, wordSize: wordSize)
result.fp.setFieldConst(ctx, wordSize, fpBits, fpMod) result.fp.setFieldConst(ctx, wordSize, fpBits, fpMod)
result.fr.setFieldConst(ctx, wordSize, frBits, frMod) result.fr.setFieldConst(ctx, wordSize, frBits, frMod)
proc genSymbol*(cm: CurveMetadata, opcode: Opcode): string {.inline.} = proc genSymbol*(cm: CurveMetadata, opcode: Opcode): string {.inline.} =
cm.prefix & cm.prefix &
(if cm.wordSize == size32: "32b_" else: "64b_") & (if cm.wordSize == size32: "32b_" else: "64b_") &
$opcode $opcode
@ -282,7 +282,7 @@ func getSpareBits*(cm: CurveMetadata, field: Field): uint8 {.inline.} =
# ############################################################ # ############################################################
# For array access we need to use: # For array access we need to use:
# #
# builder.extractValue(array, index, name) # builder.extractValue(array, index, name)
# builder.insertValue(array, index, value, name) # builder.insertValue(array, index, value, name)
# #

View File

@ -0,0 +1,195 @@
# 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.
import ./abstractions
type SignedSecretWord* = distinct SecretWord
when sizeof(int) == 8 and not defined(Constantine32):
type
SignedBaseType* = int64
else:
type
SignedBaseType* = int32
# ############################################################
#
# Arithmetic
#
# ############################################################
template fmap(x: SignedSecretWord, op: untyped, y: SignedSecretWord): SignedSecretWord =
## Unwrap x and y from their distinct type
## Apply op, and rewrap them
SignedSecretWord(op(SecretWord(x), SecretWord(y)))
template fmapAsgn(x: SignedSecretWord, op: untyped, y: SignedSecretWord) =
## Unwrap x and y from their distinct type
## Apply assignment op, and rewrap them
op(SecretWord(x), SecretWord(y))
template `and`*(x, y: SignedSecretWord): SignedSecretWord = fmap(x, `and`, y)
template `or`*(x, y: SignedSecretWord): SignedSecretWord = fmap(x, `or`, y)
template `xor`*(x, y: SignedSecretWord): SignedSecretWord = SignedSecretWord(BaseType(x) xor BaseType(y))
template `not`*(x: SignedSecretWord): SignedSecretWord = SignedSecretWord(not SecretWord(x))
template `+`*(x, y: SignedSecretWord): SignedSecretWord = fmap(x, `+`, y)
template `+=`*(x: var SignedSecretWord, y: SignedSecretWord) = fmapAsgn(x, `+=`, y)
template `-`*(x, y: SignedSecretWord): SignedSecretWord = fmap(x, `-`, y)
template `-=`*(x: var SignedSecretWord, y: SignedSecretWord) = fmapAsgn(x, `-=`, y)
template `-`*(x: SignedSecretWord): SignedSecretWord =
# We don't use Nim signed integers to avoid range checks
SignedSecretWord(-SecretWord(x))
template `*`*(x, y: SignedSecretWord): SignedSecretWord =
# Warning ⚠️ : We assume that hardware multiplication is constant time
# but this is not always true. See https://www.bearssl.org/ctmul.html
fmap(x, `*`, y)
# shifts
template ashr*(x: SignedSecretWord, y: SomeNumber): SignedSecretWord =
## Arithmetic right shift
# We need to cast to Nim ints without Nim checks
cast[SignedSecretWord](cast[SignedBaseType](x).ashr(y))
template lshr*(x: SignedSecretWord, y: SomeNumber): SignedSecretWord =
## Logical right shift
SignedSecretWord(SecretWord(x) shr y)
template lshl*(x: SignedSecretWord, y: SomeNumber): SignedSecretWord =
## Logical left shift
SignedSecretWord(SecretWord(x) shl y)
# ############################################################
#
# Hardened Boolean primitives
#
# ############################################################
template `==`*(x, y: SignedSecretWord): SecretBool =
SecretWord(x) == SecretWord(y)
# ############################################################
#
# Conditional arithmetic
#
# ############################################################
# SignedSecretWord
# ----------------
func isNeg*(a: SignedSecretWord): SignedSecretWord {.inline.} =
## Returns 1 if a is negative
## and 0 otherwise
a.lshr(WordBitWidth-1)
func isOdd*(a: SignedSecretWord): SignedSecretWord {.inline.} =
## Returns 1 if a is odd
## and 0 otherwise
a and SignedSecretWord(1)
func isZeroMask*(a: SignedSecretWord): SignedSecretWord {.inline.} =
## Produce the -1 mask if a is 0
## and 0 otherwise
# In x86 assembly, we can use "neg" + "sbb"
-SignedSecretWord(a.SecretWord().isZero())
func isNegMask*(a: SignedSecretWord): SignedSecretWord {.inline.} =
## Produce the -1 mask if a is negative
## and 0 otherwise
a.ashr(WordBitWidth-1)
func isOddMask*(a: SignedSecretWord): SignedSecretWord {.inline.} =
## Produce the -1 mask if a is odd
## and 0 otherwise
-(a and SignedSecretWord(1))
func isInRangeMask*(val, lo, hi: SignedSecretWord): SignedSecretWord {.inline.} =
## Produce 0b11111111 mask if lo <= val <= hi (inclusive range)
## and 0b00000000 otherwise
let loInvMask = isNegMask(val-lo) # if val-lo < 0 => val < lo
let hiInvMask = isNegMask(hi-val) # if hi-val < 0 => val > hi
return not(loInvMask or hiInvMask)
func csetZero*(a: var SignedSecretWord, mask: SignedSecretWord) {.inline.} =
## Conditionally set `a` to 0
## mask must be 0 (0x00000...0000) (kept as is)
## or -1 (0xFFFF...FFFF) (zeroed)
a = a and mask
func cneg*(
a: SignedSecretWord,
mask: SignedSecretWord): SignedSecretWord {.inline.} =
## Conditionally negate `a`
## mask must be 0 (0x00000...0000) (no negation)
## or -1 (0xFFFF...FFFF) (negation)
(a xor mask) - mask
func cadd*(
a: var SignedSecretWord,
b: SignedSecretWord,
mask: SignedSecretWord) {.inline.} =
## Conditionally add `b` to `a`
## mask must be 0 (0x00000...0000) (no addition)
## or -1 (0xFFFF...FFFF) (addition)
a = a + (b and mask)
func csub*(
a: var SignedSecretWord,
b: SignedSecretWord,
mask: SignedSecretWord) {.inline.} =
## Conditionally substract `b` from `a`
## mask must be 0 (0x00000...0000) (no substraction)
## or -1 (0xFFFF...FFFF) (substraction)
a = a - (b and mask)
# ############################################################
#
# Double-Width signed arithmetic
#
# ############################################################
type DSWord* = object
lo*, hi*: SignedSecretWord
func smulAccNoCarry*(r: var DSWord, a, b: SignedSecretWord) {.inline.}=
## Signed accumulated multiplication
## (_, hi, lo) += a*b
## This assumes no overflowing
var UV: array[2, SecretWord]
var carry: Carry
smul(UV[1], UV[0], SecretWord a, SecretWord b)
addC(carry, UV[0], UV[0], SecretWord r.lo, Carry(0))
addC(carry, UV[1], UV[1], SecretWord r.hi, carry)
r.lo = SignedSecretWord UV[0]
r.hi = SignedSecretWord UV[1]
func ssumprodAccNoCarry*(r: var DSWord, a, u, b, v: SignedSecretWord) {.inline.}=
## Accumulated sum of products
## (_, hi, lo) += a*u + b*v
## This assumes no overflowing
var carry: Carry
var x1, x0, y1, y0: SecretWord
smul(x1, x0, SecretWord a, SecretWord u)
addC(carry, x0, x0, SecretWord r.lo, Carry(0))
addC(carry, x1, x1, SecretWord r.hi, carry)
smul(y1, y0, SecretWord b, SecretWord v)
addC(carry, x0, x0, y0, Carry(0))
addC(carry, x1, x1, y1, carry)
r.lo = SignedSecretWord x0
r.hi = SignedSecretWord x1
func ashr*(
r: var DSWord,
k: SomeInteger) {.inline.} =
## Arithmetic right-shift of a double-word
## This does not normalize the excess bits
r.lo = r.lo.lshr(k) or r.hi.lshl(WordBitWidth - k)
r.hi = r.hi.ashr(k)

View File

@ -489,7 +489,7 @@ proc run_EC_mixed_add_impl*(
r_generic.double(a) r_generic.double(a)
r_mixed.madd(a, aAff) r_mixed.madd(a, aAff)
check: bool(r_generic == r_mixed) check: bool(r_generic == r_mixed)
# Aliasing test # Aliasing test
r_mixed = a r_mixed = a
r_mixed += aAff r_mixed += aAff
@ -612,7 +612,7 @@ proc run_EC_subgroups_cofactors_impl*(
doAssert bool Q.isInSubgroup(), "Subgroup check issue on " & $EC & " with Q: " & Q.toHex() doAssert bool Q.isInSubgroup(), "Subgroup check issue on " & $EC & " with Q: " & Q.toHex()
stdout.write '.' stdout.write '.'
stdout.write '\n' stdout.write '\n'
test(ec, randZ = false, gen = Uniform) test(ec, randZ = false, gen = Uniform)
@ -621,7 +621,7 @@ proc run_EC_subgroups_cofactors_impl*(
test(ec, randZ = true, gen = HighHammingWeight) test(ec, randZ = true, gen = HighHammingWeight)
test(ec, randZ = false, gen = Long01Sequence) test(ec, randZ = false, gen = Long01Sequence)
test(ec, randZ = true, gen = Long01Sequence) test(ec, randZ = true, gen = Long01Sequence)
echo " [SUCCESS] Test finished with ", inSubgroup, " points in ", G1_or_G2, " subgroup and ", echo " [SUCCESS] Test finished with ", inSubgroup, " points in ", G1_or_G2, " subgroup and ",
offSubgroup, " points on curve but not in subgroup (before cofactor clearing)" offSubgroup, " points on curve but not in subgroup (before cofactor clearing)"
@ -815,7 +815,7 @@ proc run_EC_batch_add_impl*[N: static int](
test $ec & " batch addition (N=" & $n & ")": test $ec & " batch addition (N=" & $n & ")":
proc test(EC: typedesc, gen: RandomGen) = proc test(EC: typedesc, gen: RandomGen) =
var points = newSeq[ECP_ShortW_Aff[EC.F, EC.G]](n) var points = newSeq[ECP_ShortW_Aff[EC.F, EC.G]](n)
for i in 0 ..< n: for i in 0 ..< n:
points[i] = rng.random_point(ECP_ShortW_Aff[EC.F, EC.G], randZ = false, gen) points[i] = rng.random_point(ECP_ShortW_Aff[EC.F, EC.G], randZ = false, gen)
@ -839,10 +839,10 @@ proc run_EC_batch_add_impl*[N: static int](
var points = newSeq[ECP_ShortW_Aff[EC.F, EC.G]](n) var points = newSeq[ECP_ShortW_Aff[EC.F, EC.G]](n)
let halfN = n div 2 let halfN = n div 2
for i in 0 ..< halfN: for i in 0 ..< halfN:
points[i] = rng.random_point(ECP_ShortW_Aff[EC.F, EC.G], randZ = false, gen) points[i] = rng.random_point(ECP_ShortW_Aff[EC.F, EC.G], randZ = false, gen)
for i in halfN ..< n: for i in halfN ..< n:
# The special cases test relies on internal knowledge that we sum(points[i], points[i+n/2] # The special cases test relies on internal knowledge that we sum(points[i], points[i+n/2]
# It should be changed if scheduling change, for example if we sum(points[2*i], points[2*i+1]) # It should be changed if scheduling change, for example if we sum(points[2*i], points[2*i+1])

View File

@ -10,7 +10,7 @@ import
std/[os, unittest, strutils], std/[os, unittest, strutils],
pkg/jsony, pkg/jsony,
../constantine/blssig_pop_on_bls12381_g2, ../constantine/blssig_pop_on_bls12381_g2,
../constantine/math/io/io_bigints, ../constantine/platforms/codecs,
../constantine/hashes ../constantine/hashes
type type
@ -70,7 +70,7 @@ type
proc parseHook*[N: static int](src: string, pos: var int, value: var array[N, byte]) = proc parseHook*[N: static int](src: string, pos: var int, value: var array[N, byte]) =
var str: string var str: string
parseHook(src, pos, str) parseHook(src, pos, str)
str.hexToPaddedByteArray(value, bigEndian) value.paddedFromHex(str, bigEndian)
const SkippedTests = [ const SkippedTests = [
# By construction, input MUST be 48 bytes, which is enforced at the type-system level. # By construction, input MUST be 48 bytes, which is enforced at the type-system level.

View File

@ -13,7 +13,7 @@ import
../constantine/math/io/io_bigints, ../constantine/math/io/io_bigints,
../constantine/math/config/curves, ../constantine/math/config/curves,
../constantine/math/arithmetic/bigints, ../constantine/math/arithmetic/bigints,
../constantine/platforms/abstractions ../constantine/platforms/[abstractions, codecs]
type SecretKey = matchingOrderBigInt(BLS12_381) type SecretKey = matchingOrderBigInt(BLS12_381)
@ -22,7 +22,7 @@ proc toBytes(hex: string): seq[byte] =
let length = hex.len shr 1 - int(hex[0] == '0' and (hex[1] in {'x', 'X'})) let length = hex.len shr 1 - int(hex[0] == '0' and (hex[1] in {'x', 'X'}))
result.newSeq(length) result.newSeq(length)
hex.hexToPaddedByteArray(result, bigEndian) result.paddedFromHex(hex, bigEndian)
proc test0 = proc test0 =
let seed = toBytes"0xc55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04" let seed = toBytes"0xc55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04"

View File

@ -12,7 +12,7 @@ import
# 3rd party # 3rd party
pkg/jsony, pkg/jsony,
# Internals # Internals
../constantine/math/io/io_bigints, ../constantine/platforms/codecs,
../constantine/ethereum_evm_precompiles ../constantine/ethereum_evm_precompiles
type type
@ -48,7 +48,7 @@ template runBN256Tests(filename: string, funcname: untyped, osize: static int) =
# Length: 2 hex characters -> 1 byte # Length: 2 hex characters -> 1 byte
var inputbytes = newSeq[byte](test.Input.len div 2) var inputbytes = newSeq[byte](test.Input.len div 2)
test.Input.hexToPaddedByteArray(inputbytes, bigEndian) inputbytes.paddedFromHex(test.Input, bigEndian)
var r: array[osize, byte] var r: array[osize, byte]
var expected: array[osize, byte] var expected: array[osize, byte]
@ -57,15 +57,15 @@ template runBN256Tests(filename: string, funcname: untyped, osize: static int) =
if status != cttEVM_Success: if status != cttEVM_Success:
reset(r) reset(r)
test.Expected.hexToPaddedByteArray(expected, bigEndian) expected.paddedFromHex(test.Expected, bigEndian)
doAssert r == expected, "[Test Failure]\n" & doAssert r == expected, "[Test Failure]\n" &
" " & funcname.astToStr & " status: " & $status & "\n" & " " & funcname.astToStr & " status: " & $status & "\n" &
" " & "result: " & r.toHex() & "\n" & " " & "result: " & r.toHex() & "\n" &
" " & "expected: " & expected.toHex() & '\n' " " & "expected: " & expected.toHex() & '\n'
stdout.write "Success\n" stdout.write "Success\n"
`bn256testrunner _ funcname`() `bn256testrunner _ funcname`()
runBN256Tests("bn256Add.json", eth_evm_ecadd, 64) runBN256Tests("bn256Add.json", eth_evm_ecadd, 64)

View File

@ -1,7 +1,7 @@
import import
# Internals # Internals
../constantine/hashes, ../constantine/hashes,
../constantine/math/io/io_bigints, ../constantine/platforms/codecs,
# Helpers # Helpers
../helpers/prng_unsafe ../helpers/prng_unsafe
@ -156,7 +156,7 @@ proc main() =
rng.innerTest(1_000_000 ..< 50_000_000) rng.innerTest(1_000_000 ..< 50_000_000)
echo "SHA256 - Differential testing vs OpenSSL - SUCCESS" echo "SHA256 - Differential testing vs OpenSSL - SUCCESS"
else: else:
echo "SHA256 - Differential testing vs OpenSSL - [SKIPPED]" echo "SHA256 - Differential testing vs OpenSSL - [SKIPPED]"

View File

@ -11,7 +11,8 @@ import
../constantine/hash_to_curve/h2c_hash_to_field, ../constantine/hash_to_curve/h2c_hash_to_field,
../constantine/math/config/[curves_declaration, type_ff], ../constantine/math/config/[curves_declaration, type_ff],
../constantine/math/extension_fields/towers, ../constantine/math/extension_fields/towers,
../constantine/math/io/[io_bigints, io_fields, io_extfields] ../constantine/math/io/[io_fields, io_extfields],
../constantine/platforms/codecs
# Test vectors for expandMessageXMD # Test vectors for expandMessageXMD
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------

View File

@ -7,7 +7,7 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms. # at your option. This file may not be copied, modified, or distributed except according to those terms.
import import
../constantine/math/io/io_bigints, ../constantine/platforms/codecs,
../constantine/[hashes, mac/mac_hmac, kdf/kdf_hkdf] ../constantine/[hashes, mac/mac_hmac, kdf/kdf_hkdf]
proc hexToBytes(s: string): seq[byte] = proc hexToBytes(s: string): seq[byte] =
@ -19,7 +19,7 @@ proc hexToBytes(s: string): seq[byte] =
(int(s[1] == 'x') or int(s[1] == 'X')) (int(s[1] == 'x') or int(s[1] == 'X'))
) )
result.setLen((s.len - skip) div 2) result.setLen((s.len - skip) div 2)
s.hexToPaddedByteArray(result, bigEndian) result.paddedFromHex(s, bigEndian)
template test(id, constants: untyped) = template test(id, constants: untyped) =
proc `test _ id`() = proc `test _ id`() =

View File

@ -10,7 +10,7 @@ import
std/unittest, std/unittest,
../constantine/mac/mac_hmac, ../constantine/mac/mac_hmac,
../constantine/hashes, ../constantine/hashes,
../constantine/math/io/io_bigints ../constantine/platforms/codecs
type TestVector = object type TestVector = object
key: seq[byte] key: seq[byte]
@ -25,15 +25,15 @@ proc doTest(key, data, digest: string) =
doAssert (data.len and 1) == 0, "An hex string must be of even length" doAssert (data.len and 1) == 0, "An hex string must be of even length"
doAssert (digest.len and 1) == 0, "An hex string must be of even length" doAssert (digest.len and 1) == 0, "An hex string must be of even length"
doAssert digest.len <= 64, "HMAC-SHA256 hex string must be at most length 64 (32 bytes)" doAssert digest.len <= 64, "HMAC-SHA256 hex string must be at most length 64 (32 bytes)"
tv.key.newSeq(key.len div 2) tv.key.newSeq(key.len div 2)
key.hexToPaddedByteArray(tv.key, bigEndian) tv.key.paddedFromHex(key, bigEndian)
tv.data.newSeq(data.len div 2) tv.data.newSeq(data.len div 2)
data.hexToPaddedByteArray(tv.data, bigEndian) tv.data.paddedFromHex(data, bigEndian)
tv.truncatedLen = digest.len div 2 tv.truncatedLen = digest.len div 2
digest.hexToPaddedByteArray(tv.digest, bigEndian) tv.digest.paddedFromHex(digest, bigEndian)
var output{.noInit.}: array[32, byte] var output{.noInit.}: array[32, byte]