Skip to content

Commit e0066c6

Browse files
committed
ergonomic builds, real instruments
1 parent 72684c8 commit e0066c6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1896
-45
lines changed

Makefile

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,43 @@
1-
.PHONY: build clean test
1+
.PHONY: build clean test notarize
2+
3+
NOTARY_KEYCHAIN_PROFILE ?=
4+
YOLO ?=
5+
BUILD_ARGS ?=
6+
7+
ifneq ($(strip $(YOLO)),)
8+
BUILD_ARGS += --yolo
9+
endif
210

311
build:
4-
./build.sh
12+
./build.sh $(BUILD_ARGS)
513

614
clean:
715
rm -rf tests/out/*
816

917
test:
1018
@echo "==> [test] run all suites"
1119
@./tests/run.sh --all
20+
21+
notarize:
22+
@if [ -z "$(NOTARY_KEYCHAIN_PROFILE)" ]; then \
23+
echo "ERROR: set NOTARY_KEYCHAIN_PROFILE to your notarytool keychain profile name"; \
24+
echo "example: make notarize NOTARY_KEYCHAIN_PROFILE=dev-profile"; \
25+
exit 2; \
26+
fi
27+
@if [ -z "$(IDENTITY)" ] && [ -z "$(YOLO)" ]; then \
28+
echo "ERROR: set IDENTITY or opt-in to auto selection with YOLO=1"; \
29+
echo "example: make notarize NOTARY_KEYCHAIN_PROFILE=dev-profile IDENTITY='Developer ID Application: ...'"; \
30+
echo "example: make notarize NOTARY_KEYCHAIN_PROFILE=dev-profile YOLO=1"; \
31+
exit 2; \
32+
fi
33+
@$(MAKE) build
34+
@echo "==> [notarize] submit PolicyWitness.zip"
35+
@xcrun notarytool submit "PolicyWitness.zip" --keychain-profile "$(NOTARY_KEYCHAIN_PROFILE)" --wait
36+
@echo "==> [notarize] staple PolicyWitness.app"
37+
@xcrun stapler staple "PolicyWitness.app"
38+
@echo "==> [notarize] validate PolicyWitness.app"
39+
@xcrun stapler validate -v "PolicyWitness.app"
40+
@spctl -a -vv --type execute "PolicyWitness.app"
41+
@echo "==> [notarize] re-zip stapled app"
42+
@rm -f "PolicyWitness.zip"
43+
@/usr/bin/ditto -c -k --sequesterRsrc --keepParent "PolicyWitness.app" "PolicyWitness.zip"

PolicyWitness.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Top-level fields:
6767
- `policy`: object
6868
- `probe_plan`: array of steps
6969
- `runner`: object (optional, for external runners)
70+
- `instrumentation`: object (optional, instrumentation port)
7071

7172
### Policy
7273

@@ -110,6 +111,83 @@ Example:
110111
}
111112
```
112113

114+
## Instrumentation (opt-in)
115+
116+
Instrumentation ports provide a user-friendly way to exercise the runner’s
117+
entitlement-backed capabilities. This field is optional; if omitted, behavior
118+
is unchanged. Results are reported under `instrumentation` in the run JSON and
119+
do not change the run outcome.
120+
Each port can specify an optional `phase`:
121+
122+
- `pre_sandbox` (default)
123+
- `post_sandbox`
124+
125+
Example specimen fragment:
126+
127+
```json
128+
"instrumentation": {
129+
"version": 1,
130+
"ports": [
131+
{ "kind": "debug_wait", "sleep_ms": 5000 },
132+
{ "kind": "dylib_load", "path": "/path/to/lib.dylib", "symbol": "pw_instrumentation_init" },
133+
{ "kind": "execmem_probe", "size_bytes": 4096 },
134+
{ "kind": "dyld_env", "keys": ["DYLD_INSERT_LIBRARIES"] }
135+
]
136+
}
137+
```
138+
139+
Notes:
140+
- `dylib_load` expects an optional no-arg symbol (C `void func(void)`).
141+
142+
Ports (v1):
143+
- `dylib_load`: load a dylib and optionally call a symbol (uses `com.apple.security.cs.disable-library-validation`).
144+
- `debug_wait`: sleep before sandbox apply for debugger attach (uses `com.apple.security.get-task-allow`).
145+
- `execmem_probe`: attempt an RWX `mmap` and report success/failure (uses `com.apple.security.cs.allow-unsigned-executable-memory`).
146+
- `dyld_env`: report expected `DYLD_*` env vars (uses `com.apple.security.cs.allow-dyld-environment-variables`).
147+
148+
Convenience flag (injects instrumentation into the request JSON at runtime):
149+
150+
```sh
151+
$PW run /path/to/request.json --instrumentation @/path/to/instrumentation.json
152+
```
153+
154+
Example specimen (full, minimal):
155+
156+
```json
157+
{
158+
"schema_version": 1,
159+
"specimen_id": "instrumentation_debug_wait",
160+
"policy": {
161+
"format": "sbpl",
162+
"sbpl_source": "(version 1) (allow default)"
163+
},
164+
"instrumentation": {
165+
"version": 1,
166+
"ports": [
167+
{ "kind": "debug_wait", "sleep_ms": 1 }
168+
]
169+
},
170+
"probe_plan": []
171+
}
172+
```
173+
174+
Explanation: this pauses briefly before sandbox apply; the run JSON includes an
175+
`instrumentation` report with the port status and `sleep_ms`, and the overall
176+
run outcome remains `ok`.
177+
178+
Guide (quick start):
179+
180+
1. Pick a port and add it to your specimen or create a small `instrumentation.json`.
181+
2. Run with `policy-witness run <request.json> --instrumentation @instrumentation.json`.
182+
3. Inspect `data.runner_result.instrumentation` in the output JSON for per-port status.
183+
184+
Note: `dyld_env` is a check only. To actually set `DYLD_*` variables, use an
185+
external runner and set launchd `EnvironmentVariables` at install time:
186+
187+
```sh
188+
$PW runner install --bundle /path/to/PWRunner.xpc --env DYLD_INSERT_LIBRARIES=/path/to/lib.dylib
189+
```
190+
113191
## External runner (BYOSig) flow
114192

115193
Use this when you need entitlements that are not in the built-in runner.
@@ -134,6 +212,7 @@ Notes:
134212
- Use `--allow-adhoc` for local ad-hoc signing.
135213
- Use `--scope system` if you want a system-wide service (requires admin).
136214
- Use `--skip-bootstrap` if you will run `launchctl` manually.
215+
- Use `--env KEY=VALUE` to set launchd `EnvironmentVariables` (for `DYLD_*`).
137216

138217
The install command writes a launchd plist, bootstraps the service, and records
139218
the runner in the local registry.
@@ -184,6 +263,7 @@ Registry location:
184263

185264
- `--timeout-ms <n>`: runner RPC timeout (default 240000)
186265
- `--log-last <dur>`: unified log lookback window for deny capture (default 10s)
266+
- `--instrumentation <json|@path>`: inject instrumentation ports into the request
187267

188268
## Troubleshooting
189269

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ A specimen run spins up a fresh `PWRunner.xpc` process. The runner begins unsand
1212

1313
Collection is made possible by executing each probe as a small, explicit attempt and recording its direct rc plus errno/kr. For each step, the runner also runs `sandbox_check` using the same operation and filter so you can compare the kernel’s prediction to the attempted outcome. When the policy uses deterministic side effects like `send-signal`, the runner installs a handler and records before/after signal counts so denials can be observed without relying on logs. The runner emits a single structured JSON report for the specimen—run metadata and per-step results—and exits immediately after replying. The result is a per-step record that favors witnessed facts over inferred explanations.
1414

15+
## Instrumentation Port (Opt-in)
16+
17+
PolicyWitness includes an optional instrumentation port that exposes the runner’s hardened‑runtime entitlements in a controlled, auditable way: specimens may include an `instrumentation` object with ports executed `pre_sandbox` or `post_sandbox`, and results are reported in the run JSON without changing the run outcome; for quick experimentation you can inject instrumentation at runtime with `policy-witness run <request.json> --instrumentation <json|@path>` and keep existing callers unchanged.
18+
19+
- `dyld_env`: report expected `DYLD_*` env vars (`com.apple.security.cs.allow-dyld-environment-variables`); to set these, use an external runner with `policy-witness runner install --env KEY=VALUE`.
20+
- `dylib_load`: load a dylib and optionally call a symbol (`com.apple.security.cs.disable-library-validation`).
21+
- `debug_wait`: pause before sandbox apply for debugger attach (`com.apple.security.get-task-allow`).
22+
- `execmem_probe`: attempt RWX `mmap` and report success/failure (`com.apple.security.cs.allow-unsigned-executable-memory`).
23+
1524
## Evidence Model
1625

1726
`Specimens → Runs → Steps → Evidence`

SIGNING.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@ Preferred entrypoint:
99
```sh
1010
make build
1111
# or:
12+
make build YOLO=1
13+
# or:
1214
IDENTITY='Developer ID Application: YOUR NAME (TEAMID)' ./build.sh
15+
# or:
16+
./build.sh --yolo
1317
```
1418

1519
Key requirements:
1620

17-
- `IDENTITY` must be set to a **Developer ID Application** identity present in your keychain.
21+
- `IDENTITY` must be set to a **Developer ID Application** identity present in your keychain, or
22+
pass `--yolo` / `YOLO=1` to auto-select the first matching identity.
1823
- Xcode Command Line Tools are required (`swiftc` is discovered via `xcrun`).
1924

2025
## What `build.sh` signs
@@ -38,5 +43,26 @@ These are derived from the **actual signed binaries on disk** (hashes and entitl
3843

3944
## Notarization (zip artifact)
4045

41-
The build also produces `PolicyWitness.zip` suitable for notarization submission. Notarytool invocation and stapling are intentionally not automated in this repo; keep those steps in your release checklist.
46+
The build produces `PolicyWitness.zip` suitable for notarization submission. The
47+
required order is: sign and zip, submit the zip to notarytool, then staple the
48+
app bundle. This is what Gatekeeper expects.
49+
50+
Preferred entrypoint:
4251

52+
```sh
53+
make notarize NOTARY_KEYCHAIN_PROFILE=dev-profile
54+
# or:
55+
NOTARY_KEYCHAIN_PROFILE=dev-profile make notarize
56+
# or (auto-select codesign identity):
57+
make notarize NOTARY_KEYCHAIN_PROFILE=dev-profile YOLO=1
58+
```
59+
60+
Manual equivalent:
61+
62+
```sh
63+
xcrun notarytool submit "PolicyWitness.zip" --keychain-profile "dev-profile" --wait
64+
xcrun stapler staple "PolicyWitness.app"
65+
xcrun stapler validate -v "PolicyWitness.app"
66+
spctl -a -vv --type execute "PolicyWitness.app"
67+
ditto -c -k --sequesterRsrc --keepParent "PolicyWitness.app" "PolicyWitness.zip"
68+
```

build.sh

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ set -euo pipefail
44
# Usage:
55
# ./build.sh
66
# IDENTITY='Developer ID Application: ...' ./build.sh
7+
# ./build.sh --yolo
78
# PW_INSPECTION=0 IDENTITY='Developer ID Application: ...' ./build.sh
89
#
910
# Produces:
@@ -35,6 +36,39 @@ SWIFT_MODULE_CACHE="${SWIFT_MODULE_CACHE:-.tmp/swift-module-cache}"
3536
SWIFT_OPT_LEVEL="${SWIFT_OPT_LEVEL:-}"
3637
SWIFT_DEBUG_FLAGS="${SWIFT_DEBUG_FLAGS:-}"
3738
PW_INSPECTION="${PW_INSPECTION:-1}"
39+
YOLO=0
40+
41+
usage() {
42+
cat <<'EOF'
43+
usage:
44+
./build.sh
45+
IDENTITY='Developer ID Application: ...' ./build.sh
46+
./build.sh --yolo
47+
PW_INSPECTION=0 IDENTITY='Developer ID Application: ...' ./build.sh
48+
49+
notes:
50+
- --yolo selects the first Developer ID Application identity from:
51+
security find-identity -v -p codesigning
52+
EOF
53+
}
54+
55+
while [[ $# -gt 0 ]]; do
56+
case "$1" in
57+
--yolo)
58+
YOLO=1
59+
shift 1
60+
;;
61+
-h|--help)
62+
usage
63+
exit 0
64+
;;
65+
*)
66+
echo "ERROR: unknown argument: $1" 1>&2
67+
usage 1>&2
68+
exit 2
69+
;;
70+
esac
71+
done
3872

3973
if [[ "${PW_INSPECTION}" == "1" ]]; then
4074
if [[ -z "${SWIFT_OPT_LEVEL}" ]]; then
@@ -56,16 +90,64 @@ fi
5690
IDENTITY="${IDENTITY:-}"
5791

5892
if [[ -z "${IDENTITY}" ]]; then
59-
cat <<'EOF' 1>&2
93+
if [[ "${YOLO}" == "1" ]]; then
94+
set +e
95+
IDENTITY="$(
96+
/usr/bin/python3 - <<'PY'
97+
import re
98+
import subprocess
99+
import sys
100+
101+
proc = subprocess.run(
102+
["/usr/bin/security", "find-identity", "-v", "-p", "codesigning"],
103+
capture_output=True,
104+
text=True,
105+
)
106+
if proc.returncode != 0:
107+
sys.exit(2)
108+
109+
for line in (proc.stdout or "").splitlines():
110+
if "Developer ID Application:" not in line:
111+
continue
112+
match = re.search(r'"(Developer ID Application: [^"]+)"', line)
113+
if match:
114+
print(match.group(1))
115+
sys.exit(0)
116+
117+
sys.exit(1)
118+
PY
119+
)"
120+
IDENTITY_STATUS=$?
121+
set -e
122+
123+
if [[ ${IDENTITY_STATUS} -ne 0 || -z "${IDENTITY}" ]]; then
124+
cat <<'EOF' 1>&2
125+
ERROR: --yolo could not find a Developer ID Application identity.
126+
127+
Run:
128+
security find-identity -v -p codesigning
129+
130+
Then set IDENTITY explicitly or install/unlock the identity in your keychain.
131+
EOF
132+
exit 2
133+
fi
134+
135+
echo "==> Using codesign identity (yolo): ${IDENTITY}"
136+
else
137+
cat <<'EOF' 1>&2
60138
ERROR: IDENTITY is not set.
61139
62140
Set it to your Developer ID Application identity string, for example:
63141
IDENTITY='Developer ID Application: Adam Hyland (42D369QV8E)' ./build.sh
64142
143+
Or re-run with --yolo to auto-select the first Developer ID Application identity:
144+
./build.sh --yolo
145+
65146
You can find valid identities via:
66147
security find-identity -v -p codesigning
67148
EOF
68-
exit 2
149+
exit 2
150+
fi
69151
fi
70152

71153
if ! /usr/bin/security find-identity -v -p codesigning 2>/dev/null | /usr/bin/grep -Fq "\"${IDENTITY}\""; then
@@ -297,8 +379,12 @@ echo " - ${SANDBOX_LOG_OBSERVER_BIN}"
297379
echo
298380
echo "Next (notarize with your saved profile):"
299381
cat <<EOF
382+
make notarize NOTARY_KEYCHAIN_PROFILE=dev-profile
383+
# add YOLO=1 to auto-select a codesign identity
384+
# or manually:
300385
xcrun notarytool submit "${ZIP_NAME}" --keychain-profile "dev-profile" --wait
301386
xcrun stapler staple "${APP_BUNDLE}"
302387
xcrun stapler validate -v "${APP_BUNDLE}"
303388
spctl -a -vv --type execute "${APP_BUNDLE}"
389+
/usr/bin/ditto -c -k --sequesterRsrc --keepParent "${APP_BUNDLE}" "${ZIP_NAME}"
304390
EOF

controller/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Standalone helper tools (embedded into the `.app`):
2424
The launcher intentionally exposes a minimal surface:
2525

2626
```text
27-
policy-witness run <request.json> [--timeout-ms <n>] [--log-last <dur>]
27+
policy-witness run <request.json> [--timeout-ms <n>] [--log-last <dur>] [--instrumentation <json|@path>]
2828
policy-witness runner <command> [options]
2929
```
3030

@@ -39,6 +39,7 @@ Runs a **single runner evaluation** against the selected runner service:
3939
- Captures supporting evidence (best-effort) using `sandbox-log-observer` and attaches it to the output.
4040
- Prints a single JSON envelope to stdout (no output directories; stdout is the artifact).
4141
- Emits `data.runner_provenance` and `data.app_provenance` to keep results auditable.
42+
- If `--instrumentation` is provided, the controller injects the instrumentation object into the request JSON (without modifying the original file).
4243

4344
Exit codes:
4445

@@ -79,6 +80,7 @@ These commands manage external runners signed with user entitlements:
7980
policy-witness runner install --bundle <path> [--service-name <name>] [--scope user|system]
8081
[--identity <codesign-id>] [--entitlements <plist>]
8182
[--executable <path>] [--bundle-id <id>] [--allow-adhoc]
83+
[--env KEY=VALUE]
8284
[--skip-bootstrap]
8385
policy-witness runner list
8486
policy-witness runner status --id <runner-id> | --service-name <name>

0 commit comments

Comments
 (0)