cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

feat(test): add bounded host auth bypass

Dami committed 9399b66
Add an explicitly insecure configured-host authorization mode for local hardware testing, with debug/feature build containment and automatic expiry.

Keep product packages on no-default-features builds and attest that the bypass is absent from release manifests.
commit
9399b667ecc0299771b142ad348325d91725c54f
parent
97a6bdb05c1a6c0b879a1da6e217dc9bc6c6265a
author
Dami <dami.etoile@gmail.com>
13 files changed, +213 -17
M.github/workflows/sigil-hardware-uat.yml3+-
Mcrates/sigil-host/Cargo.toml1+
Mcrates/sigil-host/src/authorization.rs25++++-
Mcrates/sigil-host/src/main.rs155+++++++++++++++++++++++++++++-
Mdocs/fresh-bazzite-host.md10++
Mscripts/install-bazzite-package.sh3+-
Mscripts/package-bazzite-release.sh6+-
Mscripts/run-bazzite-hardware-uat.sh3+-
Mscripts/tests/bazzite-package-install-e2e.sh3+-
Mscripts/tests/package-bazzite-product-contract.sh2+
Mscripts/tests/sigil-release-verifier.sh8+-
Mscripts/verify-demo-build.sh8++
Mscripts/verify-sigil-release.sh3+-
M .github/workflows/sigil-hardware-uat.yml +2 -1
M crates/sigil-host/Cargo.toml +1 -0
M crates/sigil-host/src/authorization.rs +23 -2
@@ -32,6 +32,9 @@ pub enum AuthorizationPolicy {
3232 /// Explicit direct `sigil serve --identity ... --source test-pattern`3333 /// proof mode. Configured hosts never select this branch.3434 TestPatternProof,35+ /// Explicitly insecure configured-host mode for development hardware tests.36+ /// Ordinary release builds cannot select this branch.37+ DevelopmentBypass,3538 }3639 3740 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -600,10 +603,10 @@ impl AuthorizationPolicy {
600603 ) -> Result<InvitationGrants> {601604 match self {602605 Self::Required(store) => store.authorize_or_redeem(remote, invitation_token, now),603- Self::TestPatternProof => {606+ Self::TestPatternProof | Self::DevelopmentBypass => {604607 ensure!(605608 invitation_token.is_none(),606- "proof mode does not accept invitations"609+ "unrestricted development modes do not accept invitations"607610 );608611 Ok(InvitationGrants::ALL)609612 }
@@ -868,6 +871,24 @@ mod tests {
868871 assert!(snapshot.peer.is_none());869872 }870873 874+ #[test]875+ fn development_bypass_grants_all_without_touching_persistent_state() {876+ let directory = tempfile::tempdir().unwrap();877+ let remote = SecretKey::from_bytes(&[29; 32]).public();878+ let policy = AuthorizationPolicy::DevelopmentBypass;879+880+ assert_eq!(881+ policy.authorize_or_redeem(remote, None, 1_000).unwrap(),882+ InvitationGrants::ALL883+ );884+ assert!(885+ policy886+ .authorize_or_redeem(remote, Some("not-an-invitation"), 1_000)887+ .is_err()888+ );889+ assert!(fs::read_dir(directory.path()).unwrap().next().is_none());890+ }891+871892 #[test]872893 fn invitation_clock_boundaries_are_inclusive_and_fail_closed_outside_skew() {873894 let (_directory, host, peer, expiry_store) = store();
M crates/sigil-host/src/main.rs +149 -6
@@ -286,6 +286,9 @@ struct ServeArgs {
286286 /// Strict TOML host configuration.287287 #[arg(long, conflicts_with_all = ["identity", "source", "state_path"])]288288 config: Option<PathBuf>,289+ /// INSECURE: admit an un-enrolled Portal peer for configured-host testing.290+ #[arg(long, requires = "config", conflicts_with = "identity")]291+ dev_allow_unauthorized: bool,289292 /// Host identity for direct proof-mode configuration.290293 #[arg(long, requires = "source")]291294 identity: Option<PathBuf>,
@@ -308,6 +311,8 @@ struct ServeArgs {
308311 max_runtime_seconds: Option<u64>,309312 }310313 314+const MAX_DEVELOPMENT_AUTH_BYPASS_SECONDS: u64 = 3_600;315+311316 enum CliFailure {312317 General(anyhow::Error),313318 Management(config_management::ManagementError),
@@ -786,6 +791,7 @@ fn validate_capture_probe_args(args: &CaptureProbeArgs, configured_framerate: u3
786791 }787792 788793 async fn serve_command(args: ServeArgs) -> Result<()> {794+ validate_development_auth_bypass(args.dev_allow_unauthorized, args.max_runtime_seconds)?;789795 let configured_service = args.config.is_some();790796 let (config, loaded_config_revision, _lifecycle) = if let Some(config_path) = &args.config {791797 let (loaded, lifecycle) =
@@ -806,12 +812,21 @@ async fn serve_command(args: ServeArgs) -> Result<()> {
806812 configured_service,807813 loaded_config_revision,808814 )?;809- let authorization_result = if configured_service {810- AuthorizationStore::open(config.state_path.clone(), secret.public())811- .map(AuthorizationPolicy::Required)812- } else {813- Ok(AuthorizationPolicy::TestPatternProof)814- };815+ let authorization_result = select_authorization_policy(816+ configured_service,817+ args.dev_allow_unauthorized,818+ &config.state_path,819+ secret.public(),820+ );821+ if args.dev_allow_unauthorized {822+ warn!(823+ max_runtime_seconds = args.max_runtime_seconds,824+ "DEVELOPMENT AUTHORIZATION BYPASS ACTIVE: any peer with this host node ID may request a session"825+ );826+ eprintln!(827+ "warning=development-authorization-bypass-active; do not expose or ship this host"828+ );829+ }815830 let authorization = match authorization_result {816831 Ok(authorization) => authorization,817832 Err(error) => {
@@ -1046,6 +1061,45 @@ async fn serve_command(args: ServeArgs) -> Result<()> {
10461061 Ok(())10471062 }10481063 1064+const fn development_auth_bypass_available() -> bool {1065+ cfg!(any(debug_assertions, feature = "demo-auth-bypass"))1066+}1067+1068+fn validate_development_auth_bypass(1069+ requested: bool,1070+ max_runtime_seconds: Option<u64>,1071+) -> Result<()> {1072+ ensure!(1073+ !requested || development_auth_bypass_available(),1074+ "--dev-allow-unauthorized requires a debug build or the explicit demo-auth-bypass feature; ordinary release builds always require enrollment"1075+ );1076+ if requested {1077+ ensure!(1078+ matches!(1079+ max_runtime_seconds,1080+ Some(1..=MAX_DEVELOPMENT_AUTH_BYPASS_SECONDS)1081+ ),1082+ "--dev-allow-unauthorized requires --max-runtime-seconds between 1 and {MAX_DEVELOPMENT_AUTH_BYPASS_SECONDS}"1083+ );1084+ }1085+ Ok(())1086+}1087+1088+fn select_authorization_policy(1089+ configured_service: bool,1090+ development_bypass: bool,1091+ state_path: &Path,1092+ host: EndpointId,1093+) -> Result<AuthorizationPolicy> {1094+ if development_bypass {1095+ Ok(AuthorizationPolicy::DevelopmentBypass)1096+ } else if configured_service {1097+ AuthorizationStore::open(state_path.to_owned(), host).map(AuthorizationPolicy::Required)1098+ } else {1099+ Ok(AuthorizationPolicy::TestPatternProof)1100+ }1101+}1102+10491103 async fn wait_for_shutdown_signal() -> Result<&'static str> {10501104 #[cfg(unix)]10511105 {
@@ -1127,6 +1181,7 @@ fn parse_size(value: &str) -> std::result::Result<(u32, u32), String> {
11271181 #[cfg(test)]11281182 mod tests {11291183 use super::*;1184+ use std::fs;11301185 #[cfg(unix)]11311186 use std::os::unix::fs::PermissionsExt;11321187
@@ -1168,6 +1223,94 @@ mod tests {
11681223 );11691224 }11701225 1226+ #[test]1227+ fn development_auth_bypass_requires_configured_host_syntax() {1228+ assert!(1229+ Cli::try_parse_from([1230+ "sigil",1231+ "serve",1232+ "--config",1233+ "/tmp/host.toml",1234+ "--dev-allow-unauthorized",1235+ "--max-runtime-seconds",1236+ "600",1237+ ])1238+ .is_ok()1239+ );1240+ assert!(1241+ Cli::try_parse_from([1242+ "sigil",1243+ "serve",1244+ "--identity",1245+ "/tmp/host.key",1246+ "--source",1247+ "test-pattern",1248+ "--dev-allow-unauthorized",1249+ "--max-runtime-seconds",1250+ "600",1251+ ])1252+ .is_err()1253+ );1254+ assert!(1255+ Cli::try_parse_from([1256+ "sigil",1257+ "serve",1258+ "--config",1259+ "/tmp/host.toml",1260+ "--dev-allow-unauthorized",1261+ ])1262+ .is_ok()1263+ );1264+ assert!(validate_development_auth_bypass(true, None).is_err());1265+ assert!(validate_development_auth_bypass(true, Some(0)).is_err());1266+ assert!(1267+ validate_development_auth_bypass(true, Some(MAX_DEVELOPMENT_AUTH_BYPASS_SECONDS + 1))1268+ .is_err()1269+ );1270+ }1271+1272+ #[test]1273+ fn ordinary_release_excludes_configured_host_auth_bypass() {1274+ if cfg!(not(debug_assertions)) && cfg!(not(feature = "demo-auth-bypass")) {1275+ assert!(!development_auth_bypass_available());1276+ assert!(validate_development_auth_bypass(true, Some(60)).is_err());1277+ }1278+ }1279+1280+ #[test]1281+ fn configured_host_auth_bypass_is_explicitly_build_contained() {1282+ assert_eq!(1283+ validate_development_auth_bypass(true, Some(60)).is_ok(),1284+ development_auth_bypass_available()1285+ );1286+ }1287+1288+ #[test]1289+ fn development_auth_bypass_does_not_open_persistent_authorization_state() {1290+ let directory = tempfile::tempdir().unwrap();1291+ let state_path = directory.path();1292+ let state_file = state_path.join("authorization-v1.json");1293+ fs::write(&state_file, b"malformed-sentinel").unwrap();1294+ #[cfg(unix)]1295+ fs::set_permissions(&state_file, fs::Permissions::from_mode(0o600)).unwrap();1296+ let before = fs::read(&state_file).unwrap();1297+ let host = iroh::SecretKey::from_bytes(&[31; 32]).public();1298+1299+ assert!(matches!(1300+ select_authorization_policy(true, true, state_path, host).unwrap(),1301+ AuthorizationPolicy::DevelopmentBypass1302+ ));1303+ assert_eq!(fs::read(&state_file).unwrap(), before);1304+ assert!(!state_path.join("authorization-v1.lock").exists());1305+1306+ let required_directory = tempfile::tempdir().unwrap();1307+ let required_state = required_directory.path().join("authorization-v1.json");1308+ fs::write(&required_state, b"malformed-sentinel").unwrap();1309+ #[cfg(unix)]1310+ fs::set_permissions(&required_state, fs::Permissions::from_mode(0o600)).unwrap();1311+ assert!(select_authorization_policy(true, false, required_directory.path(), host).is_err());1312+ }1313+11711314 #[test]11721315 fn invitation_and_enrollment_commands_parse_strictly() {11731316 let peer = iroh::SecretKey::from_bytes(&[19; 32]).public().to_string();
M docs/fresh-bazzite-host.md +10 -0
M scripts/install-bazzite-package.sh +2 -1
M scripts/package-bazzite-release.sh +4 -2
M scripts/run-bazzite-hardware-uat.sh +2 -1
M scripts/tests/bazzite-package-install-e2e.sh +2 -1
M scripts/tests/package-bazzite-product-contract.sh +2 -0
M scripts/tests/sigil-release-verifier.sh +6 -2
@@ -49,7 +49,8 @@ manifest = {
4949 "version": tag[1:],5050 "target": "x86_64-unknown-linux-gnu.2.17",5151 "profile": "release",52- "features": ["default", "in-process-gstreamer"],52+ "features": ["in-process-gstreamer"],53+ "demo_auth_bypass": False,5354 "demo_direct_node": False,5455 "git_commit": commit,5556 "git_dirty": False,
@@ -64,6 +65,8 @@ manifest = {
6465 }6566 if variant == "default-features":6667 manifest["features"] = ["default"]68+if variant == "demo-auth-bypass":69+ manifest["demo_auth_bypass"] = True6770 binary = b"fixture executable\n"6871 release = {6972 "sigil": binary,
@@ -178,7 +181,7 @@ printf '%064d %s\n' 0 "$(basename -- "$bad_archive")" >"$bad_archive.sha256"
178181 assert_rejected bad-checksum 'checksum declaration does not exactly match' \179182 "$verifier" --tag "$release_tag" --archive "$bad_archive" --candidate180183 181-for variant in symlink duplicate unexpected default-features; do184+for variant in symlink duplicate unexpected default-features demo-auth-bypass; do182185 fixture_dir="$temp_root/$variant"183186 make_fixture "$fixture_dir" "$variant"184187 fixture_archive="$fixture_dir/sigil-$release_tag-bazzite-x86_64.tar.gz"
@@ -187,6 +190,7 @@ for variant in symlink duplicate unexpected default-features; do
187190 duplicate) expected='duplicate members' ;;188191 unexpected) expected='do not match the package allowlist' ;;189192 default-features) expected='field features does not match the product contract' ;;193+ demo-auth-bypass) expected='field demo_auth_bypass does not match the product contract' ;;190194 esac191195 assert_rejected "$variant" "$expected" \192196 "$verifier" --tag "$release_tag" --archive "$fixture_archive" --candidate
M scripts/verify-demo-build.sh +8 -0
M scripts/verify-sigil-release.sh +2 -1