Add production deployment configs for the collab relay and locator: region-split compose (CN/global), direct nginx gateways and location maps, the CN docker-user firewall install/verify/validate tooling and systemd unit, and an SoftHSM-backed locator variant. Extend the collab security/deployment boundary checks to cover the new artifacts. The CN application host address is a placeholder (10.0.0.10); substitute the real private address at deploy time.
778 lines
32 KiB
Bash
778 lines
32 KiB
Bash
#!/usr/bin/env bash
|
|
# Enforces the public collaboration security and ownership boundaries.
|
|
#
|
|
# Run from any directory inside a normal checkout. The script intentionally
|
|
# uses only Bash, Cargo, and standard Unix text tools so the dedicated CI job
|
|
# does not depend on an unpinned scanner.
|
|
|
|
set -euo pipefail
|
|
|
|
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
|
|
repo_root=$(CDPATH= cd "$script_dir/.." && pwd)
|
|
cd "$repo_root"
|
|
|
|
failures=()
|
|
|
|
# Collaboration authorization and mutation gates intentionally live in ordinary
|
|
# editor, keyboard, save, widget, and locale modules. Scanning only paths whose
|
|
# names contain "collab" would miss those integration boundaries.
|
|
collab_scan_roots=(
|
|
crates/op-collab
|
|
crates/op-collab-transport
|
|
crates/op-collab-relay-protocol
|
|
crates/op-collab-relay-client
|
|
crates/op-collab-relay-server
|
|
crates/op-collab-relay-control-plane
|
|
crates/op-collab-policy-file
|
|
crates/op-collab-relay-locator-hsm
|
|
crates/op-collab-relay-locator-server
|
|
crates/op-collab-smoke
|
|
crates/op-auth-bridge
|
|
crates/op-util/src
|
|
crates/op-editor-core/src
|
|
crates/op-editor-host-core/src
|
|
crates/op-editor-ui/src
|
|
crates/op-collab-host/src
|
|
crates/op-host-native/src
|
|
crates/op-host-desktop/src
|
|
crates/op-host-services/src/profile_avatar_fetch.rs
|
|
crates/op-host-services/src/public_https_client.rs
|
|
crates/op-host-services/src/provider_dial.rs
|
|
crates/op-host-services/src/web_credentials.rs
|
|
crates/op-i18n/src
|
|
deploy/collab-relay
|
|
deploy/collab-relay-edge
|
|
deploy/collab-relay-locator
|
|
deploy/collab-relay-locator-hsm
|
|
deploy/collab-relay-locator-edge
|
|
)
|
|
|
|
collab_boundary_files() {
|
|
local root
|
|
for root in "${collab_scan_roots[@]}"; do
|
|
if [[ -d "$root" ]]; then
|
|
find "$root" -type f ! -path '*/prebuilt/*' -print
|
|
elif [[ -f "$root" ]]; then
|
|
printf '%s\n' "$root"
|
|
fi
|
|
done | LC_ALL=C sort -u
|
|
}
|
|
|
|
collab_rust_source_files() {
|
|
collab_boundary_files | grep -E '\.rs$' || true
|
|
}
|
|
|
|
# Rust permits a large test module to live in a sibling file:
|
|
#
|
|
# #[cfg(test)]
|
|
# #[path = "module_tests.rs"]
|
|
# mod tests;
|
|
#
|
|
# The sibling has no useful crate-level cfg of its own because the parent
|
|
# declaration is the compilation boundary. Resolve those declarations instead
|
|
# of treating every `src/*.rs` file as production source.
|
|
cfg_test_external_module_files() {
|
|
local source_file
|
|
local source_dir
|
|
local relative_path
|
|
while IFS= read -r source_file; do
|
|
source_dir=${source_file%/*}
|
|
base=${source_file##*/}
|
|
base=${base%.rs}
|
|
while IFS= read -r relative_path; do
|
|
printf '%s/%s\n' "$source_dir" "$relative_path"
|
|
done < <(awk -v parent_base="$base" '
|
|
function reset_attributes() {
|
|
cfg_test = 0
|
|
module_path = ""
|
|
}
|
|
|
|
/^[[:space:]]*#\[cfg\(test\)\][[:space:]]*$/ {
|
|
cfg_test = 1
|
|
next
|
|
}
|
|
|
|
cfg_test && /^[[:space:]]*#\[path[[:space:]]*=/ {
|
|
line = $0
|
|
sub(/^[^"]*"/, "", line)
|
|
sub(/".*$/, "", line)
|
|
module_path = line
|
|
next
|
|
}
|
|
|
|
cfg_test && /^[[:space:]]*(pub(\([^)]*\))?[[:space:]]+)?mod[[:space:]]+[A-Za-z_][A-Za-z0-9_]*[[:space:]]*;[[:space:]]*$/ {
|
|
if (module_path != "") {
|
|
print module_path
|
|
} else {
|
|
# No #[path]: a `#[cfg(test)] mod name;` resolves to its
|
|
# default file. A submodule declared in a non-root module
|
|
# file `foo.rs` lives under `foo/` (`foo/name.rs` or
|
|
# `foo/name/mod.rs`); only crate roots (lib/main) and
|
|
# directory roots (mod) keep it a same-directory sibling.
|
|
name = $0
|
|
sub(/^[[:space:]]*(pub(\([^)]*\))?[[:space:]]+)?mod[[:space:]]+/, "", name)
|
|
sub(/[[:space:]]*;.*$/, "", name)
|
|
prefix = ""
|
|
if (parent_base != "lib" && parent_base != "main" \
|
|
&& parent_base != "mod") {
|
|
prefix = parent_base "/"
|
|
}
|
|
print prefix name ".rs"
|
|
print prefix name "/mod.rs"
|
|
}
|
|
reset_attributes()
|
|
next
|
|
}
|
|
|
|
cfg_test && /^[[:space:]]*$/ {
|
|
next
|
|
}
|
|
|
|
cfg_test && /^[[:space:]]*#\[[^]]+\][[:space:]]*$/ {
|
|
next
|
|
}
|
|
|
|
{
|
|
reset_attributes()
|
|
}
|
|
' "$source_file")
|
|
done < <(collab_rust_source_files)
|
|
}
|
|
|
|
record_failure() {
|
|
failures+=("$1")
|
|
}
|
|
|
|
require_file() {
|
|
file=$1
|
|
if [[ ! -f "$file" ]]; then
|
|
record_failure "missing required file: $file"
|
|
fi
|
|
}
|
|
|
|
require_executable() {
|
|
file=$1
|
|
label=$2
|
|
if [[ ! -x "$file" ]]; then
|
|
record_failure "$label: expected executable file $file"
|
|
fi
|
|
}
|
|
|
|
require_literal() {
|
|
file=$1
|
|
literal=$2
|
|
label=$3
|
|
if [[ ! -f "$file" ]] || ! grep -Fq -- "$literal" "$file"; then
|
|
record_failure "$label: expected '$literal' in $file"
|
|
fi
|
|
}
|
|
|
|
require_literal_count() {
|
|
local file=$1
|
|
local literal=$2
|
|
local minimum=$3
|
|
local label=$4
|
|
local count=0
|
|
if [[ -f "$file" ]]; then
|
|
count=$(grep -Fc -- "$literal" "$file" || true)
|
|
fi
|
|
if [[ "$count" -lt "$minimum" ]]; then
|
|
record_failure "$label: expected '$literal' at least $minimum times in $file"
|
|
fi
|
|
}
|
|
|
|
require_cfg_test_literal() {
|
|
local literal=$1
|
|
local label=$2
|
|
local source_file
|
|
local cfg_test_external_sources
|
|
cfg_test_external_sources=$(cfg_test_external_module_files)
|
|
|
|
while IFS= read -r source_file; do
|
|
if ! grep -Fq -- "$literal" "$source_file"; then
|
|
continue
|
|
fi
|
|
|
|
if printf '%s\n' "$cfg_test_external_sources" \
|
|
| grep -Fxq -- "$source_file"; then
|
|
return
|
|
fi
|
|
|
|
if awk -v literal="$literal" '
|
|
/^[[:space:]]*#\[cfg\(test\)\][[:space:]]*$/ {
|
|
inside_test_boundary = 1
|
|
next
|
|
}
|
|
inside_test_boundary && index($0, literal) {
|
|
found = 1
|
|
}
|
|
END {
|
|
exit(found ? 0 : 1)
|
|
}
|
|
' "$source_file"; then
|
|
return
|
|
fi
|
|
done < <(collab_rust_source_files)
|
|
|
|
record_failure "$label: expected cfg(test) coverage containing '$literal'"
|
|
}
|
|
|
|
source "$script_dir/check-collab-deployment-boundaries.sh"
|
|
|
|
# The protocol core is allowed in browser/wasm dependency graphs. Native
|
|
# transport, authentication, key generation, and HTTP/TLS stacks are not.
|
|
wasm_tree=
|
|
if ! wasm_tree=$(cargo tree \
|
|
-p op-collab \
|
|
--target wasm32-unknown-unknown \
|
|
--no-default-features \
|
|
--edges normal,build \
|
|
--prefix none \
|
|
--locked 2>&1); then
|
|
record_failure "WASM dependency closure could not be resolved: $wasm_tree"
|
|
fi
|
|
|
|
forbidden_wasm_pattern='(^|[[:space:]])(op-collab-transport|op-auth-bridge|tokio|mio|socket2|snow|mdns-sd|x25519-dalek|getrandom|reqwest|ureq|native-tls|openssl|rustls|ring|libc)([[:space:]]|$)'
|
|
forbidden_wasm=$(printf '%s\n' "$wasm_tree" \
|
|
| grep -E "$forbidden_wasm_pattern" \
|
|
| LC_ALL=C sort -u || true)
|
|
if [[ -n "$forbidden_wasm" ]]; then
|
|
record_failure "WASM boundary includes native/auth dependencies:
|
|
$forbidden_wasm"
|
|
fi
|
|
|
|
# Collaboration crates are open MIT code. Future op-collab-* crates are picked
|
|
# up automatically instead of relying on a hand-maintained allowlist.
|
|
if ! grep -Eq '^[[:space:]]*license[[:space:]]*=[[:space:]]*"MIT"[[:space:]]*$' Cargo.toml; then
|
|
record_failure "workspace.package license must remain exactly MIT"
|
|
fi
|
|
|
|
found_collab=0
|
|
found_transport=0
|
|
found_auth=0
|
|
while IFS= read -r manifest; do
|
|
package_name=$(awk '
|
|
/^\[package\][[:space:]]*$/ { in_package = 1; next }
|
|
/^\[/ && in_package { exit }
|
|
in_package && /^[[:space:]]*name[[:space:]]*=/ {
|
|
line = $0
|
|
sub(/^[[:space:]]*name[[:space:]]*=[[:space:]]*"/, "", line)
|
|
sub(/".*$/, "", line)
|
|
print line
|
|
exit
|
|
}
|
|
' "$manifest")
|
|
case "$package_name" in
|
|
op-collab)
|
|
found_collab=1
|
|
;;
|
|
op-collab-*)
|
|
found_transport=1
|
|
;;
|
|
op-auth-bridge)
|
|
found_auth=1
|
|
;;
|
|
*)
|
|
continue
|
|
;;
|
|
esac
|
|
if ! grep -Eq '^[[:space:]]*license\.workspace[[:space:]]*=[[:space:]]*true[[:space:]]*$' \
|
|
"$manifest" \
|
|
&& ! grep -Eq '^[[:space:]]*license[[:space:]]*=[[:space:]]*"MIT"[[:space:]]*$' \
|
|
"$manifest"; then
|
|
record_failure "$package_name must inherit or declare the MIT license: $manifest"
|
|
fi
|
|
done < <(find crates -mindepth 2 -maxdepth 2 -name Cargo.toml -print | LC_ALL=C sort)
|
|
|
|
[[ "$found_collab" -eq 1 ]] \
|
|
|| record_failure "op-collab was not found by the collaboration license gate"
|
|
[[ "$found_transport" -eq 1 ]] \
|
|
|| record_failure "no op-collab-* transport/integration crate was found"
|
|
[[ "$found_auth" -eq 1 ]] \
|
|
|| record_failure "op-auth-bridge was not found by the collaboration license gate"
|
|
|
|
# Committed security artifacts remain inspectable client inputs, but silent
|
|
# corruption or substitution must fail both CI and the build-script check.
|
|
while IFS= read -r auth_artifact; do
|
|
checksum_file=$(dirname "$auth_artifact")/SHA256
|
|
if [[ ! -f "$checksum_file" ]]; then
|
|
record_failure "missing authentication artifact checksum: $checksum_file"
|
|
continue
|
|
fi
|
|
expected_checksum=$(tr -d '[:space:]' < "$checksum_file")
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
actual_checksum=$(sha256sum "$auth_artifact" | awk '{ print $1 }')
|
|
elif command -v shasum >/dev/null 2>&1; then
|
|
actual_checksum=$(shasum -a 256 "$auth_artifact" | awk '{ print $1 }')
|
|
else
|
|
record_failure "no SHA-256 tool available for authentication artifact verification"
|
|
break
|
|
fi
|
|
if [[ "$expected_checksum" != "$actual_checksum" ]]; then
|
|
record_failure "authentication artifact checksum mismatch: $auth_artifact"
|
|
fi
|
|
done < <(
|
|
find crates/op-auth-bridge/prebuilt \
|
|
-type f \( -name '*.a' -o -name '*.lib' \) \
|
|
-print 2>/dev/null | LC_ALL=C sort
|
|
)
|
|
|
|
# Required typed resource contracts. These anchors deliberately name public
|
|
# types and hard caps rather than attempting to infer semantics from numbers.
|
|
require_literal crates/op-collab/src/protocol.rs \
|
|
"pub struct WireLimits" "protocol typed limits"
|
|
require_literal crates/op-collab/src/apply_context.rs \
|
|
"pub struct ApplyLimits" "apply typed limits"
|
|
require_literal crates/op-collab/src/codec.rs \
|
|
"to_json_vec_with_limits" "bounded outbound protocol encoding"
|
|
require_literal crates/op-collab/src/codec.rs \
|
|
"from_json_slice_with_limits" "bounded inbound protocol decoding"
|
|
require_literal crates/op-collab/src/codec.rs \
|
|
"pub struct SensitiveFrameJson" "redacted zeroizing credential JSON wrapper"
|
|
require_literal crates/op-collab/src/codec.rs \
|
|
"serde_json::to_writer(&mut *encoded, &raw)" "direct credential JSON serializer"
|
|
require_literal crates/op-collab/src/codec.rs \
|
|
"serializer.serialize_str(self.0.expose())" \
|
|
"dedicated opaque-ticket serializer wrapper"
|
|
require_literal crates/op-collab/src/codec.rs \
|
|
"enum RawNonSensitiveMessage" "credential-free generic wire enum"
|
|
require_literal crates/op-collab/src/codec.rs \
|
|
"body: RawNonSensitiveMessage" "credential-free generic frame body"
|
|
require_literal crates/op-collab/src/ticket_json.rs \
|
|
"serde_json::value::RawValue" "borrowed inbound credential discriminator"
|
|
require_literal crates/op-collab/src/ticket_json.rs \
|
|
"opaque_ticket: &'a RawValue" "borrowed raw opaque-ticket payload"
|
|
require_literal crates/op-collab/src/ticket_json.rs \
|
|
"Zeroizing::new(String::with_capacity" "direct zeroizing ticket string decoder"
|
|
require_literal crates/op-collab/src/ticket_json.rs \
|
|
"OpaqueTicket::from_zeroizing(decoded)" "zeroizing opaque-ticket construction"
|
|
require_literal crates/op-collab/src/protocol.rs \
|
|
"opaque tickets require the dedicated renewal encoder" \
|
|
"fail-closed generic opaque-ticket serialization"
|
|
if grep -qF '#[derive(PartialEq, Serialize, Deserialize)]' \
|
|
crates/op-collab/src/protocol.rs; then
|
|
record_failure \
|
|
"CollabMessage must not use derived Deserialize because adjacent payloads can buffer credentials"
|
|
fi
|
|
ordinary_opaque_ticket_deserializer=$(grep -nF \
|
|
'String::deserialize' crates/op-collab/src/protocol.rs 2>/dev/null || true)
|
|
if [[ -n "$ordinary_opaque_ticket_deserializer" ]]; then
|
|
record_failure "OpaqueTicket must not deserialize through an ordinary String:
|
|
$ordinary_opaque_ticket_deserializer"
|
|
fi
|
|
ordinary_ticket_deserializer=$(grep -nE \
|
|
'String::deserialize|serde_json::from_value|serde_json::from_(str|slice)[[:space:]]*::<[[:space:]]*String' \
|
|
crates/op-collab/src/ticket_json.rs 2>/dev/null || true)
|
|
if [[ -n "$ordinary_ticket_deserializer" ]]; then
|
|
record_failure "dedicated ticket decoder must not materialize ordinary strings or Values:
|
|
$ordinary_ticket_deserializer"
|
|
fi
|
|
credential_probe_line=$(grep -nE \
|
|
'^[[:space:]]*declared_kind_rejecting_renew_ticket\(bytes\)\?;' \
|
|
crates/op-collab/src/codec.rs | head -1 | cut -d: -f1 || true)
|
|
generic_value_decode_line=$(grep -nF \
|
|
'let mut value = decode_json_value(bytes, limits)?;' \
|
|
crates/op-collab/src/codec.rs | head -1 | cut -d: -f1 || true)
|
|
if [[ -z "$credential_probe_line" || -z "$generic_value_decode_line" ]] \
|
|
|| [[ "$credential_probe_line" -ge "$generic_value_decode_line" ]]; then
|
|
record_failure \
|
|
"generic credential discriminator must run before JSON Value decoding"
|
|
fi
|
|
# The guest-to-owner envelope ceiling is selected from authenticated local
|
|
# connection direction before the discriminator or generic Value is parsed.
|
|
# An attacker-declared Snapshot kind must never select the 64 MiB owner budget.
|
|
inbound_direction_limit_line=$(grep -nE \
|
|
'^[[:space:]]*enforce_inbound_envelope_limit\(inbound_direction, bytes\.len\(\), limits\)\?;' \
|
|
crates/op-collab/src/codec.rs | head -1 | cut -d: -f1 || true)
|
|
if [[ -z "$inbound_direction_limit_line" || -z "$credential_probe_line" \
|
|
|| -z "$generic_value_decode_line" ]] \
|
|
|| [[ "$inbound_direction_limit_line" -ge "$credential_probe_line" ]] \
|
|
|| [[ "$inbound_direction_limit_line" -ge "$generic_value_decode_line" ]]; then
|
|
record_failure \
|
|
"trusted per-direction inbound envelope limit must run before discriminator and JSON Value decoding"
|
|
fi
|
|
require_literal crates/op-collab/src/frame_direction.rs \
|
|
"direction: InboundFrameDirection" \
|
|
"trusted inbound frame direction resource boundary"
|
|
require_literal crates/op-collab/src/error.rs \
|
|
"SensitiveCredentialRequiresDedicatedCodec" "dedicated credential codec failure"
|
|
require_literal crates/op-collab/tests/credential_ownership.rs \
|
|
"generic_raw_codecs_reject_credential_frames" "generic credential encoder rejection test"
|
|
for non_deserializable_type in OpaqueTicket RenewTicket CollabMessage; do
|
|
require_literal crates/op-collab/tests/credential_ownership.rs \
|
|
"assert_not_impl_any!($non_deserializable_type: serde::de::DeserializeOwned);" \
|
|
"credential-bearing protocol type must not implement generic Deserialize"
|
|
done
|
|
require_literal crates/op-collab/tests/credential_ownership.rs \
|
|
"direct_serde_renewal_serialization_is_fail_closed" \
|
|
"direct serde credential serialization rejection test"
|
|
require_literal crates/op-collab/tests/credential_ownership.rs \
|
|
"generic_decoder_rejects_renewal_before_payload_deserialization" \
|
|
"generic credential pre-deserialization rejection test"
|
|
require_literal crates/op-collab/tests/credential_ownership.rs \
|
|
"sensitive_discriminator_rejects_duplicate_message_fields" \
|
|
"credential discriminator duplicate-field regression test"
|
|
require_literal crates/op-collab/tests/credential_ownership.rs \
|
|
"dedicated_codec_round_trips_and_debug_redacts_the_secret" \
|
|
"dedicated credential codec roundtrip test"
|
|
for dedicated_ticket_test in \
|
|
dedicated_decoder_unescapes_directly_into_zeroizing_storage \
|
|
dedicated_decoder_rejects_malformed_escapes_and_surrogates \
|
|
dedicated_decoder_rejects_duplicate_and_unknown_ticket_fields \
|
|
dedicated_decoder_enforces_decoded_ticket_bounds; do
|
|
require_literal crates/op-collab/tests/credential_ownership.rs \
|
|
"$dedicated_ticket_test" "dedicated credential raw-decoder regression test"
|
|
done
|
|
require_literal crates/op-collab-transport/src/frame.rs \
|
|
"mislabeled_renewal_never_reaches_generic_payload_deserialization" \
|
|
"mislabeled credential transport regression test"
|
|
require_literal .github/workflows/collab-security.yml \
|
|
"cargo test --locked -p op-collab-transport frame::tests" \
|
|
"credential transport codec workflow test"
|
|
require_literal_count .github/workflows/collab-security.yml \
|
|
"cargo test --locked -p op-collab-transport" 3 \
|
|
"complete transport resource-limit workflow test"
|
|
require_literal crates/op-collab/tests/outbound_limits.rs \
|
|
"oversized_snapshot_kind_cannot_raise_the_owner_inbound_ceiling" \
|
|
"attacker-declared frame direction regression test"
|
|
require_literal crates/op-collab-transport/src/connection_limit_tests.rs \
|
|
"live_silent_guards_stay_charged_until_the_socket_worker_drops_them" \
|
|
"live socket pending-seat regression test"
|
|
require_literal crates/op-collab-transport/src/tcp.rs \
|
|
"silent_guarded_accept_exits_at_first_message_deadline_before_releasing_its_seat" \
|
|
"real first-message socket deadline regression test"
|
|
require_literal crates/op-collab-transport/src/chunk_tests.rs \
|
|
"completed_transfer_holds_the_declared_reservation_until_drop" \
|
|
"completed transfer aggregate-reservation regression test"
|
|
require_literal crates/op-collab-host/src/runtime/relay_bootstrap_tests.rs \
|
|
"payload_rejects_exact_cross_region_key_reuse" \
|
|
"cross-region exact key-reuse regression test"
|
|
# Cross-account collaboration replaced the subject-equality check with an
|
|
# explicit policy, and the two sides get different answers because they do not
|
|
# have the same ability to tell who the peer is. The owner admits any issued
|
|
# account because a human approves each guest against the verified identity; a
|
|
# guest admits one only when the owner's key was pinned out of band. Losing
|
|
# that asymmetry — by having the account check relax wherever nothing else
|
|
# authenticates the peer — is silent, so pin both halves here.
|
|
require_literal crates/op-collab-transport/src/admission.rs \
|
|
"pub enum PeerIdentityPolicy" \
|
|
"explicit peer account admission policy"
|
|
require_literal crates/op-collab-transport/src/admission_tests.rs \
|
|
"any_issued_account_admits_a_foreign_subject_but_keeps_every_other_check" \
|
|
"cross-account admission keeps issuer, expiry, and key binding"
|
|
require_literal crates/op-collab-host/src/runtime/network/owner.rs \
|
|
"PeerIdentityPolicy::AnyIssuedAccount" \
|
|
"owner admits any issued account behind its approval gate"
|
|
# The guest half of that asymmetry. A guest has no approval prompt of its own,
|
|
# so it may accept a foreign account only when the owner's key was pinned out
|
|
# of band or the user confirmed the verified identity. Both regression guards
|
|
# are pinned: dropping either one is silent, because the code still compiles
|
|
# and every other check still passes while nothing authenticates the peer.
|
|
require_cfg_test_literal \
|
|
"an_unpinned_join_without_confirmation_still_requires_this_account" \
|
|
"guest refuses a foreign account with neither a pin nor a confirmation"
|
|
require_cfg_test_literal \
|
|
"an_unpinned_join_admits_a_foreign_account_only_behind_the_confirmation_gate" \
|
|
"guest cross-account admission stays behind the confirmation gate"
|
|
require_literal crates/op-auth-bridge/src/collab_relay_token.rs \
|
|
"VerifiedRelayTokenClaims" \
|
|
"claim-minimized relay bearer"
|
|
for non_clone_type in OpaqueTicket RenewTicket CollabMessage FrameEnvelope; do
|
|
require_literal crates/op-collab/tests/credential_ownership.rs \
|
|
"assert_not_impl_any!($non_clone_type: Clone);" \
|
|
"credential-bearing protocol type must remain non-Clone"
|
|
done
|
|
|
|
for limit in \
|
|
MAX_ENVELOPE_BYTES \
|
|
MAX_TXN_BYTES \
|
|
MAX_OPS_PER_TXN \
|
|
MAX_DOCUMENT_NODES \
|
|
MAX_TREE_DEPTH \
|
|
MAX_IDENTIFIER_BYTES \
|
|
MAX_OPAQUE_TICKET_BYTES \
|
|
MAX_VALIDATION_NODE_VISITS_PER_TXN; do
|
|
require_literal crates/op-collab/src/protocol.rs "$limit" "protocol hard limit"
|
|
done
|
|
|
|
for typed_config in \
|
|
"pub struct TimeoutConfig" \
|
|
"pub struct ConnectionLimits" \
|
|
"pub struct RateLimitConfig" \
|
|
"pub struct TransportConfig" \
|
|
"pub fn validate(self) -> Result<Self, ConfigError>"; do
|
|
require_literal crates/op-collab-transport/src/config.rs \
|
|
"$typed_config" "transport typed configuration"
|
|
done
|
|
|
|
for transfer_limit in \
|
|
MAX_CONTROL_TRANSFER_BYTES \
|
|
MAX_TICKET_BYTES \
|
|
MAX_TXN_TRANSFER_BYTES \
|
|
MAX_SNAPSHOT_TRANSFER_BYTES; do
|
|
require_literal crates/op-collab-transport/src/config.rs \
|
|
"$transfer_limit" "transport hard limit"
|
|
done
|
|
|
|
for queue_type in \
|
|
"pub(crate) struct QueueItem" \
|
|
"pub(crate) struct BoundedTransferQueue" \
|
|
"pub struct SharedQueueBudget" \
|
|
"pub struct TokenBucket"; do
|
|
require_literal crates/op-collab-transport/src/queue.rs \
|
|
"$queue_type" "bounded queue/rate type"
|
|
done
|
|
require_literal crates/op-collab-transport/src/queue.rs \
|
|
"pub(crate) fn sensitive_ticket_frame" "unique sensitive Ticket frame queue path"
|
|
require_literal crates/op-collab-transport/src/queue.rs \
|
|
"pub(crate) fn sensitive_admission" "unique sensitive admission queue path"
|
|
require_literal_count crates/op-collab-transport/src/queue.rs \
|
|
"if class == TransferClass::Ticket" 2 \
|
|
"ordinary queue constructors reject Ticket storage"
|
|
|
|
require_literal crates/op-auth-bridge/src/collab_jwks_cache.rs \
|
|
"pub struct CollabJwksCacheLimits" "JWKS cache typed limits"
|
|
require_literal crates/op-auth-bridge/src/collab_ticket.rs \
|
|
"MAX_COLLAB_TICKET_BYTES" "opaque ticket hard limit"
|
|
require_literal crates/op-auth-bridge/build.rs \
|
|
"prebuilt_provenance::validate_prebuilt" "authentication artifact integrity gate"
|
|
require_literal crates/op-auth-bridge/prebuilt_provenance.rs \
|
|
"Sha256::digest" "authentication artifact SHA-256 verification"
|
|
require_literal crates/op-auth-bridge/prebuilt_provenance.rs \
|
|
"verify_strict" "authentication artifact signature verification"
|
|
require_literal crates/op-auth-bridge/prebuilt_provenance.rs \
|
|
"HARDENING_PROFILE_V1" "authentication artifact hardening profile"
|
|
require_literal .github/workflows/collab-security.yml \
|
|
"cargo test --locked -p op-auth-bridge --test prebuilt_provenance" \
|
|
"committed authentication matrix test"
|
|
require_literal crates/op-collab/tests/outbound_limits.rs \
|
|
"presence_payload_limit_applies_to_encode_and_decode" \
|
|
"outbound/inbound limit regression test"
|
|
require_literal crates/op-collab-transport/src/config.rs \
|
|
"invalid_resource_limits_fail_closed" \
|
|
"transport invalid-limit regression test"
|
|
|
|
for command_type in OwnerNetworkCommand GuestNetworkCommand PeerNetworkCommand; do
|
|
require_literal crates/op-collab-host/src/runtime/types.rs \
|
|
"assert_not_impl_any!($command_type: Clone);" \
|
|
"renewal verification command must remain non-Clone"
|
|
done
|
|
require_literal crates/op-collab-host/src/runtime/types.rs \
|
|
"verification_commands_move_the_original_ticket_allocation" \
|
|
"renewal command ownership regression test"
|
|
credential_vec_copies=$(grep -RInF \
|
|
'.expose().as_bytes().to_vec()' \
|
|
crates/op-collab-host/src/runtime 2>/dev/null || true)
|
|
if [[ -n "$credential_vec_copies" ]]; then
|
|
record_failure "desktop renewal commands must move OpaqueTicket instead of copying into Vec:
|
|
$credential_vec_copies"
|
|
fi
|
|
|
|
# Verified profile avatars cross an untrusted network boundary. Every redirect
|
|
# is re-resolved and pinned without proxies; both encoded and decoded sizes are
|
|
# independently capped before UI decode.
|
|
for avatar_anchor in \
|
|
"MAX_REDIRECTS" \
|
|
"MAX_AVATAR_ENCODED_BYTES" \
|
|
"public_https_client" \
|
|
"REQUEST_TIMEOUT"; do
|
|
require_literal crates/op-host-services/src/profile_avatar_fetch.rs \
|
|
"$avatar_anchor" "bounded collaboration avatar fetch"
|
|
done
|
|
for desktop_avatar_anchor in \
|
|
"request.is_current_account()" \
|
|
"fetch_account_avatar_blocking(request.url())" \
|
|
"fetch_profile_avatar_blocking(request.url())"; do
|
|
require_literal crates/op-host-desktop/src/collab_avatar_host.rs \
|
|
"$desktop_avatar_anchor" "desktop avatar security-policy delegation"
|
|
done
|
|
require_literal crates/op-host-services/src/provider_dial.rs \
|
|
".no_proxy()" "public HTTPS proxy bypass prevention"
|
|
require_literal crates/op-host-services/src/provider_dial.rs \
|
|
".resolve_to_addrs" "public HTTPS DNS pinning"
|
|
require_literal crates/op-editor-ui/src/collab_avatar_runtime.rs \
|
|
"MAX_AVATAR_SOURCE_PIXELS" "decoded avatar pixel limit"
|
|
|
|
# Public boundary failures must use domain errors, not caller-visible strings.
|
|
untyped_errors=$(grep -RInE \
|
|
--include='*.rs' \
|
|
'Result<[^>]*,[[:space:]]*(String|&[[:space:]]*str)[[:space:]]*>' \
|
|
crates/op-collab/src \
|
|
crates/op-collab-transport/src \
|
|
crates/op-collab-relay-protocol/src \
|
|
crates/op-collab-relay-client/src \
|
|
crates/op-collab-relay-server/src \
|
|
crates/op-collab-relay-control-plane/src \
|
|
crates/op-collab-policy-file/src \
|
|
crates/op-collab-relay-locator-hsm/src \
|
|
crates/op-collab-relay-locator-server/src \
|
|
crates/op-auth-bridge/src 2>/dev/null || true)
|
|
if [[ -n "$untyped_errors" ]]; then
|
|
record_failure "untyped Result<_, String/&str> at collaboration boundaries:
|
|
$untyped_errors"
|
|
fi
|
|
|
|
# Test signing material is permitted only in the explicitly gated issuer or
|
|
# behind an outer/inner cfg(test) boundary. Integration fixtures must opt into
|
|
# the test-issuer feature at the crate root.
|
|
require_literal crates/op-auth-bridge/Cargo.toml \
|
|
"test-issuer = []" "test issuer feature"
|
|
if ! awk '
|
|
previous == "#[cfg(any(test, feature = \"test-issuer\"))]" \
|
|
&& $0 == "mod collab_test_issuer;" { found = 1 }
|
|
{ previous = $0 }
|
|
END { exit(found ? 0 : 1) }
|
|
' crates/op-auth-bridge/src/lib.rs; then
|
|
record_failure "collab_test_issuer module must remain behind test/test-issuer cfg"
|
|
fi
|
|
require_literal crates/op-auth-bridge/src/collab_test_issuer.rs \
|
|
"public test material" "public fixture warning"
|
|
require_literal crates/op-auth-bridge/src/collab_test_issuer.rs \
|
|
"https://collab.test.invalid" "non-production fixture issuer"
|
|
if grep -Fq "https://sso.zseven.cn" crates/op-auth-bridge/src/collab_test_issuer.rs; then
|
|
record_failure "test issuer fixture must not contain the production issuer"
|
|
fi
|
|
# Production trust isolation is deliberately split across the signed-policy
|
|
# parser and verifier. One regression freezes the production root fixture; the
|
|
# other proves the production path fails closed instead of accepting the raw
|
|
# public test issuer JWKS.
|
|
require_cfg_test_literal \
|
|
"verifies_the_frozen_go_production_root_fixture" \
|
|
"production trust-root fixture regression test"
|
|
require_cfg_test_literal \
|
|
"production_signed_policy_path_never_falls_back_to_raw_jwks" \
|
|
"production/test issuer isolation regression test"
|
|
|
|
if [[ -f crates/op-auth-bridge/tests/collab_verifier.rs ]] \
|
|
&& ! sed -n '1,5p' crates/op-auth-bridge/tests/collab_verifier.rs \
|
|
| grep -Fq '#![cfg(feature = "test-issuer")]'; then
|
|
record_failure "auth integration fixtures must require feature = \"test-issuer\""
|
|
fi
|
|
|
|
production_fixture_hits=
|
|
cfg_test_external_sources=$(cfg_test_external_module_files)
|
|
while IFS= read -r source_file; do
|
|
case "$source_file" in
|
|
*/tests/*|*/collab_test_issuer.rs)
|
|
continue
|
|
;;
|
|
esac
|
|
if printf '%s\n' "$cfg_test_external_sources" \
|
|
| grep -Fxq -- "$source_file"; then
|
|
continue
|
|
fi
|
|
hits=$(awk '
|
|
/^[[:space:]]*#!\[cfg\(test\)\][[:space:]]*$/ { exit }
|
|
|
|
function brace_delta(line, opens, closes, copy) {
|
|
copy = line
|
|
opens = gsub(/\{/, "{", copy)
|
|
copy = line
|
|
closes = gsub(/\}/, "}", copy)
|
|
return opens - closes
|
|
}
|
|
|
|
test_module_depth > 0 {
|
|
test_module_depth += brace_delta($0)
|
|
next
|
|
}
|
|
|
|
/^[[:space:]]*#\[cfg\(test\)\][[:space:]]*$/ {
|
|
pending_test_module = 1
|
|
next
|
|
}
|
|
|
|
pending_test_module &&
|
|
/^[[:space:]]*#\[[^]]+\][[:space:]]*$/ {
|
|
next
|
|
}
|
|
|
|
pending_test_module &&
|
|
/^[[:space:]]*mod[[:space:]]+[A-Za-z_][A-Za-z0-9_]*[[:space:]]*\{/ {
|
|
test_module_depth = brace_delta($0)
|
|
pending_test_module = 0
|
|
next
|
|
}
|
|
|
|
pending_test_module &&
|
|
/^[[:space:]]*mod[[:space:]]+[A-Za-z_][A-Za-z0-9_]*[[:space:]]*;[[:space:]]*$/ {
|
|
pending_test_module = 0
|
|
next
|
|
}
|
|
|
|
{
|
|
pending_test_module = 0
|
|
print FNR ":" $0
|
|
}
|
|
' "$source_file" \
|
|
| grep -E 'SigningKey::from_bytes|[A-Z][A-Z0-9_]*_SEED[[:space:]]*:' \
|
|
|| true)
|
|
if [[ -n "$hits" ]]; then
|
|
production_fixture_hits+="$source_file:
|
|
$hits
|
|
"
|
|
fi
|
|
done < <(collab_rust_source_files)
|
|
if [[ -n "$production_fixture_hits" ]]; then
|
|
record_failure "deterministic signing/key seed leaked into production source:
|
|
$production_fixture_hits"
|
|
fi
|
|
|
|
# High-signal committed-secret patterns. This is intentionally conservative;
|
|
# comprehensive secret scanning remains a repository-host responsibility.
|
|
sensitive_files=$(collab_boundary_files \
|
|
| grep -Ei '\.(pem|key|p12|pfx|jwt|token)$|(^|/)(relay-x25519-keys[^/]*|[^/]*private[-_]keys?[^/]*|locator-signing-key[^/]*)\.json$' \
|
|
|| true)
|
|
repository_private_json_files=$(find . \
|
|
\( -path './.git' -o -path './target' -o -path '*/node_modules' \) \
|
|
-prune -o -type f -print \
|
|
| sed 's#^\./##' \
|
|
| grep -Ei '(^|/)(relay-x25519-keys[^/]*|[^/]*private[-_]keys?[^/]*|locator-signing-key[^/]*)\.json$' \
|
|
|| true)
|
|
if [[ -n "$repository_private_json_files" ]]; then
|
|
if [[ -n "$sensitive_files" ]]; then
|
|
sensitive_files+=$'\n'
|
|
fi
|
|
sensitive_files+="$repository_private_json_files"
|
|
sensitive_files=$(printf '%s\n' "$sensitive_files" | LC_ALL=C sort -u)
|
|
fi
|
|
if [[ -n "$sensitive_files" ]]; then
|
|
record_failure "sensitive key/token-shaped files are forbidden:
|
|
$sensitive_files"
|
|
fi
|
|
|
|
high_signal_pattern="-----BEGIN[[:space:]]+([A-Z0-9]+[[:space:]]+)?PRIVATE[[:space:]]+KEY-----|(^|[^A-Za-z0-9])(AKIA|ASIA)[A-Z0-9]{16}([^A-Za-z0-9]|$)|(^|[^A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{30,}([^A-Za-z0-9]|$)|(^|[^A-Za-z0-9])xox[baprs]-[A-Za-z0-9-]{20,}([^A-Za-z0-9]|$)|(^|[^A-Za-z0-9])sk-(proj-)?[A-Za-z0-9_-]{20,}([^A-Za-z0-9]|$)|[\"'][A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{32,}[\"']"
|
|
high_signal_hits=
|
|
while IFS= read -r scan_file; do
|
|
hits=$(grep -InE -- "$high_signal_pattern" "$scan_file" || true)
|
|
if [[ -n "$hits" ]]; then
|
|
high_signal_hits+="$scan_file:
|
|
$hits
|
|
"
|
|
fi
|
|
done < <(collab_boundary_files)
|
|
if [[ -n "$high_signal_hits" ]]; then
|
|
record_failure "high-signal credential/private-key material detected:
|
|
$high_signal_hits"
|
|
fi
|
|
|
|
# Keep security-sensitive modules reviewable under the repository-wide cap.
|
|
while IFS= read -r source_file; do
|
|
line_count=$(wc -l < "$source_file")
|
|
line_count=${line_count//[[:space:]]/}
|
|
if [[ "$line_count" -gt 800 ]]; then
|
|
record_failure "$source_file has $line_count lines; maximum is 800"
|
|
fi
|
|
done < <(collab_rust_source_files)
|
|
|
|
if [[ "${#failures[@]}" -ne 0 ]]; then
|
|
printf 'check-collab-security-boundaries.sh: FAILED\n' >&2
|
|
for failure in "${failures[@]}"; do
|
|
printf ' - %s\n' "$failure" >&2
|
|
done
|
|
exit 1
|
|
fi
|
|
|
|
printf '%s\n' \
|
|
"check-collab-security-boundaries.sh: all collaboration security boundaries pass."
|