Skip to content

Commit 8aa961e

Browse files
committed
feat(wasm): add Go wrapper for wasmsign2 to resolve Bazel sandbox + WASI conflict
Implements a cross-platform Go wrapper for wasmsign2 that resolves the fundamental incompatibility between Bazel's sandbox (symlinks) and WASI's cap-std security model (blocks symlinks outside preopened directories). **Solution:** - Created Go wrapper (tools/wasmsign2_wrapper) that resolves symlinks to real paths at execution time - Maps only necessary directories to WASI (not full filesystem access) - Eliminates all shell scripts from wasm_signing.bzl **Changes:** - tools/wasmsign2_wrapper/: New Go binary for path resolution and wasmtime execution - wasm/wasm_signing.bzl: Updated wasm_keygen, wasm_sign, wasm_verify to use wrapper - MODULE.bazel: Updated wasmsign2-cli.wasm to v0.2.7-rc.2 - .github/workflows/ci.yml: Enabled wasm_signing targets in CI (Linux + macOS) **Benefits:** ✅ Cross-platform (Windows, macOS, Linux) ✅ Maintains Bazel sandbox hermeticity ✅ Maintains WASI security (limited directory access) ✅ Zero shell scripts (pure Go + Bazel) ✅ Follows file-ops pattern Resolves the symlink issue discovered during wasmsign2 integration where Bazel sandbox symlinks pointing outside the sandbox directory were blocked by WASI's cap-std security model.
1 parent 4fb85cc commit 8aa961e

File tree

5 files changed

+310
-137
lines changed

5 files changed

+310
-137
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ jobs:
153153
//examples/js_component:simple_js_component \
154154
//examples/js_component:hello_js_component \
155155
//examples/js_component:calc_js_component \
156+
//examples/wasm_signing:compact_keys \
157+
//examples/wasm_signing:signed_component_embedded \
158+
//examples/wasm_signing:signed_raw_wasm \
159+
//examples/wasm_signing:verify_embedded \
156160
//rust/... \
157161
//go/... \
158162
//cpp/... \
@@ -247,6 +251,10 @@ jobs:
247251
//examples/js_component:simple_js_component \
248252
//examples/js_component:hello_js_component \
249253
//examples/js_component:calc_js_component \
254+
//examples/wasm_signing:compact_keys \
255+
//examples/wasm_signing:signed_component_embedded \
256+
//examples/wasm_signing:signed_raw_wasm \
257+
//examples/wasm_signing:verify_embedded \
250258
//rust/... \
251259
//go/... \
252260
//cpp/... \

MODULE.bazel

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,14 @@ http_file(
196196
downloaded_file_path = "file_ops_component.wasm",
197197
)
198198

