Ensuring the correctness of a cipher implementation involves rigorous testing and verification. While unit tests are effective for fixed inputs and property tests cover random values, ensuring program logic correctness requires a more systematic approach. LiquidHaskell comes to the rescue, allowing us to annotate functions with properties that are rigorously checked for consistency.
LiquidHaskell facilitates the addition of annotations or refinements to function signatures, providing an extra layer of confidence in the implementation's correctness. These annotations are examined at compile time using the z3 SMT solver. The resulting binaries maintain their original size, ensuring efficient verification without sacrificing performance.
LiquidHaskell allows us to verify various properties of our cipher implementation using liquid types.
In our codebase, we have implemented several safety checks and plan to add more as needed.
Ensuring the lengths of input and output lists adhere to the protocol is a critical check. For example:
{-@ doubleroundCompute :: { i:[_] | (len i) == 16 } -> { o:[_] | (len o) == 16 } @-}
doubleroundCompute :: [Word32] -> [Word32]
doubleroundCompute input
| length input == 16 = rowroundCompute $ columnroundCompute input
| otherwise = error "input to `doubleroundCompute` must be a list of 16 `Word32` numbers"
This refinement asserts that both input and output lists must have a length of 16, and the function ensures this requirement is met. We perform this check extensively in our codebase to ensure the consistency and correctness of our functions.
The transpose
function is used in implementing the columnround functionality in the Columnround
module. Here we verify not only the length of lists (16) but also the equality of elements:
{-@ transpose :: {i:[a] | len i == 16} -> {o:[a] | len o == 16 && (elts i == elts o) } @-}
transpose :: [a] -> [a]
This ensures that the transposition maintains element equality between input and output lists.
The Rowround
module employs custom sort functions to shuffle the inputs sent to the quarterround
functions, as specified in the salsa20 specification.
These sort functions simply shuffle input elements, ensuring that the same elements are returned in a different order. Similar to the approach used in the transpose
function, we guarantee that the input and output elements remain the same. For example:
{-@ sort2 :: { i:[_] | (len i) == 4 } -> { o:[_] | (len o) == 4 && (elts i == elts o) } @-}
sort2 :: [a] -> [a]
Within the Crypt
module, we have a series of keyByte
functions designed to extract specific bytes from a key, which is a list of bytes. These functions play a significant role in encryption and decryption processes, as outlined in the salsa20 specification.
The input length of these functions is fixed at 64. To ensure safety, we apply a refinement to validate that the index argument falls within the range of valid indices, from 0 to 63:
{-@ keybyteCompute :: { i:[_] | (len i) == 64 } -> {index: Nat | index <= 63 } -> _ @-}
keybyteCompute :: [Word32] -> Int -> Word32
Assumptions are used to provide properties to functions, such as in the concat64
function:
{-@ assume concat64 :: {outer:[{inner:[a] | len inner == 4}] | len outer == 16} -> {output:[a] | len output == 64} @-}
concat64 :: [[a]] -> [a]
concat64 = concat
This assumption allows us to convey specific properties about input and output list sizes. The aument function demonstrates how this assumption is utilized:
{-@ aument :: { i:[_] | (len i) == 16 } -> { o:[_] | (len o) == 64 } @-}
aument :: [Word32] -> [Word32]
aument input = concat64 [extractBytes4 x | x <- input]
LiquidHaskell can then validate that the assumptions made in concat64
hold, proving that an input of 16 elements always results in an output of 64 elements.