build(collab): enable local ABI v2 auth debugging

This commit is contained in:
Kayshen-X 2026-07-29 16:46:03 +08:00
parent d17c26ab39
commit da2c90fe9e
7 changed files with 184 additions and 22 deletions

View file

@ -16,6 +16,10 @@ default = []
# Deterministic, non-production signing keys for downstream integration tests.
# Production verifier configuration never trusts this fixture issuer.
test-issuer = []
# Explicit opt-in for an unsigned, external op-auth archive in local debug
# builds. `build.rs` still requires the absolute archive path and ABI version,
# and rejects this feature outside Cargo's debug profile.
dev-abi-v2 = []
[dependencies]
base64 = "0.22"

View file

@ -35,6 +35,29 @@ decryptor and key with the application adds obscurity, not a security boundary.
Production trust never depends on client artifact secrecy: signing keys and
ticket issuance remain server-side.
## Local ABI-v2 development
Developers can exercise the collaboration UI against a private ABI-v2 archive
without replacing the committed ABI-v1 compatibility artifact:
```sh
OPENPENCIL_DEV_OP_AUTH_ARCHIVE=/absolute/path/to/libop_auth.a \
OPENPENCIL_DEV_OP_AUTH_ABI_VERSION=2 \
cargo build -p op-host-desktop --features dev-op-auth-abi-v2
```
Using the override requires the feature and both variables together; enabling
the feature without either variable is a no-op so workspace `--all-features`
checks keep using the committed artifact. The archive path must be absolute,
must select a regular non-symlink file using the artifact name expected by the
current target, and is watched for changes by Cargo. The build script copies it
into Cargo's private build-output directory before linking. This override is
accepted only in Cargo's debug profile when target debug assertions are
enabled; release, release-derived, and hardened profiles reject it. It
deliberately skips release provenance only for a local, non-shipping binary.
The runtime ABI handshake and required collaboration symbols still fail
closed.
## Regional login and collaboration trust
The credential-bearing login/ticket origin and the public collaboration trust

View file

