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 }