lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

authored_signing.rs (17051B)


      1 use nostr::{EventBuilder, JsonUtil, Keys, Kind as NostrKind, SecretKey, Timestamp};
      2 use radroots_blossom::{
      3     Sha256 as BlossomSha256,
      4     authorization::{AuthoredUploadClaim, AuthorizationContent, ServerDomain},
      5 };
      6 use radroots_event::{
      7     GenericEventDraft,
      8     contract::AuthorRole,
      9     food::availability::{
     10         FoodAvailabilityDetails, FoodAvailabilityDetailsParts, FoodAvailabilityStatus, FoodContent,
     11         FoodCurrency, FoodIdentifier, FoodPrice, FoodPublishedAt, FoodText, FoodUnit,
     12     },
     13     wire::Nip01EventWire,
     14 };
     15 use radroots_event_codec::authoring::{AuthoredEventPlan, BlossomAuthorizationPlan};
     16 use radroots_identity::{AccountId, PublicKey};
     17 use radroots_protocol::runtime::v1::OperationId;
     18 use radroots_signing::{
     19     Actor, AuthoredArtifactId, CurrentAuthoringAuthority, CurrentAuthoringDecision, Error,
     20     SignReceipt, SignRequest, SigningIntentId, SigningOperationId, SigningPurpose,
     21     actor::ActorSource,
     22     authorization::ManagedSigningPolicy,
     23     error::Kind,
     24     recovery::{RecoveryDisposition, RemoteEffect, ReplayCapability, recovery_disposition},
     25     request::{CancellationPolicy, CancellationSignal, ProgressObserver, SignPolicy},
     26     status::{SignProgress, SignProgressStage},
     27 };
     28 
     29 const SECRET: &str = "7e0112ad58b2d2d13fb80532625195dc169b86d72b0e1db48347837a785cae90";
     30 const CREATED_AT: u64 = 1_700_000_000;
     31 const DEADLINE_MS: u64 = 1_700_000_100_000;
     32 
     33 #[derive(Clone, Copy)]
     34 struct FixedAuthority(CurrentAuthoringDecision);
     35 
     36 impl CurrentAuthoringAuthority for FixedAuthority {
     37     fn evaluate(&self, _plan: &AuthoredEventPlan) -> CurrentAuthoringDecision {
     38         self.0
     39     }
     40 }
     41 
     42 fn keys() -> Keys {
     43     Keys::new(SecretKey::from_hex(SECRET).expect("secret fixture"))
     44 }
     45 
     46 fn public_key() -> PublicKey {
     47     PublicKey::from_hex(&keys().public_key().to_hex()).expect("public key")
     48 }
     49 
     50 fn plan() -> AuthoredEventPlan {
     51     AuthoredEventPlan::from_generic(
     52         GenericEventDraft::new(
     53             "radroots.social.geochat.v1",
     54             20_000,
     55             CREATED_AT,
     56             Vec::new(),
     57             "exact signing plan",
     58             public_key().to_hex(),
     59         )
     60         .expect("generic draft"),
     61     )
     62     .expect("authored plan")
     63 }
     64 
     65 fn actor(source: ActorSource, roles: impl IntoIterator<Item = AuthorRole>) -> Actor {
     66     Actor::new(public_key(), source, roles).expect("actor")
     67 }
     68 
     69 fn intent(operation: u8, artifact: u8) -> SigningIntentId {
     70     SigningIntentId::new(
     71         SigningOperationId::new([operation; 16]).expect("operation ID"),
     72         AuthoredArtifactId::new([artifact; 16]).expect("artifact ID"),
     73     )
     74 }
     75 
     76 fn policy() -> SignPolicy {
     77     SignPolicy::new(DEADLINE_MS, CancellationPolicy::LocalCooperative).expect("policy")
     78 }
     79 
     80 fn request() -> SignRequest {
     81     SignRequest::new(
     82         OperationId::SyncPush,
     83         intent(1, 2),
     84         actor(ActorSource::ExplicitPublicKey, [AuthorRole::Any]),
     85         plan(),
     86         policy(),
     87     )
     88     .expect("request")
     89 }
     90 
     91 fn blossom_plan() -> BlossomAuthorizationPlan {
     92     let claim = AuthoredUploadClaim::new(
     93         AuthorizationContent::parse("Upload exact image").expect("content"),
     94         ServerDomain::parse("media.example").expect("server"),
     95         BlossomSha256::digest(b"exact-image"),
     96         CREATED_AT,
     97         60,
     98     )
     99     .expect("upload claim");
    100     BlossomAuthorizationPlan::for_upload(&claim, public_key()).expect("authorization plan")
    101 }
    102 
    103 fn blossom_request() -> SignRequest {
    104     SignRequest::blossom_upload(
    105         OperationId::SyncPush,
    106         intent(9, 10),
    107         actor(ActorSource::ExplicitPublicKey, [AuthorRole::Any]),
    108         blossom_plan(),
    109         policy(),
    110     )
    111     .expect("Blossom request")
    112 }
    113 
    114 fn signed_event() -> radroots_event::SignedEvent {
    115     signed_event_with(
    116         &keys(),
    117         20_000,
    118         "exact signing plan",
    119         CREATED_AT,
    120         Vec::new(),
    121     )
    122 }
    123 
    124 fn signed_event_with(
    125     signer: &Keys,
    126     kind: u16,
    127     content: &str,
    128     created_at: u64,
    129     tags: Vec<nostr::Tag>,
    130 ) -> radroots_event::SignedEvent {
    131     let event = EventBuilder::new(NostrKind::Custom(kind), content)
    132         .tags(tags)
    133         .custom_created_at(Timestamp::from_secs(created_at))
    134         .sign_with_keys(signer)
    135         .expect("signed fixture");
    136     let raw = event.as_json();
    137     let wire = Nip01EventWire::parse_json(&raw).expect("wire");
    138     radroots_event::SignedEvent::from_wire_verified_id(wire, raw).expect("signed event")
    139 }
    140 
    141 #[test]
    142 fn authorization_decisions_are_current_and_explicit() {
    143     let base_actor = actor(ActorSource::ExplicitPublicKey, [AuthorRole::Any]);
    144     let base_plan = plan();
    145     for decision in [
    146         CurrentAuthoringDecision::Blocked { code: "blocked" },
    147         CurrentAuthoringDecision::Revoked { code: "revoked" },
    148     ] {
    149         let error = SignRequest::new_with_authority(
    150             OperationId::SyncPush,
    151             intent(1, 2),
    152             base_actor.clone(),
    153             base_plan.clone(),
    154             policy(),
    155             &FixedAuthority(decision),
    156         )
    157         .expect_err("denied decision");
    158         assert_eq!(error.kind(), Kind::AuthorizationDenied);
    159     }
    160 
    161     let deprecated = FixedAuthority(CurrentAuthoringDecision::AllowedDeprecated {
    162         warning_code: "deprecated",
    163     });
    164     assert_eq!(
    165         SignRequest::new_with_authority(
    166             OperationId::SyncPush,
    167             intent(1, 2),
    168             base_actor.clone(),
    169             base_plan.clone(),
    170             policy(),
    171             &deprecated,
    172         )
    173         .expect_err("deprecated requires opt in")
    174         .kind(),
    175         Kind::AuthorizationDenied
    176     );
    177     let allowed = SignRequest::new_with_authority(
    178         OperationId::SyncPush,
    179         intent(1, 2),
    180         base_actor,
    181         base_plan,
    182         policy().allowing_deprecated(),
    183         &deprecated,
    184     )
    185     .expect("explicitly allowed deprecated plan");
    186     assert!(matches!(
    187         allowed.authorization(),
    188         Some(CurrentAuthoringDecision::AllowedDeprecated { .. })
    189     ));
    190 }
    191 
    192 #[test]
    193 fn authorization_enforces_key_role_and_host_provenance() {
    194     let wrong_key =
    195         PublicKey::from_hex("e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af")
    196             .expect("other public key");
    197     let wrong_actor =
    198         Actor::new(wrong_key, ActorSource::ExplicitPublicKey, [AuthorRole::Any]).expect("actor");
    199     assert_eq!(
    200         SignRequest::new(
    201             OperationId::SyncPush,
    202             intent(1, 2),
    203             wrong_actor,
    204             plan(),
    205             policy(),
    206         )
    207         .expect_err("key drift")
    208         .kind(),
    209         Kind::AuthorizationDenied
    210     );
    211 
    212     let account = AccountId::from_hex(&public_key().to_hex()).expect("account ID");
    213     let local_actor = actor(ActorSource::LocalAccount(account), [AuthorRole::Any]);
    214     SignRequest::new(
    215         OperationId::SyncPush,
    216         intent(1, 2),
    217         local_actor,
    218         plan(),
    219         policy().with_managed_signing_policy(ManagedSigningPolicy::LocalAccountOnly),
    220     )
    221     .expect("local account is allowed");
    222     assert_eq!(
    223         SignRequest::new(
    224             OperationId::SyncPush,
    225             intent(1, 2),
    226             actor(ActorSource::ExplicitPublicKey, [AuthorRole::Any]),
    227             plan(),
    228             policy().with_managed_signing_policy(ManagedSigningPolicy::AccountBackedOnly),
    229         )
    230         .expect_err("unmanaged explicit key")
    231         .kind(),
    232         Kind::AuthorizationDenied
    233     );
    234 
    235     let food = FoodAvailabilityDetails::new(FoodAvailabilityDetailsParts {
    236         content: FoodContent::new("Carrots available.").unwrap(),
    237         identifier: FoodIdentifier::parse("carrots").unwrap(),
    238         title: FoodText::new("Carrots").unwrap(),
    239         summary: FoodText::new("Fresh bunches").unwrap(),
    240         published_at: FoodPublishedAt::new(CREATED_AT).unwrap(),
    241         location: FoodText::new("Saanich").unwrap(),
    242         price: FoodPrice::new("3", FoodCurrency::parse("CAD").unwrap(), FoodUnit::Pound).unwrap(),
    243         quantity: None,
    244         status: FoodAvailabilityStatus::Active,
    245         images: Vec::new(),
    246     })
    247     .unwrap();
    248     let seller_plan =
    249         AuthoredEventPlan::from_food_availability(&food, CREATED_AT, public_key().to_hex())
    250             .unwrap();
    251     assert_eq!(
    252         SignRequest::new(
    253             OperationId::SyncPush,
    254             intent(1, 3),
    255             actor(ActorSource::ExplicitPublicKey, [AuthorRole::Buyer]),
    256             seller_plan,
    257             policy(),
    258         )
    259         .expect_err("role drift")
    260         .kind(),
    261         Kind::AuthorizationDenied
    262     );
    263 }
    264 
    265 #[test]
    266 fn blossom_request_is_http_only_domain_separated_and_author_bound() {
    267     let request = blossom_request();
    268     assert_eq!(
    269         request.purpose(),
    270         SigningPurpose::BlossomUploadAuthorization
    271     );
    272     assert!(request.authored_plan().is_none());
    273     assert_eq!(request.blossom_authorization_plan(), Some(&blossom_plan()));
    274     assert_eq!(request.authorization(), None);
    275     assert_eq!(request.kind(), 24_242);
    276 
    277     let wrong_key =
    278         PublicKey::from_hex("e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af")
    279             .expect("other public key");
    280     let wrong_actor =
    281         Actor::new(wrong_key, ActorSource::ExplicitPublicKey, [AuthorRole::Any]).expect("actor");
    282     assert_eq!(
    283         SignRequest::blossom_upload(
    284             OperationId::SyncPush,
    285             intent(9, 10),
    286             wrong_actor,
    287             blossom_plan(),
    288             policy(),
    289         )
    290         .expect_err("author mismatch")
    291         .kind(),
    292         Kind::AuthorizationDenied
    293     );
    294 }
    295 
    296 #[test]
    297 fn blossom_receipt_rejects_mismatch_lateness_and_cancellation() {
    298     let request = blossom_request();
    299     let tags = request
    300         .tags()
    301         .iter()
    302         .cloned()
    303         .map(nostr::Tag::parse)
    304         .collect::<Result<Vec<_>, _>>()
    305         .expect("BUD-11 tags");
    306     let valid = signed_event_with(
    307         &keys(),
    308         24_242,
    309         request.content(),
    310         request.created_at(),
    311         tags.clone(),
    312     );
    313     SignReceipt::from_signed_event(&request, valid, DEADLINE_MS - 1)
    314         .expect("verified BUD-11 receipt");
    315 
    316     let mismatched = signed_event_with(
    317         &keys(),
    318         24_242,
    319         "Upload a different image",
    320         request.created_at(),
    321         tags,
    322     );
    323     assert_eq!(
    324         SignReceipt::from_signed_event(&request, mismatched, DEADLINE_MS - 1)
    325             .expect_err("mismatched output")
    326             .kind(),
    327         Kind::SignerOutputInvalid
    328     );
    329     assert_eq!(
    330         SignReceipt::from_signed_event(
    331             &request,
    332             signed_event_with(
    333                 &keys(),
    334                 24_242,
    335                 request.content(),
    336                 request.created_at(),
    337                 request
    338                     .tags()
    339                     .iter()
    340                     .cloned()
    341                     .map(nostr::Tag::parse)
    342                     .collect::<Result<Vec<_>, _>>()
    343                     .expect("BUD-11 tags"),
    344             ),
    345             DEADLINE_MS,
    346         )
    347         .expect_err("late output")
    348         .kind(),
    349         Kind::DeadlineExceeded
    350     );
    351 
    352     let signal = CancellationSignal::new();
    353     let cancelled = request.with_cancellation_signal(signal.clone());
    354     signal.cancel();
    355     assert_eq!(
    356         SignReceipt::from_signed_event(
    357             &cancelled,
    358             signed_event_with(
    359                 &keys(),
    360                 24_242,
    361                 cancelled.content(),
    362                 cancelled.created_at(),
    363                 cancelled
    364                     .tags()
    365                     .iter()
    366                     .cloned()
    367                     .map(nostr::Tag::parse)
    368                     .collect::<Result<Vec<_>, _>>()
    369                     .expect("BUD-11 tags"),
    370             ),
    371             DEADLINE_MS - 1,
    372         )
    373         .expect_err("cancelled output")
    374         .kind(),
    375         Kind::SignerCancelled
    376     );
    377 }
    378 
    379 #[test]
    380 fn request_identity_deadline_and_cancellation_are_exact() {
    381     let request = request();
    382     let replay = request.clone();
    383     assert_eq!(request.operation_kind(), OperationId::SyncPush);
    384     assert_eq!(request.intent_id(), intent(1, 2));
    385     assert_eq!(request.actor().public_key(), public_key());
    386     assert_eq!(request.plan_digest(), plan().digest());
    387     assert_eq!(request.policy(), policy());
    388     assert!(!request.cancellation_signal().is_cancelled());
    389     assert_eq!(request.signer_request_id(), replay.signer_request_id());
    390     let other_artifact = SignRequest::new(
    391         OperationId::SyncPush,
    392         intent(1, 3),
    393         actor(ActorSource::ExplicitPublicKey, [AuthorRole::Any]),
    394         plan(),
    395         policy(),
    396     )
    397     .unwrap();
    398     assert_ne!(
    399         request.signer_request_id(),
    400         other_artifact.signer_request_id()
    401     );
    402     assert!(request.ensure_active(DEADLINE_MS - 1).is_ok());
    403     assert_eq!(
    404         request.ensure_active(DEADLINE_MS).unwrap_err().kind(),
    405         Kind::DeadlineExceeded
    406     );
    407 
    408     let signal = CancellationSignal::new();
    409     let cancelled = request.clone().with_cancellation_signal(signal.clone());
    410     signal.cancel();
    411     assert_eq!(
    412         cancelled.ensure_active(DEADLINE_MS - 1).unwrap_err().kind(),
    413         Kind::SignerCancelled
    414     );
    415 
    416     struct Counter(std::sync::atomic::AtomicUsize);
    417     impl ProgressObserver for Counter {
    418         fn on_progress(&self, _progress: &SignProgress) {
    419             self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
    420         }
    421     }
    422     let observer = std::sync::Arc::new(Counter(std::sync::atomic::AtomicUsize::new(0)));
    423     let observed = request.with_progress_observer(observer.clone());
    424     observed.report_progress(&SignProgress::stage(SignProgressStage::Validating).unwrap());
    425     assert_eq!(observer.0.load(std::sync::atomic::Ordering::Relaxed), 1);
    426 }
    427 
    428 #[test]
    429 fn receipt_requires_exact_fields_and_a_valid_schnorr_signature() {
    430     let request = request();
    431     let receipt = SignReceipt::from_signed_event(&request, signed_event(), DEADLINE_MS - 1)
    432         .expect("verified receipt");
    433     assert_eq!(receipt.intent_id(), request.intent_id());
    434     assert_eq!(receipt.signer_request_id(), request.signer_request_id());
    435     assert_eq!(receipt.operation_kind(), OperationId::SyncPush);
    436     assert_eq!(receipt.completed_at_unix_ms(), DEADLINE_MS - 1);
    437     assert_eq!(receipt.signed_event().id(), signed_event().id());
    438 
    439     let other_keys = Keys::generate();
    440     let mismatches = [
    441         signed_event_with(
    442             &other_keys,
    443             20_000,
    444             "exact signing plan",
    445             CREATED_AT,
    446             Vec::new(),
    447         ),
    448         signed_event_with(
    449             &keys(),
    450             20_000,
    451             "exact signing plan",
    452             CREATED_AT + 1,
    453             Vec::new(),
    454         ),
    455         signed_event_with(
    456             &keys(),
    457             20_001,
    458             "exact signing plan",
    459             CREATED_AT,
    460             Vec::new(),
    461         ),
    462         signed_event_with(
    463             &keys(),
    464             20_000,
    465             "exact signing plan",
    466             CREATED_AT,
    467             vec![nostr::Tag::parse(["t", "mismatch"]).expect("tag")],
    468         ),
    469         signed_event_with(&keys(), 20_000, "different content", CREATED_AT, Vec::new()),
    470     ];
    471     for mismatch in mismatches {
    472         assert_eq!(
    473             SignReceipt::from_signed_event(&request, mismatch, DEADLINE_MS - 1)
    474                 .expect_err("mismatched exact plan must fail")
    475                 .kind(),
    476             Kind::SignerOutputInvalid
    477         );
    478     }
    479 
    480     let invalid = invalid_signature_event();
    481     assert_eq!(
    482         SignReceipt::from_signed_event(&request, invalid, DEADLINE_MS - 1)
    483             .expect_err("invalid signature")
    484             .kind(),
    485         Kind::SignerOutputInvalid
    486     );
    487 }
    488 
    489 #[test]
    490 fn uncertain_remote_effects_never_become_unsafe_automatic_retries() {
    491     let uncertain = Error::new(Kind::SignerTimeout).with_possible_remote_effect();
    492     assert_eq!(uncertain.remote_effect(), RemoteEffect::MayHaveOccurred);
    493     assert_eq!(
    494         recovery_disposition(
    495             ReplayCapability::ExactReplayByRequestId,
    496             uncertain.remote_effect(),
    497             uncertain.retryable(),
    498         ),
    499         RecoveryDisposition::RetryExactRequest
    500     );
    501     assert_eq!(
    502         recovery_disposition(
    503             ReplayCapability::NonReplayable,
    504             uncertain.remote_effect(),
    505             uncertain.retryable(),
    506         ),
    507         RecoveryDisposition::Indeterminate
    508     );
    509     assert_eq!(
    510         recovery_disposition(ReplayCapability::LocalReplaySafe, RemoteEffect::None, true,),
    511         RecoveryDisposition::RetryLocal
    512     );
    513 }
    514 
    515 #[path = "authored_signing/evidence.rs"]
    516 mod evidence;
    517 
    518 fn invalid_signature_event() -> radroots_event::SignedEvent {
    519     let valid = signed_event();
    520     let mut wire = valid.wire().clone();
    521     wire.sig = "f".repeat(128);
    522     let mut raw: serde_json::Value = serde_json::from_str(valid.raw_json()).expect("raw event");
    523     raw["sig"] = serde_json::Value::String(wire.sig.clone());
    524     radroots_event::SignedEvent::from_wire_verified_id(
    525         wire,
    526         serde_json::to_string(&raw).expect("hostile raw event"),
    527     )
    528     .expect("ID-valid event with hostile signature")
    529 }