Skip to the content.

Introduction

This lab provides some solutions to common stumbles with Cryptol’s type system.

Prerequisites

Before working through this lab, you’ll need

You’ll also need experience with

Skills You’ll Learn

By the end of this lab you will have a pretty good understanding of Cryptol’s language constructs. At least good enough that you can come back here for reference as you work through the labs.

Specifically, you’ll also gain experience with

Load This Module

This lab is a literate Cryptol document — that is, it can be loaded directly into the Cryptol interpreter. Load this module from within the Cryptol interpreter running in the cryptol-course directory with:

Loading module Cryptol
Cryptol> :m labs::Language::IntroTypeHackery
Loading module Cryptol
Loading module labs::Language::IntroTypeHackery

We start by defining a new module for this lab:

module labs::Language::IntroTypeHackery where

You do not need to enter the above into the interpreter; the previous :m ... command loaded this literate Cryptol file automatically. In general, you should run Xcryptol-session commands in the interpreter and leave cryptol code alone to be parsed by :m ....

Introductory Type Hackery

Sometimes Cryptol’s type system does not provide an intuitive way to accomplish what appears to be simple. In this document we’ll show an example of this and how it can be overcome.

In a nutshell the problem shown herein (and in general many typing surprises in Cryptol) stems from the fact that the type system cannot do any reasoning based on values, rather it must know or deduce the size of all data at parse time. In some ways this limits us, but it makes automated reasoning practical and rarely proves to be a show-stopper for cryptographic algorithms.

The Exemplar

Following is a typical definition of gcd in Cryptol:

/**
 * A simply defined recursive gcd.
 */

gcdSimple : {n} (fin n) => [n] -> [n] -> [n]
gcdSimple x y = if y == 0 then x else gcdSimple y (x % y)

It’s very simple, but it does not symbolically terminate, so that proofs are impossible when n is nonzero. Essentially the symbolic simulator can’t understand that the one or both of the arguments’ values are strictly decreasing in the recursion which would eventually force the base case. That means the symbolic simulator attempts to generate an infinite circuit and Cryptol hangs.

In this case, % is problematic in that the symbolic simulator allows for any value to emerge from %. There are some operations that cause the symbolic simulator to realize that some bits have become fixed. For instance >> 1 causes the most significant bit of its argument to be False. We can leverage that to express the gcd function with the binary gcd algorithm in a manner that affords symbolic termination.

The Binary gcd Algorithm

The binary gcd algorithm, another recursive algorithm for gcd, has the advantage that each recursion involves a halving of one or both arguments.

Wikipedia’s Description [1]

  1. gcd(0, v) = v, because everything divides zero, and v is the largest number that divides v. Similarly, gcd(u, 0) = u. gcd(0, 0) is not typically defined, but it is convenient to set gcd(0, 0) = 0.
  2. If u and v are both even, then gcd(u, v) = 2·gcd(u/2, v/2), because 2 is a common divisor.
  3. If u is even and v is odd, then gcd(u, v) = gcd(u/2, v), because 2 is not a common divisor. Similarly, if u is odd and v is even, then gcd(u, v) = gcd(u, v/2).
  4. If u and v are both odd, and uv, then gcd(u, v) = gcd((uv)/2, v). If both are odd and u < v, then gcd(u, v) = gcd((vu)/2, u). These are combinations of one step of the simple Euclidean algorithm, which uses subtraction at each step, and an application of step 3 above. The division by 2 results in an integer because the difference of two odd numbers is even.
  5. Repeat steps 2–4 until u = v, or (one more step) until u = 0. In either case, the GCD is 2kv, where k is the number of common factors of 2 found in step 2.

[1] Wikipedia contributors. Binary GCD algorithm [Internet]. Wikipedia, The Free Encyclopedia; 2020 May 11, 17:27 UTC [cited 2020 Jul 10]. Available from: https://en.wikipedia.org/w/index.php?title=Binary_GCD_algorithm&oldid=956130910.

A First Attempt at Binary gcd in Cryptol

bgcd : {n} (fin n) => [n] -> [n] -> [n]
bgcd x y = if x == 0 \/ y == 0 then x ^ y
            | even x /\ even y then bgcd (x >> 1) (y >> 1) << 1
            | even x           then bgcd (x >> 1) y
            |           even y then bgcd x (y >> 1)
            | x >= y           then bgcd ((x - y) >> 1) y
                               else bgcd x ((y - x) >> 1)

While this computes correct values, we’re still in trouble, as the subtractions in the last two lines can, as far as the symbolic simulator is concerned, introduce any value. So, supposing we’re part way through the algorithm and we know that 3 most significant bits of x are False and the 5 most significant bis of y are False, but x and y are both odd; then we’ll compute either x - y or y - x and the symbolic simulator will think we have no knowledge of the bits of either difference. The subsequent >> 1 will introduce a single False bit to our computation, but overall we will have decreased the certainty of the bits in our arguments as we recurse.

