myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

services_hardening_provider_contract.rs (12109B)


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