taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

commit f8078cd31e1046e7ffd587b8ac6bf27793590f99
parent 0c297ca460bccbae40934d2c3a6dd2bcfe39a324
Author: Antoine A <>
Date:   Fri, 31 Jul 2026 10:32:11 +0200

common: fix security bugs and improve subject parsing

Diffstat:
Madapters/taler-cyclos/src/api.rs | 14+++++++-------
Madapters/taler-cyclos/src/db.rs | 31+++++++++++++++----------------
Madapters/taler-cyclos/src/worker.rs | 46+++++++++++++++++++++++++++++-----------------
Madapters/taler-magnet-bank/src/api.rs | 14+++++++-------
Madapters/taler-magnet-bank/src/db.rs | 31+++++++++++++++----------------
Madapters/taler-magnet-bank/src/worker.rs | 51+++++++++++++++++++++++++++++----------------------
Madapters/taler-wise/src/api.rs | 16++++++++--------
Madapters/taler-wise/src/db.rs | 21++++++++++-----------
Madapters/taler-wise/src/worker.rs | 8++++++--
Mcommon/taler-api/src/api/prepared.rs | 5+++--
Mcommon/taler-api/src/subject.rs | 121+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mcommon/taler-test-utils/src/routine.rs | 15+++++++++++++++
12 files changed, 219 insertions(+), 154 deletions(-)

