myc

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

services_hardening_nip46_verification.rs (16412B)


      1 #![forbid(unsafe_code)]
      2 
      3 use std::error::Error;
      4 
      5 use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
      6 use myc::{
      7     MycConfigProfile, MycNip46AdmissionLimits, MycNip46AuthoredTimePolicy,
      8     MycNip46EncryptionContext, MycNip46ObservedAtUnixSeconds, MycNip46VerificationErrorKind,
      9     MycProviderBinding, MycProviderRole, admit_myc_nip46_event, admit_myc_nip46_request,
     10     parse_myc_config_v1, verify_myc_nip46_event, verify_myc_nip46_request,
     11 };
     12 use nostr::{
     13     Event, EventBuilder, EventId, Keys, Kind, Tag, Timestamp,
     14     nips::{nip04, nip44},
     15 };
     16 use serde_json::{Value, json};
     17 
     18 const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
     19 const CONTRACT: &str = include_str!("../contracts/services_hardening/nip46_verification.v1.json");
     20 const OBSERVED_AT: u64 = 1_725_000_000;
     21 
     22 fn limits() -> MycNip46AdmissionLimits {
     23     let config =
     24         parse_myc_config_v1(CONFIG.as_bytes(), MycConfigProfile::RepoLocal).expect("test config");
     25     MycNip46AdmissionLimits::from_config(&config).expect("limits")
     26 }
     27 
     28 fn keys(seed: u8) -> Keys {
     29     Keys::parse(&format!("{seed:02x}{}", "00".repeat(31))).expect("test keys")
     30 }
     31 
     32 fn provider_binding(role: MycProviderRole, keys: &Keys) -> MycProviderBinding {
     33     let (section, original) = match role {
     34         MycProviderRole::Transport => (
     35             "[identity.transport]",
     36             "4444444444444444444444444444444444444444444444444444444444444444",
     37         ),
     38         MycProviderRole::User => (
     39             "[identity.user]",
     40             "2222222222222222222222222222222222222222222222222222222222222222",
     41         ),
     42         MycProviderRole::Discovery => (
     43             "[identity.discovery.binding]",
     44             "3333333333333333333333333333333333333333333333333333333333333333",
     45         ),
     46     };
     47     let section_start = CONFIG.find(section).expect("identity section");
     48     let mut source = CONFIG.to_owned();
     49     let relative = source[section_start..]
     50         .find(original)
     51         .expect("configured identity");
     52     let identity_start = section_start + relative;
     53     source.replace_range(
     54         identity_start..identity_start + original.len(),
     55         &keys.public_key().to_hex(),
     56     );
     57     parse_myc_config_v1(source.as_bytes(), MycConfigProfile::RepoLocal)
     58         .expect("test config")
     59         .provider_contract()
     60         .binding(role)
     61         .expect("provider binding")
     62         .clone()
     63 }
     64 
     65 fn signed_event(sender: &Keys, receiver: &Keys, content: String, created_at: u64) -> Event {
     66     EventBuilder::new(Kind::Custom(24_133), content)
     67         .tag(Tag::public_key(receiver.public_key()))
     68         .custom_created_at(Timestamp::from_secs(created_at))
     69         .sign_with_keys(sender)
     70         .expect("signed event")
     71 }
     72 
     73 fn verify_event(
     74     event: &Event,
     75     receiver_keys: &Keys,
     76     policy: MycNip46AuthoredTimePolicy,
     77 ) -> Result<myc::MycVerifiedNip46Event, myc::MycNip46VerificationError> {
     78     let wire = serde_json::to_vec(event).expect("event JSON");
     79     let bounded = admit_myc_nip46_event(limits(), &wire).expect("bounded event");
     80     verify_myc_nip46_event(
     81         bounded,
     82         &provider_binding(MycProviderRole::Transport, receiver_keys),
     83         MycNip46ObservedAtUnixSeconds::new(OBSERVED_AT).expect("observation"),
     84         policy,
     85     )
     86 }
     87 
     88 fn request(id: &str, method: &str, params: Value) -> Vec<u8> {
     89     serde_json::to_vec(&json!({"id": id, "method": method, "params": params}))
     90         .expect("request JSON")
     91 }
     92 
     93 #[test]
     94 fn machine_contract_freezes_exact_step_boundary() {
     95     let contract: Value = serde_json::from_str(CONTRACT).expect("verification contract");
     96     assert_eq!(contract["schema"], "radroots.myc.nip46-verification.v1");
     97     assert_eq!(contract["contract_version"], 1);
     98     assert_eq!(contract["event"]["kind"], 24_133);
     99     assert_eq!(contract["authored_time"]["default"], "none");
    100     assert_eq!(
    101         contract["encryption"]["contexts"],
    102         json!(["nip04", "nip44_v2"])
    103     );
    104     assert_eq!(contract["encryption"]["decryption"], "deferred_to_step_145");
    105     assert_eq!(
    106         contract["request"]["replay_and_conflicting_reuse"],
    107         "deferred_to_step_143"
    108     );
    109 }
    110 
    111 #[test]
    112 fn signed_nip44_and_nip04_events_verify_without_decrypting() {
    113     let sender = keys(1);
    114     let target = keys(2);
    115     let policy = MycNip46AuthoredTimePolicy::new(60, 10).expect("policy");
    116     let plaintext = br#"{"id":"request-secret-marker","method":"ping","params":[]}"#;
    117 
    118     let nip44_content = nip44::encrypt(
    119         sender.secret_key(),
    120         &target.public_key(),
    121         plaintext,
    122         nip44::Version::V2,
    123     )
    124     .expect("NIP-44 encrypt");
    125     let event = signed_event(&sender, &target, nip44_content.clone(), OBSERVED_AT);
    126     let wire = serde_json::to_vec(&event).expect("event wire");
    127     let verified = verify_event(&event, &target, policy).expect("verified NIP-44 event");
    128     assert_eq!(verified.original_bytes(), wire);
    129     assert_eq!(verified.event_id().as_bytes(), &event.id.to_bytes());
    130     assert_eq!(
    131         verified.client_public_key().as_hex(),
    132         sender.public_key().to_hex()
    133     );
    134     assert_eq!(verified.authored_at_unix_seconds(), OBSERVED_AT);
    135     assert_eq!(
    136         verified.encryption_context(),
    137         MycNip46EncryptionContext::Nip44V2
    138     );
    139     assert_eq!(verified.encrypted_content(), nip44_content);
    140 
    141     let nip04_content = nip04::encrypt(sender.secret_key(), &target.public_key(), plaintext)
    142         .expect("NIP-04 encrypt");
    143     let event = signed_event(&sender, &target, nip04_content, OBSERVED_AT);
    144     assert_eq!(
    145         verify_event(&event, &target, policy)
    146             .expect("verified NIP-04 event")
    147             .encryption_context(),
    148         MycNip46EncryptionContext::Nip04
    149     );
    150 }
    151 
    152 #[test]
    153 fn canonical_kind_sender_and_exact_receiver_shape_fail_closed() {
    154     let sender = keys(3);
    155     let target = keys(4);
    156     let policy = MycNip46AuthoredTimePolicy::new(60, 10).expect("policy");
    157     let content = BASE64_STANDARD.encode([vec![2], vec![0; 32], vec![0; 34], vec![0; 32]].concat());
    158 
    159     let wrong_kind = EventBuilder::new(Kind::Custom(24_132), content.clone())
    160         .tag(Tag::public_key(target.public_key()))
    161         .custom_created_at(Timestamp::from_secs(OBSERVED_AT))
    162         .sign_with_keys(&sender)
    163         .expect("wrong-kind event");
    164     assert_eq!(
    165         verify_event(&wrong_kind, &target, policy)
    166             .unwrap_err()
    167             .kind(),
    168         MycNip46VerificationErrorKind::InvalidKind
    169     );
    170 
    171     let self_sent = signed_event(&target, &target, content.clone(), OBSERVED_AT);
    172     assert_eq!(
    173         verify_event(&self_sent, &target, policy)
    174             .unwrap_err()
    175             .kind(),
    176         MycNip46VerificationErrorKind::InvalidSender
    177     );
    178 
    179     let wrong_target = keys(5);
    180     let wrong_recipient = signed_event(&sender, &wrong_target, content.clone(), OBSERVED_AT);
    181     assert_eq!(
    182         verify_event(&wrong_recipient, &target, policy)
    183             .unwrap_err()
    184             .kind(),
    185         MycNip46VerificationErrorKind::InvalidReceiver
    186     );
    187 
    188     let extra_tag = EventBuilder::new(Kind::Custom(24_133), content)
    189         .tags([
    190             Tag::public_key(target.public_key()),
    191             Tag::parse(["client", "untrusted-metadata"]).expect("extra tag"),
    192         ])
    193         .custom_created_at(Timestamp::from_secs(OBSERVED_AT))
    194         .sign_with_keys(&sender)
    195         .expect("extra-tag event");
    196     assert_eq!(
    197         verify_event(&extra_tag, &target, policy)
    198             .unwrap_err()
    199             .kind(),
    200         MycNip46VerificationErrorKind::InvalidReceiver
    201     );
    202 }
    203 
    204 #[test]
    205 fn identifiers_event_id_and_signature_are_independently_verified() {
    206     let sender = keys(6);
    207     let target = keys(7);
    208     let policy = MycNip46AuthoredTimePolicy::new(60, 10).expect("policy");
    209     let payload = BASE64_STANDARD.encode([vec![2], vec![0; 32], vec![0; 34], vec![0; 32]].concat());
    210     let event = signed_event(&sender, &target, payload.clone(), OBSERVED_AT);
    211 
    212     let wire = String::from_utf8(serde_json::to_vec(&event).expect("wire")).expect("UTF-8");
    213     let uppercase = wire.replacen(&event.id.to_hex(), &event.id.to_hex().to_uppercase(), 1);
    214     let bounded = admit_myc_nip46_event(limits(), uppercase.as_bytes()).expect("bounded uppercase");
    215     assert_eq!(
    216         verify_myc_nip46_event(
    217             bounded,
    218             &provider_binding(MycProviderRole::Transport, &target),
    219             MycNip46ObservedAtUnixSeconds::new(OBSERVED_AT).expect("observed"),
    220             policy,
    221         )
    222         .unwrap_err()
    223         .kind(),
    224         MycNip46VerificationErrorKind::NonCanonicalEvent
    225     );
    226 
    227     let mut wrong_id = event.clone();
    228     wrong_id.id = EventId::all_zeros();
    229     assert_eq!(
    230         verify_event(&wrong_id, &target, policy).unwrap_err().kind(),
    231         MycNip46VerificationErrorKind::InvalidEventId
    232     );
    233 
    234     let other = signed_event(&keys(8), &target, payload, OBSERVED_AT);
    235     let mut wrong_signature = event;
    236     wrong_signature.sig = other.sig;
    237     assert_eq!(
    238         verify_event(&wrong_signature, &target, policy)
    239             .unwrap_err()
    240             .kind(),
    241         MycNip46VerificationErrorKind::InvalidSignature
    242     );
    243 }
    244 
    245 #[test]
    246 fn authored_time_policy_is_explicit_inclusive_and_overflow_safe() {
    247     assert!(MycNip46ObservedAtUnixSeconds::new(0).is_err());
    248     assert!(MycNip46ObservedAtUnixSeconds::new(i64::MAX as u64).is_ok());
    249     assert!(MycNip46ObservedAtUnixSeconds::new(i64::MAX as u64 + 1).is_err());
    250     assert!(MycNip46AuthoredTimePolicy::new(i64::MAX as u64, 0).is_ok());
    251     assert!(MycNip46AuthoredTimePolicy::new(i64::MAX as u64 + 1, 0).is_err());
    252 
    253     let sender = keys(9);
    254     let target = keys(10);
    255     let payload = BASE64_STANDARD.encode([vec![2], vec![0; 32], vec![0; 34], vec![0; 32]].concat());
    256     let policy = MycNip46AuthoredTimePolicy::new(10, 2).expect("policy");
    257     for accepted in [OBSERVED_AT - 10, OBSERVED_AT + 2] {
    258         verify_event(
    259             &signed_event(&sender, &target, payload.clone(), accepted),
    260             &target,
    261             policy,
    262         )
    263         .expect("inclusive boundary");
    264     }
    265     for rejected in [OBSERVED_AT - 11, OBSERVED_AT + 3] {
    266         assert_eq!(
    267             verify_event(
    268                 &signed_event(&sender, &target, payload.clone(), rejected),
    269                 &target,
    270                 policy,
    271             )
    272             .unwrap_err()
    273             .kind(),
    274             MycNip46VerificationErrorKind::AuthoredTimeRejected
    275         );
    276     }
    277 }
    278 
    279 #[test]
    280 fn only_the_configured_transport_role_can_supply_receiver_authority() {
    281     let sender = keys(15);
    282     let target = keys(16);
    283     let payload = BASE64_STANDARD.encode([vec![2], vec![0; 32], vec![0; 34], vec![0; 32]].concat());
    284     let event = signed_event(&sender, &target, payload, OBSERVED_AT);
    285     let wire = serde_json::to_vec(&event).expect("event wire");
    286     let bounded = admit_myc_nip46_event(limits(), &wire).expect("bounded event");
    287     let error = verify_myc_nip46_event(
    288         bounded,
    289         &provider_binding(MycProviderRole::User, &target),
    290         MycNip46ObservedAtUnixSeconds::new(OBSERVED_AT).expect("observation"),
    291         MycNip46AuthoredTimePolicy::new(0, 0).expect("policy"),
    292     )
    293     .expect_err("user identity cannot become transport receiver authority");
    294     assert_eq!(
    295         error.kind(),
    296         MycNip46VerificationErrorKind::InvalidTransportBinding
    297     );
    298 }
    299 
    300 #[test]
    301 fn nip04_and_nip44_envelope_shapes_are_canonical_and_versioned() {
    302     let sender = keys(11);
    303     let target = keys(12);
    304     let policy = MycNip46AuthoredTimePolicy::new(1, 1).expect("policy");
    305 
    306     let invalid_nip04 = signed_event(&sender, &target, "AAAA?iv=AAAA".to_owned(), OBSERVED_AT);
    307     assert_eq!(
    308         verify_event(&invalid_nip04, &target, policy)
    309             .unwrap_err()
    310             .kind(),
    311         MycNip46VerificationErrorKind::InvalidNip04Envelope
    312     );
    313 
    314     let mut wrong_version = vec![0; 99];
    315     wrong_version[0] = 3;
    316     let event = signed_event(
    317         &sender,
    318         &target,
    319         BASE64_STANDARD.encode(wrong_version),
    320         OBSERVED_AT,
    321     );
    322     assert_eq!(
    323         verify_event(&event, &target, policy).unwrap_err().kind(),
    324         MycNip46VerificationErrorKind::InvalidNip44Envelope
    325     );
    326 
    327     let mut invalid_padding = vec![0; 100];
    328     invalid_padding[0] = 2;
    329     let event = signed_event(
    330         &sender,
    331         &target,
    332         BASE64_STANDARD.encode(invalid_padding),
    333         OBSERVED_AT,
    334     );
    335     assert_eq!(
    336         verify_event(&event, &target, policy).unwrap_err().kind(),
    337         MycNip46VerificationErrorKind::InvalidNip44Envelope
    338     );
    339 }
    340 
    341 #[test]
    342 fn exact_typed_request_decoding_accepts_governed_and_custom_methods_only_when_semantic() {
    343     let ping = request("request-01", "ping", json!([]));
    344     let bounded = admit_myc_nip46_request(limits(), &ping).expect("bounded ping");
    345     let verified = verify_myc_nip46_request(bounded).expect("typed ping");
    346     assert_eq!(verified.request_id().as_str(), "request-01");
    347     assert_eq!(verified.method(), "ping");
    348     assert_eq!(verified.parameter_count(), 0);
    349     assert_eq!(verified.parameter_bytes(), 0);
    350 
    351     let custom = request("custom-01", "vendor_method", json!(["a", "bc"]));
    352     let bounded = admit_myc_nip46_request(limits(), &custom).expect("bounded custom");
    353     let verified = verify_myc_nip46_request(bounded).expect("typed custom");
    354     assert_eq!(verified.method(), "vendor_method");
    355     assert_eq!(verified.parameter_count(), 2);
    356     assert_eq!(verified.parameter_bytes(), 3);
    357 
    358     for invalid in [
    359         request("", "ping", json!([])),
    360         request(" request ", "ping", json!([])),
    361         request("request-02", "ping", json!(["unexpected"])),
    362         request("request-03", "nip44_encrypt", json!(["not-a-key", "x"])),
    363     ] {
    364         let bounded = admit_myc_nip46_request(limits(), &invalid).expect("structurally bounded");
    365         assert_eq!(
    366             verify_myc_nip46_request(bounded).unwrap_err().kind(),
    367             MycNip46VerificationErrorKind::InvalidTypedRequest
    368         );
    369     }
    370 }
    371 
    372 #[test]
    373 fn verified_capabilities_and_errors_redact_protected_content() {
    374     let sender = keys(13);
    375     let target = keys(14);
    376     let marker = br#"{"id":"protected-request-marker","method":"ping","params":[]}"#;
    377     let ciphertext = nip44::encrypt(
    378         sender.secret_key(),
    379         &target.public_key(),
    380         marker,
    381         nip44::Version::V2,
    382     )
    383     .expect("ciphertext");
    384     let event = signed_event(&sender, &target, ciphertext.clone(), OBSERVED_AT);
    385     let verified_event = verify_event(
    386         &event,
    387         &target,
    388         MycNip46AuthoredTimePolicy::new(0, 0).expect("policy"),
    389     )
    390     .expect("verified event");
    391     let rendered = format!("{verified_event:?}");
    392     assert!(!rendered.contains(&ciphertext));
    393     assert!(!rendered.contains(&sender.public_key().to_hex()));
    394 
    395     let bounded = admit_myc_nip46_request(limits(), marker).expect("bounded request");
    396     let verified_request = verify_myc_nip46_request(bounded).expect("verified request");
    397     let rendered = format!("{verified_request:?}");
    398     assert!(!rendered.contains("protected-request-marker"));
    399     assert!(!rendered.contains("ping"));
    400 
    401     for kind in [
    402         MycNip46VerificationErrorKind::InvalidObservationTime,
    403         MycNip46VerificationErrorKind::InvalidTimePolicy,
    404         MycNip46VerificationErrorKind::InvalidTransportBinding,
    405         MycNip46VerificationErrorKind::MalformedEvent,
    406         MycNip46VerificationErrorKind::NonCanonicalEvent,
    407         MycNip46VerificationErrorKind::InvalidKind,
    408         MycNip46VerificationErrorKind::InvalidSender,
    409         MycNip46VerificationErrorKind::InvalidReceiver,
    410         MycNip46VerificationErrorKind::InvalidEventId,
    411         MycNip46VerificationErrorKind::InvalidSignature,
    412         MycNip46VerificationErrorKind::AuthoredTimeRejected,
    413         MycNip46VerificationErrorKind::InvalidNip04Envelope,
    414         MycNip46VerificationErrorKind::InvalidNip44Envelope,
    415         MycNip46VerificationErrorKind::InvalidTypedRequest,
    416     ] {
    417         let error = if kind == MycNip46VerificationErrorKind::InvalidObservationTime {
    418             MycNip46ObservedAtUnixSeconds::new(0).unwrap_err()
    419         } else if kind == MycNip46VerificationErrorKind::InvalidTimePolicy {
    420             MycNip46AuthoredTimePolicy::new(u64::MAX, 0).unwrap_err()
    421         } else {
    422             continue;
    423         };
    424         assert_eq!(error.kind(), kind);
    425         assert!(!error.to_string().is_empty());
    426         assert!(error.source().is_none());
    427     }
    428 }