The next trick is to explicitly remove the unnecessary bits from the arguments as we recurse. If we represent halving by trimming a bit from the argument, the symbolic simulator reckons that one or both of the arguments eventually have type [0] which serves as a base case. What we have done is change from reducing values to eventually reach a base case, to reducing the types until the base case is reached. After that it’s all details, but there are several.

Implementing Binary gcd in Cryptol 2

We’ll need a few supporting functions:

/**
 * Indicates whether the argument is even or odd.
 */

even, odd : {n} (fin n) => [n] -> Bit
even = ~odd
odd x = last ([False] # x) // "[False] #" affords type [0]

Aside: Numeric Constraint Guards

Update!!! Cryptol 3 introduced numeric constraint guards, which accommodate different branches based on the type parameters of a function. For example, our definitions of even and odd can be adapted to:

even', odd' : {n} (fin n) => [n] -> Bit
even' = ~odd'
odd' x
  | n == 0 => False
  | n > 0  => last x

Let’s add propertys to verify that these definitions are equivalent…

even_eq, odd_eq : {n} (fin n) => [n] -> Bool
property even_eq = even === even'
property odd_eq = odd === odd'

We’ll give properties a proper introduction later in the course, but here’s a sneak peek:

labs::Language::IntroTypeHackery> :prove even_eq`{0}
Q.E.D.
(Total Elapsed Time: 0.026s, using "Z3")
labs::Language::IntroTypeHackery> :prove even_eq`{32}
Q.E.D.
(Total Elapsed Time: 0.030s, using "Z3")
labs::Language::IntroTypeHackery> :prove odd_eq`{0}
Q.E.D.
(Total Elapsed Time: 0.024s, using "Z3")
labs::Language::IntroTypeHackery> :prove odd_eq`{32}
Q.E.D.
(Total Elapsed Time: 0.031s, using "Z3")

Z3 says these are equivalent, at least for empty and 32-bit sequences. Thanks, Z3! Back to our program…


/**
 * Halves by discarding the last bit if possible.
 */

halve : {n} (fin n) => [n] -> [max n 1 - 1]
halve = take
/**
 * Computes absolute difference with heterotypic arguments.
 */

absDiff : {m, n} (fin m, fin n) => [m] -> [n] -> [max m n]
absDiff x y = if x' > y' then x' - y' else y' - x'
  where
    x', y' : [max m n]
    x' = zext x
    y' = zext y
gcd : {m, n} (fin m, fin n) => [m] -> [n] -> [max m n]

We need the result type to be [max m n] since if either argument is zero, the result is the value of the other argument.

Without further ado, the code:

gcd x y = if x == 0 \/ y == 0 then zext x ^ zext y        // line 1
           | even x /\ even y then zext (gcd x' y') << 1  //      2
           | even x           then zext (gcd x' y )       //      3
           |           even y then zext (gcd x  y')       //      4
           | m' > n'          then zext (gcd z' y )       //      5
           else                    zext (gcd x  z')       //      6
  where
    x' = halve x
    y' = halve x
    z' = halve (absDiff x y)
    m', n' : [max (width m) (width n)]
    m' = `m
    n' = `n

Some notation used in the above:

Let’s discuss the lines above vs. the steps in Wikipedia’s algorithm:

Here are some extra definitions for the courageous.

(|?) : {m, n} (fin m, fin n) => [m] -> [n] -> Bit
x |? y = if x == 0 then y == 0 else y' % x' == 0
  where
    x', y' : [max m n]
    x' = zext x
    y' = zext y

gcdDividesBoth : {m, n} (fin m, fin n) => [m] -> [n] -> Bit
property gcdDividesBoth x y = z |? x /\ z |? y
  where
    z = gcd x y

gcdEuclid's : {n} (fin n) => [n] -> [n] -> [n]
gcdEuclid's x y = if x == 0 \/ y == 0 then max x y
                   | x >  y           then gcdEuclid's (x - y) y
                                      else gcdEuclid's  x     (y - x)

gcdCommon : {n} (fin n) => [n] -> [n] -> [n]
gcdCommon x y = if x == 0 \/ y == 0 then max x y
                 | x >  y           then gcdCommon (x % y) y
                                    else gcdCommon  x     (y % x)

Solicitation

How was your experience with this lab? Suggestions are welcome in the form of a ticket on the course GitHub page: https://github.com/weaversa/cryptol-course/issues

From here, you can go somewhere!

     
  - Language Basics  
  Type Hackery