github.com/ericlagergren/ctb@v0.0.0-20220810041818-96749d9c394d/dudect/dudect.go (about)

     1  // Package dudect implements a side channel leak detector.
     2  //
     3  // In order to get accurate readings, you'll want to disable as
     4  // much of the Go runtime as possible. In particular, setting
     5  // some of the following GODEBUG environment variables
     6  //
     7  //    asyncpreemptoff=1
     8  //    gcshrinkstackoff=1
     9  //    sbrk=1
    10  //
    11  // disabling garbage collection
    12  //
    13  //    debug.SetGCPercent(-1) // or GOGC=off
    14  //
    15  // and locking the goroutine running the tests to an OS thread
    16  //
    17  //    runtime.LockOSThread()
    18  //    defer runtime.UnlockOSThread()
    19  //
    20  // This package is a Go transliteration of github.com/oreparaz/dudect.
    21  package dudect
    22  
    23  import (
    24  	"crypto/rand"
    25  	"fmt"
    26  	"io"
    27  	"math"
    28  	"sort"
    29  )
    30  
    31  const (
    32  	enoughMeasurements = 10000
    33  	numPercentiles     = 100
    34  )
    35  
    36  // Config configures a Test.
    37  type Config struct {
    38  	// ChunkSize is the size of the data
    39  	// processed each iteration.
    40  	//
    41  	// For example, if testing
    42  	//    func constantTimeCompare(a, b [16]byte) bool
    43  	// set ChunkSize to 16.
    44  	ChunkSize int
    45  	// Measurements is the number of measurements per
    46  	// call to Test.
    47  	//
    48  	// Memory usage is proportional to the number of tests.
    49  	Measurements int
    50  	// Output, if non-nil, is used to print informational
    51  	// messages.
    52  	Output io.Writer
    53  }
    54  
    55  // Context records test measurements.
    56  type Context struct {
    57  	cfg       *Config
    58  	execTimes []int64 // len == Measurements
    59  	inputData []byte  // len == Measurements*ChunkSize
    60  	classes   []byte  // len == Measurements
    61  	// perform this many tests in total:
    62  	//   - 1 first order uncropped test,
    63  	//   - numPercentiles tests
    64  	//   - 1 second order test
    65  	testCtxs    [1 + numPercentiles + 1]testCtx
    66  	percentiles [numPercentiles]int64
    67  }
    68  
    69  func NewContext(cfg *Config) *Context {
    70  	return &Context{
    71  		cfg:       &(*cfg),
    72  		execTimes: make([]int64, cfg.Measurements),
    73  		classes:   make([]byte, cfg.Measurements),
    74  		inputData: make([]byte, cfg.Measurements*cfg.ChunkSize),
    75  	}
    76  }
    77  
    78  func (ctx *Context) printf(format string, args ...interface{}) {
    79  	if ctx.cfg.Output != nil {
    80  		fmt.Fprintf(ctx.cfg.Output, format, args...)
    81  	}
    82  }
    83  
    84  // PrepFunc prepares input for testing.
    85  //
    86  // The length of data is cfg.ChunkSize*cfg.Measurements.
    87  //
    88  // Each element in classes must be either 0 or 1.
    89  // The length of classes is cfg.Measurements.
    90  //
    91  // PrepFunc is only called once per test.
    92  type PrepFunc func(cfg *Config, input, classes []byte)
    93  
    94  // Prepare is an implementation of PrepFunc.
    95  func Prepare(cfg *Config, input, classes []byte) {
    96  	_, err := rand.Read(input)
    97  	if err != nil {
    98  		panic(err)
    99  	}
   100  	_, err = rand.Read(classes)
   101  	if err != nil {
   102  		panic(err)
   103  	}
   104  	for i, c := range classes {
   105  		classes[i] = c & 1
   106  		if c&1 != 0 {
   107  			// Leave random
   108  			continue
   109  		}
   110  		p := input[i*cfg.ChunkSize : (i+1)*cfg.ChunkSize]
   111  		for j := range p {
   112  			p[j] = 0
   113  		}
   114  	}
   115  }
   116  
   117  // Test samples fn and reports whether any leaks were found.
   118  //
   119  // Test should be called in a loop like so:
   120  //
   121  //    for {
   122  //        select {
   123  //        case <-timeout.C:
   124  //            break
   125  //        default:
   126  //        }
   127  //        if ctx.Test(fn, prep) {
   128  //            break
   129  //        }
   130  //    }
   131  //
   132  // In other words, Test cannot provide fn is constant
   133  // time. Instead, it tries to find leaks. For more
   134  // information, see github.com/oreparaz/dudect.
   135  //
   136  // If prep is nil, Prepare is used instead.
   137  func (ctx *Context) Test(fn func([]byte) bool, prep PrepFunc) bool {
   138  	if prep == nil {
   139  		prep = Prepare
   140  	}
   141  	prep(ctx.cfg, ctx.inputData, ctx.classes)
   142  	ctx.measure(fn)
   143  
   144  	first := ctx.percentiles[len(ctx.percentiles)-1] == 0
   145  	if first {
   146  		// Throw away the first batch of measurements.
   147  		// This helps warming things up.
   148  		ctx.prepPercentiles()
   149  		return false
   150  	}
   151  	ctx.updateStats()
   152  	return ctx.leaks()
   153  }
   154  
   155  func (ctx *Context) measure(fn func([]byte) bool) {
   156  	for i := 0; i < ctx.cfg.Measurements; i++ {
   157  		data := ctx.inputData[i*ctx.cfg.ChunkSize : (i+1)*ctx.cfg.ChunkSize]
   158  		start := cpucycles()
   159  		fn(data)
   160  		last := cpucycles()
   161  		ctx.execTimes[i] = last - start
   162  	}
   163  	// fmt.Println(ctx.execTimes)
   164  }
   165  
   166  func (ctx *Context) updateStats() {
   167  	// Set i = 10 to discard the first few measurements.
   168  	for i := 10; i < ctx.cfg.Measurements-1; i++ {
   169  		diff := ctx.execTimes[i]
   170  		if diff < 0 {
   171  			// The cpu cycle counter overflowed, just throw away the measurement.
   172  			continue
   173  		}
   174  
   175  		// t-test on the execution time
   176  		ctx.testCtxs[0].push(float64(diff), ctx.classes[i])
   177  
   178  		// t-test on cropped execution times, for several cropping thresholds.
   179  		for j, pct := range ctx.percentiles {
   180  			if diff < pct {
   181  				ctx.testCtxs[j+1].push(float64(diff), ctx.classes[i])
   182  			}
   183  		}
   184  
   185  		// second-order test (only if we have more than 10000 measurements).
   186  		// Centered product pre-processing.
   187  		if ctx.testCtxs[0].n[0] > 10000 {
   188  			centered := float64(diff) - ctx.testCtxs[0].mean[ctx.classes[i]]
   189  			ctx.testCtxs[1+numPercentiles].push(
   190  				centered*centered, ctx.classes[i])
   191  		}
   192  	}
   193  }
   194  
   195  // leaks reports whether any leaks were found.
   196  func (ctx *Context) leaks() bool {
   197  	t := ctx.maxTest()
   198  	maxT := math.Abs(t.compute())
   199  	numTracesMaxT := t.n[0] + t.n[1]
   200  	maxTau := maxT / math.Sqrt(numTracesMaxT)
   201  
   202  	// print the number of measurements of the test that yielded max t.
   203  	// sometimes you can see this number go down - this can be confusing
   204  	// but can happen (different test)
   205  	ctx.printf("meas: %7.2f M, ", (numTracesMaxT / 1e6))
   206  	if numTracesMaxT < enoughMeasurements {
   207  		ctx.printf("not enough measurements (%.0f still to go).\n",
   208  			enoughMeasurements-numTracesMaxT)
   209  		return false
   210  	}
   211  
   212  	//
   213  	// We report the following statistics:
   214  	//
   215  	//    maxT: the t value
   216  	//    maxTau: a t value normalized by sqrt(number of measurements).
   217  	//            this way we can compare maxTau taken with different
   218  	//            number of measurements. This is sort of "distance
   219  	//            between distributions", independent of number of
   220  	//            measurements.
   221  	//    (5/tau)^2: how many measurements we would need to barely
   222  	//               detect the leak, if present. "barely detect the
   223  	//               leak" here means have a t value greater than 5.
   224  	//
   225  	// The first metric is standard; the other two aren't, but
   226  	// are pretty sensible imho.
   227  	ctx.printf("max t: %+7.2f, max tau: %.2e, (5/tau)^2: %.2e.",
   228  		maxT, maxTau, 5*5/float64(maxTau*maxTau))
   229  
   230  	// threshold values for Welch's t-test
   231  	const (
   232  		thresholdBananas  = 500 // test failed, with overwhelming probability
   233  		thresholdModerate = 10  // test failed. Pankaj likes 4.5 but let's be more lenient
   234  	)
   235  	if maxT > thresholdBananas {
   236  		ctx.printf(" Definitely not constant time.\n")
   237  		return true
   238  	}
   239  	if maxT > thresholdModerate {
   240  		ctx.printf(" Probably not constant time.\n")
   241  		return true
   242  	}
   243  	if maxT < thresholdModerate {
   244  		ctx.printf(" For the moment, maybe constant time.\n")
   245  	}
   246  	return false
   247  }
   248  
   249  func (ctx *Context) maxTest() testCtx {
   250  	idx := 0
   251  	var max float64
   252  	for i, tctx := range ctx.testCtxs {
   253  		if tctx.n[0] <= enoughMeasurements {
   254  			continue
   255  		}
   256  		x := math.Abs(tctx.compute())
   257  		if x > max {
   258  			max = x
   259  			idx = i
   260  		}
   261  	}
   262  	return ctx.testCtxs[idx]
   263  }
   264  
   265  // prepPercentiles sets different thresholds for cropping measurements.
   266  //
   267  // The exponential tendency is meant to approximately match
   268  // the measurements distribution, but there's not more science
   269  // than that.
   270  func (ctx *Context) prepPercentiles() {
   271  	sort.Slice(ctx.execTimes, func(i, j int) bool {
   272  		return ctx.execTimes[i] < ctx.execTimes[j]
   273  	})
   274  	for i := range ctx.percentiles {
   275  		w := 1 - math.Pow(0.5, 10*float64(i+1)/numPercentiles)
   276  		ctx.percentiles[i] = ctx.execTimes[int(w)*ctx.cfg.Measurements]
   277  	}
   278  }
   279  
   280  type testCtx struct {
   281  	mean [2]float64
   282  	m2   [2]float64
   283  	n    [2]float64
   284  }
   285  
   286  // push implements Welch's t-test.
   287  //
   288  // Welch's t-test test whether two populations have
   289  // the same mean. This is basically Student's t-test
   290  // for unequal variances and unequal sample sizes.
   291  //
   292  // See https://en.wikipedia.org/wiki/Welch%27s_t-test
   293  func (ctx *testCtx) push(x float64, class uint8) {
   294  	if class > 1 {
   295  		panic("class > 1")
   296  	}
   297  	ctx.n[class]++
   298  	// Estimate variance on the fly as per the Welford method.
   299  	// This gives good numerical stability. See Knuth's TAOCP vol 2.
   300  	delta := x - ctx.mean[class]
   301  	ctx.mean[class] += delta / ctx.n[class]
   302  	ctx.m2[class] += delta * (x - ctx.mean[class])
   303  }
   304  
   305  func (ctx *testCtx) compute() float64 {
   306  	v0 := ctx.m2[0] / (ctx.n[0] - 1)
   307  	v1 := ctx.m2[1] / (ctx.n[1] - 1)
   308  	num := ctx.mean[0] - ctx.mean[1]
   309  	den := math.Sqrt(v0/ctx.n[0] + v1/ctx.n[1])
   310  	return num / den
   311  }