mirror of
https://github.com/ZSeven-W/openpencil.git
synced 2026-09-28 08:02:55 +08:00
365 lines
13 KiB
Bash
Executable File
365 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Audit committed op-auth archives without modifying their bytes.
|
|
#
|
|
# Legacy ABI-v1 artifacts are integrity-pinned compatibility inputs and may
|
|
# still contain source/debug metadata. ABI-v2+ artifacts are production
|
|
# collaboration inputs: they must pass the hardened profile and signed
|
|
# provenance checks in addition to exposing only the documented C ABI.
|
|
|
|
set -euo pipefail
|
|
|
|
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
|
|
repo_root=$(CDPATH= cd "$script_dir/.." && pwd)
|
|
cd "$repo_root"
|
|
|
|
require_hardened=0
|
|
if [[ "${1:-}" == "--require-hardened" ]]; then
|
|
require_hardened=1
|
|
shift
|
|
fi
|
|
if [[ "$#" -ne 0 ]]; then
|
|
printf 'usage: %s [--require-hardened]\n' "$0" >&2
|
|
exit 2
|
|
fi
|
|
|
|
prebuilt_root=${OP_AUTH_PREBUILT_ROOT:-crates/op-auth-bridge/prebuilt}
|
|
trusted_public_key=$repo_root/crates/op-auth-bridge/prebuilt/PROVENANCE_PUBKEY
|
|
failures=()
|
|
artifact_count=0
|
|
signature_verification_error=
|
|
verification_dir=
|
|
|
|
record_failure() {
|
|
failures+=("$1")
|
|
}
|
|
|
|
has_exact_manifest_line() {
|
|
local expected=$1
|
|
local manifest=$2
|
|
local count
|
|
count=$(LC_ALL=C grep -Fxc -- "$expected" "$manifest" || true)
|
|
[[ "$count" -eq 1 ]]
|
|
}
|
|
|
|
sha256_file() {
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
sha256sum "$1" | awk '{ print $1 }'
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
shasum -a 256 "$1" | awk '{ print $1 }'
|
|
else
|
|
return 127
|
|
fi
|
|
}
|
|
|
|
prepare_verification_dir() {
|
|
if [[ -n "$verification_dir" ]]; then
|
|
return 0
|
|
fi
|
|
verification_dir=$(mktemp -d "${TMPDIR:-/tmp}/op-auth-prebuilt-verify.XXXXXX") \
|
|
|| {
|
|
signature_verification_error="could not create a provenance verification directory"
|
|
return 1
|
|
}
|
|
if ! chmod 700 "$verification_dir"; then
|
|
signature_verification_error="could not secure the provenance verification directory"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
verify_ed25519_hex_signature() {
|
|
local payload=$1
|
|
local signature_file=$2
|
|
local public_key_hex signature_hex
|
|
|
|
signature_verification_error=
|
|
if ! command -v openssl >/dev/null 2>&1; then
|
|
signature_verification_error="openssl is required for provenance verification"
|
|
return 1
|
|
fi
|
|
if ! command -v xxd >/dev/null 2>&1; then
|
|
signature_verification_error="xxd is required for provenance verification"
|
|
return 1
|
|
fi
|
|
|
|
public_key_hex=$(tr -d '[:space:]' < "$trusted_public_key")
|
|
signature_hex=$(tr -d '[:space:]' < "$signature_file")
|
|
if [[ ! "$public_key_hex" =~ ^[0-9a-f]{64}$ ]]; then
|
|
signature_verification_error="release provenance public key encoding is invalid"
|
|
return 1
|
|
fi
|
|
if [[ ! "$signature_hex" =~ ^[0-9a-f]{128}$ ]]; then
|
|
signature_verification_error="provenance signature encoding is invalid"
|
|
return 1
|
|
fi
|
|
|
|
prepare_verification_dir || return 1
|
|
if ! printf '302a300506032b6570032100%s' "$public_key_hex" \
|
|
| xxd -r -p > "$verification_dir/public.der"; then
|
|
signature_verification_error="release provenance public key decoding failed"
|
|
return 1
|
|
fi
|
|
if ! printf '%s' "$signature_hex" \
|
|
| xxd -r -p > "$verification_dir/signature.bin"; then
|
|
signature_verification_error="provenance signature decoding failed"
|
|
return 1
|
|
fi
|
|
if ! openssl pkey -pubin -inform DER \
|
|
-in "$verification_dir/public.der" \
|
|
-out "$verification_dir/public.pem" >/dev/null 2>&1; then
|
|
signature_verification_error="release provenance public key is invalid"
|
|
return 1
|
|
fi
|
|
if ! openssl pkeyutl -verify -rawin -pubin \
|
|
-inkey "$verification_dir/public.pem" \
|
|
-in "$payload" \
|
|
-sigfile "$verification_dir/signature.bin" >/dev/null 2>&1; then
|
|
signature_verification_error="provenance signature verification failed"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
archive_symbols() {
|
|
LC_ALL=C strings -a "$1" \
|
|
| LC_ALL=C sed 's/^_//' \
|
|
| LC_ALL=C grep -E '^op_auth_[A-Za-z0-9_]+$' \
|
|
| LC_ALL=C sort -u \
|
|
|| true
|
|
}
|
|
|
|
expected_symbols() {
|
|
printf '%s\n' \
|
|
op_auth_abi_version \
|
|
op_auth_cancel \
|
|
op_auth_login_begin \
|
|
op_auth_poll \
|
|
op_auth_restore \
|
|
op_auth_runtime_init \
|
|
op_auth_sign_out \
|
|
op_auth_string_free
|
|
if [[ "$1" -ge 2 ]]; then
|
|
printf '%s\n' \
|
|
op_auth_collab_ticket_begin \
|
|
op_auth_collab_ticket_cancel \
|
|
op_auth_collab_ticket_poll
|
|
fi
|
|
if [[ "$1" -ge 3 ]]; then
|
|
printf '%s\n' \
|
|
op_auth_collab_relay_token_begin
|
|
fi
|
|
}
|
|
|
|
metadata_leak_count() {
|
|
LC_ALL=C strings -a "$1" \
|
|
| LC_ALL=C grep -Ec \
|
|
'/Users/|/home/|/builds/|/workspace/|/private/tmp/|[A-Za-z]:[/\\]Users[/\\]|\.cargo[/\\]registry[/\\]src|/rustc/|op_auth_(core|native)[/\\]src[/\\]' \
|
|
|| true
|
|
}
|
|
|
|
debug_marker_count() {
|
|
LC_ALL=C strings -a "$1" \
|
|
| LC_ALL=C grep -Ec \
|
|
'^(\.debug_(abbrev|info|line|line_str|ranges|rnglists|str)|__debug_(abbrev|info|line|str)|\.debug\$[A-Z]|.*\.pdb)$' \
|
|
|| true
|
|
}
|
|
|
|
private_symbol_count() {
|
|
LC_ALL=C strings -a "$1" \
|
|
| LC_ALL=C grep -Ec \
|
|
'op_auth_core|op_auth_native|_ZN[0-9A-Za-z_$.]*op_auth|_R[0-9A-Za-z_$.]*op_auth' \
|
|
|| true
|
|
}
|
|
|
|
if [[ ! -d "$prebuilt_root" ]]; then
|
|
printf 'check-op-auth-prebuilt.sh: no committed prebuilt directory; stub build only.\n'
|
|
exit 0
|
|
fi
|
|
|
|
cleanup() {
|
|
if [[ -n "$verification_dir" ]]; then
|
|
rm -rf "$verification_dir"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 129' HUP
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
|
|
while IFS= read -r artifact; do
|
|
artifact_count=$((artifact_count + 1))
|
|
target_dir=$(dirname "$artifact")
|
|
target=$(basename "$target_dir")
|
|
artifact_name=$(basename "$artifact")
|
|
expected_name=libop_auth.a
|
|
if [[ "$target" == *-pc-windows-msvc ]]; then
|
|
expected_name=op_auth.lib
|
|
fi
|
|
if [[ "$artifact_name" != "$expected_name" ]]; then
|
|
record_failure "$target: artifact must be named $expected_name"
|
|
fi
|
|
|
|
checksum_path=$target_dir/SHA256
|
|
expected_sha256=
|
|
actual_sha256=
|
|
if [[ ! -f "$checksum_path" ]]; then
|
|
record_failure "$target: SHA256 is missing"
|
|
else
|
|
expected_sha256=$(tr -d '[:space:]' < "$checksum_path")
|
|
actual_sha256=$(sha256_file "$artifact") \
|
|
|| {
|
|
record_failure "$target: no SHA-256 implementation is available"
|
|
actual_sha256=
|
|
}
|
|
if [[ ! "$expected_sha256" =~ ^[0-9a-f]{64}$ ]]; then
|
|
record_failure "$target: SHA256 must be one lowercase hexadecimal digest"
|
|
elif [[ "$expected_sha256" != "$actual_sha256" ]]; then
|
|
record_failure "$target: artifact SHA-256 mismatch"
|
|
fi
|
|
fi
|
|
|
|
version_path=$target_dir/VERSION
|
|
artifact_version=
|
|
if [[ ! -s "$version_path" ]]; then
|
|
record_failure "$target: VERSION is missing"
|
|
else
|
|
artifact_version=$(tr -d '[:space:]' < "$version_path")
|
|
fi
|
|
|
|
abi_version=1
|
|
if [[ -f "$target_dir/ABI_VERSION" ]]; then
|
|
abi_version=$(tr -d '[:space:]' < "$target_dir/ABI_VERSION")
|
|
fi
|
|
if [[ ! "$abi_version" =~ ^[123]$ ]]; then
|
|
record_failure "$target: ABI_VERSION must be 1, 2, or 3"
|
|
abi_version=1
|
|
fi
|
|
|
|
actual_symbols=$(archive_symbols "$artifact")
|
|
required_symbols=$(expected_symbols "$abi_version" | LC_ALL=C sort -u)
|
|
if [[ "$actual_symbols" != "$required_symbols" ]]; then
|
|
missing_symbols=$(comm -23 \
|
|
<(printf '%s\n' "$required_symbols") \
|
|
<(printf '%s\n' "$actual_symbols"))
|
|
extra_symbols=$(comm -13 \
|
|
<(printf '%s\n' "$required_symbols") \
|
|
<(printf '%s\n' "$actual_symbols"))
|
|
[[ -z "$missing_symbols" ]] \
|
|
|| record_failure "$target: required C ABI symbols are missing: ${missing_symbols//$'\n'/, }"
|
|
[[ -z "$extra_symbols" ]] \
|
|
|| record_failure "$target: undocumented op_auth C ABI symbols are exposed: ${extra_symbols//$'\n'/, }"
|
|
fi
|
|
|
|
path_leaks=$(metadata_leak_count "$artifact")
|
|
debug_markers=$(debug_marker_count "$artifact")
|
|
private_symbols=$(private_symbol_count "$artifact")
|
|
hardened=$require_hardened
|
|
# Obfuscated profiles must additionally scrub private Rust symbol strings,
|
|
# source paths, and debug markers. The signed-unobfuscated profile is an
|
|
# explicit, signature-bound opt-out of those scrubs; signature presence and
|
|
# the C ABI surface are still enforced.
|
|
obfuscated=1
|
|
if [[ "$abi_version" -ge 2 ]]; then
|
|
hardened=1
|
|
provenance_path=$target_dir/PROVENANCE
|
|
signature_path=$target_dir/PROVENANCE.sig
|
|
signed_files_ready=1
|
|
for signed_file in "$provenance_path" "$signature_path"; do
|
|
if [[ -L "$signed_file" \
|
|
|| ! -f "$signed_file" \
|
|
|| ! -s "$signed_file" ]]; then
|
|
signed_name=$(basename "$signed_file")
|
|
record_failure "$target: signed ABI-v2+ $signed_name is missing"
|
|
signed_files_ready=0
|
|
fi
|
|
done
|
|
trusted_key_ready=1
|
|
if [[ -L "$trusted_public_key" \
|
|
|| ! -f "$trusted_public_key" \
|
|
|| ! -s "$trusted_public_key" ]]; then
|
|
record_failure "$target: release provenance public key is missing"
|
|
trusted_key_ready=0
|
|
fi
|
|
manifest_for_checks=
|
|
signature_for_checks=
|
|
if [[ "$signed_files_ready" -eq 1 ]]; then
|
|
signature_verification_error=
|
|
if prepare_verification_dir; then
|
|
manifest_for_checks=$verification_dir/$artifact_count.PROVENANCE
|
|
signature_for_checks=$verification_dir/$artifact_count.PROVENANCE.sig
|
|
if ! cp "$provenance_path" "$manifest_for_checks" \
|
|
|| ! cp "$signature_path" "$signature_for_checks"; then
|
|
record_failure "$target: signed provenance could not be snapshotted"
|
|
manifest_for_checks=
|
|
signature_for_checks=
|
|
fi
|
|
else
|
|
record_failure "$target: $signature_verification_error"
|
|
fi
|
|
fi
|
|
if [[ -n "$manifest_for_checks" && "$trusted_key_ready" -eq 1 ]] \
|
|
&& ! verify_ed25519_hex_signature \
|
|
"$manifest_for_checks" "$signature_for_checks"; then
|
|
record_failure "$target: $signature_verification_error"
|
|
fi
|
|
if [[ -n "$manifest_for_checks" ]]; then
|
|
for signed_field in \
|
|
'format=1' \
|
|
"target=$target" \
|
|
"artifact=$artifact_name" \
|
|
"version=$artifact_version" \
|
|
"abi=$abi_version" \
|
|
"sha256=$actual_sha256"; do
|
|
has_exact_manifest_line "$signed_field" "$manifest_for_checks" \
|
|
|| record_failure \
|
|
"$target: signed provenance does not match $signed_field"
|
|
done
|
|
hardened_profile_count=$(LC_ALL=C grep -Fxc -- \
|
|
'hardening=op-auth-hardened-v1' "$manifest_for_checks" || true)
|
|
unobfuscated_profile_count=$(LC_ALL=C grep -Fxc -- \
|
|
'hardening=op-auth-signed-unobfuscated-v1' \
|
|
"$manifest_for_checks" || true)
|
|
if [[ "$hardened_profile_count" -eq 1 \
|
|
&& "$unobfuscated_profile_count" -eq 0 ]]; then
|
|
obfuscated=1
|
|
elif [[ "$hardened_profile_count" -eq 0 \
|
|
&& "$unobfuscated_profile_count" -eq 1 ]]; then
|
|
obfuscated=0
|
|
else
|
|
record_failure "$target: signed hardening profile is missing or unrecognized"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [[ "$hardened" -eq 1 && "$obfuscated" -eq 1 ]]; then
|
|
[[ "$path_leaks" -eq 0 ]] \
|
|
|| record_failure "$target: archive leaks $path_leaks source/build path strings"
|
|
[[ "$debug_markers" -eq 0 ]] \
|
|
|| record_failure "$target: archive contains $debug_markers debug metadata markers"
|
|
[[ "$private_symbols" -eq 0 ]] \
|
|
|| record_failure "$target: archive exposes $private_symbols private Rust symbol/module strings"
|
|
elif [[ "$hardened" -eq 1 && "$obfuscated" -eq 0 ]]; then
|
|
printf \
|
|
'warning: %s is signed-unobfuscated (%s path strings, %s debug markers, %s private Rust symbol/module strings retained by design)\n' \
|
|
"$target" "$path_leaks" "$debug_markers" "$private_symbols" >&2
|
|
elif [[ "$path_leaks" -gt 0 || "$debug_markers" -gt 0 || "$private_symbols" -gt 0 ]]; then
|
|
printf \
|
|
'warning: %s is legacy ABI-v1 (%s path strings, %s debug markers, %s private Rust symbol/module strings); rebuild in private CI before production ABI-v2 use\n' \
|
|
"$target" "$path_leaks" "$debug_markers" "$private_symbols" >&2
|
|
fi
|
|
done < <(
|
|
find "$prebuilt_root" -mindepth 2 -maxdepth 2 \
|
|
-type f \( -name '*.a' -o -name '*.lib' \) \
|
|
-print | LC_ALL=C sort
|
|
)
|
|
|
|
if [[ "$artifact_count" -eq 0 && "$require_hardened" -eq 1 ]]; then
|
|
record_failure "no op-auth archive was supplied for hardened release validation"
|
|
fi
|
|
|
|
if [[ "${#failures[@]}" -ne 0 ]]; then
|
|
printf 'check-op-auth-prebuilt.sh: %s failure(s):\n' "${#failures[@]}" >&2
|
|
printf ' - %s\n' "${failures[@]}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'check-op-auth-prebuilt.sh: audited %s archive(s); no bytes were modified.\n' "$artifact_count"
|