199+
# wasmsign2 CLI WASM binary for component signing
200+
http_file(
201+
name = "wasmsign2_cli_wasm",
202+
url = "https://github.com/pulseengine/wasmsign2/releases/download/v0.2.7-rc.2/wasmsign2-cli.wasm",
203+
sha256 = "0a2ba6a55621d83980daa7f38e3770ba6b9342736971a0cebf613df08377cd34",
204+
downloaded_file_path = "wasmsign2.wasm",
205+
)
206+
199207
# WASM Tools Component toolchain for universal wasm-tools operations
200208
register_toolchains("//toolchains:wasm_tools_component_toolchain_local")
201209

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Cross-platform wrapper for wasmsign2 WASM component
2+
3+
This wrapper executes the pre-built wasmsign2.wasm component via wasmtime,
4+
resolving symlinks to real paths to work with both Bazel's sandbox and
5+
WASI's security model.
6+
"""
7+
8+
load("@rules_go//go:def.bzl", "go_binary")
9+
10+
package(default_visibility = ["//visibility:public"])
11+
12+
# Wrapper binary that executes wasmsign2.wasm with path resolution
13+
go_binary(
14+
name = "wasmsign2_wrapper",
15+
srcs = ["main.go"],
16+
pure = "on", # Pure Go for cross-platform compatibility
17+
data = [
18+
"@wasmsign2_cli_wasm//file",
19+
"@wasmtime_toolchain//:wasmtime",
20+
],
21+
deps = ["@rules_go//go/runfiles"],
22+
visibility = ["//visibility:public"],
23+
)
24+
25+
# Export for easy access in toolchains
26+
alias(
27+
name = "wasmsign2",
28+
actual = ":wasmsign2_wrapper",
29+
visibility = ["//visibility:public"],
30+
)

tools/wasmsign2_wrapper/main.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/bazelbuild/rules_go/go/runfiles"
12+
)
13+
14+
// Wrapper for wasmsign2 WASM component
15+
// Resolves symlinks to real paths and calls wasmtime with limited directory access
16+
func main() {
17+
if len(os.Args) < 2 {
18+
log.Fatal("Usage: wasmsign2_wrapper <command> [args...]")
19+
}
20+
21+
// Check for --bazel-marker-file flag (internal use only)
22+
var markerFile string
23+
filteredArgs := make([]string, 0, len(os.Args))
24+
for i, arg := range os.Args {
25+
if strings.HasPrefix(arg, "--bazel-marker-file=") {
26+
markerFile = strings.TrimPrefix(arg, "--bazel-marker-file=")
27+
} else if i > 0 { // Skip program name
28+
filteredArgs = append(filteredArgs, arg)
29+
}
30+
}
31+
32+
// Initialize Bazel runfiles
33+
r, err := runfiles.New()
34+
if err != nil {
35+
log.Fatalf("Failed to initialize runfiles: %v", err)
36+
}
37+
38+
// Locate wasmtime binary
39+
wasmtimeBinary, err := r.Rlocation("+wasmtime+wasmtime_toolchain/wasmtime")
40+
if err != nil {
41+
log.Fatalf("Failed to locate wasmtime: %v", err)
42+
}
43+
44+
if _, err := os.Stat(wasmtimeBinary); err != nil {
45+
log.Fatalf("Wasmtime binary not found at %s: %v", wasmtimeBinary, err)
46+
}
47+
48+
// Locate wasmsign2 WASM component
49+
wasmsign2Wasm, err := r.Rlocation("+_repo_rules+wasmsign2_cli_wasm/file/wasmsign2.wasm")
50+
if err != nil {
51+
log.Fatalf("Failed to locate wasmsign2.wasm: %v", err)
52+
}
53+
54+
if _, err := os.Stat(wasmsign2Wasm); err != nil {
55+
log.Fatalf("wasmsign2.wasm not found at %s: %v", wasmsign2Wasm, err)
56+
}
57+
58+
// Parse command
59+
command := filteredArgs[0]
60+
cmdArgs := filteredArgs[1:]
61+
62+
// Resolve all file paths in arguments to real paths
63+
resolvedArgs, dirs, err := resolvePathsInArgs(command, cmdArgs)
64+
if err != nil {
65+
log.Fatalf("Failed to resolve paths: %v", err)
66+
}
67+
68+
// Build wasmtime command with directory mappings
69+
wasmtimeArgs := []string{
70+
"run",
71+
"-S", "cli",
72+
"-S", "http",
73+
}
74+
75+
// Add unique directory mappings
76+
uniqueDirs := uniqueStrings(dirs)
77+
for _, dir := range uniqueDirs {
78+
wasmtimeArgs = append(wasmtimeArgs, "--dir", dir)
79+
}
80+
81+
// Add wasmsign2.wasm and command with resolved arguments
82+
wasmtimeArgs = append(wasmtimeArgs, wasmsign2Wasm, command)
83+
wasmtimeArgs = append(wasmtimeArgs, resolvedArgs...)
84+
85+
// Execute wasmtime
86+
cmd := exec.Command(wasmtimeBinary, wasmtimeArgs...)
87+
cmd.Stdout = os.Stdout
88+
cmd.Stderr = os.Stderr
89+
cmd.Stdin = os.Stdin
90+
91+
if err := cmd.Run(); err != nil {
92+
if exitErr, ok := err.(*exec.ExitError); ok {
93+
os.Exit(exitErr.ExitCode())
94+
}
95+
log.Fatalf("Failed to execute wasmtime: %v", err)
96+
}
97+
98+
// If marker file was requested, create it on success
99+
if markerFile != "" {
100+
if err := os.WriteFile(markerFile, []byte("Verification passed\n"), 0644); err != nil {
101+
log.Fatalf("Failed to write marker file: %v", err)
102+
}
103+
}
104+
}
105+
106+
// resolvePathsInArgs resolves file paths in command arguments
107+
// Returns resolved arguments and list of directories to map
108+
func resolvePathsInArgs(command string, args []string) ([]string, []string, error) {
109+
resolvedArgs := make([]string, 0, len(args))
110+
dirs := make([]string, 0)
111+
112+
// Track which flags expect file paths
113+
pathFlags := map[string]bool{
114+
"-i": true, "--input": true,
115+
"-o": true, "--output": true,
116+
"-k": true, "--secret-key": true,
117+
"-K": true, "--public-key": true,
118+
"-S": true, "--signature": true,
119+
"--public-key-name": true,
120+
"--secret-key-name": true,
121+
}
122+
123+
for i := 0; i < len(args); i++ {
124+
arg := args[i]
125+
126+
// Check if this is a flag that expects a path
127+
if pathFlags[arg] && i+1 < len(args) {
128+
// Next argument is a file path
129+
resolvedArgs = append(resolvedArgs, arg)
130+
i++
131+
path := args[i]
132+
133+
// Resolve to real path
134+
realPath, err := filepath.EvalSymlinks(path)
135+
if err != nil {
136+
// If symlink evaluation fails, try absolute path
137+
realPath, err = filepath.Abs(path)
138+
if err != nil {
139+
return nil, nil, fmt.Errorf("failed to resolve path %s: %w", path, err)
140+
}
141+
}
142+
143+
resolvedArgs = append(resolvedArgs, realPath)
144+
145+
// Add directory for mapping
146+
dir := filepath.Dir(realPath)
147+
dirs = append(dirs, dir)
148+
} else if strings.Contains(arg, "=") && (strings.HasPrefix(arg, "--public-key-name=") ||
149+
strings.HasPrefix(arg, "--secret-key-name=") ||
150+
strings.HasPrefix(arg, "-i=") || strings.HasPrefix(arg, "-o=") ||
151+
strings.HasPrefix(arg, "-k=") || strings.HasPrefix(arg, "-K=") ||
152+
strings.HasPrefix(arg, "-S=")) {
153+
// Handle --flag=value format
154+
parts := strings.SplitN(arg, "=", 2)
155+
if len(parts) == 2 {
156+
flag := parts[0]
157+
path := parts[1]
158+
159+
realPath, err := filepath.EvalSymlinks(path)
160+
if err != nil {
161+
realPath, err = filepath.Abs(path)
162+
if err != nil {
163+
return nil, nil, fmt.Errorf("failed to resolve path %s: %w", path, err)
164+
}
165+
}
166+
167+
resolvedArgs = append(resolvedArgs, flag+"="+realPath)
168+
169+
dir := filepath.Dir(realPath)
170+
dirs = append(dirs, dir)
171+
} else {
172+
resolvedArgs = append(resolvedArgs, arg)
173+
}
174+
} else {
175+
// Not a path argument, pass through as-is
176+
resolvedArgs = append(resolvedArgs, arg)
177+
}
178+
}
179+
180+
return resolvedArgs, dirs, nil
181+
}
182+
183+
// uniqueStrings returns unique strings from a slice
184+
func uniqueStrings(strs []string) []string {
185+
seen := make(map[string]bool)
186+
result := make([]string, 0, len(strs))
187+
188+
for _, s := range strs {
189+
if !seen[s] {
190+
seen[s] = true
191+
result = append(result, s)
192+
}
193+
}
194+
195+
return result
196+
}

0 commit comments

Comments
 (0)