Two halves of the same problem: the relay learned who collaborates with whom, and a guest could not safely join a stranger's session without an invite. Claim-minimized relay bearer. The relay authenticated each WSS connection with the full collaboration ticket, whose claims carry the account subject, device id, and optional display name and avatar. Now that collaboration is cross-account, that let a relay operator reconstruct a social graph. The relay reads exactly one field out of that ticket — the expiry it clamps the session deadline to — and its authorization output, (route, role, expiry), comes from the signed locator and the route capability, not from identity. The disclosure was gratuitous. A separate audience-scoped token now carries only issuer, audience, version, scope, the channel binding to the caller's X25519 key, and the time bounds. `VerifiedRelayTokenClaims` deliberately exposes no identity accessor at all, so the relay cannot regress into reading one. No route or role claim was added: route authorization already comes from the locator plus the capability secret, and putting route ids in the token would move the graph to the issuer, which also knows the account. Scope of the guarantee, stated in the code so nobody over-reads it: this defends against a third-party or regional relay operator. It does not defend against the first party, who runs both the issuer and the relay and can rejoin on the channel-binding key and the issuance time. It also de-identifies rather than making the view unlinkable — the device's X25519 static is persistent and in the clear in every hello, so the operator still builds a device graph, it just cannot name the nodes or join them to the account namespace. The two token types are domain-separated by both JWS `typ` and `aud`, strictly compared, with `deny_unknown_fields` on disjoint claim structs, so each is structurally invalid against the other's parser. That property is what makes sharing one signing key defensible, so it is tested in both directions. The relay dual-accepts during migration, discriminating on `typ` before claim parsing, behind an env flag. The client never retries a rejected minimized token with the full ticket — that would be a downgrade any curious relay could trigger at will. Guest owner confirmation. A guest joining over unpinned LAN discovery still required the same account, because it has no approval prompt of its own and mDNS names nobody. It now gets the explicit decision the owner already had: the verified owner identity is surfaced and confirmed before the peer is authorized, so nothing from the session — snapshot, presence, session name — exists before the user decides. With that gate in place the unpinned LAN path admits any issued account too. Display name and avatar are attacker-chosen, so the projection separates them from the account subject and device id at the type level, strips invisible and bidi-control characters, and labels them as claimed. A display name cannot occupy an authoritative row; the test uses another account's UUID as the display name to prove it. Both halves are pinned in the boundary gate: losing either asymmetry is silent, because the code still compiles and every other check still passes while nothing authenticates the peer.
756 lines
30 KiB
Bash
756 lines
30 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-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-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-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%/*}
|
|
while IFS= read -r relative_path; do
|
|
printf '%s/%s\n' "$source_dir" "$relative_path"
|
|
done < <(awk '
|
|
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
|
|
}
|
|
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-host-desktop/src/collab_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-host-desktop/src/collab_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-host-desktop/src/collab_runtime/types.rs \
|
|
"assert_not_impl_any!($command_type: Clone);" \
|
|
"renewal verification command must remain non-Clone"
|
|
done
|
|
require_literal crates/op-host-desktop/src/collab_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-host-desktop/src/collab_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-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."
|