Skip to content

Commit 37ee907

Browse files
committed
ci: detect compiler-induced side-channels in build artifacts
1 parent 339b81f commit 37ee907

14 files changed

Lines changed: 1985 additions & 0 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Constant-Time Verification Workflow
2+
#
3+
# This workflow analyzes Go code for instructions that could leak timing information.
4+
# It cross-compiles for multiple architectures and checks for dangerous instructions
5+
# like variable-time division, which could enable timing side-channel attacks.
6+
7+
name: Constant-Time Verification
8+
permissions:
9+
contents: read
10+
11+
on:
12+
push:
13+
branches: [ main ]
14+
pull_request:
15+
branches: [ main ]
16+
17+
jobs:
18+
ct-check:
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
include:
23+
# x86-64 (Intel/AMD 64-bit)
24+
- goos: linux
25+
goarch: amd64
26+
name: Linux AMD64
27+
28+
# x86 (Intel/AMD 32-bit)
29+
- goos: linux
30+
goarch: "386"
31+
name: Linux i386
32+
33+
# ARM64 (Apple Silicon, ARM servers)
34+
- goos: linux
35+
goarch: arm64
36+
name: Linux ARM64
37+
38+
# ARM 32-bit
39+
- goos: linux
40+
goarch: arm
41+
name: Linux ARM
42+
43+
# PowerPC 64-bit Little Endian
44+
- goos: linux
45+
goarch: ppc64le
46+
name: Linux PPC64LE
47+
48+
# RISC-V 64-bit
49+
- goos: linux
50+
goarch: riscv64
51+
name: Linux RISC-V64
52+
53+
# IBM z/Architecture
54+
- goos: linux
55+
goarch: s390x
56+
name: Linux s390x
57+
58+
runs-on: ubuntu-latest
59+
name: CT Check (${{ matrix.name }})
60+
61+
steps:
62+
- name: Checkout code
63+
uses: actions/checkout@v4
64+
65+
- name: Set up Go
66+
uses: actions/setup-go@v5
67+
with:
68+
go-version: '1.24'
69+
70+
- name: Run constant-time verification
71+
run: |
72+
go run ./test_ct \
73+
-arch=${{ matrix.goarch }} \
74+
-os=${{ matrix.goos }} \
75+
-func='field\.|DivBarrett|DivConstTime|Decompose|Power2Round|Symmetric|HighBits|LowBits|MakeHint|UseHint|BitPack|BitUnpack'
76+
77+
- name: Run constant-time verification (with warnings)
78+
if: always()
79+
continue-on-error: true
80+
run: |
81+
echo "=== Analysis with warnings ==="
82+
go run ./test_ct \
83+
-arch=${{ matrix.goarch }} \
84+
-os=${{ matrix.goos }} \
85+
-func='field\.|DivBarrett|DivConstTime|Decompose|Power2Round|Symmetric|HighBits|LowBits' \
86+
-warnings || true

