Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Security: AeonDave/garble

Security

docs/SECURITY.md

Garble hardened Security Architecture

Last Updated: October 8, 2025
Status: ✅ Production Ready

This document provides the comprehensive technical security architecture of Garble's obfuscation mechanisms. It details each security component with its implementation, threat model, and operational characteristics.


Table of Contents

  1. Executive Summary
  2. Seed & Nonce Architecture
  3. Runtime Metadata Hardening (Feistel Cipher)
  4. Literal Obfuscation (ASCON-128 + Simple)
  5. Reflection Control & Reversibility
  6. Build Cache Encryption (ASCON-128)
  7. Control-Flow Obfuscation
  8. Threat Model & Mitigation Matrix
  9. Security Limitations & Roadmap
  10. References & Resources

1. Executive Summary

Security Posture Snapshot

Component Status Implementation
Runtime Metadata ✅ Deployed 4-round Feistel cipher with per-function tweak
Literal Protection ✅ Deployed ASCON-128 inline + reversible simple obfuscator
Name Hashing ✅ Deployed SHA-256 with per-build nonce mixing
Reflection Oracle ✅ Mitigated Empty by default; opt-in via -reversible
Cache Encryption ✅ Deployed ASCON-128 at rest with authentication
Control-Flow ⚠️ Optional Multiple modes available; default off

Key Security Properties

  • Per-Build Uniqueness: Every build uses a cryptographically random nonce mixed with the seed, ensuring symbol names and keys differ even with identical source code (unless explicitly reproduced).
  • Metadata Hardening: Runtime function tables are encrypted with format-preserving Feistel encryption; decryption happens transparently at runtime via injected helpers.
  • Literal Protection: Strings and constants are encrypted inline using NIST-standard ASCON-128 or multi-layer reversible transforms.
  • Reflection Suppression: Original identifier names are omitted from binaries by default, eliminating the reverse-engineering oracle.
  • Cache Security: Build artifacts are encrypted at rest; tampering is detected via authentication tags.

2. Seed & Nonce Architecture

Purpose

Provide reproducible yet secure randomness for all obfuscation operations, with explicit control over determinism vs. per-build uniqueness.

Architecture Diagram

┌─────────────────────────────────────────────────────────────┐
│                   Build Time - Entropy Flow                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  User Seed (optional)              Build Nonce              │
│  -seed=<base64> or random      GARBLE_BUILD_NONCE=<base64>  │
│         │                              │                    │
│         ├─ SHA-256 ─────►  32 bytes    │                    │
│         │                              │                    │
│         └──────────────┬───────────────┘                    │
│                        │                                    │
│              ┌─────────▼─────────┐                          │
│              │  combineSeedAndNonce()                       │
│              │  SHA-256(seed || nonce)                      │
│              └─────────┬─────────┘                          │
│                        │                                    │
│                        ▼                                    │
│            Combined Hash (32 bytes)                         │
│                        │                                    │
│        ┌───────────────┼───────────────┐                    │
│        │               │               │                    │
│        ▼               ▼               ▼                    │
│   Name Hashing    Feistel Keys   Literal Keys              │
│   (per-package)   (4x32-bit)     (per-literal)             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Components

Seed (-seed flag)

  • Format: Base64-encoded bytes or literal random
  • Processing: Hashed to 32 bytes via SHA-256 for uniform entropy
  • Default: Unset (entropy derived from nonce only)
  • Random Mode: Generates 32 cryptographic random bytes; printed to stderr for reproducibility

Build Nonce (GARBLE_BUILD_NONCE env)

  • Format: Base64-encoded 32 bytes (no padding)
  • Default: Randomly generated per build
  • Printed: When randomly generated (format: -nonce chosen at random: <base64>)
  • Purpose: Ensures different builds produce different hashes even with identical seed and source

Combining Function

func combineSeedAndNonce(seed, nonce []byte) []byte {
    h := sha256.New()
    if len(seed) > 0 {
        h.Write(seed)
    }
    if len(nonce) > 0 {
        h.Write(nonce)
    }
    return h.Sum(nil)  // Always 32 bytes
}

Reproducible Builds

To achieve bit-for-bit identical builds:

  1. Fix the seed: -seed=<known-base64-value>
  2. Fix the nonce: GARBLE_BUILD_NONCE=<known-base64-value>
  3. Use identical source code and Go toolchain version

Without fixing both: Each build is cryptographically unique by design.

Implementation References

  • main.go: Flag parsing, seed generation, nonce printing
  • hash.go: combineSeedAndNonce(), seedHashInput(), hashWithPackage()

3. Runtime Metadata Hardening (Feistel Cipher)

Purpose

Encrypt function entry point offsets in the runtime symbol table (pclntab) to prevent static analysis from mapping function metadata to code locations.

Architecture Diagram

┌────────────────────────────────────────────────────────────────────┐
│                    Build Time (Linker Stage)                       │
├────────────────────────────────────────────────────────────────────┤
│                                                                    │
│  1. Garble exports LINK_SEED (base64 32-byte seed)                 │
│     Environment: LINK_SEED=<base64>                                │
│                                                                    │
│  2. Linker derives 4 round keys via SHA-256                        │
│     for i = 0 to 3:                                                │
│       h = SHA256(seed || byte(i))                                  │
│       keys[i] = uint32(h[0:4])  // First 4 bytes                   │
│                                                                    │
│  3. For each function in pclntab:                                  │
│     entryOff  = function's entry point offset (32-bit)             │
│     nameOff   = function's name offset (32-bit, used as tweak)     │
│                                                                    │
│     // 4-round Feistel network encryption                          │
│     left = uint16(entryOff >> 16)                                  │
│     right = uint16(entryOff & 0xFFFF)                              │
│                                                                    │
│     for round = 0 to 3:                                            │
│       f = feistelRound(right, nameOff, keys[round])                │
│       left, right = right, left ^ f                                │
│                                                                    │
│     encrypted = (uint32(left) << 16) | uint32(right)               │
│     write encrypted value to binary                                │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

                           ↓ Binary Written ↓

