mostlyright-data 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mostlyright/data_harness/__init__.py +158 -0
- mostlyright/data_harness/acquisition/__init__.py +55 -0
- mostlyright/data_harness/acquisition/http.py +2773 -0
- mostlyright/data_harness/acquisition/parsing.py +809 -0
- mostlyright/data_harness/acquisition/ranges.py +495 -0
- mostlyright/data_harness/acquisition/result_download.py +360 -0
- mostlyright/data_harness/acquisition/retention_admission.py +248 -0
- mostlyright/data_harness/acquisition/sandbox.py +4888 -0
- mostlyright/data_harness/acquisition/url_policy.py +530 -0
- mostlyright/data_harness/agent_runtime.py +2743 -0
- mostlyright/data_harness/assets/logo-ink.svg +31 -0
- mostlyright/data_harness/backends/__init__.py +28 -0
- mostlyright/data_harness/backends/pandas_backend.py +350 -0
- mostlyright/data_harness/backends/polars_backend.py +366 -0
- mostlyright/data_harness/backends/protocol.py +124 -0
- mostlyright/data_harness/backends/reference.py +83 -0
- mostlyright/data_harness/backends/registry.py +55 -0
- mostlyright/data_harness/backends/restrictions.py +126 -0
- mostlyright/data_harness/canonical.py +333 -0
- mostlyright/data_harness/catalog_job.py +625 -0
- mostlyright/data_harness/cli.py +5398 -0
- mostlyright/data_harness/contracts.py +53 -0
- mostlyright/data_harness/coordinator.py +1307 -0
- mostlyright/data_harness/deploy.py +924 -0
- mostlyright/data_harness/deploy_target.py +312 -0
- mostlyright/data_harness/deployment_evidence.py +1067 -0
- mostlyright/data_harness/event_presentation.py +576 -0
- mostlyright/data_harness/events.py +2152 -0
- mostlyright/data_harness/fast_delimited.py +239 -0
- mostlyright/data_harness/fleet.py +237 -0
- mostlyright/data_harness/formats.py +236 -0
- mostlyright/data_harness/governors.py +1163 -0
- mostlyright/data_harness/hosted_bootstrap.py +972 -0
- mostlyright/data_harness/hosted_crawler.py +1115 -0
- mostlyright/data_harness/hosted_crawler_container_smoke.py +351 -0
- mostlyright/data_harness/hosted_crawler_fetch.py +423 -0
- mostlyright/data_harness/hosted_crawler_job.py +1277 -0
- mostlyright/data_harness/hosted_crawler_protocol.py +676 -0
- mostlyright/data_harness/hosted_dataset.py +1500 -0
- mostlyright/data_harness/hosted_deploy.py +3037 -0
- mostlyright/data_harness/hosted_handoff.py +62 -0
- mostlyright/data_harness/hosted_ingestion_contract.py +504 -0
- mostlyright/data_harness/hosted_ingestion_job.py +356 -0
- mostlyright/data_harness/hosted_ingestion_job_smoke.py +40 -0
- mostlyright/data_harness/hosted_session_container_smoke.py +194 -0
- mostlyright/data_harness/hosted_session_worker.py +3554 -0
- mostlyright/data_harness/hosted_session_worker_job_smoke.py +46 -0
- mostlyright/data_harness/hosted_worker.py +6784 -0
- mostlyright/data_harness/ingestion/__init__.py +56 -0
- mostlyright/data_harness/ingestion/contracts.py +461 -0
- mostlyright/data_harness/ingestion/faults.py +42 -0
- mostlyright/data_harness/ingestion/gcs_store.py +1162 -0
- mostlyright/data_harness/ingestion/spool.py +130 -0
- mostlyright/data_harness/ingestion/store.py +885 -0
- mostlyright/data_harness/key_seam.py +434 -0
- mostlyright/data_harness/linux_process_boundary.py +262 -0
- mostlyright/data_harness/local_contracts.py +2880 -0
- mostlyright/data_harness/local_search/__init__.py +5 -0
- mostlyright/data_harness/local_search/build_index.py +1087 -0
- mostlyright/data_harness/local_search/contracts.py +920 -0
- mostlyright/data_harness/local_search/query_trace.py +266 -0
- mostlyright/data_harness/local_search/retrieval.py +700 -0
- mostlyright/data_harness/local_search/sealed.py +474 -0
- mostlyright/data_harness/local_search/service.py +784 -0
- mostlyright/data_harness/nbrender/CONTRACT.md +212 -0
- mostlyright/data_harness/nbrender/__init__.py +12 -0
- mostlyright/data_harness/nbrender/chrome.py +359 -0
- mostlyright/data_harness/nbrender/code_body.py +266 -0
- mostlyright/data_harness/nbrender/document.py +407 -0
- mostlyright/data_harness/nbrender/frame.py +275 -0
- mostlyright/data_harness/nbrender/interactive.py +337 -0
- mostlyright/data_harness/nbrender/markdown_body.py +477 -0
- mostlyright/data_harness/nbrender/mr_components.py +134 -0
- mostlyright/data_harness/nbrender/outputs_data.py +595 -0
- mostlyright/data_harness/nbrender/outputs_rich.py +906 -0
- mostlyright/data_harness/nbrender/outputs_source.py +260 -0
- mostlyright/data_harness/nbrender/outputs_stage.py +176 -0
- mostlyright/data_harness/nbrender/outputs_text.py +400 -0
- mostlyright/data_harness/nbrender/parse.py +394 -0
- mostlyright/data_harness/nbrender/status.py +40 -0
- mostlyright/data_harness/nbrender/tokens.py +1295 -0
- mostlyright/data_harness/notebook.py +1710 -0
- mostlyright/data_harness/offline.py +2049 -0
- mostlyright/data_harness/operation_registry.py +1007 -0
- mostlyright/data_harness/operator_setup.py +239 -0
- mostlyright/data_harness/pipeline.py +6428 -0
- mostlyright/data_harness/plan_graph.py +2026 -0
- mostlyright/data_harness/preparation/__init__.py +104 -0
- mostlyright/data_harness/preparation/contracts.py +1017 -0
- mostlyright/data_harness/preparation/engine.py +221 -0
- mostlyright/data_harness/preparation/errors.py +14 -0
- mostlyright/data_harness/preparation/gates.py +751 -0
- mostlyright/data_harness/preparation/joins.py +574 -0
- mostlyright/data_harness/preparation/profile.py +384 -0
- mostlyright/data_harness/preparation/table.py +217 -0
- mostlyright/data_harness/preparation/transforms.py +568 -0
- mostlyright/data_harness/progress_events.py +534 -0
- mostlyright/data_harness/readers/__init__.py +46 -0
- mostlyright/data_harness/readers/containers.py +963 -0
- mostlyright/data_harness/readers/contracts.py +542 -0
- mostlyright/data_harness/readers/delimited.py +257 -0
- mostlyright/data_harness/readers/grib2/__init__.py +33 -0
- mostlyright/data_harness/readers/grib2/admission.py +722 -0
- mostlyright/data_harness/readers/grib2/decode.py +1009 -0
- mostlyright/data_harness/readers/grib2/geometry.py +1133 -0
- mostlyright/data_harness/readers/grib2/portable_math.py +501 -0
- mostlyright/data_harness/readers/json_tabular.py +485 -0
- mostlyright/data_harness/readers/registry.py +514 -0
- mostlyright/data_harness/readers/samples/README.md +110 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/cities.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/cities.csv +3 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/expected.json +23 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/cities.tsv +5 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/expected.json +30 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/observations.csv +5 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/expected.json +44 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/stations.json +1 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/expected.json +48 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/observations.ndjson +4 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/cities.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/expected.json +24 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/README.md +20 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/expected.json +55 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/gfs-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/hrrr-categorical-rain.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples.py +582 -0
- mostlyright/data_harness/readers/spreadsheet.py +803 -0
- mostlyright/data_harness/readers/tabular.py +510 -0
- mostlyright/data_harness/recipe.py +5321 -0
- mostlyright/data_harness/repair/__init__.py +78 -0
- mostlyright/data_harness/repair/adapters.py +274 -0
- mostlyright/data_harness/repair/contracts.py +872 -0
- mostlyright/data_harness/repair/coordinator.py +1099 -0
- mostlyright/data_harness/repair/errors.py +16 -0
- mostlyright/data_harness/review.py +2533 -0
- mostlyright/data_harness/rowset.py +283 -0
- mostlyright/data_harness/serving.py +1975 -0
- mostlyright/data_harness/serving_edge.py +590 -0
- mostlyright/data_harness/serving_http.py +1031 -0
- mostlyright/data_harness/session_probes.py +759 -0
- mostlyright/data_harness/signing.py +101 -0
- mostlyright/data_harness/source_discovery.py +898 -0
- mostlyright/data_harness/sources/__init__.py +209 -0
- mostlyright/data_harness/sources/_adapter_steps.py +213 -0
- mostlyright/data_harness/sources/adapters.py +1214 -0
- mostlyright/data_harness/sources/cadence.py +1428 -0
- mostlyright/data_harness/sources/cadence_emission.py +453 -0
- mostlyright/data_harness/sources/cadence_history.py +546 -0
- mostlyright/data_harness/sources/catalog/__init__.py +17 -0
- mostlyright/data_harness/sources/catalog/admission.py +477 -0
- mostlyright/data_harness/sources/catalog/authoring.py +1701 -0
- mostlyright/data_harness/sources/catalog/authoring_policy.py +701 -0
- mostlyright/data_harness/sources/catalog/authoring_shards.py +1217 -0
- mostlyright/data_harness/sources/catalog/bounded_io.py +231 -0
- mostlyright/data_harness/sources/catalog/channel.py +523 -0
- mostlyright/data_harness/sources/catalog/channel_client.py +296 -0
- mostlyright/data_harness/sources/catalog/contracts.py +825 -0
- mostlyright/data_harness/sources/catalog/coverage.py +137 -0
- mostlyright/data_harness/sources/catalog/delta.py +1340 -0
- mostlyright/data_harness/sources/catalog/embedding.py +532 -0
- mostlyright/data_harness/sources/catalog/entry_v2.py +1182 -0
- mostlyright/data_harness/sources/catalog/fill.py +3889 -0
- mostlyright/data_harness/sources/catalog/fill_partitions.py +459 -0
- mostlyright/data_harness/sources/catalog/fill_staging.py +1105 -0
- mostlyright/data_harness/sources/catalog/gating.py +374 -0
- mostlyright/data_harness/sources/catalog/generation_receipt.py +1607 -0
- mostlyright/data_harness/sources/catalog/harvest/__init__.py +7 -0
- mostlyright/data_harness/sources/catalog/harvest/ckan.py +384 -0
- mostlyright/data_harness/sources/catalog/harvest/datagov_v4.py +798 -0
- mostlyright/data_harness/sources/catalog/harvest/protocol.py +964 -0
- mostlyright/data_harness/sources/catalog/harvest/sdmx.py +445 -0
- mostlyright/data_harness/sources/catalog/harvest/stac.py +384 -0
- mostlyright/data_harness/sources/catalog/health.py +447 -0
- mostlyright/data_harness/sources/catalog/hosted_catalog.py +105 -0
- mostlyright/data_harness/sources/catalog/identity_history.py +1549 -0
- mostlyright/data_harness/sources/catalog/neural.py +1618 -0
- mostlyright/data_harness/sources/catalog/packed_catalog.py +2345 -0
- mostlyright/data_harness/sources/catalog/packed_retrieval.py +1517 -0
- mostlyright/data_harness/sources/catalog/packed_writer.py +2802 -0
- mostlyright/data_harness/sources/catalog/query_trace.py +1037 -0
- mostlyright/data_harness/sources/catalog/recommend.py +171 -0
- mostlyright/data_harness/sources/catalog/retrieval.py +230 -0
- mostlyright/data_harness/sources/catalog/retrieval_manifest.py +995 -0
- mostlyright/data_harness/sources/catalog/rights_decisions.py +254 -0
- mostlyright/data_harness/sources/catalog/sealed.py +560 -0
- mostlyright/data_harness/sources/catalog/search.py +230 -0
- mostlyright/data_harness/sources/catalog/streaming_delta.py +1097 -0
- mostlyright/data_harness/sources/catalog/update.py +891 -0
- mostlyright/data_harness/sources/collections.py +815 -0
- mostlyright/data_harness/sources/contracts.py +2223 -0
- mostlyright/data_harness/sources/deletion.py +761 -0
- mostlyright/data_harness/sources/fitness.py +162 -0
- mostlyright/data_harness/sources/governance.py +163 -0
- mostlyright/data_harness/sources/hosted.py +173 -0
- mostlyright/data_harness/sources/integration.py +218 -0
- mostlyright/data_harness/sources/range_reader.py +418 -0
- mostlyright/data_harness/sources/registry.py +514 -0
- mostlyright/data_harness/sources/rights_rule.py +59 -0
- mostlyright/data_harness/sources/source_cadence_vectors.v1.json +1 -0
- mostlyright/data_harness/sources/sports.py +521 -0
- mostlyright/data_harness/sources/stream.py +524 -0
- mostlyright/data_harness/sources/stream_connector.py +418 -0
- mostlyright/data_harness/sources/stream_recorder.py +1404 -0
- mostlyright/data_harness/studio_boundary.py +2019 -0
- mostlyright/data_harness/thin/__init__.py +37 -0
- mostlyright/data_harness/thin/acquire.py +1137 -0
- mostlyright/data_harness/thin/acquire_cancel.py +579 -0
- mostlyright/data_harness/thin/approvals.py +617 -0
- mostlyright/data_harness/thin/commands.py +406 -0
- mostlyright/data_harness/thin/download.py +194 -0
- mostlyright/data_harness/thin/narrative.py +589 -0
- mostlyright/data_harness/thin/parity.py +1070 -0
- mostlyright/data_harness/thin/propose.py +2759 -0
- mostlyright/data_harness/thin/research.py +1663 -0
- mostlyright/data_harness/thin/router.py +924 -0
- mostlyright/data_harness/thin/runs.py +519 -0
- mostlyright/data_harness/thin/session.py +281 -0
- mostlyright/data_harness/thin/stream.py +501 -0
- mostlyright/data_harness/thin/transport.py +187 -0
- mostlyright/data_harness/thin/vocabulary.py +368 -0
- mostlyright/data_harness/thin/workers.py +164 -0
- mostlyright/data_harness/ucum/TABLE-PIN.json +40 -0
- mostlyright/data_harness/ucum/ucum-subset.v1.json +632 -0
- mostlyright/data_harness/unit_flow.py +927 -0
- mostlyright/data_harness/units.py +572 -0
- mostlyright/data_harness/ux/__init__.py +9 -0
- mostlyright/data_harness/ux/approve.py +485 -0
- mostlyright/data_harness/ux/author_yaml.py +597 -0
- mostlyright/data_harness/ux/cloud_auth.py +447 -0
- mostlyright/data_harness/ux/commands/__init__.py +260 -0
- mostlyright/data_harness/ux/commands/approve.py +136 -0
- mostlyright/data_harness/ux/commands/auth.py +744 -0
- mostlyright/data_harness/ux/commands/author.py +79 -0
- mostlyright/data_harness/ux/commands/catalog_author.py +403 -0
- mostlyright/data_harness/ux/commands/catalog_fill.py +523 -0
- mostlyright/data_harness/ux/commands/catalog_harvest.py +545 -0
- mostlyright/data_harness/ux/commands/catalog_publish.py +1838 -0
- mostlyright/data_harness/ux/commands/catalog_search.py +71 -0
- mostlyright/data_harness/ux/commands/catalog_update.py +437 -0
- mostlyright/data_harness/ux/commands/deploy.py +134 -0
- mostlyright/data_harness/ux/commands/deploy_dataset.py +98 -0
- mostlyright/data_harness/ux/commands/deploy_plan.py +105 -0
- mostlyright/data_harness/ux/commands/deploy_status.py +104 -0
- mostlyright/data_harness/ux/commands/diff.py +74 -0
- mostlyright/data_harness/ux/commands/index.py +84 -0
- mostlyright/data_harness/ux/commands/inventory.py +47 -0
- mostlyright/data_harness/ux/commands/list_builds.py +143 -0
- mostlyright/data_harness/ux/commands/login.py +63 -0
- mostlyright/data_harness/ux/commands/peek.py +236 -0
- mostlyright/data_harness/ux/commands/plan_check.py +90 -0
- mostlyright/data_harness/ux/commands/preflight.py +97 -0
- mostlyright/data_harness/ux/commands/record.py +107 -0
- mostlyright/data_harness/ux/commands/review_setup.py +47 -0
- mostlyright/data_harness/ux/commands/search.py +440 -0
- mostlyright/data_harness/ux/commands/show.py +61 -0
- mostlyright/data_harness/ux/commands/whoami.py +37 -0
- mostlyright/data_harness/ux/credential_native.py +551 -0
- mostlyright/data_harness/ux/credential_store.py +1055 -0
- mostlyright/data_harness/ux/credentials.py +631 -0
- mostlyright/data_harness/ux/diffing.py +444 -0
- mostlyright/data_harness/ux/headline.py +671 -0
- mostlyright/data_harness/ux/hosted_acquisition.py +974 -0
- mostlyright/data_harness/ux/hosted_run_status.py +619 -0
- mostlyright/data_harness/ux/inventory.py +427 -0
- mostlyright/data_harness/ux/local_review.py +375 -0
- mostlyright/data_harness/ux/login.py +691 -0
- mostlyright/data_harness/ux/path_kind.py +147 -0
- mostlyright/data_harness/ux/peek.py +1000 -0
- mostlyright/data_harness/ux/plain_file.py +178 -0
- mostlyright/data_harness/ux/plan_check.py +311 -0
- mostlyright/data_harness/ux/preflight.py +918 -0
- mostlyright/data_harness/ux/readers.py +1124 -0
- mostlyright/data_harness/ux/remediation.py +2195 -0
- mostlyright/data_harness/ux/render.py +657 -0
- mostlyright/data_harness/ux/workload.py +1077 -0
- mostlyright/data_harness/viewer.py +3713 -0
- mostlyright/data_harness/visual_run/__init__.py +83 -0
- mostlyright/data_harness/visual_run/authoring.py +235 -0
- mostlyright/data_harness/visual_run/contracts.py +673 -0
- mostlyright/data_harness/visual_run/materialize.py +486 -0
- mostlyright/data_harness/visual_run/observations.py +874 -0
- mostlyright/data_harness/visual_run/query.py +259 -0
- mostlyright/data_harness/visual_run/reducer.py +280 -0
- mostlyright/data_harness/visual_run/sdk.py +892 -0
- mostlyright/data_harness/visual_run/store.py +584 -0
- mostlyright/data_harness/visual_run/transport.py +239 -0
- mostlyright/data_harness/watch.py +2999 -0
- mostlyright_data-0.9.0.dist-info/METADATA +607 -0
- mostlyright_data-0.9.0.dist-info/RECORD +314 -0
- mostlyright_data-0.9.0.dist-info/WHEEL +4 -0
- mostlyright_data-0.9.0.dist-info/entry_points.txt +12 -0
|
@@ -0,0 +1,1307 @@
|
|
|
1
|
+
"""Separate-process review enrollment and signing authority for trusted H2 operators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import base64
|
|
7
|
+
import hashlib
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import secrets
|
|
11
|
+
import select
|
|
12
|
+
import socket
|
|
13
|
+
import stat
|
|
14
|
+
import struct
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
import uuid
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from cryptography.exceptions import InvalidSignature
|
|
24
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
25
|
+
Ed25519PrivateKey,
|
|
26
|
+
Ed25519PublicKey,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from mostlyright.data_harness.canonical import (
|
|
30
|
+
CanonicalJSONError,
|
|
31
|
+
canonical_json_bytes,
|
|
32
|
+
parse_canonical_json,
|
|
33
|
+
)
|
|
34
|
+
from mostlyright.data_harness.offline import (
|
|
35
|
+
_H2_PRODUCER_ATTEMPT,
|
|
36
|
+
_H2_PRODUCER_PRINCIPAL,
|
|
37
|
+
review_target_path,
|
|
38
|
+
workspace_result_path, # noqa: F401 (re-exported: enrollment tests replay the workbench binding)
|
|
39
|
+
)
|
|
40
|
+
from mostlyright.data_harness.pipeline import verify_candidate
|
|
41
|
+
from mostlyright.data_harness.review import (
|
|
42
|
+
ASSIGNMENT_SCHEMA_VERSION,
|
|
43
|
+
REPORT_SCHEMA_VERSION,
|
|
44
|
+
ReviewError,
|
|
45
|
+
_candidate_artifact_paths,
|
|
46
|
+
_normalized_assignment_digest,
|
|
47
|
+
_parse_assignment,
|
|
48
|
+
load_public_key_base64,
|
|
49
|
+
public_key_to_base64,
|
|
50
|
+
)
|
|
51
|
+
from mostlyright.data_harness.signing import SignerProviderError
|
|
52
|
+
|
|
53
|
+
_REQUEST_SCHEMA_VERSION = "review-coordinator-request.v1"
|
|
54
|
+
_RESPONSE_SCHEMA_VERSION = "review-coordinator-response.v1"
|
|
55
|
+
_SESSION_SCHEMA_VERSION = "review-coordinator-session.v1"
|
|
56
|
+
_STARTUP_SCHEMA_VERSION = "review-coordinator-startup.v1"
|
|
57
|
+
_ANCHOR_SCHEMA_VERSION = "review-coordinator-anchor.v1"
|
|
58
|
+
_SEALED_ENROLLMENT_SCHEMA_VERSION = "review-coordinator-sealed-enrollment.v1"
|
|
59
|
+
_KEY_BUNDLE_SCHEMA_VERSION = "review-coordinator-key-bundle.v1"
|
|
60
|
+
_SYSTEM_ANCHOR_PATH = (
|
|
61
|
+
Path("/private/etc/mostlyright/trust/anchor.json")
|
|
62
|
+
if sys.platform == "darwin"
|
|
63
|
+
else Path("/etc/mostlyright/trust/anchor.json")
|
|
64
|
+
)
|
|
65
|
+
_ANCHOR_MODE = 0o444
|
|
66
|
+
_MAX_MESSAGE_BYTES = 1024 * 1024
|
|
67
|
+
_MAX_SIGNING_BYTES = 768 * 1024
|
|
68
|
+
_MAX_SESSION_REQUESTS = 32
|
|
69
|
+
_DESCRIPTOR_READ_TIMEOUT_SECONDS = 5.0
|
|
70
|
+
_SESSION_IDLE_TIMEOUT_SECONDS = 5.0
|
|
71
|
+
_FRAME_IO_TIMEOUT_SECONDS = 2.0
|
|
72
|
+
_CHILD_EXIT_TIMEOUT_SECONDS = 120.0
|
|
73
|
+
_CHILD_REAP_TIMEOUT_SECONDS = 2.0
|
|
74
|
+
_CHILD_KILL_ATTEMPTS = 3
|
|
75
|
+
_CHILD_REAP_ATTEMPTS = 3
|
|
76
|
+
_AUTHORITY_FD_ENV = "MOSTLYRIGHT_REVIEW_AUTHORITY_FD"
|
|
77
|
+
# Hex shape checks, applied beyond the fields they are named for: _REQUEST_ID
|
|
78
|
+
# also checks enrollment_id and anchor_id, and _NONCE also checks session ids and
|
|
79
|
+
# SHA-256 digests.
|
|
80
|
+
_REQUEST_ID = re.compile(r"^[0-9a-f]{32}$")
|
|
81
|
+
_NONCE = re.compile(r"^[0-9a-f]{64}$")
|
|
82
|
+
_HANDLE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{31,127}$")
|
|
83
|
+
_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$")
|
|
84
|
+
_REQUEST_FIELDS = {
|
|
85
|
+
"schema_version",
|
|
86
|
+
"session_id",
|
|
87
|
+
"request_id",
|
|
88
|
+
"client_nonce",
|
|
89
|
+
"operation",
|
|
90
|
+
"reviewer_attempt",
|
|
91
|
+
"payload",
|
|
92
|
+
}
|
|
93
|
+
_RESPONSE_BODY_FIELDS = {
|
|
94
|
+
"schema_version",
|
|
95
|
+
"session_id",
|
|
96
|
+
"request_id",
|
|
97
|
+
"client_nonce",
|
|
98
|
+
"server_nonce",
|
|
99
|
+
"ok",
|
|
100
|
+
"result",
|
|
101
|
+
"error",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class _CoordinatorProtocolError(RuntimeError):
|
|
106
|
+
"""The coordinator transport or strict protocol failed closed."""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class _TrustRecord:
|
|
111
|
+
"""Client-side trust state taken from one session hello frame.
|
|
112
|
+
|
|
113
|
+
Only public_key is enforced afterwards: it verifies every response
|
|
114
|
+
signature. The enrollment fields record what the hello carried and bind
|
|
115
|
+
nothing later in the session.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
public_key: Ed25519PublicKey
|
|
119
|
+
enrollment_id: str
|
|
120
|
+
enrollment_digest: str
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class _Enrollment:
|
|
125
|
+
"""One signing role activated from the sealed enrollment and the key bundle.
|
|
126
|
+
|
|
127
|
+
role and reviewer_attempt decide which signing requests the session may
|
|
128
|
+
serve. handle_sha256 and lens record how the role was selected at activation
|
|
129
|
+
and are not rechecked per request.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
handle_sha256: str
|
|
133
|
+
role: str
|
|
134
|
+
lens: str | None
|
|
135
|
+
reviewer_attempt: str | None
|
|
136
|
+
private_key: Ed25519PrivateKey
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class _PeerClosed(RuntimeError):
|
|
140
|
+
"""The only inherited session peer closed its descriptor."""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class _InheritedRemoteSigner:
|
|
144
|
+
"""A role-bound signer proxy that never receives the coordinator private key."""
|
|
145
|
+
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
provider: _InheritedReviewAuthorityProvider,
|
|
149
|
+
reviewer_attempt: str | None,
|
|
150
|
+
public_key: Ed25519PublicKey,
|
|
151
|
+
) -> None:
|
|
152
|
+
self._provider = provider
|
|
153
|
+
self._reviewer_attempt = reviewer_attempt
|
|
154
|
+
self._public_key = public_key
|
|
155
|
+
|
|
156
|
+
def sign(self, data: bytes) -> bytes:
|
|
157
|
+
if not isinstance(data, bytes):
|
|
158
|
+
raise SignerProviderError("coordinator signer accepts exact bytes only")
|
|
159
|
+
result = self._provider._request(
|
|
160
|
+
operation="sign",
|
|
161
|
+
reviewer_attempt=self._reviewer_attempt,
|
|
162
|
+
payload=base64.b64encode(data).decode("ascii"),
|
|
163
|
+
)
|
|
164
|
+
if set(result) != {"signature"} or not isinstance(result["signature"], str):
|
|
165
|
+
raise SignerProviderError("coordinator returned an invalid signature response")
|
|
166
|
+
try:
|
|
167
|
+
signature = base64.b64decode(result["signature"], validate=True)
|
|
168
|
+
except (TypeError, ValueError) as exc:
|
|
169
|
+
raise SignerProviderError("coordinator returned invalid signature encoding") from exc
|
|
170
|
+
if len(signature) != 64:
|
|
171
|
+
raise SignerProviderError("coordinator returned an invalid Ed25519 signature")
|
|
172
|
+
try:
|
|
173
|
+
self._public_key.verify(signature, data)
|
|
174
|
+
except InvalidSignature as exc:
|
|
175
|
+
raise SignerProviderError(
|
|
176
|
+
"coordinator signature does not match the enrolled public key"
|
|
177
|
+
) from exc
|
|
178
|
+
return signature
|
|
179
|
+
|
|
180
|
+
def public_key(self) -> Ed25519PublicKey:
|
|
181
|
+
return self._public_key
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class _InheritedReviewAuthority:
|
|
185
|
+
"""One role-scoped authority view over an inherited anonymous session."""
|
|
186
|
+
|
|
187
|
+
def __init__(self, provider: _InheritedReviewAuthorityProvider) -> None:
|
|
188
|
+
self._provider = provider
|
|
189
|
+
|
|
190
|
+
def assignment_value(self) -> dict[str, Any]:
|
|
191
|
+
result = self._provider._request(
|
|
192
|
+
operation="assignment",
|
|
193
|
+
reviewer_attempt=None,
|
|
194
|
+
payload=None,
|
|
195
|
+
)
|
|
196
|
+
if set(result) != {"assignment"} or not isinstance(result["assignment"], dict):
|
|
197
|
+
raise SignerProviderError("coordinator returned an invalid assignment response")
|
|
198
|
+
return result["assignment"]
|
|
199
|
+
|
|
200
|
+
def reviewer_signer_for(self, reviewer_attempt: str) -> _InheritedRemoteSigner:
|
|
201
|
+
if not isinstance(reviewer_attempt, str) or _IDENTIFIER.fullmatch(reviewer_attempt) is None:
|
|
202
|
+
raise SignerProviderError("reviewer attempt is not a valid identifier")
|
|
203
|
+
public_key = self._public_key_for(reviewer_attempt)
|
|
204
|
+
return _InheritedRemoteSigner(
|
|
205
|
+
self._provider,
|
|
206
|
+
reviewer_attempt,
|
|
207
|
+
public_key,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def verifier_signer(self) -> _InheritedRemoteSigner:
|
|
211
|
+
public_key = self._public_key_for(None)
|
|
212
|
+
return _InheritedRemoteSigner(self._provider, None, public_key)
|
|
213
|
+
|
|
214
|
+
def _public_key_for(self, reviewer_attempt: str | None) -> Ed25519PublicKey:
|
|
215
|
+
result = self._provider._request(
|
|
216
|
+
operation="public_key",
|
|
217
|
+
reviewer_attempt=reviewer_attempt,
|
|
218
|
+
payload=None,
|
|
219
|
+
)
|
|
220
|
+
if set(result) != {"public_key"} or not isinstance(result["public_key"], str):
|
|
221
|
+
raise SignerProviderError("coordinator returned an invalid public-key response")
|
|
222
|
+
try:
|
|
223
|
+
return load_public_key_base64(result["public_key"])
|
|
224
|
+
except Exception as exc:
|
|
225
|
+
raise SignerProviderError("coordinator returned an invalid public key") from exc
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class _InheritedReviewAuthorityProvider:
|
|
229
|
+
"""Resolve one role-scoped authority through a launcher-created socketpair."""
|
|
230
|
+
|
|
231
|
+
def __init__(self) -> None:
|
|
232
|
+
if os.name == "nt" or not hasattr(socket, "AF_UNIX"):
|
|
233
|
+
raise SignerProviderError("Unix review coordinator transport is unavailable")
|
|
234
|
+
descriptor = _take_inherited_authority_descriptor()
|
|
235
|
+
connection: socket.socket | None = None
|
|
236
|
+
try:
|
|
237
|
+
connection = socket.socket(fileno=descriptor)
|
|
238
|
+
if connection.family != socket.AF_UNIX:
|
|
239
|
+
raise _CoordinatorProtocolError("authority descriptor is not a Unix socket")
|
|
240
|
+
raw_hello = _receive_frame(
|
|
241
|
+
connection,
|
|
242
|
+
idle_timeout=_SESSION_IDLE_TIMEOUT_SECONDS,
|
|
243
|
+
)
|
|
244
|
+
self._trust, self._session_id = _parse_session_hello(raw_hello)
|
|
245
|
+
self._connection = connection
|
|
246
|
+
except Exception as exc:
|
|
247
|
+
if connection is None:
|
|
248
|
+
try:
|
|
249
|
+
os.close(descriptor)
|
|
250
|
+
except OSError:
|
|
251
|
+
pass
|
|
252
|
+
else:
|
|
253
|
+
connection.close()
|
|
254
|
+
raise SignerProviderError("inherited coordinator session is unavailable") from exc
|
|
255
|
+
|
|
256
|
+
def authority(self) -> _InheritedReviewAuthority:
|
|
257
|
+
return _InheritedReviewAuthority(self)
|
|
258
|
+
|
|
259
|
+
def _request(
|
|
260
|
+
self,
|
|
261
|
+
*,
|
|
262
|
+
operation: str,
|
|
263
|
+
reviewer_attempt: str | None,
|
|
264
|
+
payload: str | None,
|
|
265
|
+
) -> dict[str, Any]:
|
|
266
|
+
request_id = uuid.uuid4().hex
|
|
267
|
+
client_nonce = secrets.token_hex(32)
|
|
268
|
+
request = {
|
|
269
|
+
"schema_version": _REQUEST_SCHEMA_VERSION,
|
|
270
|
+
"session_id": self._session_id,
|
|
271
|
+
"request_id": request_id,
|
|
272
|
+
"client_nonce": client_nonce,
|
|
273
|
+
"operation": operation,
|
|
274
|
+
"reviewer_attempt": reviewer_attempt,
|
|
275
|
+
"payload": payload,
|
|
276
|
+
}
|
|
277
|
+
try:
|
|
278
|
+
_send_frame(
|
|
279
|
+
self._connection,
|
|
280
|
+
canonical_json_bytes(request),
|
|
281
|
+
)
|
|
282
|
+
raw_response = _receive_frame(
|
|
283
|
+
self._connection,
|
|
284
|
+
idle_timeout=_SESSION_IDLE_TIMEOUT_SECONDS,
|
|
285
|
+
)
|
|
286
|
+
envelope = _parse_canonical_message(raw_response, "coordinator response")
|
|
287
|
+
return self._verify_response(
|
|
288
|
+
envelope,
|
|
289
|
+
request_id=request_id,
|
|
290
|
+
client_nonce=client_nonce,
|
|
291
|
+
)
|
|
292
|
+
except SignerProviderError:
|
|
293
|
+
raise
|
|
294
|
+
except Exception as exc:
|
|
295
|
+
raise SignerProviderError("coordinator request failed closed") from exc
|
|
296
|
+
|
|
297
|
+
def _verify_response(
|
|
298
|
+
self,
|
|
299
|
+
value: Any,
|
|
300
|
+
*,
|
|
301
|
+
request_id: str,
|
|
302
|
+
client_nonce: str,
|
|
303
|
+
) -> dict[str, Any]:
|
|
304
|
+
if not isinstance(value, dict) or set(value) != {"body", "signature"}:
|
|
305
|
+
raise _CoordinatorProtocolError("coordinator response envelope fields are invalid")
|
|
306
|
+
body = value["body"]
|
|
307
|
+
signature_text = value["signature"]
|
|
308
|
+
if not isinstance(body, dict) or set(body) != _RESPONSE_BODY_FIELDS:
|
|
309
|
+
raise _CoordinatorProtocolError("coordinator response body fields are invalid")
|
|
310
|
+
if (
|
|
311
|
+
body["schema_version"] != _RESPONSE_SCHEMA_VERSION
|
|
312
|
+
or body["session_id"] != self._session_id
|
|
313
|
+
or body["request_id"] != request_id
|
|
314
|
+
or body["client_nonce"] != client_nonce
|
|
315
|
+
or not isinstance(body["server_nonce"], str)
|
|
316
|
+
or _NONCE.fullmatch(body["server_nonce"]) is None
|
|
317
|
+
or type(body["ok"]) is not bool
|
|
318
|
+
):
|
|
319
|
+
raise _CoordinatorProtocolError("coordinator response binding is invalid")
|
|
320
|
+
if not isinstance(signature_text, str):
|
|
321
|
+
raise _CoordinatorProtocolError("coordinator response signature is invalid")
|
|
322
|
+
try:
|
|
323
|
+
signature = base64.b64decode(signature_text, validate=True)
|
|
324
|
+
except (TypeError, ValueError) as exc:
|
|
325
|
+
raise _CoordinatorProtocolError(
|
|
326
|
+
"coordinator response signature encoding is invalid"
|
|
327
|
+
) from exc
|
|
328
|
+
try:
|
|
329
|
+
self._trust.public_key.verify(signature, canonical_json_bytes(body))
|
|
330
|
+
except InvalidSignature as exc:
|
|
331
|
+
raise _CoordinatorProtocolError(
|
|
332
|
+
"coordinator response root signature is invalid"
|
|
333
|
+
) from exc
|
|
334
|
+
|
|
335
|
+
if body["ok"] is True:
|
|
336
|
+
if not isinstance(body["result"], dict) or body["error"] is not None:
|
|
337
|
+
raise _CoordinatorProtocolError("coordinator success response is invalid")
|
|
338
|
+
return body["result"]
|
|
339
|
+
error = body["error"]
|
|
340
|
+
if body["result"] is not None or not isinstance(error, dict):
|
|
341
|
+
raise _CoordinatorProtocolError("coordinator error response is invalid")
|
|
342
|
+
if set(error) != {"code", "message"} or not all(
|
|
343
|
+
isinstance(error[field], str) for field in ("code", "message")
|
|
344
|
+
):
|
|
345
|
+
raise _CoordinatorProtocolError("coordinator error fields are invalid")
|
|
346
|
+
raise SignerProviderError(f"coordinator rejected request: {error['code']}")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class _CoordinatorSessionServer:
|
|
350
|
+
"""Serve one socketpair session for the single role the child was launched as.
|
|
351
|
+
|
|
352
|
+
The first frame is a hello carrying the session id and the sealed
|
|
353
|
+
enrollment, from which the client derives the response root public key.
|
|
354
|
+
Every response after it is signed by that root key over its canonical body.
|
|
355
|
+
A request id or client nonce may be used once per session; a repeat is
|
|
356
|
+
answered with a replayed_request error rather than the operation. The
|
|
357
|
+
assignment is readable by any session, while public_key and sign are refused
|
|
358
|
+
unless the request names this session's own role. The session ends after
|
|
359
|
+
_MAX_SESSION_REQUESTS requests, at the child deadline, or when the peer
|
|
360
|
+
closes.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
def __init__(
|
|
364
|
+
self,
|
|
365
|
+
*,
|
|
366
|
+
connection: socket.socket,
|
|
367
|
+
assignment: dict[str, Any],
|
|
368
|
+
enrollment: _Enrollment,
|
|
369
|
+
root_private_key: Ed25519PrivateKey,
|
|
370
|
+
sealed_enrollment: dict[str, Any],
|
|
371
|
+
) -> None:
|
|
372
|
+
self.connection = connection
|
|
373
|
+
self.assignment = assignment
|
|
374
|
+
self.enrollment = enrollment
|
|
375
|
+
self.root_private_key = root_private_key
|
|
376
|
+
self.sealed_enrollment = sealed_enrollment
|
|
377
|
+
self.session_id = secrets.token_hex(32)
|
|
378
|
+
self._seen_request_ids: set[str] = set()
|
|
379
|
+
self._seen_client_nonces: set[str] = set()
|
|
380
|
+
|
|
381
|
+
def serve(self, *, deadline: float) -> None:
|
|
382
|
+
hello = {
|
|
383
|
+
"schema_version": _SESSION_SCHEMA_VERSION,
|
|
384
|
+
"session_id": self.session_id,
|
|
385
|
+
"sealed_enrollment": self.sealed_enrollment,
|
|
386
|
+
}
|
|
387
|
+
_send_frame(self.connection, canonical_json_bytes(hello))
|
|
388
|
+
for _request_number in range(_MAX_SESSION_REQUESTS):
|
|
389
|
+
remaining = deadline - time.monotonic()
|
|
390
|
+
if remaining <= 0:
|
|
391
|
+
raise TimeoutError("coordinator session deadline expired")
|
|
392
|
+
try:
|
|
393
|
+
raw_request = _receive_frame(
|
|
394
|
+
self.connection,
|
|
395
|
+
idle_timeout=min(_SESSION_IDLE_TIMEOUT_SECONDS, remaining),
|
|
396
|
+
)
|
|
397
|
+
except _PeerClosed:
|
|
398
|
+
return
|
|
399
|
+
request = _parse_request(raw_request)
|
|
400
|
+
if request["session_id"] != self.session_id:
|
|
401
|
+
raise _CoordinatorProtocolError("request is bound to another session")
|
|
402
|
+
_send_frame(self.connection, self._dispatch(request))
|
|
403
|
+
raise _CoordinatorProtocolError("coordinator session request limit exceeded")
|
|
404
|
+
|
|
405
|
+
def _dispatch(self, request: dict[str, Any]) -> bytes:
|
|
406
|
+
request_id = request["request_id"]
|
|
407
|
+
client_nonce = request["client_nonce"]
|
|
408
|
+
if request_id in self._seen_request_ids or client_nonce in self._seen_client_nonces:
|
|
409
|
+
return self._signed_response(
|
|
410
|
+
request_id,
|
|
411
|
+
client_nonce,
|
|
412
|
+
ok=False,
|
|
413
|
+
error_code="replayed_request",
|
|
414
|
+
error_message="request identity has already been used",
|
|
415
|
+
)
|
|
416
|
+
self._seen_request_ids.add(request_id)
|
|
417
|
+
self._seen_client_nonces.add(client_nonce)
|
|
418
|
+
|
|
419
|
+
operation = request["operation"]
|
|
420
|
+
reviewer_attempt = request["reviewer_attempt"]
|
|
421
|
+
if operation == "assignment":
|
|
422
|
+
return self._signed_response(
|
|
423
|
+
request_id,
|
|
424
|
+
client_nonce,
|
|
425
|
+
ok=True,
|
|
426
|
+
result={"assignment": self.assignment},
|
|
427
|
+
)
|
|
428
|
+
if not self._authorized_signer_request(self.enrollment, reviewer_attempt):
|
|
429
|
+
return self._signed_response(
|
|
430
|
+
request_id,
|
|
431
|
+
client_nonce,
|
|
432
|
+
ok=False,
|
|
433
|
+
error_code="role_not_authorized",
|
|
434
|
+
error_message="inherited session is not authorized for this signing role",
|
|
435
|
+
)
|
|
436
|
+
if operation == "public_key":
|
|
437
|
+
return self._signed_response(
|
|
438
|
+
request_id,
|
|
439
|
+
client_nonce,
|
|
440
|
+
ok=True,
|
|
441
|
+
result={
|
|
442
|
+
"public_key": public_key_to_base64(self.enrollment.private_key.public_key())
|
|
443
|
+
},
|
|
444
|
+
)
|
|
445
|
+
if operation == "sign":
|
|
446
|
+
try:
|
|
447
|
+
payload = base64.b64decode(request["payload"], validate=True)
|
|
448
|
+
except (TypeError, ValueError):
|
|
449
|
+
return self._signed_response(
|
|
450
|
+
request_id,
|
|
451
|
+
client_nonce,
|
|
452
|
+
ok=False,
|
|
453
|
+
error_code="invalid_payload",
|
|
454
|
+
error_message="signing payload encoding is invalid",
|
|
455
|
+
)
|
|
456
|
+
if len(payload) > _MAX_SIGNING_BYTES:
|
|
457
|
+
return self._signed_response(
|
|
458
|
+
request_id,
|
|
459
|
+
client_nonce,
|
|
460
|
+
ok=False,
|
|
461
|
+
error_code="payload_too_large",
|
|
462
|
+
error_message="signing payload exceeds the byte limit",
|
|
463
|
+
)
|
|
464
|
+
signature = self.enrollment.private_key.sign(payload)
|
|
465
|
+
return self._signed_response(
|
|
466
|
+
request_id,
|
|
467
|
+
client_nonce,
|
|
468
|
+
ok=True,
|
|
469
|
+
result={"signature": base64.b64encode(signature).decode("ascii")},
|
|
470
|
+
)
|
|
471
|
+
raise _CoordinatorProtocolError("validated request operation is unreachable")
|
|
472
|
+
|
|
473
|
+
@staticmethod
|
|
474
|
+
def _authorized_signer_request(
|
|
475
|
+
enrollment: _Enrollment,
|
|
476
|
+
reviewer_attempt: str | None,
|
|
477
|
+
) -> bool:
|
|
478
|
+
if enrollment.role == "verifier":
|
|
479
|
+
return reviewer_attempt is None
|
|
480
|
+
return reviewer_attempt == enrollment.reviewer_attempt
|
|
481
|
+
|
|
482
|
+
def _signed_response(
|
|
483
|
+
self,
|
|
484
|
+
request_id: str,
|
|
485
|
+
client_nonce: str,
|
|
486
|
+
*,
|
|
487
|
+
ok: bool,
|
|
488
|
+
result: dict[str, Any] | None = None,
|
|
489
|
+
error_code: str | None = None,
|
|
490
|
+
error_message: str | None = None,
|
|
491
|
+
) -> bytes:
|
|
492
|
+
body = {
|
|
493
|
+
"schema_version": _RESPONSE_SCHEMA_VERSION,
|
|
494
|
+
"session_id": self.session_id,
|
|
495
|
+
"request_id": request_id,
|
|
496
|
+
"client_nonce": client_nonce,
|
|
497
|
+
"server_nonce": secrets.token_hex(32),
|
|
498
|
+
"ok": ok,
|
|
499
|
+
"result": result if ok else None,
|
|
500
|
+
"error": (
|
|
501
|
+
None
|
|
502
|
+
if ok
|
|
503
|
+
else {
|
|
504
|
+
"code": error_code,
|
|
505
|
+
"message": error_message,
|
|
506
|
+
}
|
|
507
|
+
),
|
|
508
|
+
}
|
|
509
|
+
signature = self.root_private_key.sign(canonical_json_bytes(body))
|
|
510
|
+
response = {
|
|
511
|
+
"body": body,
|
|
512
|
+
"signature": base64.b64encode(signature).decode("ascii"),
|
|
513
|
+
}
|
|
514
|
+
encoded = canonical_json_bytes(response)
|
|
515
|
+
if len(encoded) > _MAX_MESSAGE_BYTES:
|
|
516
|
+
raise _CoordinatorProtocolError("coordinator response exceeds the byte limit")
|
|
517
|
+
return encoded
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def coordinator_main(argv: list[str] | None = None) -> int:
|
|
521
|
+
"""Prepare metadata or launch one role-scoped anonymous authority session."""
|
|
522
|
+
|
|
523
|
+
parser = argparse.ArgumentParser(prog="mr-data-coordinator")
|
|
524
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
525
|
+
prepare = subparsers.add_parser("prepare")
|
|
526
|
+
prepare.add_argument("--workspace", required=True)
|
|
527
|
+
prepare.add_argument("--sealed-enrollment-fd", type=_descriptor_argument, required=True)
|
|
528
|
+
run = subparsers.add_parser("run")
|
|
529
|
+
run.add_argument("--workspace", required=True)
|
|
530
|
+
run.add_argument("--sealed-enrollment-fd", type=_descriptor_argument, required=True)
|
|
531
|
+
run.add_argument("--key-bundle-fd", type=_descriptor_argument, required=True)
|
|
532
|
+
run.add_argument("--role-token-fd", type=_descriptor_argument, required=True)
|
|
533
|
+
run.add_argument("child_command", nargs=argparse.REMAINDER)
|
|
534
|
+
args = parser.parse_args(argv)
|
|
535
|
+
if os.name == "nt" or not hasattr(socket, "AF_UNIX"):
|
|
536
|
+
parser.error("Unix socketpair transport is required")
|
|
537
|
+
|
|
538
|
+
workspace = Path(os.path.abspath(args.workspace))
|
|
539
|
+
try:
|
|
540
|
+
candidate = verify_candidate(review_target_path(workspace))
|
|
541
|
+
descriptor_values = [args.sealed_enrollment_fd]
|
|
542
|
+
if args.command == "run":
|
|
543
|
+
descriptor_values.extend([args.key_bundle_fd, args.role_token_fd])
|
|
544
|
+
if len(set(descriptor_values)) != len(descriptor_values):
|
|
545
|
+
raise _CoordinatorProtocolError("coordinator input descriptors must be distinct")
|
|
546
|
+
sealed_enrollment = _read_canonical_descriptor(
|
|
547
|
+
args.sealed_enrollment_fd,
|
|
548
|
+
label="sealed enrollment",
|
|
549
|
+
)
|
|
550
|
+
if args.command == "prepare":
|
|
551
|
+
assignment, enrollment_id = _prepare_enrollment(
|
|
552
|
+
candidate,
|
|
553
|
+
sealed_enrollment=sealed_enrollment,
|
|
554
|
+
)
|
|
555
|
+
startup = {
|
|
556
|
+
"schema_version": _STARTUP_SCHEMA_VERSION,
|
|
557
|
+
"assignment": assignment,
|
|
558
|
+
"report_templates": _report_templates(candidate, assignment),
|
|
559
|
+
"enrollment_id": enrollment_id,
|
|
560
|
+
}
|
|
561
|
+
sys.stdout.buffer.write(canonical_json_bytes(startup) + b"\n")
|
|
562
|
+
sys.stdout.buffer.flush()
|
|
563
|
+
return 0
|
|
564
|
+
|
|
565
|
+
key_bundle = _read_canonical_descriptor(
|
|
566
|
+
args.key_bundle_fd,
|
|
567
|
+
label="coordinator key bundle",
|
|
568
|
+
)
|
|
569
|
+
role_token_commitment = _read_role_token_commitment_descriptor(args.role_token_fd)
|
|
570
|
+
child_command = list(args.child_command)
|
|
571
|
+
if child_command and child_command[0] == "--":
|
|
572
|
+
child_command.pop(0)
|
|
573
|
+
if not child_command:
|
|
574
|
+
raise _CoordinatorProtocolError("coordinator child command is required")
|
|
575
|
+
assignment, enrollments, root_private_key, _enrollment_id = _activate_enrollment(
|
|
576
|
+
candidate,
|
|
577
|
+
sealed_enrollment=sealed_enrollment,
|
|
578
|
+
key_bundle=key_bundle,
|
|
579
|
+
)
|
|
580
|
+
enrollment = enrollments.get(role_token_commitment)
|
|
581
|
+
if enrollment is None:
|
|
582
|
+
raise _CoordinatorProtocolError("role token is not enrolled")
|
|
583
|
+
return _run_authorized_child(
|
|
584
|
+
child_command,
|
|
585
|
+
assignment=assignment,
|
|
586
|
+
enrollment=enrollment,
|
|
587
|
+
root_private_key=root_private_key,
|
|
588
|
+
sealed_enrollment=sealed_enrollment,
|
|
589
|
+
)
|
|
590
|
+
except (
|
|
591
|
+
CanonicalJSONError,
|
|
592
|
+
OSError,
|
|
593
|
+
subprocess.SubprocessError,
|
|
594
|
+
TimeoutError,
|
|
595
|
+
ValueError,
|
|
596
|
+
ReviewError,
|
|
597
|
+
SignerProviderError,
|
|
598
|
+
_CoordinatorProtocolError,
|
|
599
|
+
) as exc:
|
|
600
|
+
print(f"coordinator startup failed: {type(exc).__name__}", file=sys.stderr)
|
|
601
|
+
return 2
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _prepare_enrollment(
|
|
605
|
+
candidate: Any,
|
|
606
|
+
*,
|
|
607
|
+
sealed_enrollment: Any,
|
|
608
|
+
) -> tuple[dict[str, Any], str]:
|
|
609
|
+
body = _verified_enrollment_body(candidate, sealed_enrollment)
|
|
610
|
+
return _assignment_from_enrollment_body(body), body["enrollment_id"]
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _activate_enrollment(
|
|
614
|
+
candidate: Any,
|
|
615
|
+
*,
|
|
616
|
+
sealed_enrollment: Any,
|
|
617
|
+
key_bundle: Any,
|
|
618
|
+
) -> tuple[
|
|
619
|
+
dict[str, Any],
|
|
620
|
+
dict[str, _Enrollment],
|
|
621
|
+
Ed25519PrivateKey,
|
|
622
|
+
str,
|
|
623
|
+
]:
|
|
624
|
+
body = _verified_enrollment_body(candidate, sealed_enrollment)
|
|
625
|
+
assignment = _assignment_from_enrollment_body(body)
|
|
626
|
+
bundle = _parse_key_bundle(key_bundle, enrollment_id=body["enrollment_id"])
|
|
627
|
+
root_private_key = bundle["response_root_private_key"]
|
|
628
|
+
if public_key_to_base64(root_private_key.public_key()) != body["response_root_public_key"]:
|
|
629
|
+
raise _CoordinatorProtocolError(
|
|
630
|
+
"coordinator response key does not match the sealed enrollment"
|
|
631
|
+
)
|
|
632
|
+
enrollments: dict[str, _Enrollment] = {}
|
|
633
|
+
for reviewer in body["reviewers"]:
|
|
634
|
+
lens = reviewer["lens"]
|
|
635
|
+
key = bundle["reviewer_private_keys"][lens]
|
|
636
|
+
if public_key_to_base64(key.public_key()) != reviewer["public_key"]:
|
|
637
|
+
raise _CoordinatorProtocolError(
|
|
638
|
+
"reviewer key bundle does not match the sealed enrollment"
|
|
639
|
+
)
|
|
640
|
+
handle_sha256 = reviewer["handle_sha256"]
|
|
641
|
+
enrollments[handle_sha256] = _Enrollment(
|
|
642
|
+
handle_sha256=handle_sha256,
|
|
643
|
+
role="reviewer",
|
|
644
|
+
lens=lens,
|
|
645
|
+
reviewer_attempt=reviewer["attempt_id"],
|
|
646
|
+
private_key=key,
|
|
647
|
+
)
|
|
648
|
+
verifier_key = bundle["verifier_private_key"]
|
|
649
|
+
if public_key_to_base64(verifier_key.public_key()) != body["verifier"]["public_key"]:
|
|
650
|
+
raise _CoordinatorProtocolError("verifier key bundle does not match the sealed enrollment")
|
|
651
|
+
verifier_handle_sha256 = body["verifier"]["handle_sha256"]
|
|
652
|
+
enrollments[verifier_handle_sha256] = _Enrollment(
|
|
653
|
+
handle_sha256=verifier_handle_sha256,
|
|
654
|
+
role="verifier",
|
|
655
|
+
lens=None,
|
|
656
|
+
reviewer_attempt=None,
|
|
657
|
+
private_key=verifier_key,
|
|
658
|
+
)
|
|
659
|
+
if len(enrollments) != 3:
|
|
660
|
+
raise _CoordinatorProtocolError("sealed enrollment has duplicate role handles")
|
|
661
|
+
return assignment, enrollments, root_private_key, body["enrollment_id"]
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _verified_enrollment_body(candidate: Any, sealed_enrollment: Any) -> dict[str, Any]:
|
|
665
|
+
body = _verify_sealed_enrollment(sealed_enrollment, _load_anchor_public_key())
|
|
666
|
+
trusted_producer = {
|
|
667
|
+
"principal_id": _H2_PRODUCER_PRINCIPAL,
|
|
668
|
+
"attempt_id": _H2_PRODUCER_ATTEMPT,
|
|
669
|
+
}
|
|
670
|
+
if (
|
|
671
|
+
body["candidate_digest"] != candidate.candidate_digest
|
|
672
|
+
or body["validation_policy_digest"] != candidate.validation_policy_digest
|
|
673
|
+
or body["producer"] != trusted_producer
|
|
674
|
+
or {
|
|
675
|
+
"principal_id": candidate.producer_principal,
|
|
676
|
+
"attempt_id": candidate.producer_attempt,
|
|
677
|
+
}
|
|
678
|
+
!= trusted_producer
|
|
679
|
+
):
|
|
680
|
+
raise _CoordinatorProtocolError(
|
|
681
|
+
"sealed enrollment does not bind the exact candidate and fixed H2 producer"
|
|
682
|
+
)
|
|
683
|
+
return body
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _assignment_from_enrollment_body(body: dict[str, Any]) -> dict[str, Any]:
|
|
687
|
+
reviewers = [
|
|
688
|
+
{key: value for key, value in reviewer.items() if key != "handle_sha256"}
|
|
689
|
+
for reviewer in body["reviewers"]
|
|
690
|
+
]
|
|
691
|
+
assignment = _parse_assignment(
|
|
692
|
+
{
|
|
693
|
+
"schema_version": ASSIGNMENT_SCHEMA_VERSION,
|
|
694
|
+
"candidate_digest": body["candidate_digest"],
|
|
695
|
+
"validation_policy_digest": body["validation_policy_digest"],
|
|
696
|
+
"cycle_lineage": body["cycle_lineage"],
|
|
697
|
+
"failed_cycle_count": 0,
|
|
698
|
+
"producer": body["producer"],
|
|
699
|
+
"reviewers": reviewers,
|
|
700
|
+
"verifier": {
|
|
701
|
+
"principal_id": body["verifier"]["principal_id"],
|
|
702
|
+
"public_key": body["verifier"]["public_key"],
|
|
703
|
+
},
|
|
704
|
+
}
|
|
705
|
+
)
|
|
706
|
+
return assignment
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _verify_sealed_enrollment(
|
|
710
|
+
value: Any,
|
|
711
|
+
anchor_public_key: Ed25519PublicKey,
|
|
712
|
+
) -> dict[str, Any]:
|
|
713
|
+
if not isinstance(value, dict) or set(value) != {"body", "signature"}:
|
|
714
|
+
raise _CoordinatorProtocolError("sealed enrollment envelope is invalid")
|
|
715
|
+
body = value["body"]
|
|
716
|
+
signature_text = value["signature"]
|
|
717
|
+
if not isinstance(body, dict) or set(body) != {
|
|
718
|
+
"schema_version",
|
|
719
|
+
"enrollment_id",
|
|
720
|
+
"candidate_digest",
|
|
721
|
+
"validation_policy_digest",
|
|
722
|
+
"cycle_lineage",
|
|
723
|
+
"producer",
|
|
724
|
+
"reviewers",
|
|
725
|
+
"verifier",
|
|
726
|
+
"response_root_public_key",
|
|
727
|
+
}:
|
|
728
|
+
raise _CoordinatorProtocolError("sealed enrollment fields are invalid")
|
|
729
|
+
if (
|
|
730
|
+
body["schema_version"] != _SEALED_ENROLLMENT_SCHEMA_VERSION
|
|
731
|
+
or not isinstance(body["enrollment_id"], str)
|
|
732
|
+
or _REQUEST_ID.fullmatch(body["enrollment_id"]) is None
|
|
733
|
+
or not isinstance(body["candidate_digest"], str)
|
|
734
|
+
or _NONCE.fullmatch(body["candidate_digest"]) is None
|
|
735
|
+
or not isinstance(body["validation_policy_digest"], str)
|
|
736
|
+
or _NONCE.fullmatch(body["validation_policy_digest"]) is None
|
|
737
|
+
or not isinstance(body["cycle_lineage"], str)
|
|
738
|
+
or _IDENTIFIER.fullmatch(body["cycle_lineage"]) is None
|
|
739
|
+
or not isinstance(body["producer"], dict)
|
|
740
|
+
or set(body["producer"]) != {"principal_id", "attempt_id"}
|
|
741
|
+
):
|
|
742
|
+
raise _CoordinatorProtocolError("sealed enrollment binding is invalid")
|
|
743
|
+
reviewers = body["reviewers"]
|
|
744
|
+
if not isinstance(reviewers, list) or len(reviewers) != 2:
|
|
745
|
+
raise _CoordinatorProtocolError("sealed enrollment reviewer set is invalid")
|
|
746
|
+
expected_reviewer_fields = {
|
|
747
|
+
"principal_id",
|
|
748
|
+
"attempt_id",
|
|
749
|
+
"lens",
|
|
750
|
+
"key_id",
|
|
751
|
+
"public_key",
|
|
752
|
+
"handle_sha256",
|
|
753
|
+
}
|
|
754
|
+
for reviewer in reviewers:
|
|
755
|
+
if (
|
|
756
|
+
not isinstance(reviewer, dict)
|
|
757
|
+
or set(reviewer) != expected_reviewer_fields
|
|
758
|
+
or not isinstance(reviewer["handle_sha256"], str)
|
|
759
|
+
or _NONCE.fullmatch(reviewer["handle_sha256"]) is None
|
|
760
|
+
):
|
|
761
|
+
raise _CoordinatorProtocolError("sealed enrollment reviewer entry is invalid")
|
|
762
|
+
verifier = body["verifier"]
|
|
763
|
+
if (
|
|
764
|
+
not isinstance(verifier, dict)
|
|
765
|
+
or set(verifier) != {"principal_id", "public_key", "handle_sha256"}
|
|
766
|
+
or not isinstance(verifier["handle_sha256"], str)
|
|
767
|
+
or _NONCE.fullmatch(verifier["handle_sha256"]) is None
|
|
768
|
+
or not isinstance(body["response_root_public_key"], str)
|
|
769
|
+
):
|
|
770
|
+
raise _CoordinatorProtocolError("sealed enrollment verifier entry is invalid")
|
|
771
|
+
role_hashes = [
|
|
772
|
+
*(reviewer["handle_sha256"] for reviewer in reviewers),
|
|
773
|
+
verifier["handle_sha256"],
|
|
774
|
+
]
|
|
775
|
+
if len(set(role_hashes)) != 3:
|
|
776
|
+
raise _CoordinatorProtocolError("sealed enrollment role handle hashes must be distinct")
|
|
777
|
+
role_public_keys = [
|
|
778
|
+
*(reviewer["public_key"] for reviewer in reviewers),
|
|
779
|
+
verifier["public_key"],
|
|
780
|
+
]
|
|
781
|
+
for public_key in [*role_public_keys, body["response_root_public_key"]]:
|
|
782
|
+
load_public_key_base64(public_key)
|
|
783
|
+
if len(set(role_public_keys)) != 3 or body["response_root_public_key"] in role_public_keys:
|
|
784
|
+
raise _CoordinatorProtocolError("sealed enrollment signing keys must be distinct")
|
|
785
|
+
if not isinstance(signature_text, str):
|
|
786
|
+
raise _CoordinatorProtocolError("sealed enrollment signature is invalid")
|
|
787
|
+
try:
|
|
788
|
+
signature = base64.b64decode(signature_text, validate=True)
|
|
789
|
+
anchor_public_key.verify(signature, canonical_json_bytes(body))
|
|
790
|
+
except (InvalidSignature, TypeError, ValueError) as exc:
|
|
791
|
+
raise _CoordinatorProtocolError(
|
|
792
|
+
"sealed enrollment signature is not from the trusted anchor"
|
|
793
|
+
) from exc
|
|
794
|
+
return body
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def _parse_key_bundle(
|
|
798
|
+
value: Any,
|
|
799
|
+
*,
|
|
800
|
+
enrollment_id: str,
|
|
801
|
+
) -> dict[str, Any]:
|
|
802
|
+
if not isinstance(value, dict) or set(value) != {
|
|
803
|
+
"schema_version",
|
|
804
|
+
"enrollment_id",
|
|
805
|
+
"response_root_private_key",
|
|
806
|
+
"reviewer_private_keys",
|
|
807
|
+
"verifier_private_key",
|
|
808
|
+
}:
|
|
809
|
+
raise _CoordinatorProtocolError("coordinator key bundle fields are invalid")
|
|
810
|
+
if (
|
|
811
|
+
value["schema_version"] != _KEY_BUNDLE_SCHEMA_VERSION
|
|
812
|
+
or value["enrollment_id"] != enrollment_id
|
|
813
|
+
or not isinstance(value["reviewer_private_keys"], dict)
|
|
814
|
+
or set(value["reviewer_private_keys"])
|
|
815
|
+
!= {"question_source_fit", "transformation_data_quality"}
|
|
816
|
+
):
|
|
817
|
+
raise _CoordinatorProtocolError("coordinator key bundle binding is invalid")
|
|
818
|
+
return {
|
|
819
|
+
"response_root_private_key": _load_private_key(value["response_root_private_key"]),
|
|
820
|
+
"reviewer_private_keys": {
|
|
821
|
+
lens: _load_private_key(encoded)
|
|
822
|
+
for lens, encoded in value["reviewer_private_keys"].items()
|
|
823
|
+
},
|
|
824
|
+
"verifier_private_key": _load_private_key(value["verifier_private_key"]),
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def _load_private_key(value: Any) -> Ed25519PrivateKey:
|
|
829
|
+
if not isinstance(value, str):
|
|
830
|
+
raise _CoordinatorProtocolError("coordinator private key encoding is invalid")
|
|
831
|
+
try:
|
|
832
|
+
raw = base64.b64decode(value, validate=True)
|
|
833
|
+
if len(raw) != 32:
|
|
834
|
+
raise ValueError
|
|
835
|
+
return Ed25519PrivateKey.from_private_bytes(raw)
|
|
836
|
+
except (TypeError, ValueError) as exc:
|
|
837
|
+
raise _CoordinatorProtocolError("coordinator private key encoding is invalid") from exc
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _report_templates(candidate: Any, assignment: dict[str, Any]) -> dict[str, Any]:
|
|
841
|
+
assignment_digest = _normalized_assignment_digest(assignment)
|
|
842
|
+
reviewed_paths = _candidate_artifact_paths(candidate)
|
|
843
|
+
return {
|
|
844
|
+
reviewer["lens"]: {
|
|
845
|
+
"schema_version": REPORT_SCHEMA_VERSION,
|
|
846
|
+
"assignment_digest": assignment_digest,
|
|
847
|
+
"candidate_digest": assignment["candidate_digest"],
|
|
848
|
+
"validation_policy_digest": assignment["validation_policy_digest"],
|
|
849
|
+
"cycle_lineage": assignment["cycle_lineage"],
|
|
850
|
+
"failed_cycle_count": assignment["failed_cycle_count"],
|
|
851
|
+
"reviewer_principal": reviewer["principal_id"],
|
|
852
|
+
"reviewer_attempt": reviewer["attempt_id"],
|
|
853
|
+
"reviewer_key_id": reviewer["key_id"],
|
|
854
|
+
"lens": reviewer["lens"],
|
|
855
|
+
"reviewed_artifact_paths": reviewed_paths,
|
|
856
|
+
"summary": "REPLACE_WITH_INDEPENDENT_REVIEW_SUMMARY",
|
|
857
|
+
"findings": [],
|
|
858
|
+
}
|
|
859
|
+
for reviewer in assignment["reviewers"]
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def _parse_request(raw: bytes) -> dict[str, Any]:
|
|
864
|
+
value = _parse_canonical_message(raw, "coordinator request")
|
|
865
|
+
if not isinstance(value, dict) or set(value) != _REQUEST_FIELDS:
|
|
866
|
+
raise _CoordinatorProtocolError("coordinator request fields are invalid")
|
|
867
|
+
if (
|
|
868
|
+
value["schema_version"] != _REQUEST_SCHEMA_VERSION
|
|
869
|
+
or not isinstance(value["session_id"], str)
|
|
870
|
+
or _NONCE.fullmatch(value["session_id"]) is None
|
|
871
|
+
or not isinstance(value["request_id"], str)
|
|
872
|
+
or _REQUEST_ID.fullmatch(value["request_id"]) is None
|
|
873
|
+
or not isinstance(value["client_nonce"], str)
|
|
874
|
+
or _NONCE.fullmatch(value["client_nonce"]) is None
|
|
875
|
+
or value["operation"] not in {"assignment", "public_key", "sign"}
|
|
876
|
+
):
|
|
877
|
+
raise _CoordinatorProtocolError("coordinator request binding is invalid")
|
|
878
|
+
operation = value["operation"]
|
|
879
|
+
reviewer_attempt = value["reviewer_attempt"]
|
|
880
|
+
payload = value["payload"]
|
|
881
|
+
if reviewer_attempt is not None and (
|
|
882
|
+
not isinstance(reviewer_attempt, str) or _IDENTIFIER.fullmatch(reviewer_attempt) is None
|
|
883
|
+
):
|
|
884
|
+
raise _CoordinatorProtocolError("coordinator reviewer attempt is invalid")
|
|
885
|
+
if operation == "assignment":
|
|
886
|
+
if reviewer_attempt is not None or payload is not None:
|
|
887
|
+
raise _CoordinatorProtocolError("assignment request contains signer fields")
|
|
888
|
+
elif operation == "public_key":
|
|
889
|
+
if payload is not None:
|
|
890
|
+
raise _CoordinatorProtocolError("public-key request contains a payload")
|
|
891
|
+
elif not isinstance(payload, str):
|
|
892
|
+
raise _CoordinatorProtocolError("sign request payload is invalid")
|
|
893
|
+
return value
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def _parse_session_hello(raw: bytes) -> tuple[_TrustRecord, str]:
|
|
897
|
+
value = _parse_canonical_message(raw, "coordinator session hello")
|
|
898
|
+
if not isinstance(value, dict) or set(value) != {
|
|
899
|
+
"schema_version",
|
|
900
|
+
"session_id",
|
|
901
|
+
"sealed_enrollment",
|
|
902
|
+
}:
|
|
903
|
+
raise _CoordinatorProtocolError("coordinator session hello fields are invalid")
|
|
904
|
+
session_id = value["session_id"]
|
|
905
|
+
if (
|
|
906
|
+
value["schema_version"] != _SESSION_SCHEMA_VERSION
|
|
907
|
+
or not isinstance(session_id, str)
|
|
908
|
+
or _NONCE.fullmatch(session_id) is None
|
|
909
|
+
):
|
|
910
|
+
raise _CoordinatorProtocolError("coordinator session hello binding is invalid")
|
|
911
|
+
enrollment_body = _verify_sealed_enrollment(
|
|
912
|
+
value["sealed_enrollment"],
|
|
913
|
+
_load_anchor_public_key(),
|
|
914
|
+
)
|
|
915
|
+
public_key = load_public_key_base64(enrollment_body["response_root_public_key"])
|
|
916
|
+
return (
|
|
917
|
+
_TrustRecord(
|
|
918
|
+
public_key=public_key,
|
|
919
|
+
enrollment_id=enrollment_body["enrollment_id"],
|
|
920
|
+
enrollment_digest=hashlib.sha256(
|
|
921
|
+
canonical_json_bytes(value["sealed_enrollment"])
|
|
922
|
+
).hexdigest(),
|
|
923
|
+
),
|
|
924
|
+
session_id,
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def _send_frame(connection: socket.socket, payload: bytes) -> None:
|
|
929
|
+
if not payload or len(payload) > _MAX_MESSAGE_BYTES:
|
|
930
|
+
raise _CoordinatorProtocolError("coordinator frame size is invalid")
|
|
931
|
+
framed = struct.pack("!I", len(payload)) + payload
|
|
932
|
+
deadline = time.monotonic() + _FRAME_IO_TIMEOUT_SECONDS
|
|
933
|
+
offset = 0
|
|
934
|
+
while offset < len(framed):
|
|
935
|
+
remaining = deadline - time.monotonic()
|
|
936
|
+
if remaining <= 0:
|
|
937
|
+
raise TimeoutError("coordinator frame write deadline expired")
|
|
938
|
+
_readable, writable, _exceptional = select.select([], [connection], [], remaining)
|
|
939
|
+
if not writable:
|
|
940
|
+
raise TimeoutError("coordinator frame write deadline expired")
|
|
941
|
+
try:
|
|
942
|
+
written = connection.send(framed[offset:])
|
|
943
|
+
except BlockingIOError:
|
|
944
|
+
continue
|
|
945
|
+
if written <= 0:
|
|
946
|
+
raise _PeerClosed("coordinator session peer closed during frame write")
|
|
947
|
+
offset += written
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _receive_frame(
|
|
951
|
+
connection: socket.socket,
|
|
952
|
+
*,
|
|
953
|
+
idle_timeout: float,
|
|
954
|
+
) -> bytes:
|
|
955
|
+
header = _receive_exact(connection, 4, timeout=idle_timeout)
|
|
956
|
+
(length,) = struct.unpack("!I", header)
|
|
957
|
+
if length <= 0 or length > _MAX_MESSAGE_BYTES:
|
|
958
|
+
raise _CoordinatorProtocolError("coordinator frame size is invalid")
|
|
959
|
+
return _receive_exact(
|
|
960
|
+
connection,
|
|
961
|
+
length,
|
|
962
|
+
timeout=_FRAME_IO_TIMEOUT_SECONDS,
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _receive_exact(connection: socket.socket, size: int, *, timeout: float) -> bytes:
|
|
967
|
+
deadline = time.monotonic() + timeout
|
|
968
|
+
chunks: list[bytes] = []
|
|
969
|
+
remaining_bytes = size
|
|
970
|
+
while remaining_bytes:
|
|
971
|
+
remaining_time = deadline - time.monotonic()
|
|
972
|
+
if remaining_time <= 0:
|
|
973
|
+
raise TimeoutError("coordinator frame read deadline expired")
|
|
974
|
+
readable, _writable, _exceptional = select.select([connection], [], [], remaining_time)
|
|
975
|
+
if not readable:
|
|
976
|
+
raise TimeoutError("coordinator frame read deadline expired")
|
|
977
|
+
try:
|
|
978
|
+
chunk = connection.recv(remaining_bytes)
|
|
979
|
+
except BlockingIOError:
|
|
980
|
+
continue
|
|
981
|
+
except ConnectionResetError as exc:
|
|
982
|
+
# A peer that exits with unread frames still buffered resets the socketpair
|
|
983
|
+
# on Linux, where macOS delivers plain EOF for the same abandonment; the two
|
|
984
|
+
# kernels must share one contract, so reset-at-frame-start is the peer
|
|
985
|
+
# closing and reset-mid-frame is the same truncation EOF would be.
|
|
986
|
+
if remaining_bytes == size:
|
|
987
|
+
raise _PeerClosed("coordinator session peer closed") from exc
|
|
988
|
+
raise _CoordinatorProtocolError("coordinator frame was truncated") from exc
|
|
989
|
+
if not chunk:
|
|
990
|
+
if remaining_bytes == size:
|
|
991
|
+
raise _PeerClosed("coordinator session peer closed")
|
|
992
|
+
raise _CoordinatorProtocolError("coordinator frame was truncated")
|
|
993
|
+
chunks.append(chunk)
|
|
994
|
+
remaining_bytes -= len(chunk)
|
|
995
|
+
return b"".join(chunks)
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def _parse_canonical_message(raw: bytes, label: str) -> Any:
|
|
999
|
+
if not raw or len(raw) > _MAX_MESSAGE_BYTES:
|
|
1000
|
+
raise _CoordinatorProtocolError(f"{label} exceeds the byte limit")
|
|
1001
|
+
try:
|
|
1002
|
+
return parse_canonical_json(raw)
|
|
1003
|
+
except CanonicalJSONError as exc:
|
|
1004
|
+
raise _CoordinatorProtocolError(f"{label} is not strict canonical JSON") from exc
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def _inherited_authority_available() -> bool:
|
|
1008
|
+
"""Return whether a launcher supplied the non-secret session descriptor number."""
|
|
1009
|
+
|
|
1010
|
+
return _AUTHORITY_FD_ENV in os.environ
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def _take_inherited_authority_descriptor() -> int:
|
|
1014
|
+
raw = os.environ.pop(_AUTHORITY_FD_ENV, None)
|
|
1015
|
+
if raw is None:
|
|
1016
|
+
raise _CoordinatorProtocolError("inherited authority descriptor is absent")
|
|
1017
|
+
descriptor = _descriptor_argument(raw)
|
|
1018
|
+
try:
|
|
1019
|
+
duplicate = os.dup(descriptor)
|
|
1020
|
+
finally:
|
|
1021
|
+
os.close(descriptor)
|
|
1022
|
+
return duplicate
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def _descriptor_argument(value: str) -> int:
|
|
1026
|
+
if not isinstance(value, str) or not value.isascii() or not value.isdecimal():
|
|
1027
|
+
raise argparse.ArgumentTypeError("descriptor must be a decimal integer")
|
|
1028
|
+
descriptor = int(value)
|
|
1029
|
+
if descriptor < 3:
|
|
1030
|
+
raise argparse.ArgumentTypeError("descriptor must not be stdin, stdout, or stderr")
|
|
1031
|
+
return descriptor
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _read_canonical_descriptor(descriptor: int, *, label: str) -> Any:
|
|
1035
|
+
raw = _read_descriptor_bytes(
|
|
1036
|
+
descriptor,
|
|
1037
|
+
label=label,
|
|
1038
|
+
limit=_MAX_MESSAGE_BYTES,
|
|
1039
|
+
)
|
|
1040
|
+
try:
|
|
1041
|
+
return parse_canonical_json(raw)
|
|
1042
|
+
except CanonicalJSONError as exc:
|
|
1043
|
+
raise _CoordinatorProtocolError(
|
|
1044
|
+
f"coordinator {label} is not strict canonical JSON"
|
|
1045
|
+
) from exc
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def _read_role_token_commitment_descriptor(descriptor: int) -> str:
|
|
1049
|
+
raw = _read_descriptor_bytes(descriptor, label="role token", limit=256)
|
|
1050
|
+
if not raw.endswith(b"\n") or raw.count(b"\n") != 1:
|
|
1051
|
+
raise _CoordinatorProtocolError("role token must be exactly one newline-terminated value")
|
|
1052
|
+
token = raw[:-1]
|
|
1053
|
+
try:
|
|
1054
|
+
token_text = token.decode("ascii")
|
|
1055
|
+
except UnicodeDecodeError as exc:
|
|
1056
|
+
raise _CoordinatorProtocolError("role token is not ASCII") from exc
|
|
1057
|
+
if _HANDLE.fullmatch(token_text) is None:
|
|
1058
|
+
raise _CoordinatorProtocolError("role token has an invalid format")
|
|
1059
|
+
return hashlib.sha256(token).hexdigest()
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def _read_descriptor_bytes(descriptor: int, *, label: str, limit: int) -> bytes:
|
|
1063
|
+
deadline = time.monotonic() + _DESCRIPTOR_READ_TIMEOUT_SECONDS
|
|
1064
|
+
chunks: list[bytes] = []
|
|
1065
|
+
total = 0
|
|
1066
|
+
try:
|
|
1067
|
+
os.fstat(descriptor)
|
|
1068
|
+
while True:
|
|
1069
|
+
remaining = deadline - time.monotonic()
|
|
1070
|
+
if remaining <= 0:
|
|
1071
|
+
raise TimeoutError(f"coordinator {label} read deadline expired")
|
|
1072
|
+
readable, _writable, _exceptional = select.select([descriptor], [], [], remaining)
|
|
1073
|
+
if not readable:
|
|
1074
|
+
raise TimeoutError(f"coordinator {label} read deadline expired")
|
|
1075
|
+
chunk = os.read(descriptor, min(65_536, limit + 1 - total))
|
|
1076
|
+
if not chunk:
|
|
1077
|
+
break
|
|
1078
|
+
chunks.append(chunk)
|
|
1079
|
+
total += len(chunk)
|
|
1080
|
+
if total > limit:
|
|
1081
|
+
raise _CoordinatorProtocolError(f"coordinator {label} exceeds the byte limit")
|
|
1082
|
+
finally:
|
|
1083
|
+
os.close(descriptor)
|
|
1084
|
+
raw = b"".join(chunks)
|
|
1085
|
+
if not raw:
|
|
1086
|
+
raise _CoordinatorProtocolError(f"coordinator {label} is empty")
|
|
1087
|
+
return raw
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
def _run_authorized_child(
|
|
1091
|
+
child_command: list[str],
|
|
1092
|
+
*,
|
|
1093
|
+
assignment: dict[str, Any],
|
|
1094
|
+
enrollment: _Enrollment,
|
|
1095
|
+
root_private_key: Ed25519PrivateKey,
|
|
1096
|
+
sealed_enrollment: dict[str, Any],
|
|
1097
|
+
) -> int:
|
|
1098
|
+
server_socket, child_socket = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
1099
|
+
server = _CoordinatorSessionServer(
|
|
1100
|
+
connection=server_socket,
|
|
1101
|
+
assignment=assignment,
|
|
1102
|
+
enrollment=enrollment,
|
|
1103
|
+
root_private_key=root_private_key,
|
|
1104
|
+
sealed_enrollment=sealed_enrollment,
|
|
1105
|
+
)
|
|
1106
|
+
child_environment = os.environ.copy()
|
|
1107
|
+
child_environment.pop(_AUTHORITY_FD_ENV, None)
|
|
1108
|
+
child_environment[_AUTHORITY_FD_ENV] = str(child_socket.fileno())
|
|
1109
|
+
process: subprocess.Popen[bytes] | None = None
|
|
1110
|
+
deadline = time.monotonic() + _CHILD_EXIT_TIMEOUT_SECONDS
|
|
1111
|
+
try:
|
|
1112
|
+
process = subprocess.Popen(
|
|
1113
|
+
child_command,
|
|
1114
|
+
env=child_environment,
|
|
1115
|
+
close_fds=True,
|
|
1116
|
+
pass_fds=(child_socket.fileno(),),
|
|
1117
|
+
)
|
|
1118
|
+
child_socket.close()
|
|
1119
|
+
server.serve(deadline=deadline)
|
|
1120
|
+
try:
|
|
1121
|
+
remaining = deadline - time.monotonic()
|
|
1122
|
+
if remaining <= 0:
|
|
1123
|
+
raise subprocess.TimeoutExpired(child_command, _CHILD_EXIT_TIMEOUT_SECONDS)
|
|
1124
|
+
return_code = process.wait(timeout=remaining)
|
|
1125
|
+
except subprocess.TimeoutExpired as exc:
|
|
1126
|
+
raise TimeoutError("authorized child exceeded its exit deadline") from exc
|
|
1127
|
+
except BaseException:
|
|
1128
|
+
if process is not None:
|
|
1129
|
+
try:
|
|
1130
|
+
_terminate_child(process)
|
|
1131
|
+
except BaseException:
|
|
1132
|
+
pass
|
|
1133
|
+
try:
|
|
1134
|
+
_close_session_sockets(child_socket, server_socket)
|
|
1135
|
+
except BaseException:
|
|
1136
|
+
pass
|
|
1137
|
+
raise
|
|
1138
|
+
_close_session_sockets(child_socket, server_socket)
|
|
1139
|
+
return return_code
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
def _terminate_child(process: subprocess.Popen[bytes]) -> None:
|
|
1143
|
+
first_failure: BaseException | None = None
|
|
1144
|
+
|
|
1145
|
+
def record_failure(exc: BaseException) -> None:
|
|
1146
|
+
nonlocal first_failure
|
|
1147
|
+
if first_failure is None:
|
|
1148
|
+
first_failure = exc
|
|
1149
|
+
|
|
1150
|
+
def reap_child(*, attempts: int) -> bool:
|
|
1151
|
+
for _attempt in range(attempts):
|
|
1152
|
+
try:
|
|
1153
|
+
process.wait(timeout=_CHILD_REAP_TIMEOUT_SECONDS)
|
|
1154
|
+
except subprocess.TimeoutExpired:
|
|
1155
|
+
pass
|
|
1156
|
+
except BaseException as exc:
|
|
1157
|
+
record_failure(exc)
|
|
1158
|
+
else:
|
|
1159
|
+
return True
|
|
1160
|
+
|
|
1161
|
+
try:
|
|
1162
|
+
return_code = process.poll()
|
|
1163
|
+
except BaseException as exc:
|
|
1164
|
+
record_failure(exc)
|
|
1165
|
+
else:
|
|
1166
|
+
if return_code is not None:
|
|
1167
|
+
return True
|
|
1168
|
+
return False
|
|
1169
|
+
|
|
1170
|
+
try:
|
|
1171
|
+
process.terminate()
|
|
1172
|
+
except BaseException as exc:
|
|
1173
|
+
record_failure(exc)
|
|
1174
|
+
|
|
1175
|
+
if reap_child(attempts=1):
|
|
1176
|
+
if first_failure is not None:
|
|
1177
|
+
raise first_failure
|
|
1178
|
+
return
|
|
1179
|
+
|
|
1180
|
+
for _attempt in range(_CHILD_KILL_ATTEMPTS):
|
|
1181
|
+
try:
|
|
1182
|
+
process.kill()
|
|
1183
|
+
except BaseException as exc:
|
|
1184
|
+
record_failure(exc)
|
|
1185
|
+
continue
|
|
1186
|
+
break
|
|
1187
|
+
|
|
1188
|
+
if reap_child(attempts=_CHILD_REAP_ATTEMPTS):
|
|
1189
|
+
if first_failure is not None:
|
|
1190
|
+
raise first_failure
|
|
1191
|
+
return
|
|
1192
|
+
|
|
1193
|
+
if first_failure is not None:
|
|
1194
|
+
raise first_failure
|
|
1195
|
+
raise TimeoutError("authorized child could not be killed and reaped")
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
def _close_session_sockets(*connections: socket.socket) -> None:
|
|
1199
|
+
first_failure: BaseException | None = None
|
|
1200
|
+
for connection in connections:
|
|
1201
|
+
try:
|
|
1202
|
+
connection.close()
|
|
1203
|
+
except BaseException as exc:
|
|
1204
|
+
if first_failure is None:
|
|
1205
|
+
first_failure = exc
|
|
1206
|
+
if first_failure is not None:
|
|
1207
|
+
raise first_failure
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
def _load_anchor_public_key() -> Ed25519PublicKey:
|
|
1211
|
+
anchor_path = (
|
|
1212
|
+
Path("/private/etc/mostlyright/trust/anchor.json")
|
|
1213
|
+
if sys.platform == "darwin"
|
|
1214
|
+
else Path("/etc/mostlyright/trust/anchor.json")
|
|
1215
|
+
)
|
|
1216
|
+
value = _read_system_anchor(
|
|
1217
|
+
anchor_path,
|
|
1218
|
+
owner_uid=0,
|
|
1219
|
+
)
|
|
1220
|
+
if not isinstance(value, dict) or set(value) != {
|
|
1221
|
+
"schema_version",
|
|
1222
|
+
"algorithm",
|
|
1223
|
+
"anchor_id",
|
|
1224
|
+
"public_key",
|
|
1225
|
+
}:
|
|
1226
|
+
raise _CoordinatorProtocolError("coordinator trust anchor fields are invalid")
|
|
1227
|
+
if (
|
|
1228
|
+
value["schema_version"] != _ANCHOR_SCHEMA_VERSION
|
|
1229
|
+
or value["algorithm"] != "Ed25519"
|
|
1230
|
+
or not isinstance(value["anchor_id"], str)
|
|
1231
|
+
or _REQUEST_ID.fullmatch(value["anchor_id"]) is None
|
|
1232
|
+
or not isinstance(value["public_key"], str)
|
|
1233
|
+
):
|
|
1234
|
+
raise _CoordinatorProtocolError("coordinator trust anchor binding is invalid")
|
|
1235
|
+
return load_public_key_base64(value["public_key"])
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
def _read_system_anchor(path: Path, *, owner_uid: int) -> Any:
|
|
1239
|
+
if not isinstance(path, Path) or not path.is_absolute():
|
|
1240
|
+
raise _CoordinatorProtocolError("coordinator system anchor path is not fixed and absolute")
|
|
1241
|
+
if type(owner_uid) is not int or owner_uid < 0:
|
|
1242
|
+
raise _CoordinatorProtocolError("coordinator system anchor owner is invalid")
|
|
1243
|
+
absolute = Path(os.path.abspath(os.fspath(path)))
|
|
1244
|
+
current = Path(absolute.anchor)
|
|
1245
|
+
for component in absolute.parts[1:-1]:
|
|
1246
|
+
current /= component
|
|
1247
|
+
info = os.lstat(current)
|
|
1248
|
+
if (
|
|
1249
|
+
not stat.S_ISDIR(info.st_mode)
|
|
1250
|
+
or info.st_uid not in {0, owner_uid}
|
|
1251
|
+
or stat.S_IMODE(info.st_mode) & 0o022
|
|
1252
|
+
):
|
|
1253
|
+
raise _CoordinatorProtocolError(
|
|
1254
|
+
"coordinator system anchor ancestor is not operator-owned"
|
|
1255
|
+
)
|
|
1256
|
+
descriptor = os.open(
|
|
1257
|
+
current,
|
|
1258
|
+
os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0),
|
|
1259
|
+
)
|
|
1260
|
+
try:
|
|
1261
|
+
if _stat_identity(os.fstat(descriptor)) != _stat_identity(info):
|
|
1262
|
+
raise _CoordinatorProtocolError(
|
|
1263
|
+
"coordinator system anchor ancestor changed identity"
|
|
1264
|
+
)
|
|
1265
|
+
finally:
|
|
1266
|
+
os.close(descriptor)
|
|
1267
|
+
info = os.lstat(absolute)
|
|
1268
|
+
if (
|
|
1269
|
+
not stat.S_ISREG(info.st_mode)
|
|
1270
|
+
or info.st_uid != owner_uid
|
|
1271
|
+
or info.st_nlink != 1
|
|
1272
|
+
or stat.S_IMODE(info.st_mode) != _ANCHOR_MODE
|
|
1273
|
+
):
|
|
1274
|
+
raise _CoordinatorProtocolError("coordinator system anchor metadata is invalid")
|
|
1275
|
+
descriptor = os.open(
|
|
1276
|
+
absolute,
|
|
1277
|
+
os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0),
|
|
1278
|
+
)
|
|
1279
|
+
try:
|
|
1280
|
+
opened = os.fstat(descriptor)
|
|
1281
|
+
if _stat_identity(opened) != _stat_identity(info):
|
|
1282
|
+
raise _CoordinatorProtocolError("coordinator system anchor changed identity")
|
|
1283
|
+
raw = os.read(descriptor, _MAX_MESSAGE_BYTES + 1)
|
|
1284
|
+
if len(raw) > _MAX_MESSAGE_BYTES:
|
|
1285
|
+
raise _CoordinatorProtocolError("coordinator system anchor exceeds the byte limit")
|
|
1286
|
+
finally:
|
|
1287
|
+
os.close(descriptor)
|
|
1288
|
+
try:
|
|
1289
|
+
return parse_canonical_json(raw)
|
|
1290
|
+
except CanonicalJSONError as exc:
|
|
1291
|
+
raise _CoordinatorProtocolError(
|
|
1292
|
+
"coordinator system anchor is not strict canonical JSON"
|
|
1293
|
+
) from exc
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
def _stat_identity(info: os.stat_result) -> tuple[int, int, int, int, int, int]:
|
|
1297
|
+
return (
|
|
1298
|
+
info.st_dev,
|
|
1299
|
+
info.st_ino,
|
|
1300
|
+
info.st_uid,
|
|
1301
|
+
stat.S_IFMT(info.st_mode),
|
|
1302
|
+
stat.S_IMODE(info.st_mode),
|
|
1303
|
+
info.st_size,
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
__all__ = ["coordinator_main"]
|