test_ct/analyzer/analyzer.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Package analyzer provides the core analysis engine for detecting
2+
// constant-time violations in disassembled code.
3+
package analyzer
4+
5+
import (
6+
"github.com/trailofbits/ml-dsa/test_ct/arch"
7+
"github.com/trailofbits/ml-dsa/test_ct/disasm"
8+
)
9+
10+
// Config holds the configuration for analysis.
11+
type Config struct {
12+
// Architecture is the target architecture analyzer.
13+
Architecture arch.Architecture
14+
15+
// Functions are the disassembled functions to analyze.
16+
Functions []disasm.Function
17+
18+
// IncludeWarnings includes warning-level violations (e.g., conditional branches).
19+
IncludeWarnings bool
20+
21+
// Verbose enables verbose output during analysis.
22+
Verbose bool
23+
}
24+
25+
// Violation represents a detected constant-time violation.
26+
type Violation struct {
27+
// Function is the name of the function containing the violation.
28+
Function string
29+
30+
// ShortFunction is the short name of the function.
31+
ShortFunction string
32+
33+
// File is the source file path.
34+
File string
35+
36+
// Address is the instruction address.
37+
Address uint64
38+
39+
// Instruction is the violating instruction.
40+
Instruction string
41+
42+
// Mnemonic is the instruction mnemonic.
43+
Mnemonic string
44+
45+
// Reason explains why this instruction is dangerous.
46+
Reason string
47+
48+
// Severity is either "error" or "warning".
49+
Severity string
50+
}
51+
52+
// Report contains the results of an analysis.
53+
type Report struct {
54+
// Architecture is the target architecture name.
55+
Architecture string
56+
57+
// GOOS is the target operating system.
58+
GOOS string
59+
60+
// GOARCH is the target architecture.
61+
GOARCH string
62+
63+
// TotalFunctions is the number of functions analyzed.
64+
TotalFunctions int
65+
66+
// TotalInstructions is the total number of instructions analyzed.
67+
TotalInstructions int
68+
69+
// Violations contains all detected violations.
70+
Violations []Violation
71+
72+
// ErrorCount is the number of error-level violations.
73+
ErrorCount int
74+
75+
// WarningCount is the number of warning-level violations.
76+
WarningCount int
77+
78+
// Passed indicates whether the analysis passed (no errors).
79+
Passed bool
80+
}
81+
82+
// Analyze performs constant-time analysis on the given functions.
83+
func Analyze(cfg Config) *Report {
84+
report := &Report{
85+
Architecture: cfg.Architecture.Name(),
86+
GOARCH: cfg.Architecture.GOARCH(),
87+
TotalFunctions: len(cfg.Functions),
88+
}
89+
90+
for _, fn := range cfg.Functions {
91+
report.TotalInstructions += len(fn.Instructions)
92+
93+
// Analyze instructions using the architecture
94+
archViolations := cfg.Architecture.AnalyzeInstructions(fn.Instructions)
95+
96+
for _, v := range archViolations {
97+
// Skip warnings if not included
98+
if v.Severity == "warning" && !cfg.IncludeWarnings {
99+
continue
100+
}
101+
102+
var instr arch.Instruction
103+
if v.InstructionIndex >= 0 && v.InstructionIndex < len(fn.Instructions) {
104+
instr = fn.Instructions[v.InstructionIndex]
105+
}
106+
107+
violation := Violation{
108+
Function: fn.Name,
109+
ShortFunction: fn.ShortName,
110+
File: fn.File,
111+
Address: instr.Address,
112+
Instruction: v.Instruction,
113+
Mnemonic: instr.Mnemonic,
114+
Reason: v.Reason,
115+
Severity: v.Severity,
116+
}
117+
118+
report.Violations = append(report.Violations, violation)
119+
120+
if v.Severity == "error" {
121+
report.ErrorCount++
122+
} else {
123+
report.WarningCount++
124+
}
125+
}
126+
}
127+
128+
// Analysis passes if there are no errors
129+
report.Passed = report.ErrorCount == 0
130+
131+
return report
132+
}
133+
134+
// AnalyzeSingle analyzes a single function and returns violations.
135+
func AnalyzeSingle(architecture arch.Architecture, fn disasm.Function, includeWarnings bool) []Violation {
136+
cfg := Config{
137+
Architecture: architecture,
138+
Functions: []disasm.Function{fn},
139+
IncludeWarnings: includeWarnings,
140+
}
141+
report := Analyze(cfg)
142+
return report.Violations
143+
}

