Reject changed rental metadata before background verification
This commit is contained in:
@@ -67,6 +67,7 @@ impl ApiHandler {
|
||||
content_id: String,
|
||||
#[serde(default)]
|
||||
retry: bool,
|
||||
expected: Option<crate::content_purchase_caller::ExpectedRental>,
|
||||
}
|
||||
let input: Prepare = serde_json::from_slice(&bytes)?;
|
||||
let identity = std::sync::Arc::new(
|
||||
@@ -76,6 +77,14 @@ impl ApiHandler {
|
||||
let root = data_dir.clone();
|
||||
serde_json::to_value(
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if let Some(expected) = &input.expected {
|
||||
let (receipt, _) = crate::registered_media::registered_metadata(
|
||||
&root,
|
||||
&identity,
|
||||
&input.content_id,
|
||||
)?;
|
||||
expected.verify_metadata(&identity.did_key()?, &receipt)?;
|
||||
}
|
||||
crate::registered_media::prepare_registered(
|
||||
root,
|
||||
identity,
|
||||
|
||||
@@ -15,6 +15,7 @@ pub(crate) trait PurchaseTransport: Send + Sync {
|
||||
fn prepare_offer(
|
||||
&self,
|
||||
_content_id: &str,
|
||||
_expected: Option<&ExpectedRental>,
|
||||
) -> impl Future<Output = Result<Option<(u64, u64)>>> + Send {
|
||||
async { Ok(None) }
|
||||
}
|
||||
@@ -86,7 +87,8 @@ pub(crate) async fn purchase(
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct ExpectedRental {
|
||||
pub seller_did: String,
|
||||
pub content_id: String,
|
||||
@@ -95,6 +97,23 @@ pub(crate) struct ExpectedRental {
|
||||
pub viewing_seconds: u64,
|
||||
}
|
||||
impl ExpectedRental {
|
||||
pub(crate) fn verify_metadata(
|
||||
&self,
|
||||
seller_did: &str,
|
||||
receipt: &crate::media_registration::Receipt,
|
||||
) -> Result<()> {
|
||||
anyhow::ensure!(
|
||||
self.content_id.starts_with("registered_")
|
||||
&& receipt.content_id == self.content_id
|
||||
&& seller_did == self.seller_did
|
||||
&& receipt.sha256 == self.sha256
|
||||
&& receipt.price_sats == self.price_sats
|
||||
&& receipt.viewing_seconds == self.viewing_seconds,
|
||||
"Published rental hash, price, duration or seller changed; no payment started"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn verify(&self, offer: &Offer) -> Result<()> {
|
||||
anyhow::ensure!(
|
||||
self.content_id.starts_with("registered_")
|
||||
@@ -222,7 +241,7 @@ pub(crate) async fn purchase_bound(
|
||||
);
|
||||
if content_id.starts_with("registered_") {
|
||||
if let Some((completed_bytes, total_bytes)) =
|
||||
transport.prepare_offer(content_id).await?
|
||||
transport.prepare_offer(content_id, expected).await?
|
||||
{
|
||||
return Ok(ReadyPurchase::Preparing {
|
||||
completed_bytes,
|
||||
|
||||
@@ -79,11 +79,15 @@ impl PurchaseTransport for FipsPurchaseTransport {
|
||||
fn seller_did(&self) -> &str {
|
||||
&self.seller_did
|
||||
}
|
||||
async fn prepare_offer(&self, content_id: &str) -> Result<Option<(u64, u64)>> {
|
||||
async fn prepare_offer(
|
||||
&self,
|
||||
content_id: &str,
|
||||
expected: Option<&crate::content_purchase_caller::ExpectedRental>,
|
||||
) -> Result<Option<(u64, u64)>> {
|
||||
let state: crate::rental_readiness::Status = self
|
||||
.post(
|
||||
protocol::PREPARE_OFFER_ROUTE,
|
||||
&serde_json::json!({"content_id":content_id,"retry":self.retry_preparation}),
|
||||
&serde_json::json!({"content_id":content_id,"retry":self.retry_preparation,"expected":expected}),
|
||||
)
|
||||
.await?;
|
||||
match state {
|
||||
|
||||
@@ -2214,7 +2214,14 @@ impl crate::content_purchase_caller::PurchaseTransport for PurchaseTestTransport
|
||||
fn seller_onion(&self) -> &str {
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.onion"
|
||||
}
|
||||
async fn prepare_offer(&self, content_id: &str) -> anyhow::Result<Option<(u64, u64)>> {
|
||||
async fn prepare_offer(
|
||||
&self,
|
||||
content_id: &str,
|
||||
expected: Option<&crate::content_purchase_caller::ExpectedRental>,
|
||||
) -> anyhow::Result<Option<(u64, u64)>> {
|
||||
if let Some(expected) = expected {
|
||||
expected.verify(&self.template)?;
|
||||
}
|
||||
// A registered movie still being hashed, without asking the fake mint.
|
||||
Ok(content_id.starts_with("registered_").then_some((3, 16)))
|
||||
}
|
||||
@@ -2613,6 +2620,20 @@ async fn rental_catalog_term_mismatch_never_plans_or_creates_buyer_intent() {
|
||||
price_sats: 8,
|
||||
viewing_seconds: 60,
|
||||
};
|
||||
// Preparation checks already-verified signed metadata, without scanning bytes
|
||||
// or allocating a quote. Keep this independent of the fake offer response.
|
||||
let metadata: crate::media_registration::Receipt = serde_json::from_value(json!({
|
||||
"version":1,"request_id":uuid::Uuid::new_v4().to_string(),"nonce":"fixture",
|
||||
"app_audience":"indeedhub","node_did":expected.seller_did,
|
||||
"producer":"fixture","project_id":"fixture","price_sats":8,
|
||||
"viewing_seconds":60,"expires_at":now+300,"content_id":"registered_film",
|
||||
"sha256":"ab".repeat(32),"size_bytes":"16","payment_methods":["cashu"],
|
||||
"issued_at":now,"signature":"verified by registration boundary"
|
||||
}))
|
||||
.unwrap();
|
||||
expected
|
||||
.verify_metadata(&expected.seller_did, &metadata)
|
||||
.unwrap();
|
||||
let mut changed_hash = expected.clone();
|
||||
changed_hash.sha256 = "ef".repeat(32);
|
||||
let mut changed_price = expected.clone();
|
||||
@@ -2620,6 +2641,9 @@ async fn rental_catalog_term_mismatch_never_plans_or_creates_buyer_intent() {
|
||||
let mut changed_duration = expected.clone();
|
||||
changed_duration.viewing_seconds = 120;
|
||||
for wrong in [changed_hash, changed_price, changed_duration] {
|
||||
assert!(wrong
|
||||
.verify_metadata(&expected.seller_did, &metadata)
|
||||
.is_err());
|
||||
let error = purchase_bound(
|
||||
buyer.path(),
|
||||
&buyer_did,
|
||||
|
||||
Reference in New Issue
Block a user