diff --git a/adapters/taler-cyclos/src/api.rs b/adapters/taler-cyclos/src/api.rs @@ -19,7 +19,7 @@ use jiff::Timestamp; use taler_api::{ api::{TalerApi, prepared::PreparedTransfer, revenue::Revenue, wire::WireGateway}, error::{ApiResult, failure_code}, - subject::{IncomingSubject, fmt_in_subject}, + subject::{IncomingKey, fmt_in_subject}, }; use taler_common::{ api::{ @@ -186,7 +186,7 @@ impl WireGateway for CyclosApi { subject: format!("Admin incoming {}", req.reserve_pub), debtor_id: *debtor.id, debtor_name: debtor.name, - metadata: IncomingSubject::Reserve(req.reserve_pub), + metadata: IncomingKey::reserve(req.reserve_pub), }, &Timestamp::now(), ) @@ -216,7 +216,7 @@ impl WireGateway for CyclosApi { subject: format!("Admin incoming KYC:{}", req.account_pub), debtor_id: *debtor.id, debtor_name: debtor.name, - metadata: IncomingSubject::Kyc(req.account_pub), + metadata: IncomingKey::kyc(req.account_pub), }, &Timestamp::now(), ) @@ -246,7 +246,7 @@ impl WireGateway for CyclosApi { subject: format!("Admin incoming MAP:{}", req.authorization_pub), debtor_id: *debtor.id, debtor_name: debtor.name, - metadata: IncomingSubject::Map(req.authorization_pub), + metadata: IncomingKey::map(req.authorization_pub), }, &Timestamp::now(), ) @@ -341,7 +341,7 @@ mod test { api::TalerRouter as _, auth::AuthMethod, db::TypeHelper as _, - subject::{IncomingSubject, OutgoingSubject}, + subject::{IncomingKey, OutgoingSubject}, }; use taler_common::{ api::{ @@ -429,7 +429,7 @@ mod test { static CODE: AtomicI64 = AtomicI64::new(0); - async fn r#in(db: &PgPool, subject: Option<IncomingSubject>) { + async fn r#in(db: &PgPool, subject: Option<IncomingKey>) { let now = Timestamp::now(); db::register_tx_in( &mut db.acquire().await.unwrap(), @@ -454,7 +454,7 @@ mod test { } async fn in_talerable(db: &PgPool) { - r#in(db, Some(IncomingSubject::Reserve(EddsaPublicKey::rand()))).await + r#in(db, Some(IncomingKey::reserve(EddsaPublicKey::rand()))).await } async fn out(db: &PgPool, kind: &TxOutKind) { diff --git a/adapters/taler-cyclos/src/db.rs b/adapters/taler-cyclos/src/db.rs @@ -23,7 +23,7 @@ use sqlx::{PgConnection, PgPool, QueryBuilder, Row, postgres::PgRow}; use taler_api::{ db::{BindHelper, TypeHelper, history, page}, serialized, - subject::{IncomingSubject, OutgoingSubject, fmt_out_subject}, + subject::{IncomingKey, OutgoingSubject, fmt_out_subject}, }; use taler_common::{ api::{ @@ -187,7 +187,7 @@ pub struct TxInAdmin { pub subject: String, pub debtor_id: i64, pub debtor_name: CompactString, - pub metadata: IncomingSubject, + pub metadata: IncomingKey, } #[derive(Debug, PartialEq, Eq)] @@ -228,8 +228,8 @@ pub async fn register_tx_in_admin( .bind(tx.debtor_id) .bind(&tx.debtor_name) .bind_timestamp(now) - .bind(tx.metadata.ty()) - .bind(tx.metadata.key()) + .bind(tx.metadata.ty) + .bind(tx.metadata.key) .try_map(|r: PgRow| { Ok(if r.try_get_flag(0)? { AddIncomingResult::ReservePubReuse @@ -253,7 +253,7 @@ pub async fn register_tx_in_admin( pub async fn register_tx_in( db: &mut PgConnection, tx: &TxIn, - subject: &Option<IncomingSubject>, + subject: &Option<IncomingKey>, now: &Timestamp, ) -> sqlx::Result<AddIncomingResult> { serialized!( @@ -270,8 +270,8 @@ pub async fn register_tx_in( .bind(tx.debtor_id) .bind(&tx.debtor_name) .bind(tx.valued_at.as_microsecond()) - .bind(subject.as_ref().map(|it| it.ty())) - .bind(subject.as_ref().map(|it| it.key())) + .bind(subject.as_ref().map(|it| it.ty)) + .bind(subject.as_ref().map(|it| it.key)) .bind(now.as_microsecond()) .try_map(|r: PgRow| { Ok(if r.try_get_flag(0)? { @@ -922,7 +922,7 @@ mod test { use taler_api::{ db::TypeHelper, notification::dummy_listen, - subject::{IncomingSubject, OutgoingSubject}, + subject::{IncomingKey, OutgoingSubject}, }; use taler_common::{ api::{ @@ -981,8 +981,7 @@ mod test { async fn tx_in() { let (mut db, pool) = setup().await; - let mut routine = async |first: &Option<IncomingSubject>, - second: &Option<IncomingSubject>| { + let mut routine = async |first: &Option<IncomingKey>, second: &Option<IncomingKey>| { let id = sqlx::query("SELECT count(*) + 1 FROM tx_in") .try_map(|r: PgRow| r.try_get_u64(0)) .fetch_one(&mut *db) @@ -1076,15 +1075,15 @@ mod test { // Reserve transaction routine( - &Some(IncomingSubject::Reserve(first)), - &Some(IncomingSubject::Reserve(second)), + &Some(IncomingKey::reserve(first)), + &Some(IncomingKey::reserve(second)), ) .await; // Kyc transaction routine( - &Some(IncomingSubject::Kyc(first)), - &Some(IncomingSubject::Kyc(first)), + &Some(IncomingKey::kyc(first)), + &Some(IncomingKey::kyc(first)), ) .await; @@ -1124,7 +1123,7 @@ mod test { subject: "subject".to_owned(), debtor_id: 31000163100000000, debtor_name: "Name".into(), - metadata: IncomingSubject::Reserve(EddsaPublicKey::rand()), + metadata: IncomingKey::reserve(EddsaPublicKey::rand()), }; // Insert assert_eq!( @@ -1144,7 +1143,7 @@ mod test { &pool, &TxInAdmin { subject: "Other".to_owned(), - metadata: IncomingSubject::Reserve(EddsaPublicKey::rand()), + metadata: IncomingKey::reserve(EddsaPublicKey::rand()), ..tx.clone() }, &later diff --git a/adapters/taler-cyclos/src/worker.rs b/adapters/taler-cyclos/src/worker.rs @@ -20,7 +20,7 @@ use failure_injection::{InjectedErr, fail_point}; use http_client::ApiErr; use jiff::Timestamp; use sqlx::{Acquire as _, PgConnection, PgPool, postgres::PgListener}; -use taler_api::subject::{self, parse_incoming_unstructured}; +use taler_api::subject::{self, IncomingSubject, parse_incoming_unstructured}; use taler_common::{ ExpoBackoffDecorr, config::Config, @@ -353,24 +353,36 @@ impl Worker<'_> { } match parse_incoming_unstructured(&tx.subject) { Ok(subject) => { - match db::register_tx_in(self.db, &tx, &Some(subject), &Timestamp::now()) - .await? - { - AddIncomingResult::Success { new, .. } => { - if new { - info!(target: "worker", "in {tx}"); - } else { - trace!(target: "worker", "in {tx} already seen"); + match subject { + IncomingSubject::Key(subject) => { + match db::register_tx_in( + self.db, + &tx, + &Some(subject), + &Timestamp::now(), + ) + .await? + { + AddIncomingResult::Success { new, .. } => { + if new { + info!(target: "worker", "in {tx}"); + } else { + trace!(target: "worker", "in {tx} already seen"); + } + } + AddIncomingResult::ReservePubReuse => { + bounce(self.db, "reserve pub reuse").await? + } + AddIncomingResult::UnknownMapping => { + bounce(self.db, "unknown mapping").await? + } + AddIncomingResult::MappingReuse => { + bounce(self.db, "mapping reuse").await? + } } } - AddIncomingResult::ReservePubReuse => { - bounce(self.db, "reserve pub reuse").await? - } - AddIncomingResult::UnknownMapping => { - bounce(self.db, "unknown mapping").await? - } - AddIncomingResult::MappingReuse => { - bounce(self.db, "mapping reuse").await? + IncomingSubject::AdminBalanceAdjust => { + // TODO bounce or skip ? } } } diff --git a/adapters/taler-magnet-bank/src/api.rs b/adapters/taler-magnet-bank/src/api.rs @@ -18,7 +18,7 @@ use jiff::Timestamp; use taler_api::{ api::{TalerApi, prepared::PreparedTransfer, revenue::Revenue, wire::WireGateway}, error::{ApiResult, failure_code}, - subject::{IncomingSubject, fmt_in_subject}, + subject::{IncomingKey, fmt_in_subject}, }; use taler_common::{ api::{ @@ -166,7 +166,7 @@ impl WireGateway for MagnetApi { amount: req.amount, subject: format!("Admin incoming {}", req.reserve_pub), debtor, - metadata: IncomingSubject::Reserve(req.reserve_pub), + metadata: IncomingKey::reserve(req.reserve_pub), }, &Timestamp::now(), ) @@ -195,7 +195,7 @@ impl WireGateway for MagnetApi { amount: req.amount, subject: format!("Admin incoming KYC:{}", req.account_pub), debtor, - metadata: IncomingSubject::Kyc(req.account_pub), + metadata: IncomingKey::kyc(req.account_pub), }, &Timestamp::now(), ) @@ -222,7 +222,7 @@ impl WireGateway for MagnetApi { amount: req.amount, subject: format!("Admin incoming MAP:{}", req.authorization_pub), debtor, - metadata: IncomingSubject::Map(req.authorization_pub), + metadata: IncomingKey::map(req.authorization_pub), }, &Timestamp::now(), ) @@ -313,7 +313,7 @@ mod test { api::TalerRouter as _, auth::AuthMethod, db::TypeHelper as _, - subject::{IncomingSubject, OutgoingSubject}, + subject::{IncomingKey, OutgoingSubject}, }; use taler_common::{ api::{ @@ -397,7 +397,7 @@ mod test { static CODE: AtomicU64 = AtomicU64::new(0); - async fn r#in(db: &PgPool, subject: Option<IncomingSubject>) { + async fn r#in(db: &PgPool, subject: Option<IncomingKey>) { db::register_tx_in( &mut db.acquire().await.unwrap(), &TxIn { @@ -422,7 +422,7 @@ mod test { } async fn in_talerable(db: &PgPool) { - r#in(db, Some(IncomingSubject::Reserve(EddsaPublicKey::rand()))).await + r#in(db, Some(IncomingKey::reserve(EddsaPublicKey::rand()))).await } async fn out(db: &PgPool, kind: &TxOutKind) { diff --git a/adapters/taler-magnet-bank/src/db.rs b/adapters/taler-magnet-bank/src/db.rs @@ -23,7 +23,7 @@ use sqlx::{PgConnection, PgPool, QueryBuilder, Row, postgres::PgRow}; use taler_api::{ db::{BindHelper, TypeHelper, history, page}, serialized, - subject::{IncomingSubject, OutgoingSubject, fmt_out_subject}, + subject::{IncomingKey, OutgoingSubject, fmt_out_subject}, }; use taler_common::{ api::{ @@ -175,7 +175,7 @@ pub struct TxInAdmin { pub amount: Amount, pub subject: String, pub debtor: FullHuPayto, - pub metadata: IncomingSubject, + pub metadata: IncomingKey, } /// Lock the database for worker execution @@ -216,8 +216,8 @@ pub async fn register_tx_in_admin( .bind(tx.debtor.iban()) .bind(&tx.debtor.name) .bind_date(&now.to_zoned(TimeZone::UTC).date()) - .bind(tx.metadata.ty()) - .bind(tx.metadata.key()) + .bind(tx.metadata.ty) + .bind(tx.metadata.key) .try_map(|r: PgRow| { Ok(if r.try_get_flag(0)? { AddIncomingResult::ReservePubReuse @@ -241,7 +241,7 @@ pub async fn register_tx_in_admin( pub async fn register_tx_in( db: &mut PgConnection, tx: &TxIn, - subject: &Option<IncomingSubject>, + subject: &Option<IncomingKey>, now: &Timestamp, ) -> sqlx::Result<AddIncomingResult> { serialized!( @@ -257,8 +257,8 @@ pub async fn register_tx_in( .bind(tx.debtor.iban()) .bind(&tx.debtor.name) .bind_date(&tx.value_date) - .bind(subject.as_ref().map(|it| it.ty())) - .bind(subject.as_ref().map(|it| it.key())) + .bind(subject.as_ref().map(|it| it.ty)) + .bind(subject.as_ref().map(|it| it.key)) .bind_timestamp(now) .try_map(|r: PgRow| { Ok(if r.try_get_flag(0)? { @@ -887,7 +887,7 @@ mod test { use taler_api::{ db::TypeHelper, notification::dummy_listen, - subject::{IncomingSubject, OutgoingSubject}, + subject::{IncomingKey, OutgoingSubject}, }; use taler_common::{ api::{ @@ -942,8 +942,7 @@ mod test { async fn tx_in() { let (mut db, pool) = setup().await; - let mut routine = async |first: &Option<IncomingSubject>, - second: &Option<IncomingSubject>| { + let mut routine = async |first: &Option<IncomingKey>, second: &Option<IncomingKey>| { let (id, code) = sqlx::query("SELECT count(*) + 1, COALESCE(max(magnet_code), 0) + 20 FROM tx_in") .try_map(|r: PgRow| Ok((r.try_get_u64(0)?, r.try_get_u64(1)?))) @@ -1037,15 +1036,15 @@ mod test { // Reserve transaction routine( - &Some(IncomingSubject::Reserve(EddsaPublicKey::rand())), - &Some(IncomingSubject::Reserve(EddsaPublicKey::rand())), + &Some(IncomingKey::reserve(EddsaPublicKey::rand())), + &Some(IncomingKey::reserve(EddsaPublicKey::rand())), ) .await; // Kyc transaction routine( - &Some(IncomingSubject::Kyc(EddsaPublicKey::rand())), - &Some(IncomingSubject::Kyc(EddsaPublicKey::rand())), + &Some(IncomingKey::kyc(EddsaPublicKey::rand())), + &Some(IncomingKey::kyc(EddsaPublicKey::rand())), ) .await; @@ -1085,7 +1084,7 @@ mod test { amount: amount("EUR:10"), subject: "subject".to_owned(), debtor: magnet_payto("payto://iban/HU30162000031000163100000000?receiver-name=name"), - metadata: IncomingSubject::Reserve(EddsaPublicKey::rand()), + metadata: IncomingKey::reserve(EddsaPublicKey::rand()), }; // Insert assert_eq!( @@ -1105,7 +1104,7 @@ mod test { &pool, &TxInAdmin { subject: "Other".to_owned(), - metadata: IncomingSubject::Reserve(EddsaPublicKey::rand()), + metadata: IncomingKey::reserve(EddsaPublicKey::rand()), ..tx.clone() }, &later diff --git a/adapters/taler-magnet-bank/src/worker.rs b/adapters/taler-magnet-bank/src/worker.rs @@ -21,7 +21,7 @@ use failure_injection::{InjectedErr, fail_point}; use http_client::ApiErr; use jiff::{Timestamp, Zoned, civil::Date}; use sqlx::{Acquire as _, PgConnection, PgPool, postgres::PgListener}; -use taler_api::subject::{self, parse_incoming_unstructured}; +use taler_api::subject::{self, IncomingSubject, parse_incoming_unstructured}; use taler_common::{ ExpoBackoffDecorr, config::Config, @@ -247,29 +247,36 @@ impl Worker<'_> { match self.account_type { AccountType::Exchange => { match parse_incoming_unstructured(&tx_in.subject) { - Ok(subject) => match db::register_tx_in( - self.db, - &tx_in, - &Some(subject), - &Timestamp::now(), - ) - .await? - { - AddIncomingResult::Success { new, .. } => { - if new { - info!(target: "worker", "in {tx_in}"); - } else { - trace!(target: "worker", "in {tx_in} already seen"); + Ok(subject) => match subject { + IncomingSubject::Key(subject) => { + match db::register_tx_in( + self.db, + &tx_in, + &Some(subject), + &Timestamp::now(), + ) + .await? + { + AddIncomingResult::Success { new, .. } => { + if new { + info!(target: "worker", "in {tx_in}"); + } else { + trace!(target: "worker", "in {tx_in} already seen"); + } + } + AddIncomingResult::ReservePubReuse => { + bounce(self.db, "reserve pub reuse").await? + } + AddIncomingResult::UnknownMapping => { + bounce(self.db, "unknown mapping").await? + } + AddIncomingResult::MappingReuse => { + bounce(self.db, "mapping reuse").await? + } } } - AddIncomingResult::ReservePubReuse => { - bounce(self.db, "reserve pub reuse").await? - } - AddIncomingResult::UnknownMapping => { - bounce(self.db, "unknown mapping").await? - } - AddIncomingResult::MappingReuse => { - bounce(self.db, "mapping reuse").await? + IncomingSubject::AdminBalanceAdjust => { + // TODO bounce or skip ? } }, Err(e) => bounce(self.db, &e.to_string()).await?, diff --git a/adapters/taler-wise/src/api.rs b/adapters/taler-wise/src/api.rs @@ -24,7 +24,7 @@ use taler_api::{ }, config::ApiCfg, error::{ApiResult, failure_code, not_implemented}, - subject::{IncomingSubject, fmt_in_subject}, + subject::{IncomingKey, fmt_in_subject}, }; use taler_common::{ api::{ @@ -122,7 +122,7 @@ impl WiseBalanceApi { async fn add_incoming( &self, tx: &TxIn, - subject: IncomingSubject, + subject: IncomingKey, ) -> ApiResult<AddIncomingResponse> { let now = Timestamp::now(); match db::register_tx_in(&self.pool, tx, &Some(subject), &now).await? { @@ -198,7 +198,7 @@ impl WireGateway for WiseBalanceApi { debtor: Some(account), value_at: Timestamp::now(), }, - IncomingSubject::Reserve(req.reserve_pub), + IncomingKey::reserve(req.reserve_pub), ) .await } @@ -216,7 +216,7 @@ impl WireGateway for WiseBalanceApi { debtor: Some(account), value_at: Timestamp::now(), }, - IncomingSubject::Kyc(req.account_pub), + IncomingKey::kyc(req.account_pub), ) .await } @@ -234,7 +234,7 @@ impl WireGateway for WiseBalanceApi { debtor: Some(account), value_at: Timestamp::now(), }, - IncomingSubject::Map(req.authorization_pub), + IncomingKey::map(req.authorization_pub), ) .await } @@ -307,7 +307,7 @@ mod test { use taler_api::{ api::TalerRouter as _, config::{ApiCfg, AuthCfg}, - subject::IncomingSubject, + subject::IncomingKey, }; use taler_common::{ api::{ @@ -392,7 +392,7 @@ mod test { } } - async fn r#in(db: &PgPool, subject: Option<IncomingSubject>) { + async fn r#in(db: &PgPool, subject: Option<IncomingKey>) { register_tx_in( db, &TxIn { @@ -419,7 +419,7 @@ mod test { } async fn in_talerable(db: &PgPool) { - r#in(db, Some(IncomingSubject::Reserve(EddsaPublicKey::rand()))).await + r#in(db, Some(IncomingKey::reserve(EddsaPublicKey::rand()))).await } #[tokio::test] diff --git a/adapters/taler-wise/src/db.rs b/adapters/taler-wise/src/db.rs @@ -22,7 +22,7 @@ use sqlx::{PgPool, QueryBuilder, Row, postgres::PgRow}; use taler_api::{ db::{BindHelper, TypeHelper, history}, serialized, - subject::IncomingSubject, + subject::IncomingKey, }; use taler_common::{ api::{ @@ -133,7 +133,7 @@ pub enum AddIncomingResult { pub async fn register_tx_in( db: &PgPool, tx: &TxIn, - subject: &Option<IncomingSubject>, + subject: &Option<IncomingKey>, now: &Timestamp, ) -> sqlx::Result<AddIncomingResult> { let payto = tx.debtor.as_ref().map(|it| it.as_uri()); @@ -151,8 +151,8 @@ pub async fn register_tx_in( .bind(payto.as_ref().map(|it| it.as_ref().as_str())) .bind(&tx.name) .bind_timestamp(&tx.value_at) - .bind(subject.as_ref().map(|it| it.ty())) - .bind(subject.as_ref().map(|it| it.key())) + .bind(subject.as_ref().map(|it| it.ty)) + .bind(subject.as_ref().map(|it| it.key)) .bind_timestamp(now) .try_map(|r: PgRow| { Ok(if r.try_get_flag(0)? { @@ -332,7 +332,7 @@ mod test { use compact_str::{CompactString, format_compact}; use jiff::Span; use sqlx::{PgPool, Postgres, pool::PoolConnection, postgres::PgRow}; - use taler_api::{db::TypeHelper, notification::dummy_listen, subject::IncomingSubject}; + use taler_api::{db::TypeHelper, notification::dummy_listen, subject::IncomingKey}; use taler_common::{ api::{EddsaPublicKey, params::History}, types::{ @@ -360,8 +360,7 @@ mod test { async fn tx_in() { let (mut db, pool) = setup().await; - let mut routine = async |first: &Option<IncomingSubject>, - second: &Option<IncomingSubject>| { + let mut routine = async |first: &Option<IncomingKey>, second: &Option<IncomingKey>| { let id = sqlx::query("SELECT count(*) + 1 FROM tx_in") .try_map(|r: PgRow| r.try_get_u64(0)) .fetch_one(&mut *db) @@ -455,15 +454,15 @@ mod test { // Reserve transaction routine( - &Some(IncomingSubject::Reserve(EddsaPublicKey::rand())), - &Some(IncomingSubject::Reserve(EddsaPublicKey::rand())), + &Some(IncomingKey::reserve(EddsaPublicKey::rand())), + &Some(IncomingKey::reserve(EddsaPublicKey::rand())), ) .await; // Kyc transaction routine( - &Some(IncomingSubject::Kyc(EddsaPublicKey::rand())), - &Some(IncomingSubject::Kyc(EddsaPublicKey::rand())), + &Some(IncomingKey::kyc(EddsaPublicKey::rand())), + &Some(IncomingKey::kyc(EddsaPublicKey::rand())), ) .await; diff --git a/adapters/taler-wise/src/worker.rs b/adapters/taler-wise/src/worker.rs @@ -20,7 +20,7 @@ use http_client::ApiErr; use jiff::Timestamp; use regex::Regex; use sqlx::PgPool; -use taler_api::subject::parse_incoming_unstructured; +use taler_api::subject::{IncomingSubject, parse_incoming_unstructured}; use taler_common::{ExpoBackoffDecorr, config::Config, types::payto::BankID}; use tracing::{error, info, trace, warn}; @@ -103,8 +103,12 @@ pub async fn run_worker( debtor: payto, value_at: tx.date, }; + let subject = &match subject { + Ok(IncomingSubject::Key(key)) => Some(key), + Ok(IncomingSubject::AdminBalanceAdjust) | Err(_) => None, + }; let failure = - match register_tx_in(pool, &t, &subject.ok(), &now).await? { + match register_tx_in(pool, &t, subject, &now).await? { AddIncomingResult::Success { new, .. } => { if new { info!(target: "worker", "in {t}"); diff --git a/common/taler-api/src/api/prepared.rs b/common/taler-api/src/api/prepared.rs @@ -67,9 +67,10 @@ impl Validation for RegistrationRequest { impl Validation for Unregistration { fn check(&self, _: &Currency) -> ApiResult<()> { - if self.timestamp.duration_until(Timestamp::now()) > SignedDuration::from_mins(5) { + let duration = self.timestamp.duration_until(Timestamp::now()); + if duration > SignedDuration::from_mins(5) || duration.is_negative() { return Err(failure_code(ErrorCode::BANK_OLD_TIMESTAMP)); - } + }; if !self.verify(&self.authorization_pub, &self.authorization_sig) { return Err(failure_code(ErrorCode::BANK_BAD_SIGNATURE)); diff --git a/common/taler-api/src/subject.rs b/common/taler-api/src/subject.rs @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2024, 2025, 2026 Taler Systems SA + Copyright (C) 2024-2026 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -31,28 +31,35 @@ use url::Url; #[derive(Debug, Clone, PartialEq, Eq)] pub enum IncomingSubject { - Reserve(EddsaPublicKey), - Kyc(EddsaPublicKey), - Map(EddsaPublicKey), + Key(IncomingKey), AdminBalanceAdjust, } -impl IncomingSubject { - pub fn ty(&self) -> IncomingType { - match self { - IncomingSubject::Reserve(_) => IncomingType::reserve, - IncomingSubject::Kyc(_) => IncomingType::kyc, - IncomingSubject::Map(_) => IncomingType::map, - IncomingSubject::AdminBalanceAdjust => panic!("Admin balance adjust"), +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IncomingKey { + pub ty: IncomingType, + pub key: EddsaPublicKey, +} + +impl IncomingKey { + pub fn reserve(key: EddsaPublicKey) -> Self { + Self { + ty: IncomingType::reserve, + key, + } + } + + pub fn kyc(key: EddsaPublicKey) -> Self { + Self { + ty: IncomingType::kyc, + key, } } - pub fn key(&self) -> &EddsaPublicKey { - match self { - IncomingSubject::Kyc(key) - | IncomingSubject::Reserve(key) - | IncomingSubject::Map(key) => key, - IncomingSubject::AdminBalanceAdjust => panic!("Admin balance adjust"), + pub fn map(key: EddsaPublicKey) -> Self { + Self { + ty: IncomingType::map, + key, } } } @@ -93,8 +100,9 @@ impl Base32Quality { let mut uppercase = true; let mut standard = true; for b in s.bytes() { - uppercase &= b.is_ascii_uppercase(); + uppercase &= b.is_ascii_uppercase() | b.is_ascii_digit(); standard &= CROCKFORD_ALPHABET.contains(&b) + | CROCKFORD_ALPHABET.contains(&b.to_ascii_uppercase()) } match (uppercase, standard) { (true, true) => Base32Quality::UpperStandard, @@ -225,11 +233,7 @@ pub fn parse_incoming_unstructured(subject: &str) -> Result<IncomingSubject, Inc let quality = Base32Quality::measure(raw); Some(Candidate { - subject: match ty { - IncomingType::reserve => IncomingSubject::Reserve(key), - IncomingType::kyc => IncomingSubject::Kyc(key), - IncomingType::map => IncomingSubject::Map(key), - }, + subject: IncomingSubject::Key(IncomingKey { ty, key }), quality, }) } @@ -248,7 +252,7 @@ pub fn parse_incoming_unstructured(subject: &str) -> Result<IncomingSubject, Inc }; // Find best candidates - let mut best: Option<Candidate> = None; + let mut best: Option<(IncomingType, EddsaPublicKey, Base32Quality)> = None; // For each part as a starting point for (i, &start) in parts.iter().enumerate() { // Use progressively longer concatenation @@ -264,35 +268,41 @@ pub fn parse_incoming_unstructured(subject: &str) -> Result<IncomingSubject, Inc // Parse the concatenated parts // SAFETY: we now end.end <= concatenated.len let slice = unsafe { &concatenated.get_unchecked(start as usize..end as usize) }; - if let Some(other) = parse_single(slice) { + if let Some(new) = parse_single(slice) { + let (nty, nkey) = match new.subject { + IncomingSubject::AdminBalanceAdjust => { + return Ok(IncomingSubject::AdminBalanceAdjust); + } + IncomingSubject::Key(IncomingKey { ty, key }) => (ty, key), + }; // On success update best candidate - match &mut best { - Some(best) => { - if other.quality > best.quality // We prefer high quality keys + match best { + Some((bty, bkey, bquality)) => { + if new.quality > bquality // We prefer high quality keys || matches!( // We prefer prefixed keys over reserve keys - (&best.subject.ty(), &other.subject.ty()), + (bty, nty), (IncomingType::reserve, IncomingType::kyc | IncomingType::map) ) { - *best = other - } else if best.subject.key() != other.subject.key() // If keys are different - && best.quality == other.quality // Of same quality + best = Some((nty, nkey, new.quality)) + } else if bkey != nkey // If keys are different + && bquality == new.quality // Of same quality && !matches!( // And prefixing is different - (&best.subject.ty(), &other.subject.ty()), + (bty, nty), (IncomingType::kyc | IncomingType::map, IncomingType::reserve) ) { return Err(IncomingSubjectErr::Ambiguous); } } - None => best = Some(other), + None => best = Some((nty, nkey, new.quality)), } } } } - if let Some(it) = best { - Ok(it.subject) + if let Some((ty, key, _)) = best { + Ok(IncomingSubject::Key(IncomingKey { ty, key })) } else { Err(IncomingSubjectErr::Missing) } @@ -354,8 +364,9 @@ mod test { }; use crate::subject::{ - IncomingSubject, IncomingSubjectErr, OutgoingSubject, fmt_out_subject, mod10_recursive, - parse_incoming_unstructured, parse_outgoing, subject_fmt_qr_bill, subject_is_qr_bill, + Base32Quality, IncomingKey, IncomingSubject, IncomingSubjectErr, OutgoingSubject, + fmt_out_subject, mod10_recursive, parse_incoming_unstructured, parse_outgoing, + subject_fmt_qr_bill, subject_is_qr_bill, }; #[test] @@ -383,6 +394,26 @@ mod test { } #[test] + fn quality() { + assert_eq!( + Base32Quality::measure("4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"), + Base32Quality::UpperStandard + ); + assert_eq!( + Base32Quality::measure("4MZT6RS3RVB3B0E2RDMYW0YRA3Y0UPHYV0CYDE6XBB0YMPFXCEG0"), + Base32Quality::Upper + ); + assert_eq!( + Base32Quality::measure("4mZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"), + Base32Quality::Standard + ); + assert_eq!( + Base32Quality::measure("4mZT6RS3RVB3B0E2RDMYW0YRA3Y0UPHYV0CYDE6XBB0YMPFXCEG0"), + Base32Quality::Mixed + ); + } + + #[test] /** Test parsing logic */ fn incoming_parse() { let key = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"; @@ -403,11 +434,7 @@ mod test { let other_mixed = &format!("{prefix}TEGY6d9mh9pgwvwpgs0z0095z854xegfy7jj202yd0esp8p0za60"); let key = EddsaPublicKey::from_str(key).unwrap(); - let result = Ok(match ty { - IncomingType::reserve => IncomingSubject::Reserve(key), - IncomingType::kyc => IncomingSubject::Kyc(key), - IncomingType::map => IncomingSubject::Map(key), - }); + let result = Ok(IncomingSubject::Key(IncomingKey { ty, key })); // Check succeed if standard or mixed for case in [standard, mixed] { @@ -529,9 +556,9 @@ mod test { ), ] { assert_eq!( - Ok(IncomingSubject::Reserve( + Ok(IncomingSubject::Key(IncomingKey::reserve( EddsaPublicKey::from_str(key).unwrap(), - )), + ))), parse_incoming_unstructured(subject) ) } @@ -541,7 +568,9 @@ mod test { "JW398X85FWPKKMS0EYB6TQ1799RMY5DDXTZFPW4YC3WJ2DWSJT70", )] { assert_eq!( - Ok(IncomingSubject::Kyc(EddsaPublicKey::from_str(key).unwrap(),)), + Ok(IncomingSubject::Key(IncomingKey::kyc( + EddsaPublicKey::from_str(key).unwrap(), + ))), parse_incoming_unstructured(subject) ) } diff --git a/common/taler-test-utils/src/routine.rs b/common/taler-test-utils/src/routine.rs @@ -1598,6 +1598,21 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( .await .assert_error(ErrorCode::BANK_OLD_TIMESTAMP); + // Future timestamp + prepared_transfer + .post("/unregistration") + .json( + Unregistration { + timestamp: TalerTimestamp::Timestamp( + Timestamp::now() + SignedDuration::from_mins(1), + ), + ..un_req.clone() + } + .signed(&auth_pair), + ) + .await + .assert_error(ErrorCode::BANK_OLD_TIMESTAMP); + /* ----- API ----- */ let history: Vec<_> = wire_gateway