service_source_lock_v3.rs (20647B)
1 use std::fmt; 2 3 use serde::Deserialize; 4 5 use crate::service_source_lock::{ContractVersions, LIB_REPOSITORY}; 6 7 pub(crate) const LOCK_FILENAME: &str = "radroots.service.source-lock.v3.toml"; 8 pub(crate) const PREDECESSOR_LOCK_FILENAME: &str = "radroots.service.source-lock.v2.toml"; 9 const MAX_LOCK_BYTES: usize = 16 * 1024; 10 const ARCHITECTURE: &str = "radroots.crates.release.v2"; 11 const LIB_VERSION: &str = "0.1.0-alpha"; 12 const RUST_VERSION: &str = "1.97.1"; 13 const HOST_FEATURE_PROFILE: &str = "service-host"; 14 15 #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] 16 #[serde(deny_unknown_fields)] 17 struct RawSourceArchiveContract { 18 binding: String, 19 format: String, 20 compression: String, 21 compression_timestamp: String, 22 entry_order: String, 23 path_prefix: String, 24 file_mode: String, 25 uid: u32, 26 gid: u32, 27 uname: String, 28 gname: String, 29 mtime: String, 30 pax_headers: String, 31 directory_entries: String, 32 symlinks: String, 33 hardlinks: String, 34 submodules: String, 35 trailer: String, 36 } 37 38 #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] 39 #[serde(deny_unknown_fields)] 40 struct RawPublicInputLock { 41 path: String, 42 sha256: String, 43 binding: String, 44 mutable_reference: String, 45 lib_input: String, 46 } 47 48 #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] 49 #[serde(deny_unknown_fields)] 50 struct RawParentResult { 51 embedded_in_public_input_lock: bool, 52 embedded_in_source_lock: bool, 53 storage: String, 54 } 55 56 #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] 57 #[serde(deny_unknown_fields)] 58 struct RawQualifiedNix { 59 material: String, 60 lib_revision: String, 61 supported_systems: Vec<String>, 62 public_input_lock: RawPublicInputLock, 63 parent_result: RawParentResult, 64 } 65 66 #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] 67 #[serde(deny_unknown_fields)] 68 struct RawArtifactContract { 69 path: String, 70 sha256: String, 71 binding: String, 72 } 73 74 #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] 75 #[serde(deny_unknown_fields)] 76 struct RawSqliteContract { 77 high_level_authority: String, 78 second_pool_connection_query_transaction_migration_authority: String, 79 incremental_backup_adapter: String, 80 native_linkage_count: u32, 81 } 82 83 #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] 84 #[serde(deny_unknown_fields)] 85 struct RawServiceSourceLockV3 { 86 schema: String, 87 contract_version: u32, 88 service: String, 89 repository: String, 90 revision: String, 91 architecture: String, 92 workspace_catalog_sha256: String, 93 version: String, 94 source_archive_sha256: String, 95 source_archive_contract: RawSourceArchiveContract, 96 cargo_lock_sha256: String, 97 rust_version: String, 98 host_feature_profile: String, 99 nix: RawQualifiedNix, 100 artifact_contract: RawArtifactContract, 101 sqlite: RawSqliteContract, 102 contract_versions: ContractVersions, 103 } 104 105 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 106 pub(crate) enum ServiceSourceLockV3Error { 107 TooLarge, 108 Malformed, 109 Noncanonical, 110 Invalid, 111 } 112 113 impl fmt::Display for ServiceSourceLockV3Error { 114 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 115 formatter.write_str(match self { 116 Self::TooLarge => "service source lock v3 exceeds its byte limit", 117 Self::Malformed => "service source lock v3 is malformed", 118 Self::Noncanonical => "service source lock v3 is not canonical", 119 Self::Invalid => "service source lock v3 is invalid", 120 }) 121 } 122 } 123 124 impl std::error::Error for ServiceSourceLockV3Error {} 125 126 #[derive(Clone, Eq, PartialEq)] 127 pub(crate) struct ServiceSourceLockV3 { 128 raw: RawServiceSourceLockV3, 129 canonical: Box<[u8]>, 130 } 131 132 #[cfg(test)] 133 pub(crate) struct FixtureParts<'a> { 134 pub(crate) service: &'a str, 135 pub(crate) revision: &'a str, 136 pub(crate) workspace_catalog_sha256: &'a str, 137 pub(crate) source_archive_sha256: &'a str, 138 pub(crate) cargo_lock_sha256: &'a str, 139 pub(crate) flake_lock_sha256: &'a str, 140 pub(crate) artifact_contract_sha256: &'a str, 141 pub(crate) contract_versions: ContractVersions, 142 } 143 144 impl fmt::Debug for ServiceSourceLockV3 { 145 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 146 formatter 147 .debug_struct("ServiceSourceLockV3") 148 .finish_non_exhaustive() 149 } 150 } 151 152 impl ServiceSourceLockV3 { 153 pub(crate) fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, ServiceSourceLockV3Error> { 154 if bytes.len() > MAX_LOCK_BYTES { 155 return Err(ServiceSourceLockV3Error::TooLarge); 156 } 157 let text = std::str::from_utf8(bytes).map_err(|_| ServiceSourceLockV3Error::Malformed)?; 158 let raw = toml::from_str::<RawServiceSourceLockV3>(text) 159 .map_err(|_| ServiceSourceLockV3Error::Malformed)?; 160 validate(&raw)?; 161 let canonical = canonical_bytes(&raw); 162 if bytes != canonical.as_slice() { 163 return Err(ServiceSourceLockV3Error::Noncanonical); 164 } 165 Ok(Self { 166 raw, 167 canonical: canonical.into_boxed_slice(), 168 }) 169 } 170 171 #[cfg(test)] 172 pub(crate) fn canonical_bytes(&self) -> &[u8] { 173 &self.canonical 174 } 175 176 pub(crate) fn service(&self) -> &str { 177 &self.raw.service 178 } 179 180 pub(crate) fn revision(&self) -> &str { 181 &self.raw.revision 182 } 183 184 pub(crate) fn workspace_catalog_sha256(&self) -> &str { 185 &self.raw.workspace_catalog_sha256 186 } 187 188 pub(crate) fn source_archive_sha256(&self) -> &str { 189 &self.raw.source_archive_sha256 190 } 191 192 pub(crate) fn cargo_lock_sha256(&self) -> &str { 193 &self.raw.cargo_lock_sha256 194 } 195 196 pub(crate) fn flake_lock_sha256(&self) -> &str { 197 &self.raw.nix.public_input_lock.sha256 198 } 199 200 pub(crate) fn artifact_contract_path(&self) -> &str { 201 &self.raw.artifact_contract.path 202 } 203 204 pub(crate) fn artifact_contract_sha256(&self) -> &str { 205 &self.raw.artifact_contract.sha256 206 } 207 208 pub(crate) const fn contract_versions(&self) -> ContractVersions { 209 self.raw.contract_versions 210 } 211 212 #[cfg(test)] 213 pub(crate) fn fixture(parts: FixtureParts<'_>) -> Result<Self, ServiceSourceLockV3Error> { 214 let FixtureParts { 215 service, 216 revision, 217 workspace_catalog_sha256, 218 source_archive_sha256, 219 cargo_lock_sha256, 220 flake_lock_sha256, 221 artifact_contract_sha256, 222 contract_versions, 223 } = parts; 224 let artifact_path = format!("contracts/release/{service}-artifact-contract.v3.json"); 225 let raw = RawServiceSourceLockV3 { 226 schema: "radroots.service.source-lock.v3".to_owned(), 227 contract_version: 3, 228 service: service.to_owned(), 229 repository: LIB_REPOSITORY.to_owned(), 230 revision: revision.to_owned(), 231 architecture: ARCHITECTURE.to_owned(), 232 workspace_catalog_sha256: workspace_catalog_sha256.to_owned(), 233 version: LIB_VERSION.to_owned(), 234 source_archive_sha256: source_archive_sha256.to_owned(), 235 source_archive_contract: RawSourceArchiveContract { 236 binding: "sha256_of_canonical_exact_lib_revision_tree_archive".to_owned(), 237 format: "ustar".to_owned(), 238 compression: "none".to_owned(), 239 compression_timestamp: "not_applicable".to_owned(), 240 entry_order: "bytewise_git_path".to_owned(), 241 path_prefix: "none".to_owned(), 242 file_mode: "git_index_100644_or_100755".to_owned(), 243 uid: 0, 244 gid: 0, 245 uname: String::new(), 246 gname: String::new(), 247 mtime: "lib_revision_commit_timestamp".to_owned(), 248 pax_headers: "forbidden".to_owned(), 249 directory_entries: "omitted".to_owned(), 250 symlinks: "forbidden".to_owned(), 251 hardlinks: "forbidden".to_owned(), 252 submodules: "forbidden".to_owned(), 253 trailer: "two_zero_blocks".to_owned(), 254 }, 255 cargo_lock_sha256: cargo_lock_sha256.to_owned(), 256 rust_version: RUST_VERSION.to_owned(), 257 host_feature_profile: HOST_FEATURE_PROFILE.to_owned(), 258 nix: RawQualifiedNix { 259 material: "qualified".to_owned(), 260 lib_revision: revision.to_owned(), 261 supported_systems: vec!["aarch64-darwin".to_owned(), "x86_64-linux".to_owned()], 262 public_input_lock: RawPublicInputLock { 263 path: "flake.lock".to_owned(), 264 sha256: flake_lock_sha256.to_owned(), 265 binding: "exact_regular_file_bytes".to_owned(), 266 mutable_reference: "forbidden".to_owned(), 267 lib_input: "lib".to_owned(), 268 }, 269 parent_result: RawParentResult { 270 embedded_in_public_input_lock: false, 271 embedded_in_source_lock: false, 272 storage: "separate_generation_scoped_evidence".to_owned(), 273 }, 274 }, 275 artifact_contract: RawArtifactContract { 276 path: artifact_path, 277 sha256: artifact_contract_sha256.to_owned(), 278 binding: "exact_regular_file_bytes_in_same_source_revision".to_owned(), 279 }, 280 sqlite: RawSqliteContract { 281 high_level_authority: "sqlx_only".to_owned(), 282 second_pool_connection_query_transaction_migration_authority: "forbidden" 283 .to_owned(), 284 incremental_backup_adapter: "sealed_native_sqlx_owned_locked_handle_only" 285 .to_owned(), 286 native_linkage_count: 1, 287 }, 288 contract_versions, 289 }; 290 validate(&raw)?; 291 let canonical = canonical_bytes(&raw).into_boxed_slice(); 292 Ok(Self { raw, canonical }) 293 } 294 } 295 296 fn validate(raw: &RawServiceSourceLockV3) -> Result<(), ServiceSourceLockV3Error> { 297 let expected_artifact = match raw.service.as_str() { 298 "myc" => "contracts/release/myc-artifact-contract.v3.json", 299 "rhi" => "contracts/release/rhi-artifact-contract.v3.json", 300 _ => return Err(ServiceSourceLockV3Error::Invalid), 301 }; 302 let archive = &raw.source_archive_contract; 303 if raw.schema != "radroots.service.source-lock.v3" 304 || raw.contract_version != 3 305 || raw.repository != LIB_REPOSITORY 306 || !valid_lower_hex(&raw.revision, 40) 307 || raw.architecture != ARCHITECTURE 308 || !valid_lower_hex(&raw.workspace_catalog_sha256, 64) 309 || raw.version != LIB_VERSION 310 || !valid_lower_hex(&raw.source_archive_sha256, 64) 311 || !valid_lower_hex(&raw.cargo_lock_sha256, 64) 312 || raw.rust_version != RUST_VERSION 313 || raw.host_feature_profile != HOST_FEATURE_PROFILE 314 || archive.binding != "sha256_of_canonical_exact_lib_revision_tree_archive" 315 || archive.format != "ustar" 316 || archive.compression != "none" 317 || archive.compression_timestamp != "not_applicable" 318 || archive.entry_order != "bytewise_git_path" 319 || archive.path_prefix != "none" 320 || archive.file_mode != "git_index_100644_or_100755" 321 || archive.uid != 0 322 || archive.gid != 0 323 || !archive.uname.is_empty() 324 || !archive.gname.is_empty() 325 || archive.mtime != "lib_revision_commit_timestamp" 326 || archive.pax_headers != "forbidden" 327 || archive.directory_entries != "omitted" 328 || archive.symlinks != "forbidden" 329 || archive.hardlinks != "forbidden" 330 || archive.submodules != "forbidden" 331 || archive.trailer != "two_zero_blocks" 332 || raw.nix.material != "qualified" 333 || raw.nix.lib_revision != raw.revision 334 || raw.nix.supported_systems != ["aarch64-darwin", "x86_64-linux"] 335 || raw.nix.public_input_lock.path != "flake.lock" 336 || !valid_lower_hex(&raw.nix.public_input_lock.sha256, 64) 337 || raw.nix.public_input_lock.binding != "exact_regular_file_bytes" 338 || raw.nix.public_input_lock.mutable_reference != "forbidden" 339 || raw.nix.public_input_lock.lib_input != "lib" 340 || raw.nix.parent_result.embedded_in_public_input_lock 341 || raw.nix.parent_result.embedded_in_source_lock 342 || raw.nix.parent_result.storage != "separate_generation_scoped_evidence" 343 || raw.artifact_contract.path != expected_artifact 344 || !valid_lower_hex(&raw.artifact_contract.sha256, 64) 345 || raw.artifact_contract.binding != "exact_regular_file_bytes_in_same_source_revision" 346 || raw.sqlite.high_level_authority != "sqlx_only" 347 || raw 348 .sqlite 349 .second_pool_connection_query_transaction_migration_authority 350 != "forbidden" 351 || raw.sqlite.incremental_backup_adapter != "sealed_native_sqlx_owned_locked_handle_only" 352 || raw.sqlite.native_linkage_count != 1 353 || !contract_versions_valid(raw.contract_versions) 354 { 355 Err(ServiceSourceLockV3Error::Invalid) 356 } else { 357 Ok(()) 358 } 359 } 360 361 fn contract_versions_valid(versions: ContractVersions) -> bool { 362 versions.config() != 0 363 && versions.state() != 0 364 && versions.admin() != 0 365 && versions.status() != 0 366 && versions.provider() != 0 367 } 368 369 fn canonical_bytes(raw: &RawServiceSourceLockV3) -> Vec<u8> { 370 format!( 371 concat!( 372 "schema = \"radroots.service.source-lock.v3\"\n", 373 "contract_version = 3\n", 374 "service = \"{}\"\n", 375 "repository = \"https://github.com/radrootslabs/lib\"\n", 376 "revision = \"{}\"\n", 377 "architecture = \"radroots.crates.release.v2\"\n", 378 "workspace_catalog_sha256 = \"{}\"\n", 379 "version = \"0.1.0-alpha\"\n", 380 "source_archive_sha256 = \"{}\"\n", 381 "cargo_lock_sha256 = \"{}\"\n", 382 "rust_version = \"1.97.1\"\n", 383 "host_feature_profile = \"service-host\"\n\n", 384 "[source_archive_contract]\n", 385 "binding = \"sha256_of_canonical_exact_lib_revision_tree_archive\"\n", 386 "format = \"ustar\"\n", 387 "compression = \"none\"\n", 388 "compression_timestamp = \"not_applicable\"\n", 389 "entry_order = \"bytewise_git_path\"\n", 390 "path_prefix = \"none\"\n", 391 "file_mode = \"git_index_100644_or_100755\"\n", 392 "uid = 0\n", 393 "gid = 0\n", 394 "uname = \"\"\n", 395 "gname = \"\"\n", 396 "mtime = \"lib_revision_commit_timestamp\"\n", 397 "pax_headers = \"forbidden\"\n", 398 "directory_entries = \"omitted\"\n", 399 "symlinks = \"forbidden\"\n", 400 "hardlinks = \"forbidden\"\n", 401 "submodules = \"forbidden\"\n", 402 "trailer = \"two_zero_blocks\"\n\n", 403 "[nix]\n", 404 "material = \"qualified\"\n", 405 "lib_revision = \"{}\"\n", 406 "supported_systems = [\"aarch64-darwin\", \"x86_64-linux\"]\n\n", 407 "[nix.public_input_lock]\n", 408 "path = \"flake.lock\"\n", 409 "sha256 = \"{}\"\n", 410 "binding = \"exact_regular_file_bytes\"\n", 411 "mutable_reference = \"forbidden\"\n", 412 "lib_input = \"lib\"\n\n", 413 "[nix.parent_result]\n", 414 "embedded_in_public_input_lock = false\n", 415 "embedded_in_source_lock = false\n", 416 "storage = \"separate_generation_scoped_evidence\"\n\n", 417 "[artifact_contract]\n", 418 "path = \"{}\"\n", 419 "sha256 = \"{}\"\n", 420 "binding = \"exact_regular_file_bytes_in_same_source_revision\"\n\n", 421 "[sqlite]\n", 422 "high_level_authority = \"sqlx_only\"\n", 423 "second_pool_connection_query_transaction_migration_authority = \"forbidden\"\n", 424 "incremental_backup_adapter = \"sealed_native_sqlx_owned_locked_handle_only\"\n", 425 "native_linkage_count = 1\n\n", 426 "[contract_versions]\n", 427 "config = {}\n", 428 "state = {}\n", 429 "admin = {}\n", 430 "status = {}\n", 431 "provider = {}\n" 432 ), 433 raw.service, 434 raw.revision, 435 raw.workspace_catalog_sha256, 436 raw.source_archive_sha256, 437 raw.cargo_lock_sha256, 438 raw.nix.lib_revision, 439 raw.nix.public_input_lock.sha256, 440 raw.artifact_contract.path, 441 raw.artifact_contract.sha256, 442 raw.contract_versions.config(), 443 raw.contract_versions.state(), 444 raw.contract_versions.admin(), 445 raw.contract_versions.status(), 446 raw.contract_versions.provider(), 447 ) 448 .into_bytes() 449 } 450 451 fn valid_lower_hex(value: &str, length: usize) -> bool { 452 value.len() == length 453 && value 454 .bytes() 455 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) 456 } 457 458 #[cfg(test)] 459 mod tests { 460 use super::*; 461 462 const MYC_LOCK: &[u8] = br#"schema = "radroots.service.source-lock.v3" 463 contract_version = 3 464 service = "myc" 465 repository = "https://github.com/radrootslabs/lib" 466 revision = "1111111111111111111111111111111111111111" 467 architecture = "radroots.crates.release.v2" 468 workspace_catalog_sha256 = "2222222222222222222222222222222222222222222222222222222222222222" 469 version = "0.1.0-alpha" 470 source_archive_sha256 = "3333333333333333333333333333333333333333333333333333333333333333" 471 cargo_lock_sha256 = "4444444444444444444444444444444444444444444444444444444444444444" 472 rust_version = "1.97.1" 473 host_feature_profile = "service-host" 474 475 [source_archive_contract] 476 binding = "sha256_of_canonical_exact_lib_revision_tree_archive" 477 format = "ustar" 478 compression = "none" 479 compression_timestamp = "not_applicable" 480 entry_order = "bytewise_git_path" 481 path_prefix = "none" 482 file_mode = "git_index_100644_or_100755" 483 uid = 0 484 gid = 0 485 uname = "" 486 gname = "" 487 mtime = "lib_revision_commit_timestamp" 488 pax_headers = "forbidden" 489 directory_entries = "omitted" 490 symlinks = "forbidden" 491 hardlinks = "forbidden" 492 submodules = "forbidden" 493 trailer = "two_zero_blocks" 494 495 [nix] 496 material = "qualified" 497 lib_revision = "1111111111111111111111111111111111111111" 498 supported_systems = ["aarch64-darwin", "x86_64-linux"] 499 500 [nix.public_input_lock] 501 path = "flake.lock" 502 sha256 = "5555555555555555555555555555555555555555555555555555555555555555" 503 binding = "exact_regular_file_bytes" 504 mutable_reference = "forbidden" 505 lib_input = "lib" 506 507 [nix.parent_result] 508 embedded_in_public_input_lock = false 509 embedded_in_source_lock = false 510 storage = "separate_generation_scoped_evidence" 511 512 [artifact_contract] 513 path = "contracts/release/myc-artifact-contract.v3.json" 514 sha256 = "6666666666666666666666666666666666666666666666666666666666666666" 515 binding = "exact_regular_file_bytes_in_same_source_revision" 516 517 [sqlite] 518 high_level_authority = "sqlx_only" 519 second_pool_connection_query_transaction_migration_authority = "forbidden" 520 incremental_backup_adapter = "sealed_native_sqlx_owned_locked_handle_only" 521 native_linkage_count = 1 522 523 [contract_versions] 524 config = 1 525 state = 2 526 admin = 3 527 status = 4 528 provider = 5 529 "#; 530 531 #[test] 532 fn canonical_v3_lock_round_trips_and_exposes_exact_bindings() { 533 let lock = ServiceSourceLockV3::from_canonical_bytes(MYC_LOCK).expect("v3 lock"); 534 assert_eq!(lock.canonical_bytes(), MYC_LOCK); 535 assert_eq!(lock.service(), "myc"); 536 assert_eq!(lock.revision(), "1".repeat(40)); 537 assert_eq!( 538 lock.artifact_contract_path(), 539 "contracts/release/myc-artifact-contract.v3.json" 540 ); 541 assert_eq!( 542 lock.contract_versions(), 543 ContractVersions::new(1, 2, 3, 4, 5) 544 ); 545 } 546 547 #[test] 548 fn v3_lock_rejects_noncanonical_and_independent_semantic_drift() { 549 let noncanonical = String::from_utf8(MYC_LOCK.to_vec()) 550 .expect("UTF-8") 551 .replace("schema =", "schema ="); 552 assert!(ServiceSourceLockV3::from_canonical_bytes(noncanonical.as_bytes()).is_err()); 553 for (from, to) in [ 554 ("service = \"myc\"", "service = \"other\""), 555 ("material = \"qualified\"", "material = \"deferred\""), 556 ("native_linkage_count = 1", "native_linkage_count = 2"), 557 ("trailer = \"two_zero_blocks\"", "trailer = \"other\""), 558 ] { 559 let drifted = String::from_utf8(MYC_LOCK.to_vec()) 560 .expect("UTF-8") 561 .replace(from, to); 562 assert!( 563 ServiceSourceLockV3::from_canonical_bytes(drifted.as_bytes()).is_err(), 564 "{from}" 565 ); 566 } 567 } 568 }