services_hardening_provider_contract.rs (12109B)
1 use std::error::Error; 2 use std::path::PathBuf; 3 4 use myc::{ 5 MYC_PROVIDER_CONCURRENCY_MAX, MYC_PROVIDER_CONTRACT_VERSION, MYC_PROVIDER_INPUT_MAX_BYTES, 6 MYC_PROVIDER_OUTPUT_MAX_BYTES, MYC_PROVIDER_REQUEST_DEADLINE_MAX_MS, 7 MYC_PROVIDER_REQUEST_MAX_BYTES, MYC_PROVIDER_RESPONSE_MAX_BYTES, MycConfigProfile, 8 MycProviderCapability, MycProviderContractErrorKind, MycProviderCorrelationId, 9 MycProviderDeadlineUnixMs, MycProviderKind, MycProviderNip44Version, MycProviderOperation, 10 MycProviderOperationId, MycProviderOperationInput, MycProviderRole, MycUntrustedProviderOutput, 11 parse_myc_config_v1, 12 }; 13 use serde_json::{Value, json}; 14 15 const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); 16 const CONTRACT: &str = include_str!("../contracts/services_hardening/provider_contract.v1.json"); 17 18 #[test] 19 fn machine_contract_freezes_the_complete_provider_inventory() { 20 let actual: Value = serde_json::from_str(CONTRACT).expect("provider contract JSON"); 21 assert_eq!( 22 actual, 23 json!({ 24 "schema": "radroots.myc.provider-contract", 25 "schema_version": 1, 26 "contract_version": MYC_PROVIDER_CONTRACT_VERSION, 27 "provider_kinds": ["encrypted_file", "local_signer"], 28 "capabilities": [ 29 "describe", 30 "public_identity", 31 "sign_event", 32 "nip04_encrypt", 33 "nip04_decrypt", 34 "nip44_encrypt", 35 "nip44_decrypt" 36 ], 37 "roles": [ 38 { 39 "role": "transport", 40 "provider_instance": "transport", 41 "required_capabilities": [ 42 "describe", 43 "public_identity", 44 "nip04_encrypt", 45 "nip04_decrypt", 46 "nip44_encrypt", 47 "nip44_decrypt" 48 ] 49 }, 50 { 51 "role": "user", 52 "provider_instance": "user", 53 "required_capabilities": [ 54 "describe", 55 "public_identity", 56 "sign_event", 57 "nip04_encrypt", 58 "nip04_decrypt", 59 "nip44_encrypt", 60 "nip44_decrypt" 61 ] 62 }, 63 { 64 "role": "discovery", 65 "provider_instance": "discovery", 66 "required_capabilities": ["describe", "public_identity", "sign_event"] 67 } 68 ], 69 "operation_binding": { 70 "operation_id_bytes": 32, 71 "correlation_id_bytes": 32, 72 "absolute_deadline_unix_ms": { 73 "minimum": 1, 74 "maximum": i64::MAX 75 }, 76 "nip44_versions": [2], 77 "semantic_input_max_bytes": MYC_PROVIDER_INPUT_MAX_BYTES, 78 "untrusted_output_max_bytes": MYC_PROVIDER_OUTPUT_MAX_BYTES 79 }, 80 "local_signer_limits": { 81 "request_deadline_ms": { 82 "minimum": 1, 83 "maximum": MYC_PROVIDER_REQUEST_DEADLINE_MAX_MS 84 }, 85 "request_max_bytes": { 86 "minimum": 1, 87 "maximum": MYC_PROVIDER_REQUEST_MAX_BYTES 88 }, 89 "response_max_bytes": { 90 "minimum": 1, 91 "maximum": MYC_PROVIDER_RESPONSE_MAX_BYTES 92 }, 93 "concurrency": { 94 "minimum": 1, 95 "maximum": MYC_PROVIDER_CONCURRENCY_MAX 96 } 97 }, 98 "credential_reference": { 99 "maximum_utf8_bytes": 128, 100 "shared_type": "ServiceCredentialArtifactName", 101 "material_in_configuration": false 102 }, 103 "execution": { 104 "database_transaction_held": false, 105 "provider_result_trusted": false, 106 "cancellation_proves_no_effect": false, 107 "signing_is_publication": false 108 } 109 }) 110 ); 111 } 112 113 #[test] 114 fn admitted_configuration_derives_exact_role_bindings_and_limits() { 115 let config = parse_myc_config_v1(CONFIG.as_bytes(), MycConfigProfile::Production) 116 .expect("canonical configuration"); 117 let contract = config.provider_contract(); 118 assert_eq!(contract.bindings().len(), 3); 119 120 let transport = contract 121 .binding(MycProviderRole::Transport) 122 .expect("transport binding"); 123 assert_eq!(transport.kind(), MycProviderKind::EncryptedFile); 124 assert_eq!(transport.instance().as_str(), "transport"); 125 assert_eq!( 126 transport 127 .credential_reference() 128 .expect("credential") 129 .as_str(), 130 "transport_wrapping_key" 131 ); 132 assert!(transport.local_signer_limits().is_none()); 133 assert!( 134 transport 135 .required_capabilities() 136 .contains(MycProviderCapability::SignEvent) 137 ); 138 assert_eq!(transport.required_capabilities().len(), 7); 139 140 let user = contract 141 .binding(MycProviderRole::User) 142 .expect("user binding"); 143 assert_eq!(user.kind(), MycProviderKind::LocalSigner); 144 assert_eq!(user.instance().as_str(), "user"); 145 assert!(user.credential_reference().is_none()); 146 let limits = user.local_signer_limits().expect("local signer limits"); 147 assert_eq!(limits.request_deadline_ms(), 15_000); 148 assert_eq!(limits.request_max_bytes(), 65_536); 149 assert_eq!(limits.response_max_bytes(), 1_048_576); 150 assert_eq!(limits.concurrency(), 32); 151 assert_eq!(user.required_capabilities().len(), 7); 152 153 let discovery = contract 154 .binding(MycProviderRole::Discovery) 155 .expect("discovery binding"); 156 assert_eq!(discovery.kind(), MycProviderKind::EncryptedFile); 157 assert_eq!(discovery.required_capabilities().len(), 3); 158 assert_eq!( 159 discovery 160 .required_capabilities() 161 .iter() 162 .map(MycProviderCapability::as_str) 163 .collect::<Vec<_>>(), 164 ["describe", "public_identity", "sign_event"] 165 ); 166 } 167 168 #[test] 169 fn explicitly_disabled_discovery_omits_only_that_provider_binding() { 170 let mut value = CONFIG.parse::<toml::Table>().expect("configuration TOML"); 171 value.insert( 172 "discovery".to_owned(), 173 toml::Value::Table(toml::Table::from_iter([( 174 "enabled".to_owned(), 175 toml::Value::Boolean(false), 176 )])), 177 ); 178 value 179 .get_mut("identity") 180 .and_then(toml::Value::as_table_mut) 181 .expect("identity") 182 .insert( 183 "discovery".to_owned(), 184 toml::Value::Table(toml::Table::from_iter([( 185 "enabled".to_owned(), 186 toml::Value::Boolean(false), 187 )])), 188 ); 189 let source = toml::to_string(&value).expect("disabled discovery fixture"); 190 let config = parse_myc_config_v1(source.as_bytes(), MycConfigProfile::Production) 191 .expect("disabled discovery config"); 192 assert_eq!(config.provider_contract().bindings().len(), 2); 193 assert!( 194 config 195 .provider_contract() 196 .binding(MycProviderRole::Discovery) 197 .is_none() 198 ); 199 } 200 201 #[test] 202 fn every_operation_is_identity_deadline_and_role_bound_before_execution() { 203 let config = parse_myc_config_v1(CONFIG.as_bytes(), MycConfigProfile::Production) 204 .expect("canonical configuration"); 205 let operation_id = MycProviderOperationId::from_bytes([7; 32]); 206 let correlation_id = MycProviderCorrelationId::from_bytes([9; 32]); 207 let deadline = MycProviderDeadlineUnixMs::new(1_800_000_000_000).expect("deadline"); 208 209 let user = config 210 .provider_contract() 211 .binding(MycProviderRole::User) 212 .expect("user"); 213 let sign = MycProviderOperation::new( 214 user, 215 operation_id, 216 correlation_id, 217 deadline, 218 MycProviderOperationInput::sign_event(br#"{"kind":1}"#).expect("event input"), 219 ) 220 .expect("bound operation"); 221 assert_eq!(sign.contract_version(), MYC_PROVIDER_CONTRACT_VERSION); 222 assert_eq!(sign.role(), MycProviderRole::User); 223 assert_eq!(sign.instance().as_str(), "user"); 224 assert_eq!(sign.provider(), MycProviderKind::LocalSigner); 225 assert_eq!(sign.operation_id().as_bytes(), &[7; 32]); 226 assert_eq!(sign.correlation_id().as_bytes(), &[9; 32]); 227 assert_eq!(sign.deadline().get(), 1_800_000_000_000); 228 assert_eq!(sign.expected_identity(), user.expected_identity()); 229 assert_eq!(sign.input().capability(), MycProviderCapability::SignEvent); 230 231 let transport = config 232 .provider_contract() 233 .binding(MycProviderRole::Transport) 234 .expect("transport"); 235 let response_sign = MycProviderOperation::new( 236 transport, 237 operation_id, 238 correlation_id, 239 deadline, 240 MycProviderOperationInput::sign_event(br#"{"kind":1}"#).expect("event input"), 241 ) 242 .expect("transport response signing operation"); 243 assert_eq!(response_sign.role(), MycProviderRole::Transport); 244 assert_eq!(response_sign.instance().as_str(), "transport"); 245 assert_eq!(response_sign.provider(), MycProviderKind::EncryptedFile); 246 assert_eq!( 247 response_sign.expected_identity(), 248 transport.expected_identity() 249 ); 250 assert_eq!( 251 response_sign.input().capability(), 252 MycProviderCapability::SignEvent 253 ); 254 } 255 256 #[test] 257 fn protected_inputs_outputs_bind_nip_version_and_never_render_values() { 258 let peer = myc::MycProviderPublicIdentity::new(&"2".repeat(64)).expect("peer"); 259 let input = MycProviderOperationInput::nip44_decrypt( 260 peer, 261 MycProviderNip44Version::V2, 262 b"ciphertext-secret-marker", 263 ) 264 .expect("decrypt input"); 265 assert_eq!(input.capability(), MycProviderCapability::Nip44Decrypt); 266 assert_eq!( 267 input.nip44_version().map(MycProviderNip44Version::as_u8), 268 Some(2) 269 ); 270 assert_eq!(input.bytes(), Some(b"ciphertext-secret-marker".as_slice())); 271 assert!(!format!("{input:?}").contains("ciphertext-secret-marker")); 272 273 let output = 274 MycUntrustedProviderOutput::new(b"provider-output-secret-marker").expect("bounded output"); 275 assert_eq!(output.as_bytes(), b"provider-output-secret-marker"); 276 assert!(!format!("{output:?}").contains("provider-output-secret-marker")); 277 } 278 279 #[test] 280 fn all_provider_debug_and_errors_are_path_secret_and_source_free() { 281 let config = parse_myc_config_v1(CONFIG.as_bytes(), MycConfigProfile::Production) 282 .expect("canonical configuration"); 283 let rendered = format!("{:?}", config.provider_contract()); 284 for forbidden in [ 285 "/var/lib/radroots", 286 "/run/radroots", 287 "transport_wrapping_key", 288 "4444444444444444", 289 "2222222222222222", 290 ] { 291 assert!(!rendered.contains(forbidden), "leaked {forbidden}"); 292 for binding in config.provider_contract().bindings() { 293 assert!(!format!("{binding:?}").contains(forbidden)); 294 } 295 } 296 297 let error = MycProviderDeadlineUnixMs::new(0).expect_err("zero deadline"); 298 assert_eq!(error.kind(), MycProviderContractErrorKind::InvalidDeadline); 299 assert!(error.source().is_none()); 300 assert!(!error.to_string().contains('/')); 301 } 302 303 #[test] 304 fn provider_contract_has_no_io_or_public_path_escape_hatch() { 305 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 306 let source = std::fs::read_to_string(root.join("src/provider_contract.rs")) 307 .expect("provider contract source"); 308 for forbidden in [ 309 "std::fs::", 310 "tokio::", 311 "sqlx::", 312 "reqwest::", 313 "Command::new", 314 "pub fn envelope_path", 315 "pub fn socket_path", 316 "impl serde::Serialize", 317 "impl serde::Deserialize", 318 ] { 319 assert!(!source.contains(forbidden), "forbidden source: {forbidden}"); 320 } 321 }