# Note that in this test we use "! bincmp" on plaintext output files, # as a workaround for "cmp" not supporting "! cmp". # TODO: now that obfuscation with -seed is deterministic, # can we just rely on the regular "cmp" with fixed output files? # TODO: consider setting these seeds globally, # so we can reuse them across tests and make better use of the shared build cache. env SEED1=OQg9kACEECQ env SEED2=NruiDmVz6/s # Check the binary with a given base64 encoded seed. garble -seed=${SEED1} build exec ./main$exe cmp stderr main.stderr binsubstr main$exe 'teststring' 'imported var value' ! binsubstr main$exe 'ImportedVar' ${SEED1} [short] stop # the extra checks are relatively expensive exec ./main$exe test/main/imported cp stderr importedpkg-seed-static-1 # Also check that the binary is reproducible. # No packages should be rebuilt either, thanks to the build cache. cp main$exe main_seed1$exe rm main$exe garble -seed=${SEED1}= build -v ! stderr . bincmp main$exe main_seed1$exe exec ./main$exe test/main/imported cmp stderr importedpkg-seed-static-1 # Even if we use the same seed, the same names in a different package # should still be obfuscated in a different way. exec ./main$exe test/main cp stderr mainpkg-seed-static-1 ! bincmp mainpkg-seed-static-1 importedpkg-seed-static-1 # Using different flags which affect the build, such as -literals or -tiny, # should result in the same obfuscation as long as the seed is constant. # TODO: also test that changing non-garble build parameters, # such as GOARCH or -tags, still results in the same hashing via the seed. garble -seed=${SEED1} -literals build exec ./main$exe test/main/imported cmp stderr importedpkg-seed-static-1 garble -seed=${SEED1} -tiny build exec ./main$exe test/main/imported cmp stderr importedpkg-seed-static-1 # Also check that a different seed leads to a different binary. # We can't know if caching happens here, because of previous test runs. cp main$exe main_seed2$exe rm main$exe garble -seed=${SEED2} build ! bincmp main$exe main_seed2$exe exec ./main$exe test/main/imported cp stderr importedpkg-seed-static-2 ! bincmp importedpkg-seed-static-2 importedpkg-seed-static-1 # Use a random seed, which should always trigger a full build. garble -seed=random build -v stderr -count=1 '^runtime$' stderr -count=1 '^test/main$' exec ./main$exe cmp stderr main.stderr binsubstr main$exe 'teststring' 'imported var value' ! binsubstr main$exe 'ImportedVar' exec ./main$exe test/main/imported cp stderr importedpkg-seed-random-1 ! bincmp importedpkg-seed-random-1 importedpkg-seed-static-1 # Also check that the random binary is not reproducible. cp main$exe main_random$exe rm main$exe garble -seed=random build -v stderr . ! bincmp main$exe main_random$exe exec ./main$exe test/main/imported cp stderr importedpkg-seed-random-2 ! bincmp importedpkg-seed-random-2 importedpkg-seed-random-1 # Finally, ensure that our runtime and reflect test code does what we think. go build exec ./main$exe cmp stderr main.stderr exec ./main$exe test/main cmp stderr mainpkg.stderr exec ./main$exe test/main/imported cmp stderr importedpkg.stderr -- go.mod -- module test/main go 1.19 -- main.go -- package main import ( "os" "test/main/imported" ) var teststringVar = "teststring" func main() { mainFunc() } func mainFunc() { if len(os.Args) > 1 { switch os.Args[1] { case "test/main": imported.PrintNames(NamedTypeValue, NamedFunc) case "test/main/imported": imported.PrintNames(imported.NamedType{}, imported.NamedFunc) default: panic("unknown package") } } else { println(teststringVar) println(imported.ImportedVar) // When we're obfuscating, check that the obfuscated name lengths vary. // With eight hashed names, and a range between 8 and 15, // the chances of six repeats are (1 / 8) ** 6, or about 0.00038%. // If that happens, then our randomness is clearly broken. if hashedNames[0] != "main.hashed0" { var count [16]int for _, name := range hashedNames { name = name[len("main."):] if len(name) < 6 { panic("ended up with a hashed name that's too short: "+name) } if len(name) > 12 { panic("ended up with a hashed name that's too long: "+name) } count[len(name)]++ if count[len(name)] >= 6 { for _, name := range hashedNames { println(name) } panic("six or more hashed names with the same length") } } } } } // A workaround to fool garble's reflect detection, // because we want it to show us the obfuscated NamedType. var NamedTypeValue any = NamedType{} type NamedType struct { NamedField int } func NamedFunc() string { return imported.CallerFuncName() } var hashedNames = []string{ hashed0(), hashed1(), hashed2(), hashed3(), hashed4(), hashed5(), hashed6(), hashed7(), } func hashed0() string { return imported.CallerFuncName() } func hashed1() string { return imported.CallerFuncName() } func hashed2() string { return imported.CallerFuncName() } func hashed3() string { return imported.CallerFuncName() } func hashed4() string { return imported.CallerFuncName() } func hashed5() string { return imported.CallerFuncName() } func hashed6() string { return imported.CallerFuncName() } func hashed7() string { return imported.CallerFuncName() } -- imported/imported.go -- package imported import ( "reflect" "runtime" ) var ImportedVar = "imported var value" type NamedType struct { NamedField int } func NamedFunc() string { return CallerFuncName() } func PrintNames(v any, fn func() string) { typ := reflect.TypeOf(v) println("path:", typ.PkgPath()) println("type:", typ.Name()) println("field:", typ.Field(0).Name) println("func: ", fn()) } func CallerFuncName() string { pc, _, _, _ := runtime.Caller(1) fn := runtime.FuncForPC(pc) return fn.Name() } -- main.stderr -- teststring imported var value -- mainpkg.stderr -- path: main type: NamedType field: NamedField func: main.NamedFunc -- importedpkg.stderr -- path: test/main/imported type: NamedType field: NamedField func: test/main/imported.NamedFunc