test_ct/analyzer/report.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package analyzer
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"strings"
8+
)
9+
10+
// ReportFormat specifies the output format for reports.
11+
type ReportFormat string
12+
13+
const (
14+
// FormatText outputs a human-readable text report.
15+
FormatText ReportFormat = "text"
16+
17+
// FormatJSON outputs a JSON report.
18+
FormatJSON ReportFormat = "json"
19+
20+
// FormatGitHubActions outputs annotations for GitHub Actions.
21+
FormatGitHubActions ReportFormat = "github"
22+
)
23+
24+
// WriteReport writes a report to the given writer in the specified format.
25+
func WriteReport(w io.Writer, report *Report, format ReportFormat) error {
26+
switch format {
27+
case FormatJSON:
28+
return writeJSONReport(w, report)
29+
case FormatGitHubActions:
30+
return writeGitHubActionsReport(w, report)
31+
default:
32+
return writeTextReport(w, report)
33+
}
34+
}
35+
36+
// writeTextReport writes a human-readable text report.
37+
func writeTextReport(w io.Writer, report *Report) error {
38+
fmt.Fprintf(w, "Constant-Time Analysis Report\n")
39+
fmt.Fprintf(w, "==============================\n\n")
40+
fmt.Fprintf(w, "Architecture: %s (%s)\n", report.Architecture, report.GOARCH)
41+
fmt.Fprintf(w, "Functions analyzed: %d\n", report.TotalFunctions)
42+
fmt.Fprintf(w, "Instructions analyzed: %d\n\n", report.TotalInstructions)
43+
44+
if len(report.Violations) == 0 {
45+
fmt.Fprintf(w, "No violations found.\n\n")
46+
} else {
47+
// Group violations by function
48+
byFunction := make(map[string][]Violation)
49+
for _, v := range report.Violations {
50+
byFunction[v.Function] = append(byFunction[v.Function], v)
51+
}
52+
53+
for fn, violations := range byFunction {
54+
fmt.Fprintf(w, "Function: %s\n", fn)
55+
if violations[0].File != "" {
56+
fmt.Fprintf(w, " File: %s\n", violations[0].File)
57+
}
58+
fmt.Fprintf(w, "\n")
59+
60+
for _, v := range violations {
61+
severityStr := strings.ToUpper(v.Severity)
62+
fmt.Fprintf(w, " [%s] 0x%x: %s\n", severityStr, v.Address, v.Instruction)
63+
fmt.Fprintf(w, " %s\n\n", v.Reason)
64+
}
65+
}
66+
}
67+
68+
fmt.Fprintf(w, "Summary\n")
69+
fmt.Fprintf(w, "-------\n")
70+
fmt.Fprintf(w, "Errors: %d\n", report.ErrorCount)
71+
fmt.Fprintf(w, "Warnings: %d\n", report.WarningCount)
72+
73+
if report.Passed {
74+
fmt.Fprintf(w, "\nPASSED\n")
75+
} else {
76+
fmt.Fprintf(w, "\nFAILED\n")
77+
}
78+
79+
return nil
80+
}
81+
82+
// JSONReport is the JSON-serializable report structure.
83+
type JSONReport struct {
84+
Architecture string `json:"architecture"`
85+
GOARCH string `json:"goarch"`
86+
GOOS string `json:"goos,omitempty"`
87+
TotalFunctions int `json:"total_functions"`
88+
TotalInstructions int `json:"total_instructions"`
89+
ErrorCount int `json:"error_count"`
90+
WarningCount int `json:"warning_count"`
91+
Passed bool `json:"passed"`
92+
Violations []JSONViolation `json:"violations"`
93+
}
94+
95+
// JSONViolation is the JSON-serializable violation structure.
96+
type JSONViolation struct {
97+
Function string `json:"function"`
98+
File string `json:"file,omitempty"`
99+
Address string `json:"address"`
100+
Instruction string `json:"instruction"`
101+
Mnemonic string `json:"mnemonic"`
102+
Reason string `json:"reason"`
103+
Severity string `json:"severity"`
104+
}
105+
106+
// writeJSONReport writes a JSON report.
107+
func writeJSONReport(w io.Writer, report *Report) error {
108+
jsonReport := JSONReport{
109+
Architecture: report.Architecture,
110+
GOARCH: report.GOARCH,
111+
GOOS: report.GOOS,
112+
TotalFunctions: report.TotalFunctions,
113+
TotalInstructions: report.TotalInstructions,
114+
ErrorCount: report.ErrorCount,
115+
WarningCount: report.WarningCount,
116+
Passed: report.Passed,
117+
Violations: make([]JSONViolation, len(report.Violations)),
118+
}
119+
120+
for i, v := range report.Violations {
121+
jsonReport.Violations[i] = JSONViolation{
122+
Function: v.Function,
123+
File: v.File,
124+
Address: fmt.Sprintf("0x%x", v.Address),
125+
Instruction: v.Instruction,
126+
Mnemonic: v.Mnemonic,
127+
Reason: v.Reason,
128+
Severity: v.Severity,
129+
}
130+
}
131+
132+
encoder := json.NewEncoder(w)
133+
encoder.SetIndent("", " ")
134+
return encoder.Encode(jsonReport)
135+
}
136+
137+
// writeGitHubActionsReport writes GitHub Actions workflow commands.
138+
func writeGitHubActionsReport(w io.Writer, report *Report) error {
139+
for _, v := range report.Violations {
140+
// GitHub Actions annotation format:
141+
// ::error file={name},line={line},endLine={endLine},title={title}::{message}
142+
// ::warning file={name},line={line},endLine={endLine},title={title}::{message}
143+
144+
level := "error"
145+
if v.Severity == "warning" {
146+
level = "warning"
147+
}
148+
149+
title := fmt.Sprintf("Constant-time violation: %s", v.Mnemonic)
150+
message := fmt.Sprintf("%s in %s: %s", v.Instruction, v.ShortFunction, v.Reason)
151+
152+
if v.File != "" {
153+
fmt.Fprintf(w, "::%s file=%s,title=%s::%s\n", level, v.File, title, message)
154+
} else {
155+
fmt.Fprintf(w, "::%s title=%s::%s\n", level, title, message)
156+
}
157+
}
158+
159+
// Write summary
160+
if report.Passed {
161+
fmt.Fprintf(w, "::notice::Constant-time analysis passed. %d functions, %d instructions analyzed.\n",
162+
report.TotalFunctions, report.TotalInstructions)
163+
} else {
164+
fmt.Fprintf(w, "::error::Constant-time analysis failed. %d error(s) found.\n", report.ErrorCount)
165+
}
166+
167+
return nil
168+
}

0 commit comments

Comments
 (0)