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:
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