┌─────────────────────────────────────────────────────────────────────┐
│                    Runtime (Program Execution)                      │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  1. Injected decryption functions (//go:nosplit)                    │
│                                                                     │
│     var linkFeistelKeys = [4]uint32{...}  // Embedded at compile    │
│                                                                     │
│     //go:nosplit                                                    │
│     func linkFeistelRound(right uint16, tweak, key uint32) uint16   │
│                                                                     │
│     //go:nosplit                                                    │
│     func linkFeistelDecrypt(value, tweak uint32) uint32             │
│                                                                     │
│  2. Patched funcInfo.entry() method                                 │
│                                                                     │
│     func (f funcInfo) entry() uintptr {                             │
│       // Decrypt on-the-fly                                         │
│       decrypted := linkFeistelDecrypt(f.entryoff, uint32(f.nameOff))│
│       return f.datap.textAddr(decrypted)                            │
│     }                                                               │
│                                                                     │
│  3. Transparent to application code                                 │
│     ✓ Stack traces work normally                                    │
│     ✓ runtime.Caller() returns correct information                  │
│     ✓ runtime.FuncForPC() resolves function names                   │
│     ✓ No performance impact (nosplit prevents extra stack frames)   │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Feistel Round Function

F(right uint16, tweak uint32, key uint32) → uint16:
  x = uint32(right)
  x ^= tweak                         // Mix in per-function uniqueness
  x += key × 0x9e3779b1 + 0x7f4a7c15  // Golden ratio constant
  x = rotateLeft32(x ^ key, key & 31) // Key-dependent rotation  
  x ^= x >> 16                       // Mixing step
  return uint16(x)

Security Properties

Property Value Security Benefit
Key Size 4×32-bit (128-bit total) Cryptographically strong key space
Rounds 4 Sufficient for strong diffusion
Tweak nameOff (32-bit) Each function encrypted uniquely
Diffusion ~100% All output bits depend on all input bits
Non-linearity High Resistant to linear cryptanalysis
Performance <10 CPU cycles Negligible runtime overhead

Why Feistel?

  1. Provable Security: Well-studied structure used in DES, Blowfish, Twofish
  2. Perfect Reversibility: Same structure for encryption/decryption (reverse key order)
  3. Format-Preserving: 32-bit input → 32-bit output (maintains offset size)
  4. Tweak Support: nameOff parameter ensures unique encryption per function
  5. Fast: Simple bitwise operations, no memory allocations

Implementation Details

Runtime Injection (runtime_patch.go)

// Injected into runtime/symtab.go

//go:nosplit  // CRITICAL: Prevents stack frame creation
func linkFeistelRound(right uint16, tweak uint32, key uint32) uint16 {
    x := uint32(right)
    x ^= tweak
    x += key*0x9e3779b1 + 0x7f4a7c15
    n := key & 31
    tmp := x ^ key
    if n != 0 {
        x = (tmp << n) | (tmp >> (32 - n))
    } else {
        x = tmp
    }
    x ^= x >> 16
    return uint16(x)
}

//go:nosplit  // CRITICAL: Maintains runtime.Caller() correctness
func linkFeistelDecrypt(value, tweak uint32) uint32 {
    left := uint16(value >> 16)
    right := uint16(value)
    
    // Decrypt in reverse (rounds 3, 2, 1, 0)
    for round := len(linkFeistelKeys) - 1; round >= 0; round-- {
        key := linkFeistelKeys[round]
        f := linkFeistelRound(left, tweak, key)
        left, right = right^f, left
    }
    
    return (uint32(left) << 16) | uint32(right)
}

// Patched entry() method
func (f funcInfo) entry() uintptr {
    decrypted := linkFeistelDecrypt(f.entryoff, uint32(f.nameOff))
    return f.datap.textAddr(decrypted)
}

Critical Design Note: The //go:nosplit directive prevents Go from creating stack frames for these functions. This is essential because:

  • runtime.Caller() counts stack frames to determine call depth
  • Extra frames would break stack trace accuracy
  • Functions remain invisible to the call stack mechanism

Linker Patch (internal/linker/patches/go1.25/0003-add-entryOff-encryption.patch)

Applied to cmd/link/internal/ld/pcln.go:

// Read LINK_SEED from environment
seedBase64 := os.Getenv("LINK_SEED")
seedBytes, _ := base64.StdEncoding.DecodeString(seedBase64)
var seed [32]byte
copy(seed[:], seedBytes)

// Derive round keys
keys := [4]uint32{}
for i := 0; i < 4; i++ {
    h := sha256.New()
    h.Write(seed[:])
    h.Write([]byte{byte(i)})
    sum := h.Sum(nil)
    keys[i] = binary.LittleEndian.Uint32(sum[:4])
}

// Encrypt all entryOff values
for _, offset := range entryOffLocations {
    entryOff := binary.LittleEndian.Uint32(data[offset:])
    nameOff := binary.LittleEndian.Uint32(data[offset+4:])
    
    encrypted := feistelEncrypt(entryOff, nameOff, keys)
    binary.LittleEndian.PutUint32(data[offset:], encrypted)
}

Testing & Verification

Unit Tests

  • feistel_test.go: Encrypt/decrypt symmetry, edge cases
  • feistel_integration_test.go: Full round-trip validation

Integration Tests

  • testdata/script/runtime_metadata.txtar: Validates:
    • runtime.FuncForPC() works with encrypted metadata
    • ✅ Stack traces via runtime.Caller() remain correct
    • ✅ Method name resolution functions properly
    • ✅ Reflection type names accessible

Threat Mitigation

Attack Mitigation Residual Risk
Static pclntab enumeration Entry offsets encrypted Dynamic tracing observes actual behavior
Pattern matching Per-function tweak breaks patterns -
Brute force key recovery 128-bit keyspace infeasible -
Known-plaintext attack Tweak ensures unique ciphertexts Requires recovering seed

Implementation References

  • feistel.go: Core encryption/decryption logic
  • runtime_patch.go: Runtime injection
  • internal/linker/linker.go: LINK_SEED environment setup
  • internal/linker/patches/go1.25/0003-add-entryOff-encryption.patch: Linker modifications

4. Literal Obfuscation (ASCON-128 + Simple)

Purpose

Transform string and numeric literals into encrypted or obfuscated expressions that resolve at runtime, preventing static extraction via tools like strings or gostringungarbler.

Architecture Overview

Garble employs multiple obfuscation strategies selected randomly per literal for defense-in-depth:

  1. ASCON-128 (Primary): NIST Lightweight Cryptography standard, authenticated encryption
  2. Simple Reversible (Secondary): Multi-layer XOR with position-dependent keys and byte chaining

ASCON-128 Architecture

┌─────────────────────────────────────────────────────────────────┐
│              ASCON-128 Inline Encryption Flow                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Build Time (internal/literals/ascon_obfuscator.go):            │
│                                                                 │
│  1. Generate random key (16 bytes) and nonce (16 bytes)         │
│     key := cryptoRand.Read(16)                                  │
│     nonce := cryptoRand.Read(16)                                │
│                                                                 │
│  2. Encrypt plaintext with ASCON-128                            │
│     ciphertext||tag = AsconEncrypt(key, nonce, plaintext)       │
│     // Output: ciphertext + 16-byte authentication tag          │
│                                                                 │
│  3. Generate inline decryption code (~2947 bytes)               │
│     • Complete ASCON implementation inlined                     │
│     • No imports required (crypto-free binary)                  │
│     • Unique decryptor per literal                              │
│                                                                 │
│  Runtime (generated code):                                      │
│                                                                 │
│  data, ok := _garbleAsconDecrypt(                               │
│      []byte{...key...},                                         │
│      []byte{...nonce...},                                       │
│      []byte{...ciphertext||tag...}                              │
│  )                                                              │
│  if !ok {                                                       │
│      panic("garble: authentication failed")                     │
│  }                                                              │
│  // data now contains decrypted plaintext                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

ASCON-128 Properties

Property Value Benefit
Security Level 128-bit NIST-approved security strength
Key Size 128-bit (16 bytes) Strong key space
Nonce Size 128-bit (16 bytes) Unique per literal
Tag Size 128-bit (16 bytes) Detects tampering
Authentication Yes (AEAD) Integrity + confidentiality
Performance Lightweight Optimized for constrained environments

Simple Reversible Architecture

┌─────────────────────────────────────────────────────────────────┐
│         Simple Reversible Multi-Layer Obfuscation                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Build Time (internal/literals/simple.go):                      │
│                                                                 │
│  1. Generate random nonce (8 bytes) and key (len(data) bytes)   │
│                                                                 │
│  2. Select random operators for layers                          │
│     op1 = random(XOR, ADD, SUB)                                 │
│     op2 = random(XOR, ADD, SUB)                                 │
│                                                                 │
│  3. For each byte i in plaintext:                               │
│     // Layer 1: Position-dependent key                          │
│     posKey = key[i] ^ byte(i*7+13)  // Prime mixing             │
│     layer1 = data[i] ^ posKey                                   │
│                                                                 │
│     // Layer 2: Nonce mixing                                    │
│     layer2 = layer1 OP1 nonce[i % len(nonce)]                   │
│                                                                 │
│     // Layer 3: Byte chaining (if not first byte)               │
│     if i > 0:                                                   │
│       layer2 = layer2 OP2 (obfuscated[i-1] >> 3)                │
│                                                                 │
│     obfuscated[i] = layer2                                      │
│                                                                 │
│  Runtime (generated code):                                      │
│                                                                 │
│  // Reverse the layers in opposite order                        │
│  for i := 0; i < len(data); i++ {                               │
│      // Reverse layer 3 (chain dependency)                      │
│      if i > 0 {                                                 │
│          data[i] = data[i] REVERSE_OP2 (prevTemp >> 3)          │
│      }                                                          │
│      // Reverse layer 2 (nonce)                                 │
│      data[i] = data[i] REVERSE_OP1 nonce[i % len(nonce)]        │
│      // Reverse layer 1 (position key)                          │
│      posKey := key[i] ^ byte(i*7+13)                            │
│      data[i] = data[i] ^ posKey                                 │
│  }                                                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Simple Obfuscator Properties

  • Layers: 3 (position-key XOR, nonce mixing, byte chaining)
  • Nonce: 8 bytes per literal (prevents cross-build correlation)
  • Operators: Random selection (XOR/ADD/SUB) per layer
  • Reversibility: Fully reversible (supports garble reverse)
  • Performance: Fast for small literals

Obfuscator Selection Strategy

Implemented in internal/literals/obfuscators.go:

// Approximate selection probabilities:
// - ASCON-128: ~60% of literals (strong encryption)
// - Simple: ~40% of literals (performance, diversity)

Selection factors:

  • Literal size (ASCON preferred for larger literals)
  • Performance requirements (simple for hot paths)
  • Build randomness (varies per build for diversity)

Known Limitations

Const Expressions

const VERSION = "1.0"          // ✅ Rewritten to var + obfuscated when no compile-time dependency exists
const SIZE = len(VERSION)       // ⚠️ Must stay const (array length)
const CaseLabel = "case-only"  // ⚠️ Must stay const (switch label)

Reason: Garble rewrites string constants into vars when they are only used at runtime. Values that participate in compile-time contexts (array lengths, iota arithmetic, case clauses, etc.) must remain constants to keep the program valid and may stay in plaintext.

Linker-Injected Strings (-ldflags -X)

Status: ✅ Fully Protected since October 2025

Go's -ldflags -X flag allows injecting string values at link time:

go build -ldflags="-X main.apiKey=sk_live_51234567890abcdefABCDEF"

Traditional Vulnerability: These strings appear in plaintext in the binary, easily extractable with strings or hex editors.

Garble Protection Pipeline:

┌──────────────────────────────────────────────────────────────┐
│  Phase 1: FLAG SANITIZATION (main.go)                       │
├──────────────────────────────────────────────────────────────┤
│  Input:  -ldflags="-X main.apiKey=sk_live_51234567890..."    │
│                                     └─────────┬────────┘      │
│                           Extracted & cached  │               │
│                                              ▼               │
│  sharedCache.LinkerInjectedStrings["main.apiKey"] =         │
│      "sk_live_51234567890abcdefABCDEF"                       │
│                                              │               │
│                    Sanitized flag rewritten  │               │
│                                              ▼               │
│  Output: -ldflags="-X main.apiKey="  ← Empty to linker!     │
│                                                              │
│  ✅ Go toolchain NEVER sees the original value               │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│  Phase 2: RUNTIME INJECTION (transformer.go)                 │
├──────────────────────────────────────────────────────────────┤
│  During package compilation, Garble injects:                 │
│                                                              │
│  func init() {                                               │
│      apiKey = <obfuscated_literal>("sk_live_51234567...")>  │
│  }                                                           │
│                                                              │
│  ✅ Uses identical obfuscation as normal string literals     │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│  Phase 3: ENCRYPTION (literals.go)                           │
├──────────────────────────────────────────────────────────────┤
│  Value encrypted with ASCON-128 (~60%) or Simple (~40%):    │
│                                                              │
│  • ASCON: AES-like encryption + inline decrypt function     │
│  • Simple: XOR + shuffle + split + index remapping          │
│                                                              │
│  ✅ Binary contains only ciphertext + decrypt code           │
└──────────────────────────────────────────────────────────────┘

Supported Formats:

# All three -X formats are protected:
-ldflags="-X=main.version=1.0"
-ldflags="-X main.version=1.0"
-ldflags="-X \"main.message=hello world\""

Security Guarantees:

Attack Vector Normal Build Garble Build
strings binary | grep apiKey ❌ Plaintext found ✅ Not found
Static analysis ❌ Immediate extraction ⚠️ Requires decrypt reverse
Hex editor search ❌ Visible bytes ✅ Only ciphertext
Memory dump (runtime) ⚠️ Always plaintext ⚠️ Decrypted in memory

Real-World Example:

package main
var apiKey = "default-key"  // Will be replaced via -ldflags

// Build without Garble
$ go build -ldflags="-X main.apiKey=sk_live_ABC123"
$ strings binary | grep sk_live
sk_live_ABC123Exposed!

// Build with Garble
$ garble -literals build -ldflags="-X main.apiKey=sk_live_ABC123"
$ strings binary | grep sk_live
(no results)  ← Protected!

// But runtime still works:
$ ./binary
Using API key: sk_live_ABC123Decrypted at runtime

Implementation Details:

  • main.go: sanitizeLinkerFlags() - extracts and sanitizes flags
  • transformer.go: injectLinkerVariableInit() - generates init() function
  • internal/literals/literals.go: Builder.ObfuscateStringLiteral() - encrypts value
  • Tests: testdata/script/ldflags.txtar - end-to-end verification

Current Status

Feature Status Notes
String literals ✅ Obfuscated ASCON + simple mix
Numeric literals ✅ Obfuscated When -literals enabled
Byte slices ✅ Obfuscated Treated as literals
Const expressions ⚠️ Partially covered Safe string consts are rewritten; compile-time contexts remain const
-ldflags -X strings ✅ Covered Sanitised at flag parse; runtime decrypt
Irreversible simple ⚠️ Planned Currently uses reversible path

Implementation References

  • internal/literals/ascon.go: Core ASCON-128 implementation
  • internal/literals/ascon_inline.go: Inline code generator
  • internal/literals/ascon_obfuscator.go: Obfuscator integration
  • internal/literals/simple.go: Simple reversible obfuscator
  • internal/literals/obfuscators.go: Selection strategy
  • Tests: ascon_test.go, simple_test.go, ascon_integration_test.go

5. Reflection Control & Reversibility

Purpose

Eliminate the "reflection oracle" that leaked obfuscated-to-original identifier mappings, while preserving opt-in support for debugging workflows via garble reverse.

Architecture Diagram

┌──────────────────────────────────────────────────────────────┐
│              Default Mode (Secure)                           │
│              garble build                                    │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  reflect.go: reflectMainPostPatch()                          │
│                                                              │
│  if !flagReversible {                                        │
│      _originalNamePairs = []string{}  // EMPTY               │
│  }                                                           │
│                                                              │
│  Binary Contents:                                            │
│    ✓ Obfuscated names only                                   │
│    ✓ No original identifier mapping                          │
│    ✓ Reflection still works (with obfuscated names)          │
│    ✓ No reverse-engineering oracle                           │
│    ✗ garble reverse not supported                            │
│                                                              │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│              Reversible Mode (Debug/Staging)                 │
│              garble -reversible build                        │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  reflect.go: reflectMainPostPatch()                          │
│                                                              │
│  if flagReversible {                                         │
│      _originalNamePairs = []string{                          │
│          "ObfName1", "OrigName1",                            │
│          "ObfName2", "OrigName2",                            │
│          ...                                                 │
│      }  // POPULATED                                         │
│  }                                                           │
│                                                              │
│  Binary Contents:                                            │
│    ✓ Obfuscated names                                        │
│    ✓ Original names embedded (mapping array)                 │
│    ✓ garble reverse supported                                │
│    ⚠️  Reverse-engineering oracle present                     │
│    ⚠️  Reduced security (explicit trade-off)                  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Implementation

// reflect.go - Security-first approach

func reflectMainPostPatch(file []byte, lpkg *listedPackage, pkg pkgCache) []byte {
    obfVarName := hashWithPackage(lpkg, "_originalNamePairs")
    namePairs := fmt.Appendf(nil, "%s = []string{", obfVarName)

    // Default: Keep array empty (no name leakage)
    if !flagReversible {
        return bytes.Replace(file, namePairs, namePairs, 1)
    }

    // Reversible mode: Populate mapping for garble reverse
    keys := slices.Sorted(maps.Keys(pkg.ReflectObjectNames))
    namePairsFilled := bytes.Clone(namePairs)
    for _, obf := range keys {
        namePairsFilled = fmt.Appendf(namePairsFilled, "%q, %q,", 
            obf, pkg.ReflectObjectNames[obf])
    }

    return bytes.Replace(file, namePairs, namePairsFilled, 1)
}

Security Impact Comparison

Aspect Default Mode -reversible Mode
_originalNamePairs Empty array Populated with mappings
Original names in binary ✅ Not present ❌ Embedded in plaintext
Reflection functionality ✅ Works (obfuscated names) ✅ Works (obfuscated names)
garble reverse ❌ Not supported ✅ Supported
Reverse engineering oracle ✅ Eliminated ❌ Present (by design)
Security level 🔒 High 🔓 Medium (trade-off)

Usage Recommendations

Production Builds:

garble build              # Default: maximum security

Development/Staging:

garble -reversible build  # Enable debugging support
garble reverse binary < stack_trace.txt

Propagation to Linker

The -reversible flag is propagated to the linker via environment variable:

GARBLE_LINK_REVERSIBLE=true   # When -reversible is set
GARBLE_LINK_REVERSIBLE=false  # When -reversible is not set

Implementation References

  • reflect.go: reflectMainPostPatch() - Core logic
  • main.go: Flag definition and linker environment setup

6. Build Cache Encryption (ASCON-128)

Purpose

Encrypt Garble's persistent build cache to prevent offline analysis of obfuscation metadata, import paths, and build artifacts.

Architecture Diagram

┌─────────────────────────────────────────────────────────────────┐
│              Cache Encryption Flow (ASCON-128)                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Write Path (cache_pkg.go: computePkgCache):                    │
│                                                                 │
│  1. Serialize pkg cache to gob                                  │
│     var buf bytes.Buffer                                        │
│     gob.NewEncoder(&buf).Encode(pkgCache)                       │
│     plaintext := buf.Bytes()                                    │
│                                                                 │
│  2. Derive encryption key from seed                             │
│     key = SHA256(seed || "garble-cache-encryption-v1")          │
│     key = key[0:16]  // 128-bit ASCON key                       │
│                                                                 │
│  3. Encrypt with ASCON-128                                      │
│     nonce := cryptoRand.Read(16)  // Random per cache entry     │
│     ciphertext||tag = AsconEncrypt(key, nonce, plaintext)       │
│                                                                 │
│  4. Write to disk                                               │
│     format: [16-byte nonce][ciphertext][16-byte tag]            │
│     path: $GARBLE_CACHE/<action-id>                             │
│                                                                 │
│  Read Path (cache_pkg.go: loadPkgCache):                        │
│                                                                 │
│  1. Read encrypted cache from disk                              │
│     data := readFile($GARBLE_CACHE/<action-id>)                 │
│                                                                 │
│  2. Check if encrypted (has seed)                               │
│     if seed := cacheEncryptionSeed(); seed != nil {             │
│         // Decrypt path                                         │
│     } else {                                                    │
│         // Legacy plaintext gob fallback                        │
│     }                                                           │
│                                                                 │
│  3. Extract components                                          │
│     nonce := data[0:16]                                         │
│     ciphertext_and_tag := data[16:]                             │
│                                                                 │
│  4. Derive same key and decrypt                                 │
│     key = SHA256(seed || "garble-cache-encryption-v1")[0:16]    │
│     plaintext, ok := AsconDecrypt(key, nonce, ciphertext_and_tag)│
│                                                                 │
│  5. Verify authentication tag                                   │
│     if !ok {                                                    │
│         // Tag mismatch: cache corrupted or tampered            │
│         return nil  // Triggers rebuild                         │
│     }                                                           │
│                                                                 │
│  6. Deserialize gob                                             │
│     gob.NewDecoder(bytes.NewReader(plaintext)).Decode(&pkgCache)│
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Encryption Key Derivation

func deriveCacheKey(seed []byte) []byte {
    // Domain separation for cache encryption
    h := sha256.New()
    h.Write(seed)
    h.Write([]byte("garble-cache-encryption-v1"))
    digest := h.Sum(nil)
    return digest[0:16]  // 128-bit ASCON-128 key
}

Cache File Format

┌─────────────────────────────────────────────────────────┐
│                 Encrypted Cache Entry                   │
├─────────────────────────────────────────────────────────┤
│  Bytes 0-15:    Random nonce (16 bytes)                 │
│  Bytes 16-N-16: Encrypted pkg cache (variable length)   │
│  Bytes N-16-N:  Authentication tag (16 bytes)           │
└─────────────────────────────────────────────────────────┘

Activation Conditions

Cache encryption is enabled by default when:

  1. A seed is available (-seed flag or inherited)
  2. -no-cache-encrypt flag is NOT present
# Encrypted cache (default with seed)
garble -seed=<base64> build

# Explicitly disable encryption
garble -seed=<base64> -no-cache-encrypt build

# No encryption (no seed)
garble build  # Cache remains plaintext

Shared Cache vs Persistent Cache

Cache Type Location Encrypted Lifetime
Persistent $GARBLE_CACHE/<action-id> ✅ Yes (when enabled) Permanent until trimmed
Shared $GARBLE_SHARED (temp) ❌ No Deleted after build

Design Rationale:

  • Persistent cache: Long-lived, disk-resident → encrypted to protect offline analysis
  • Shared cache: Ephemeral, process-local → plaintext for performance, cleaned automatically

Tamper Detection

ASCON-128's authentication tag provides cryptographic verification:

  • Valid tag: Cache decrypts successfully
  • Invalid tag: Decryption fails → treated as cache miss → rebuild triggered
  • No crash: Corruption degrades gracefully to rebuild

Backward Compatibility

Legacy plaintext caches are automatically detected and read:

func decodePkgCacheBytes(data []byte) (pkgCache, error) {
    if seed := cacheEncryptionSeed(); len(seed) > 0 {
        // Try ASCON decryption
        return decryptCacheIntoShared(data, seed)
    }
    // Fallback: plaintext gob
    var cache pkgCache
    gob.NewDecoder(bytes.NewReader(data)).Decode(&cache)
    return cache, nil
}

Security Properties

Property Value Benefit
Algorithm ASCON-128 AEAD NIST-approved authenticated encryption
Key Size 128-bit Strong security margin
Nonce 128-bit random Unique per cache entry
Authentication 128-bit tag Detects tampering
Domain Separation "garble-cache-encryption-v1" Prevents key reuse attacks

Threat Mitigation

Attack Mitigation Result
Offline cache analysis Encrypted with ASCON-128 Plaintext metadata inaccessible
Cache tampering Authentication tag verification Corruption detected, rebuild triggered
Cache poisoning Tag forgery requires key Infeasible (128-bit security)
Key recovery Seed never stored in cache Attacker needs build-time seed

Implementation References

  • cache_ascon.go: deriveCacheKey(), encryptCacheWithASCON(), decryptCacheIntoShared()
  • cache_pkg.go: computePkgCache(), loadPkgCache(), decodePkgCacheBytes()
  • main.go: Seed and -no-cache-encrypt flag handling

7. Control-Flow Obfuscation

Purpose

Transform control-flow structures to increase complexity and hinder static analysis, making it harder to understand program logic.

Modes

Mode Behavior Use Case
off (default) No transformation Standard builds
directives Only functions with //garble:controlflow Selective protection
auto All eligible functions except //garble:nocontrolflow Broad protection with escape hatch
all Every function Maximum obfuscation

Architecture

┌─────────────────────────────────────────────────────────────────┐
│            Control-Flow Obfuscation Decision Tree                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  For each function:                                             │
│                                                                 │
│  1. Check mode                                                  │
│     if mode == off:                                             │
│         return (no transformation)                              │
│                                                                 │
│  2. Check directives                                            │
│     if mode == directives:                                      │
│         if function has //garble:controlflow:                   │
│             transform()                                         │
│         else:                                                   │
│             return (no transformation)                          │
│                                                                 │
│  3. Check eligibility (mode == auto)                            │
│     if function has //garble:nocontrolflow:                     │
│         return (explicit skip)                                  │
│     if function is too simple:                                  │
│         return (heuristic skip)                                 │
│     if SSA safety check fails:                                  │
│         return (unsafe to transform)                            │
│     transform()                                                 │
│                                                                 │
│  4. Force transform (mode == all)                               │
│     transform() regardless of complexity/safety                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Configuration

CLI Flag (highest priority)

garble -controlflow=auto build
garble -controlflow=directives build
garble -controlflow=all build

Environment Variable (fallback)

export GARBLE_CONTROLFLOW=auto
garble build

Precedence: CLI flag > environment variable > default (off)

Directive Usage

Opt-In (directives mode)

//garble:controlflow
func sensitiveFunction() {
    // Only transformed when mode=directives or mode=auto/all
}

Opt-Out (auto/all modes)

//garble:nocontrolflow
func hotPath() {
    // Skipped even in auto mode; still transformed in all mode
}

Transformation Strategy

Control-flow obfuscation (implemented in internal/ctrlflow):

  1. Flatten: Convert structured control flow to flat switch/goto
  2. Opaque Predicates: Insert conditions always true/false but hard to analyze
  3. Dead Code Injection: Add unreachable but plausible code paths

Current Status

Feature Status Notes
Mode selection ✅ Implemented off/directives/auto/all
Directive support ✅ Implemented //garble:controlflow, //garble:nocontrolflow
SSA safety checks ✅ Implemented Prevents unsafe transforms
Performance optimization ⚠️ Ongoing Heuristics for hot-path detection
Default-on ❌ Planned Needs perf validation

Performance Considerations

Control-flow obfuscation can impact:

  • Binary size: +5-15% typical increase
  • Performance: Variable depending on function complexity
  • Compilation time: +10-30% longer builds

Recommendation: Use auto mode with selective //garble:nocontrolflow in hot paths.

Implementation References

  • internal/ctrlflow/mode.go: Mode enum and parsing
  • internal/ctrlflow/ctrlflow.go: Eligibility checks, transformation logic
  • internal/ctrlflow/transform.go: AST transformation
  • docs/CONTROLFLOW.md: Detailed design documentation
  • main.go: Flag and environment resolution

8. Threat Model & Mitigation Matrix

Threat Classification

Attack Vector Difficulty Impact Mitigation Status
Static pclntab analysis Medium → Hard High ✅ Mitigated (Feistel)
Cross-build name correlation Easy → Hard Medium ✅ Mitigated (Nonce)
Static string extraction Easy → Medium High ✅ Mitigated (ASCON + Simple)
Reflection oracle exploitation Easy → N/A Critical ✅ Eliminated (Default)
Cache offline analysis Easy → Hard Medium ✅ Mitigated (ASCON Encryption)
Dynamic runtime tracing Easy Variable ⚠️ By Design (Observable)
Const expression extraction Easy Medium ⚠️ Partial Gap (compile-time contexts)
-ldflags -X plaintext leakage Easy Medium ✅ Mitigated (Sanitized + obfuscated)
Control-flow analysis Medium Medium ⚠️ Optional (CF modes)

Detailed Mitigation Matrix

Attack Vector Mitigation Mechanism Residual Risk Notes
Static Symbol Table Analysis Feistel-encrypted entry offsets with per-build keys and per-function tweak Dynamic tracing observes actual runtime behavior Format-preserving; 128-bit keyspace
Cross-Build Pattern Matching SHA-256 seed+nonce mixing; cryptographically random nonce per build If seed and nonce are fixed (reproducibility), correlation possible Intentional for deterministic builds
String/Literal Scraping ASCON-128 inline encryption (~60%); multi-layer simple obfuscator (~40%) Compile-time-only consts remain in plaintext Remaining gap limited to array lengths / case labels
Injected -ldflags Strings CLI sanitization + shared-cache rehydration via literal builder Plaintext exists only transiently in garble parent process Sanitized flags never reach toolchain or final binary
Reflection Name Oracle _originalNamePairs array empty by default Opting into -reversible re-introduces oracle by design Security vs. debugging trade-off
Cache Inspection/Tampering ASCON-128 encryption at rest with 128-bit authentication tag Shared ephemeral cache plaintext (deleted after build) Tag verification prevents poisoning
Known-Plaintext Attack on Literals Per-literal random keys/nonces; ASCON authentication Requires recovering per-literal key (infeasible) Each literal independently secured
Brute-Force Key Recovery 128-bit Feistel keyspace; 128-bit ASCON keys Computationally infeasible Meets NIST security standards
Dynamic Code Injection Not addressed Requires runtime protections (out of scope) Obfuscation != runtime security
Control-Flow Reconstruction Optional CF obfuscation modes If disabled (default), structure remains clear User must enable explicitly

Attack Scenarios & Defenses

Scenario 1: Offline Binary Analysis

Attacker Goal: Extract original identifiers and strings without running the program.

Defenses:

  • ✅ Feistel encryption hides function mappings
  • ✅ ASCON/Simple encryption protects literals
  • ✅ Sanitized -ldflags -X strings are rehydrated via obfuscated init-time assignments
  • ✅ Empty reflection map eliminates name oracle
  • ⚠️ String constants required at compile time (array lengths, switch labels, iota math) remain visible

Result: Significantly harder; requires reverse engineering each obfuscation layer.

Scenario 2: Cross-Binary Correlation

Attacker Goal: Compare multiple builds to identify patterns and recover originals.

Defenses:

  • ✅ Per-build nonce ensures different hashes
  • ✅ Random ASCON nonces per literal
  • ⚠️ Fixed seed+nonce (reproducibility) breaks this defense

Result: Effective unless reproducible builds are used (intentional trade-off).

Scenario 3: Dynamic Runtime Tracing

Attacker Goal: Observe program behavior at runtime to infer logic.

Defenses:

  • ❌ Not addressed (out of scope for static obfuscation)
  • ⚠️ Control-flow obfuscation can make tracing harder (if enabled)

Result: Dynamic analysis always possible; obfuscation raises the bar but doesn't prevent it.

Scenario 4: Cache-Based Analysis

Attacker Goal: Analyze Garble's cache to recover build metadata.

Defenses:

  • ✅ ASCON-128 encryption protects persistent cache
  • ✅ Authentication tag prevents tampering
  • ✅ Seed not stored in cache

Result: Cache contents inaccessible without build-time seed.


9. Security Limitations & Roadmap

Current Limitations

1. Literal Coverage Gaps

Issue: Certain literal types are not obfuscated.

Type Status Reason Priority
Compile-time const contexts ⚠️ Partial Array lengths, case labels, iota must stay const Medium
-ldflags -X strings Covered Sanitized at CLI, encrypted via init() ✅ Complete
Runtime-generated strings ❌ Not covered Created dynamically Low

Example of remaining gap:

const arraySize = "XXXX"
var arr = [len(arraySize)]byte{}  // ⚠️ Must stay const (array length)

const caseLabel = "case-only"
switch x {
case caseLabel:  // ⚠️ Must stay const (switch case)
    return true
}

What IS protected:

const runtimeSecret = "hide-me"  // ✅ Converted to var + encrypted
var sink = runtimeSecret         // ✅ Value obfuscated at runtime

// Via -ldflags
var apiKey = "default"
// Build: garble -literals build -ldflags="-X main.apiKey=secret123"
// ✅ "secret123" is ASCON-encrypted, never appears in plaintext

Planned: Advanced const-folding analysis to detect more safe-to-rewrite constants.

2. Irreversible Simple Obfuscator

Issue: The "simple" obfuscator currently uses the same reversible algorithm in both modes.

Current:

  • -reversible: Uses reversible simple (✅ intended)
  • No -reversible: Still uses reversible simple (⚠️ should be irreversible)

Planned: Implement true one-way simple variant (e.g., hash chains, S-box substitution).

3. Control-Flow Default State

Issue: Control-flow obfuscation is opt-in (default: off).

Reason: Performance impact not fully characterized; needs heuristics.

Planned:

  1. Gather performance benchmarks across typical codebases
  2. Develop heuristics for auto-exclusion of hot paths
  3. Consider default-on with smart exclusions

4. Exported Identifiers

Issue: Exported names remain unobfuscated.

Reason: Required for Go's interface compatibility and reflection.

Status: By design; whole-program obfuscation not feasible in Go's compilation model.

Alternative: Document the trade-off; consider separate "closed-ecosystem" mode in future.

5. Error/Panic Message Leakage

Issue: Error strings and panic messages may reveal implementation details.

Examples:

panic("failed to parse config at line 42")
fmt.Errorf("database %s not found", dbName)

Planned: Optional -strip-errors flag to sanitize messages in production builds.

Roadmap

Short-Term (Q4 2025)

Item Status Priority
Improve const expression handling 🔄 In Progress Medium
Implement irreversible simple obfuscator 📋 Planned High
Document -ldflags workarounds 📋 Planned Low
Performance benchmarks for CF modes 📋 Planned Medium

Medium-Term (Q1-Q2 2026)

Item Status Priority
Control-flow default-on evaluation 📋 Planned Medium
-strip-errors flag implementation 📋 Planned Low
Link-time -ldflags interception 🔬 Research Medium
Cache encryption performance tuning 📋 Planned Low

Long-Term (2026+)

Item Status Priority
Anti-debugging countermeasures 💡 Concept Low
Whole-program obfuscation mode 💡 Concept Low
Hardware-backed key storage 💡 Concept Very Low

Legend: 💡 Concept | 🔬 Research | 📋 Planned | 🔄 In Progress | ✅ Complete

Known Trade-Offs

Reproducibility vs. Uniqueness

  • Fixed seed+nonce: Reproducible builds, but correlation possible
  • Random nonce: Unique per build, but not reproducible
  • Choice: User decides based on requirements (CI/CD vs. anti-correlation)

Security vs. Debugging

  • Default mode: Maximum security, no garble reverse
  • -reversible mode: Debugging support, reduced security
  • Choice: Production uses default; staging uses -reversible

Performance vs. Obfuscation

  • Control-flow off: Fast builds, clear structure
  • Control-flow auto/all: Slower builds, complex structure
  • Choice: Balance based on threat model

10. References & Resources

Documentation

Document Purpose Location
FEATURE_TOGGLES.md Complete flag and environment reference docs/FEATURE_TOGGLES.md
CONTROLFLOW.md Control-flow obfuscation design docs/CONTROLFLOW.md
README.md User-facing overview and quick start README.md
This document Security architecture and threat model docs/SECURITY.md

Implementation Files

Core Obfuscation

  • main.go: Entry point, flag parsing, seed/nonce handling
  • hash.go: Name hashing, seed+nonce mixing
  • transformer.go: AST transformation orchestration

Runtime Metadata

  • feistel.go: Feistel encryption/decryption primitives
  • runtime_patch.go: Runtime injection logic
  • internal/linker/linker.go: Linker patching coordination
  • internal/linker/patches/go1.25/0003-add-entryOff-encryption.patch: Linker modifications

Literals

  • internal/literals/ascon.go: ASCON-128 core implementation
  • internal/literals/ascon_inline.go: Inline code generation
  • internal/literals/ascon_obfuscator.go: Obfuscator integration
  • internal/literals/simple.go: Simple reversible obfuscator
  • internal/literals/obfuscators.go: Selection strategy

Reflection

  • reflect.go: Reflection metadata handling, reflectMainPostPatch()

Cache

  • cache_ascon.go: ASCON encryption for cache
  • cache_pkg.go: Cache persistence and loading

Control-Flow

  • internal/ctrlflow/mode.go: Mode definitions
  • internal/ctrlflow/ctrlflow.go: Transformation logic
  • internal/ctrlflow/transform.go: AST manipulation

Testing

Unit Tests

  • feistel_test.go: Feistel primitives
  • feistel_integration_test.go: End-to-end Feistel
  • internal/literals/*_test.go: Literal obfuscation
  • cache_encryption_test.go: Cache encryption

Integration Tests

  • testdata/script/runtime_metadata.txtar: Runtime metadata
  • testdata/script/reflect_secure.txtar: Reflection default mode
  • testdata/script/reflect_reversible.txtar: Reflection reversible mode
  • testdata/script/seed.txtar: Seed and nonce behavior
  • testdata/script/ctrlflow_*.txtar: Control-flow modes

External References

Standards

Threat Intelligence


Document Maintenance

  • Version: 1.0
  • Last Updated: October 8, 2025
  • Next Review: December 2025
  • Owner: x430n Spectre Team

There aren’t any published security advisories