@ -3,19 +3,27 @@
//! UI stays hidden. Open-source checkouts therefore always build.
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::PathBuf;
#[path = "prebuilt_provenance.rs"]
mod prebuilt_provenance;
const DEV_ARCHIVE_ENV: &str = "OPENPENCIL_DEV_OP_AUTH_ARCHIVE";
const DEV_ABI_VERSION_ENV: &str = "OPENPENCIL_DEV_OP_AUTH_ABI_VERSION";
const DEV_FEATURE_ENV: &str = "CARGO_FEATURE_DEV_ABI_V2";
fn main() {
println!("cargo:rustc-check-cfg=cfg(op_auth_prebuilt)");
println!("cargo:rustc-check-cfg=cfg(op_auth_collab_ticket_prebuilt)");
println!("cargo:rustc-check-cfg=cfg(op_auth_development_prebuilt)");
println!("cargo:rerun-if-changed=prebuilt");
println!("cargo:rerun-if-env-changed={DEV_ARCHIVE_ENV}");
println!("cargo:rerun-if-env-changed={DEV_ABI_VERSION_ENV}");
let target = env::var("TARGET").unwrap_or_default();
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let prebuilt_dir = manifest_dir.join("prebuilt").join(&target);
// MSVC static libraries follow the `<name>.lib` convention; every
// other target uses the Unix `lib<name>.a` archive name.
let artifact = if target.ends_with("-pc-windows-msvc") {
@ -23,28 +31,49 @@ fn main() {
} else {
"libop_auth.a"
};
let artifact_path = prebuilt_dir.join(artifact);
if !artifact_path.is_file() {
return;
}
let validated = match prebuilt_provenance::validate_prebuilt(
&manifest_dir.join("prebuilt"),
&prebuilt_dir,
&target,
artifact,
&env::var("CARGO_PKG_VERSION").unwrap_or_default(),
) {
Ok(validated) => validated,
Err(error) => {
println!("cargo:warning=ignoring op-auth prebuilt: {error}");
return;
}
};
let abi_version = validated.abi_version;
let development = development_prebuilt(artifact);
let (prebuilt_dir, abi_version, development_override, signed_provenance) =
if let Some((directory, abi_version)) = development {
println!(
"cargo:warning=using unsigned local op-auth ABI {abi_version} \
archive for a debug build"
);
println!("cargo:rustc-cfg=op_auth_development_prebuilt");
(directory, abi_version, true, false)
} else {
let prebuilt_dir = manifest_dir.join("prebuilt").join(&target);
let artifact_path = prebuilt_dir.join(artifact);
if !artifact_path.is_file() {
return;
}
let validated = match prebuilt_provenance::validate_prebuilt(
&manifest_dir.join("prebuilt"),
&prebuilt_dir,
&target,
artifact,
&env::var("CARGO_PKG_VERSION").unwrap_or_default(),
) {
Ok(validated) => validated,
Err(error) => {
println!("cargo:warning=ignoring op-auth prebuilt: {error}");
return;
}
};
(
prebuilt_dir,
validated.abi_version,
false,
validated.signed_provenance,
)
};
println!("cargo:rustc-cfg=op_auth_prebuilt");
if abi_version == 2 {
debug_assert!(validated.signed_provenance);
assert!(
development_override || signed_provenance,
"production ABI-v2 archives require signed provenance"
);
println!("cargo:rustc-cfg=op_auth_collab_ticket_prebuilt");
}
println!("cargo:rustc-env=OP_AUTH_PREBUILT_ABI_VERSION={abi_version}");
@ -66,3 +95,65 @@ fn main() {
println!("cargo:rustc-link-lib=ntdll");
}
}
fn development_prebuilt(artifact: &str) -> Option<(PathBuf, u32)> {
let feature_enabled = env::var_os(DEV_FEATURE_ENV).is_some();
let archive = env::var_os(DEV_ARCHIVE_ENV);
let abi_version = env::var_os(DEV_ABI_VERSION_ENV);
let (archive, abi_version) = match (feature_enabled, archive, abi_version) {
(_, None, None) => return None,
(true, Some(archive), Some(abi_version)) => (archive, abi_version),
(false, _, _) => {
panic!("{DEV_ARCHIVE_ENV} requires the op-auth-bridge/dev-abi-v2 feature");
}
_ => {
panic!(
"op-auth-bridge/dev-abi-v2 requires {DEV_ARCHIVE_ENV} and {DEV_ABI_VERSION_ENV}"
);
}
};
let debug_build = env::var("PROFILE").is_ok_and(|profile| profile == "debug");
assert!(
debug_build,
"{DEV_ARCHIVE_ENV} is accepted only in Cargo's debug profile"
);
let requested_archive = PathBuf::from(archive);
assert!(
requested_archive.is_absolute(),
"{DEV_ARCHIVE_ENV} must be an absolute path"
);
let metadata = fs::symlink_metadata(&requested_archive)
.unwrap_or_else(|_| panic!("{DEV_ARCHIVE_ENV} is missing or unreadable"));
assert!(
metadata.file_type().is_file(),
"{DEV_ARCHIVE_ENV} must select a regular non-symlink file"
);
let archive = fs::canonicalize(&requested_archive)
.unwrap_or_else(|_| panic!("{DEV_ARCHIVE_ENV} is missing or unreadable"));
assert!(
archive.is_file(),
"{DEV_ARCHIVE_ENV} must select a regular file"
);
assert_eq!(
archive.file_name(),
Some(OsStr::new(artifact)),
"{DEV_ARCHIVE_ENV} must select the target's {artifact}"
);
let abi_version = abi_version
.into_string()
.ok()
.filter(|value| value == "2")
.map(|_| 2)
.unwrap_or_else(|| panic!("{DEV_ABI_VERSION_ENV} must be exactly 2"));
let directory = PathBuf::from(
env::var("OUT_DIR").expect("Cargo provides OUT_DIR to the op-auth build script"),
);
fs::copy(&archive, directory.join(artifact))
.unwrap_or_else(|_| panic!("failed to stage {DEV_ARCHIVE_ENV} in OUT_DIR"));
println!("cargo:rerun-if-changed={}", requested_archive.display());
println!("cargo:rerun-if-changed={}", archive.display());
Some((directory, abi_version))
}

View file

@ -16,6 +16,9 @@
//! validation, and bounded JWKS caching are entirely open source. The private
//! library may only issue opaque tickets from its authenticated device session.
#[cfg(all(op_auth_development_prebuilt, not(debug_assertions)))]
compile_error!("the local op-auth archive override requires target debug assertions");
mod collab_claims;
mod collab_jwks;
mod collab_jwks_cache;

View file

@ -16,6 +16,9 @@ path = "src/main.rs"
[features]
default = []
mcp-debug-tools = ["op-mcp/debug-tools"]
# Local-only collaboration development against an external private ABI-v2
# archive. Release builds are rejected by op-auth-bridge's build script.
dev-op-auth-abi-v2 = ["op-auth-bridge/dev-abi-v2"]
# File-association metadata for `cargo-bundle` (`cargo bundle`). This
# declares OpenPencil as the OS-level handler for `.op` / `.pen`

View file

@ -27,6 +27,7 @@ collab_scan_roots=(
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
@ -512,9 +513,16 @@ for avatar_anchor in \
"MAX_AVATAR_ENCODED_BYTES" \
"public_https_client" \
"REQUEST_TIMEOUT"; do
require_literal crates/op-host-desktop/src/collab_avatar_host.rs \
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 \

View file

@ -332,11 +332,21 @@ EOF
fn public_fixture_is_explicitly_enabled() {}
EOF
cat > "$fixture_root/crates/op-host-desktop/src/collab_avatar_host.rs" <<'EOF'
cat > "$fixture_root/crates/op-host-services/src/profile_avatar_fetch.rs" <<'EOF'
const MAX_REDIRECTS: usize = 3;
const REQUEST_TIMEOUT: u64 = 5;
const MAX_AVATAR_ENCODED_BYTES: usize = 1024;
fn public_https_client() {}
EOF
cat > "$fixture_root/crates/op-host-desktop/src/collab_avatar_host.rs" <<'EOF'
fn dispatch(request: AvatarRequest) {
if request.is_current_account() {
fetch_account_avatar_blocking(request.url());
} else {
fetch_profile_avatar_blocking(request.url());
}
}
EOF
cat > "$fixture_root/crates/op-host-services/src/public_https_client.rs" <<'EOF'
@ -634,6 +644,26 @@ new_fixture desktop-sensitive-file
expect_failure "rejects sensitive files in desktop collaboration integration" \
"sensitive key/token-shaped files are forbidden"
new_fixture avatar-redirect-limit-removed
sed '/MAX_REDIRECTS/d' \
"$fixture_root/crates/op-host-services/src/profile_avatar_fetch.rs" \
> "$fixture_root/crates/op-host-services/src/profile_avatar_fetch.rs.next"
mv \
"$fixture_root/crates/op-host-services/src/profile_avatar_fetch.rs.next" \
"$fixture_root/crates/op-host-services/src/profile_avatar_fetch.rs"
expect_failure "requires the shared avatar redirect limit" \
"bounded collaboration avatar fetch"
new_fixture desktop-public-avatar-delegation-removed
sed '/fetch_profile_avatar_blocking(request.url())/d' \
"$fixture_root/crates/op-host-desktop/src/collab_avatar_host.rs" \
> "$fixture_root/crates/op-host-desktop/src/collab_avatar_host.rs.next"
mv \
"$fixture_root/crates/op-host-desktop/src/collab_avatar_host.rs.next" \
"$fixture_root/crates/op-host-desktop/src/collab_avatar_host.rs"
expect_failure "requires public-only desktop collaboration avatar delegation" \
"desktop avatar security-policy delegation"
new_fixture avatar-proxy-bypass-removed
: > "$fixture_root/crates/op-host-services/src/provider_dial.rs"
expect_failure "requires proxy-free pinned avatar dialing" \