commit 2a7d903ae13ef097b4503df7394d936d3ee8265f parent 349fb8bcde2da24d32249824f5a6c7a09758d069 Author: Antoine A <> Date: Tue, 30 Jun 2026 16:17:19 +0200 prepared-transfer-api: new signatures Diffstat:
19 files changed, 605 insertions(+), 343 deletions(-)
diff --git a/README.md b/README.md @@ -32,3 +32,13 @@ Setup documentation can be found [here](https://docs.taler.net/taler-cyclos-manu - **taler-common**: GNU Taler component logic and types - **taler-test-utils**: Test utils for GNU Taler adapter written in Rust GNU - **taler-apns-relay**: APNs relay + +## Getting Started for Development + +```sh +sudo apt install rustup +rustup default stable +./bootstrap +./configure +make check +``` diff --git a/adapters/taler-cyclos/src/db.rs b/adapters/taler-cyclos/src/db.rs @@ -350,7 +350,7 @@ pub async fn register_tx_out( .bind(None::<&str>) .bind(*bounced), TxOutKind::Talerable(subject) => query - .bind(&subject.wtid) + .bind(subject.wtid) .bind(subject.exchange_base_url.as_str()) .bind(&subject.metadata) .bind(None::<i64>), @@ -398,8 +398,8 @@ pub async fn make_transfer( FROM taler_transfer($1, $2, $3, $4, $5, $6, $7, $8, $9) ", ) - .bind(&tx.request_uid) - .bind(&tx.wtid) + .bind(tx.request_uid) + .bind(tx.wtid) .bind(&subject) .bind(tx.amount) .bind(tx.exchange_base_url.as_str()) @@ -838,9 +838,9 @@ pub async fn transfer_register( "SELECT out_reserve_pub_reuse FROM register_prepared_transfers($1,$2,$3,$4,$5,$6)" ) .bind(ty) - .bind(&req.account_pub) - .bind(&req.authorization_pub) - .bind(&req.authorization_sig) + .bind(req.account_pub) + .bind(req.authorization_pub) + .bind(req.authorization_sig) .bind(req.recurrent) .bind_timestamp(&Timestamp::now()) .try_map(|r: PgRow| { @@ -857,7 +857,7 @@ pub async fn transfer_register( pub async fn transfer_unregister(db: &PgPool, req: &Unregistration) -> sqlx::Result<bool> { serialized!( sqlx::query("SELECT out_found FROM delete_prepared_transfers($1)") - .bind(&req.authorization_pub) + .bind(req.authorization_pub) .try_map(|r: PgRow| r.try_get_flag("out_found")) .fetch_one(db) ) @@ -1076,14 +1076,14 @@ mod test { // Reserve transaction routine( - &Some(IncomingSubject::Reserve(first.clone())), + &Some(IncomingSubject::Reserve(first)), &Some(IncomingSubject::Reserve(second)), ) .await; // Kyc transaction routine( - &Some(IncomingSubject::Kyc(first.clone())), + &Some(IncomingSubject::Kyc(first)), &Some(IncomingSubject::Kyc(first)), ) .await; diff --git a/adapters/taler-magnet-bank/src/db.rs b/adapters/taler-magnet-bank/src/db.rs @@ -336,7 +336,7 @@ pub async fn register_tx_out( .bind(None::<&str>) .bind(*bounced as i64), TxOutKind::Talerable(subject) => query - .bind(&subject.wtid) + .bind(subject.wtid) .bind(subject.exchange_base_url.as_str()) .bind(&subject.metadata) .bind(None::<i64>), @@ -414,8 +414,8 @@ pub async fn make_transfer( FROM taler_transfer($1, $2, $3, $4, $5, $6, $7, $8, $9) ", ) - .bind(&tx.request_uid) - .bind(&tx.wtid) + .bind(tx.request_uid) + .bind(tx.wtid) .bind(&subject) .bind(tx.amount) .bind(tx.exchange_base_url.as_str()) @@ -851,9 +851,9 @@ pub async fn transfer_register( "SELECT out_reserve_pub_reuse FROM register_prepared_transfers($1,$2,$3,$4,$5,$6)" ) .bind(ty) - .bind(&req.account_pub) - .bind(&req.authorization_pub) - .bind(&req.authorization_sig) + .bind(req.account_pub) + .bind(req.authorization_pub) + .bind(req.authorization_sig) .bind(req.recurrent) .bind_timestamp(&Timestamp::now()) .try_map(|r: PgRow| { @@ -870,7 +870,7 @@ pub async fn transfer_register( pub async fn transfer_unregister(db: &PgPool, req: &Unregistration) -> sqlx::Result<bool> { serialized!( sqlx::query("SELECT out_found FROM delete_prepared_transfers($1,$2)") - .bind(&req.authorization_pub) + .bind(req.authorization_pub) .bind_timestamp(&Timestamp::now()) .try_map(|r: PgRow| r.try_get_flag("out_found")) .fetch_one(db) diff --git a/adapters/taler-magnet-bank/src/setup.rs b/adapters/taler-magnet-bank/src/setup.rs @@ -224,7 +224,7 @@ mod test { let content: KeysFile = json_file::load("./fixtures/setup.json").unwrap(); // Check full assert!(content.access_token.is_some()); - let key = content.signing_key.clone().unwrap(); + let key = content.signing_key.unwrap(); // Load signing key let secret_key = parse_private_key(&key).unwrap(); diff --git a/common/taler-api/src/api/prepared.rs b/common/taler-api/src/api/prepared.rs @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::{str::FromStr, sync::Arc}; +use std::sync::Arc; use axum::{ Json, Router, @@ -31,6 +31,7 @@ use taler_common::{ }, db::IncomingType, error_code::ErrorCode, + signature::Signature, types::amount::Currency, }; @@ -38,8 +39,7 @@ use super::TalerApi; use crate::{ api::{Validation, check_currency}, constants::PREPARED_TRANSFER_API_VERSION, - crypto::check_eddsa_signature, - error::{ApiResult, failure, failure_code}, + error::{ApiResult, failure_code}, extract::Req, subject::fmt_in_subject, }; @@ -58,11 +58,7 @@ pub trait PreparedTransfer: TalerApi { impl Validation for RegistrationRequest { fn check(&self, currency: &Currency) -> ApiResult<()> { - if !check_eddsa_signature( - &self.authorization_pub, - self.account_pub.as_ref(), - &self.authorization_sig, - ) { + if !self.verify(&self.authorization_pub, &self.authorization_sig) { return Err(failure_code(ErrorCode::BANK_BAD_SIGNATURE)); } check_currency(currency, &self.credit_amount) @@ -71,18 +67,11 @@ impl Validation for RegistrationRequest { impl Validation for Unregistration { fn check(&self, _: &Currency) -> ApiResult<()> { - let timestamp = Timestamp::from_str(&self.timestamp).map_err(|e| { - failure(ErrorCode::GENERIC_JSON_INVALID, e.to_string()).with_path("timestamp") - })?; - if timestamp.duration_until(Timestamp::now()) > SignedDuration::from_mins(5) { + if self.timestamp.duration_until(Timestamp::now()) > SignedDuration::from_mins(5) { return Err(failure_code(ErrorCode::BANK_OLD_TIMESTAMP)); } - if !check_eddsa_signature( - &self.authorization_pub, - self.timestamp.as_ref(), - &self.authorization_sig, - ) { + if !self.verify(&self.authorization_pub, &self.authorization_sig) { return Err(failure_code(ErrorCode::BANK_BAD_SIGNATURE)); } Ok(()) diff --git a/common/taler-api/src/crypto.rs b/common/taler-api/src/crypto.rs @@ -1,29 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 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 - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -*/ - -use aws_lc_rs::signature::{self, Ed25519KeyPair, UnparsedPublicKey}; -use taler_common::api::{EddsaPublicKey, EddsaSignature}; - -pub fn check_eddsa_signature(key: &EddsaPublicKey, msg: &[u8], sign: &EddsaSignature) -> bool { - UnparsedPublicKey::new(&signature::ED25519, key.as_ref()) - .verify(msg, sign.as_ref()) - .is_ok() -} - -pub fn eddsa_sign(key: &Ed25519KeyPair, msg: &[u8]) -> EddsaSignature { - let signature = key.sign(msg); - EddsaSignature::try_from(signature.as_ref()).unwrap() -} diff --git a/common/taler-api/src/lib.rs b/common/taler-api/src/lib.rs @@ -24,7 +24,6 @@ pub mod api; pub mod auth; pub mod config; pub mod constants; -pub mod crypto; pub mod db; pub mod error; pub mod extract; diff --git a/common/taler-api/src/subject.rs b/common/taler-api/src/subject.rs @@ -549,15 +549,15 @@ mod test { #[test] fn outgoing() { - let key = ShortHashCode::rand(); + let wtid = ShortHashCode::rand(); // Without metadata - let subject = format!("{key} http://exchange.example.com/"); + let subject = format!("{wtid} http://exchange.example.com/"); let parsed = parse_outgoing(&subject).unwrap(); assert_eq!( parsed, OutgoingSubject { - wtid: key.clone(), + wtid, exchange_base_url: url("http://exchange.example.com/"), metadata: None } @@ -572,12 +572,12 @@ mod test { ); // With metadata - let subject = format!("Accounting:id.4 {key} http://exchange.example.com/"); + let subject = format!("Accounting:id.4 {wtid} http://exchange.example.com/"); let parsed = parse_outgoing(&subject).unwrap(); assert_eq!( parsed, OutgoingSubject { - wtid: key.clone(), + wtid, exchange_base_url: url("http://exchange.example.com/"), metadata: Some("Accounting:id.4".into()) } diff --git a/common/taler-api/src/test/db.rs b/common/taler-api/src/test/db.rs @@ -76,8 +76,8 @@ pub async fn transfer(db: &PgPool, req: &TransferRequest) -> sqlx::Result<Transf .bind(&req.metadata) .bind(&subject) .bind(req.credit_account.raw()) - .bind(&req.request_uid) - .bind(&req.wtid) + .bind(req.request_uid) + .bind(req.wtid) .bind_timestamp(&Timestamp::now()) .try_map(|r: PgRow| { Ok(if r.try_get_flag("out_request_uid_reuse")? { @@ -377,9 +377,9 @@ pub async fn transfer_register( "SELECT out_reserve_pub_reuse FROM register_prepared_transfers($1,$2,$3,$4,$5,$6)" ) .bind(ty) - .bind(&req.account_pub) - .bind(&req.authorization_pub) - .bind(&req.authorization_sig) + .bind(req.account_pub) + .bind(req.authorization_pub) + .bind(req.authorization_sig) .bind(req.recurrent) .bind_timestamp(&Timestamp::now()) .try_map(|r: PgRow| { @@ -396,7 +396,7 @@ pub async fn transfer_register( pub async fn transfer_unregister(db: &PgPool, req: &Unregistration) -> sqlx::Result<bool> { serialized!( sqlx::query("SELECT out_found FROM delete_prepared_transfers($1,$2)") - .bind(&req.authorization_pub) + .bind(req.authorization_pub) .bind_timestamp(&Timestamp::now()) .try_map(|r: PgRow| r.try_get_flag("out_found")) .fetch_one(db) diff --git a/common/taler-common/src/api.rs b/common/taler-common/src/api.rs @@ -131,7 +131,7 @@ pub type EddsaSignature = Base32<64>; /// EdDSA and ECDHE public keys always point on Curve25519 /// and represented using the standard 256 bits Ed25519 compact format, /// converted to Crockford Base32. -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub struct EddsaPublicKey(Base32<32>); impl Deref for EddsaPublicKey { diff --git a/common/taler-common/src/api/prepared.rs b/common/taler-common/src/api/prepared.rs @@ -16,7 +16,7 @@ //! Type for the Taler Wire Transfer Gateway HTTP API <https://docs.taler.net/core/api-bank-transfer.html#taler-prepared-transfer-http-api> -use compact_str::CompactString; +use aws_lc_rs::signature::Ed25519KeyPair; use serde::{Deserialize, Serialize}; use taler_macros::api_config; use url::Url; @@ -25,6 +25,7 @@ use super::EddsaPublicKey; use crate::{ api::EddsaSignature, db::IncomingType, + signature::{Buf, Signature}, types::{ amount::{Amount, Currency}, payto::PaytoURI, @@ -83,6 +84,34 @@ pub struct RegistrationRequest { pub authorization_sig: EddsaSignature, } +impl RegistrationRequest { + pub fn signed(mut self, pair: &Ed25519KeyPair) -> Self { + self.authorization_sig = self.sign(pair); + self + } +} + +impl Signature<104> for RegistrationRequest { + const PURPOSE: u32 = 1224; + + fn signature_bytes(&self, buf: Buf<104>) -> Buf<104> { + buf.put_payto(&self.credit_account) + .put_amount(self.credit_amount) + .put_u32(match self.r#type { + TransferType::reserve => 1, + TransferType::kyc => 2, + }) + .put_u16(match self.recurrent { + true => 2, + false => 1, + }) + .put_u16(match self.alg { + PublicKeyAlg::EdDSA => 1, + }) + .put(self.account_pub.as_ref()) + } +} + /// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-TransferSubject> #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(tag = "type")] @@ -111,7 +140,22 @@ pub struct RegistrationResponse { /// <https://docs.taler.net/core/api-bank-transfer.html#tsref-type-Unregistration> #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Unregistration { - pub timestamp: CompactString, + pub timestamp: TalerTimestamp, pub authorization_pub: EddsaPublicKey, pub authorization_sig: EddsaSignature, } + +impl Unregistration { + pub fn signed(mut self, pair: &Ed25519KeyPair) -> Self { + self.authorization_sig = self.sign(pair); + self + } +} + +impl Signature<16> for Unregistration { + const PURPOSE: u32 = 1225; + + fn signature_bytes(&self, buf: Buf<16>) -> Buf<16> { + buf.put_timestamp(self.timestamp) + } +} diff --git a/common/taler-common/src/error_code.rs b/common/taler-common/src/error_code.rs @@ -545,6 +545,8 @@ pub enum ErrorCode { EXCHANGE_AUDITORS_AUDITOR_UNKNOWN = 1901, /// The auditor that was specified is no longer used by this exchange. EXCHANGE_AUDITORS_AUDITOR_INACTIVE = 1902, + /// The client submitted the wrong form for the request. This is some invalid use of the API. Please contact technical support. + EXCHANGE_KYC_INVALID_FORM_SUBMITTED = 1917, /// The exchange tried to run an AML program, but that program did not terminate on time. Contact the exchange operator to address the AML program bug or performance issue. If it is not a performance issue, the timeout might have to be increased (requires changes to the source code). EXCHANGE_KYC_GENERIC_AML_PROGRAM_TIMEOUT = 1918, /// The KYC info access token is not recognized. Hence the request was denied. @@ -1285,6 +1287,8 @@ pub enum ErrorCode { WALLET_CORE_API_BAD_REQUEST = 7048, /// The order could not be found. Maybe the merchant deleted it. WALLET_MERCHANT_ORDER_NOT_FOUND = 7049, + /// The wallet's balance for paying a merchant is insufficient. + WALLET_PAY_MERCHANT_INSUFFICIENT_BALANCE = 7050, /// We encountered a timeout with our payment backend. ANASTASIS_GENERIC_BACKEND_TIMEOUT = 8000, /// The backend requested payment, but the request is malformed. @@ -1443,6 +1447,12 @@ pub enum ErrorCode { DONAU_DONOR_IDENTIFIER_NONCE_REUSE = 8617, /// A charity with the same public key is already registered. DONAU_CHARITY_PUB_EXISTS = 8618, + /// The donation unit has expired and cannot be used any longer. + DONAU_GENERIC_DONATION_UNIT_EXPIRED = 8619, + /// The donation unit is not yet valid. The client should repeat the request in the future when it might succeed. + DONAU_GENERIC_DONATION_UNIT_TOO_EARLY = 8620, + /// The donation unit is not valid for the year specified by the client. + DONAU_GENERIC_DONATION_UNIT_WRONG_YEAR = 8621, /// A generic error happened in the LibEuFin nexus. See the enclose details JSON for more information. LIBEUFIN_NEXUS_GENERIC_ERROR = 9000, /// An uncaught exception happened in the LibEuFin nexus service. @@ -1493,6 +1503,8 @@ pub enum ErrorCode { PAIVANA_TOO_LATE = 9806, /// The payment template specified in the request is unknown to the backend. PAIVANA_TEMPLATE_UNKNOWN = 9807, + /// The paywall functionality is currently disabled. Thus, proofs of payment are unnecessary and also not supported. + PAIVANA_PAYWALL_DISABLED = 9808, /// End of error code range. END = 9999, } @@ -1759,6 +1771,7 @@ impl ErrorCode { EXCHANGE_AUDITORS_AUDITOR_SIGNATURE_INVALID => 403, EXCHANGE_AUDITORS_AUDITOR_UNKNOWN => 412, EXCHANGE_AUDITORS_AUDITOR_INACTIVE => 410, + EXCHANGE_KYC_INVALID_FORM_SUBMITTED => 409, EXCHANGE_KYC_GENERIC_AML_PROGRAM_TIMEOUT => 500, EXCHANGE_KYC_INFO_AUTHORIZATION_FAILED => 403, EXCHANGE_KYC_RECURSIVE_RULE_DETECTED => 500, @@ -2061,7 +2074,7 @@ impl ErrorCode { BANK_NAME_REUSE => 409, BANK_UNSUPPORTED_SUBJECT_FORMAT => 409, BANK_DERIVATION_REUSE => 409, - BANK_BAD_SIGNATURE => 409, + BANK_BAD_SIGNATURE => 403, BANK_OLD_TIMESTAMP => 409, BANK_TRANSFER_MAPPING_REUSED => 409, BANK_TRANSFER_MAPPING_UNKNOWN => 409, @@ -2129,6 +2142,7 @@ impl ErrorCode { WALLET_TRANSACTION_PROTOCOL_VIOLATION => 0, WALLET_CORE_API_BAD_REQUEST => 0, WALLET_MERCHANT_ORDER_NOT_FOUND => 0, + WALLET_PAY_MERCHANT_INSUFFICIENT_BALANCE => 0, ANASTASIS_GENERIC_BACKEND_TIMEOUT => 504, ANASTASIS_GENERIC_INVALID_PAYMENT_REQUEST => 0, ANASTASIS_GENERIC_BACKEND_ERROR => 502, @@ -2208,6 +2222,9 @@ impl ErrorCode { DONAU_DONATION_RECEIPT_SIGNATURE_INVALID => 403, DONAU_DONOR_IDENTIFIER_NONCE_REUSE => 409, DONAU_CHARITY_PUB_EXISTS => 404, + DONAU_GENERIC_DONATION_UNIT_EXPIRED => 410, + DONAU_GENERIC_DONATION_UNIT_TOO_EARLY => 425, + DONAU_GENERIC_DONATION_UNIT_WRONG_YEAR => 409, LIBEUFIN_NEXUS_GENERIC_ERROR => 0, LIBEUFIN_NEXUS_UNCAUGHT_EXCEPTION => 500, LIBEUFIN_SANDBOX_GENERIC_ERROR => 0, @@ -2225,7 +2242,7 @@ impl ErrorCode { CHALLENGER_INVALID_PIN => 403, CHALLENGER_MISSING_ADDRESS => 409, CHALLENGER_CLIENT_FORBIDDEN_READ_ONLY => 403, - PAIVANA_PAYMENT_MISSING => 400, + PAIVANA_PAYMENT_MISSING => 409, PAIVANA_BACKEND_REFUSED => 502, PAIVANA_ORDER_UNKNOWN => 404, PAIVANA_BACKEND_ERROR => 500, @@ -2233,6 +2250,7 @@ impl ErrorCode { PAIVANA_WRONG_ORDER => 409, PAIVANA_TOO_LATE => 410, PAIVANA_TEMPLATE_UNKNOWN => 404, + PAIVANA_PAYWALL_DISABLED => 501, END => 0, } } diff --git a/common/taler-common/src/lib.rs b/common/taler-common/src/lib.rs @@ -33,6 +33,7 @@ pub mod error; pub mod error_code; pub mod json_file; pub mod log; +pub mod signature; pub mod types; #[global_allocator] diff --git a/common/taler-common/src/signature.rs b/common/taler-common/src/signature.rs @@ -0,0 +1,111 @@ +/* + This file is part of TALER + Copyright (C) 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 + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +use aws_lc_rs::{ + digest::{Context, SHA512}, + signature::{ED25519, Ed25519KeyPair, UnparsedPublicKey}, +}; + +use crate::{ + api::{EddsaPublicKey, EddsaSignature}, + types::{amount::Amount, payto::PaytoURI, timestamp::TalerTimestamp}, +}; + +pub trait Signature<const N: usize> { + const PURPOSE: u32; + + fn signature_bytes(&self, buf: Buf<N>) -> Buf<N>; + + fn sign(&self, pair: &Ed25519KeyPair) -> EddsaSignature { + let msg = self + .signature_bytes(Buf::new().put_u32(N as u32).put_u32(Self::PURPOSE)) + .finish(); + let signature = pair.sign(&msg); + EddsaSignature::try_from(signature.as_ref()).unwrap() + } + + fn verify(&self, key: &EddsaPublicKey, sig: &EddsaSignature) -> bool { + let msg = self + .signature_bytes(Buf::new().put_u32(N as u32).put_u32(Self::PURPOSE)) + .finish(); + UnparsedPublicKey::new(&ED25519, key.as_ref()) + .verify(&msg, sig.as_ref()) + .is_ok() + } +} + +/** Buffer for signature bytes */ +pub struct Buf<const N: usize> { + buf: [u8; N], + pos: usize, +} + +impl<const N: usize> Buf<N> { + pub fn new() -> Self { + Self { + buf: [0; N], + pos: 0, + } + } + + pub fn put_u16(self, nb: u16) -> Self { + // Numbers are network order encoded + self.put(&nb.to_be_bytes()) + } + + pub fn put_u32(self, nb: u32) -> Self { + // Numbers are network order encoded + self.put(&nb.to_be_bytes()) + } + + pub fn put_u64(self, nb: u64) -> Self { + // Numbers are network order encoded + self.put(&nb.to_be_bytes()) + } + + pub fn put_amount(self, amount: Amount) -> Self { + self.put(&amount.signature_bytes()) + } + + pub fn put_timestamp(self, timestamp: TalerTimestamp) -> Self { + self.put(×tamp.signature_bytes()) + } + + pub fn put_payto(self, payto: &PaytoURI) -> Self { + // Payto are encoded into a c string, hashed using sha-512 and then truncated to 32 bytes + let mut ctx = Context::new(&SHA512); + ctx.update(payto.as_ref().as_str().as_bytes()); + ctx.update(&[0]); + self.put(&ctx.finish().as_ref()[..32]) + } + + pub fn put(mut self, bytes: &[u8]) -> Self { + let end = self.pos + bytes.len(); + self.buf[self.pos..end].copy_from_slice(bytes); + self.pos = end; + self + } + + pub fn finish(self) -> [u8; N] { + self.buf + } +} + +impl<const N: usize> Default for Buf<N> { + fn default() -> Self { + Self::new() + } +} diff --git a/common/taler-common/src/types/amount.rs b/common/taler-common/src/types/amount.rs @@ -25,6 +25,7 @@ use std::{ use compact_str::format_compact; use super::utils::InlineStr; +use crate::signature::Buf; /** Number of characters we use to represent currency names */ // We use the same value than the exchange -1 because we use a byte for the len instead of 0 termination @@ -368,6 +369,15 @@ impl Amount { let decimal = self.decimal().try_sub(&rhs.decimal())?.normalize()?; Some((self.currency, decimal).into()) } + + /** Encode amount for signature */ + pub fn signature_bytes(self) -> [u8; 24] { + Buf::new() + .put_u64(self.val) + .put_u32(self.frac) + .put(&self.currency.0) + .finish() + } } impl From<(Currency, Decimal)> for Amount { diff --git a/common/taler-common/src/types/base32.rs b/common/taler-common/src/types/base32.rs @@ -21,10 +21,12 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; use crate::encoding::base32::{self, Base32Error, decode_static}; -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Base32<const L: usize>([u8; L]); impl<const L: usize> Base32<L> { + pub const ZEROED: Base32<L> = Self([0; L]); + pub fn rand() -> Self { Self(rand::random()) } diff --git a/common/taler-common/src/types/payto.rs b/common/taler-common/src/types/payto.rs @@ -71,6 +71,10 @@ pub trait PaytoImpl: Sized { pub struct PaytoURI(Url); impl PaytoURI { + pub unsafe fn from_raw(str: &str) -> Self { + Self(Url::from_str(str).unwrap()) + } + pub fn raw(&self) -> &str { self.0.as_str() } diff --git a/common/taler-common/src/types/timestamp.rs b/common/taler-common/src/types/timestamp.rs @@ -16,7 +16,7 @@ use std::{fmt::Display, ops::Add, str::FromStr, time::Duration}; -use jiff::{Timestamp, civil::Time, tz::TimeZone}; +use jiff::{SignedDuration, Timestamp, civil::Time, tz::TimeZone}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error, ser::SerializeStruct}; // codespell:ignore use serde_json::Value; @@ -27,6 +27,26 @@ pub enum TalerTimestamp { Timestamp(Timestamp), } +impl TalerTimestamp { + /** Encode timestamp for signature */ + pub fn signature_bytes(self) -> [u8; 8] { + match self { + TalerTimestamp::Never => u64::MAX, + // Truncate to second and then encode into microseconds as JSON format only support second precision + TalerTimestamp::Timestamp(timestamp) => (timestamp.as_second() as u64) * 1000 * 1000, + } + .to_be_bytes() + } + + /// Returns an absolute duration representing the elapsed time from this timestamp until the given other timestamp. + pub fn duration_until(self, other: Timestamp) -> SignedDuration { + match self { + TalerTimestamp::Never => SignedDuration::MAX, + TalerTimestamp::Timestamp(tm) => tm.duration_until(other), + } + } +} + impl FromStr for TalerTimestamp { type Err = anyhow::Error; diff --git a/common/taler-test-utils/src/routine.rs b/common/taler-test-utils/src/routine.rs @@ -17,6 +17,8 @@ use std::{ fmt::Debug, future::Future, + str::FromStr, + sync::LazyLock, time::{Duration, Instant}, }; @@ -24,15 +26,15 @@ use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair as _}; use axum::{Router, http::StatusCode}; use jiff::{SignedDuration, Timestamp}; use serde::de::DeserializeOwned; -use taler_api::{ - crypto::{check_eddsa_signature, eddsa_sign}, - subject::fmt_in_subject, -}; +use taler_api::subject::fmt_in_subject; use taler_common::{ api::{ - EddsaPublicKey, HashCode, ShortHashCode, + EddsaPublicKey, EddsaSignature, HashCode, ShortHashCode, params::PageParams, - prepared::{RegistrationResponse, TransferSubject, TransferType}, + prepared::{ + PublicKeyAlg, RegistrationRequest, RegistrationResponse, TransferSubject, TransferType, + Unregistration, + }, revenue::RevenueIncomingHistory, wire::{ IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferList, @@ -41,7 +43,13 @@ use taler_common::{ }, db::IncomingType, error_code::ErrorCode, - types::{amount::amount, base32::Base32, payto::PaytoURI, url}, + types::{ + amount::{Amount, Currency, amount}, + base32::Base32, + payto::PaytoURI, + timestamp::TalerTimestamp, + url, + }, }; use tokio::time::sleep; @@ -50,7 +58,9 @@ use crate::{ server::{TestResponse, TestServer as _}, }; -const UNKNOWN: &str = "payto://malformed/unused?receiver-name=Malformed"; +static UNKNOWN: LazyLock<PaytoURI> = LazyLock::new(|| { + PaytoURI::from_str("payto://malformed/unused?receiver-name=Malformed").unwrap() +}); pub trait Page: DeserializeOwned + Debug { fn ids(&self) -> Vec<i64>; @@ -307,13 +317,13 @@ impl TestResponse { } // Get currency from config -async fn get_currency(server: &Router) -> String { +async fn get_currency(server: &Router) -> Currency { let config = server .get("/config") .await .assert_ok_json::<serde_json::Value>(); let currency = config["currency"].as_str().unwrap(); - currency.to_owned() + Currency::from_str(currency).unwrap() } /// Test standard behavior of the transfer endpoints @@ -411,7 +421,7 @@ pub async fn transfer_routine( // Check request uid reuse wire_gateway .post("/transfer") - .json(&json!(valid_req + { + .json(json!(valid_req + { "wtid": ShortHashCode::rand() })) .await @@ -419,7 +429,7 @@ pub async fn transfer_routine( // Check wtid reuse wire_gateway .post("/transfer") - .json(&json!(valid_req + { + .json(json!(valid_req + { "request_uid": HashCode::rand(), })) .await @@ -428,7 +438,7 @@ pub async fn transfer_routine( // Check currency mismatch wire_gateway .post("/transfer") - .json(&json!(valid_req + { + .json(json!(valid_req + { "amount": "BAD:42" })) .await @@ -437,28 +447,28 @@ pub async fn transfer_routine( // Base Base32 wire_gateway .post("/transfer") - .json(&json!(valid_req + { + .json(json!(valid_req + { "wtid": "I love chocolate" })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); wire_gateway .post("/transfer") - .json(&json!(valid_req + { + .json(json!(valid_req + { "wtid": Base32::<31>::rand() })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); wire_gateway .post("/transfer") - .json(&json!(valid_req + { + .json(json!(valid_req + { "request_uid": "I love chocolate" })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); wire_gateway .post("/transfer") - .json(&json!(valid_req + { + .json(json!(valid_req + { "request_uid": Base32::<65>::rand() })) .await @@ -467,7 +477,7 @@ pub async fn transfer_routine( // Missing receiver-name let res = wire_gateway .post("/transfer") - .json(&json!(valid_req + { + .json(json!(valid_req + { "credit_account": credit_account.as_ref().as_str().split('?').next().unwrap() })) .await; @@ -478,13 +488,13 @@ pub async fn transfer_routine( // Unsupported payto kind wire_gateway .post("/transfer") - .json(&json!(valid_req + { "credit_account": UNKNOWN })) + .json(json!(valid_req + { "credit_account": *UNKNOWN })) .await .assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED); // Malformed payto wire_gateway .post("/transfer") - .json(&json!(valid_req + { "credit_account": "http://email@test.com" })) + .json(json!(valid_req + { "credit_account": "http://email@test.com" })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); @@ -572,7 +582,7 @@ pub async fn transfer_routine( async fn add_incoming_routine( wire_gateway: &Router, prepared_transfer: &Router, - currency: &str, + currency: &Currency, kind: IncomingType, debit_acount: &PaytoURI, credit_account: &PaytoURI, @@ -585,18 +595,21 @@ async fn add_incoming_routine( let key_pair = Ed25519KeyPair::generate().unwrap(); let pub_key = EddsaPublicKey::try_from(key_pair.public_key().as_ref()).unwrap(); // Valid + let req = RegistrationRequest { + credit_account: credit_account.clone(), + r#type: TransferType::reserve, + recurrent: false, + credit_amount: Amount::new(currency, 44, 0), + alg: PublicKeyAlg::EdDSA, + account_pub: pub_key, + authorization_pub: pub_key, + authorization_sig: EddsaSignature::ZEROED, + } + .signed(&key_pair); + prepared_transfer .post("/registration") - .json(json!({ - "credit_account": credit_account, - "type": "reserve", - "recurrent": false, - "credit_amount": format!("{currency}:44"), - "alg": "EdDSA", - "account_pub": pub_key, - "authorization_pub": pub_key, - "authorization_sig": eddsa_sign(&key_pair, pub_key.as_ref()), - })) + .json(&req) .await .assert_ok_json::<RegistrationResponse>(); let valid_req = json!({ @@ -651,26 +664,26 @@ async fn add_incoming_routine( // Bad BASE32 reserve_pub wire_gateway .post(path) - .json(&json!(valid_req + { key: "I love chocolate" })) + .json(json!(valid_req + { key: "I love chocolate" })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); wire_gateway .post(path) - .json(&json!(valid_req + { key: Base32::<31>::rand() })) + .json(json!(valid_req + { key: Base32::<31>::rand() })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); // Unsupported payto kind wire_gateway .post(path) - .json(&json!(valid_req + { "debit_account": UNKNOWN })) + .json(json!(valid_req + { "debit_account": *UNKNOWN })) .await .assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED); // Malformed payto wire_gateway .post(path) - .json(&json!(valid_req + { "debit_account": "http://email@test.com" })) + .json(json!(valid_req + { "debit_account": "http://email@test.com" })) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); } @@ -851,19 +864,22 @@ pub async fn in_history_routine( key = Ed25519KeyPair::generate().unwrap(); let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap(); let reserve_pub = EddsaPublicKey::rand(); - let amount = format!("{currency}:2"); + let amount = Amount::new(currency, 2, 0); prepared_transfer .post("/registration") - .json(json!({ - "credit_account": credit_account, - "credit_amount": amount, - "type": "reserve", - "alg": "EdDSA", - "account_pub": reserve_pub, - "authorization_pub": auth_pub, - "authorization_sig": eddsa_sign(&key, reserve_pub.as_ref()), - "recurrent": true - })) + .json( + RegistrationRequest { + credit_account: credit_account.clone(), + r#type: TransferType::reserve, + recurrent: true, + credit_amount: amount, + alg: PublicKeyAlg::EdDSA, + account_pub: reserve_pub, + authorization_pub: auth_pub, + authorization_sig: EddsaSignature::ZEROED, + } + .signed(&key) + ) .await .assert_ok_json::<RegistrationResponse>(); wire_gateway @@ -890,16 +906,19 @@ pub async fn in_history_routine( let reserve_pub = EddsaPublicKey::rand(); prepared_transfer .post("/registration") - .json(json!({ - "credit_account": credit_account, - "credit_amount": format!("{currency}:3"), - "type": "reserve", - "alg": "EdDSA", - "account_pub": reserve_pub, - "authorization_pub": auth_pub, - "authorization_sig": eddsa_sign(&key, reserve_pub.as_ref()), - "recurrent": true - })) + .json( + RegistrationRequest { + credit_account: credit_account.clone(), + r#type: TransferType::reserve, + recurrent: true, + credit_amount: Amount::new(currency, 3, 0), + alg: PublicKeyAlg::EdDSA, + account_pub: reserve_pub, + authorization_pub: auth_pub, + authorization_sig: EddsaSignature::ZEROED, + } + .signed(&key) + ) .await .assert_ok_json::<RegistrationResponse>(); }, @@ -918,19 +937,22 @@ pub async fn in_history_routine( key = Ed25519KeyPair::generate().unwrap(); let auth_pub = EddsaPublicKey::try_from(key.public_key().as_ref()).unwrap(); let account_pub = EddsaPublicKey::rand(); - let amount = format!("{currency}:5"); + let amount = Amount::new(currency, 5, 0); prepared_transfer .post("/registration") - .json(json!({ - "credit_account": credit_account, - "credit_amount": amount, - "type": "kyc", - "alg": "EdDSA", - "account_pub": account_pub, - "authorization_pub": auth_pub, - "authorization_sig": eddsa_sign(&key, account_pub.as_ref()), - "recurrent": true - })) + .json( + RegistrationRequest { + credit_account: credit_account.clone(), + r#type: TransferType::kyc, + recurrent: true, + credit_amount: amount, + alg: PublicKeyAlg::EdDSA, + account_pub, + authorization_pub: auth_pub, + authorization_sig: EddsaSignature::ZEROED, + } + .signed(&key) + ) .await .assert_ok_json::<RegistrationResponse>(); wire_gateway @@ -957,16 +979,19 @@ pub async fn in_history_routine( let account_pub = EddsaPublicKey::rand(); prepared_transfer .post("/registration") - .json(json!({ - "credit_account": credit_account, - "credit_amount": format!("{currency}:6"), - "type": "kyc", - "alg": "EdDSA", - "account_pub": account_pub, - "authorization_pub": auth_pub, - "authorization_sig": eddsa_sign(&key, account_pub.as_ref()), - "recurrent": true - })) + .json( + RegistrationRequest { + credit_account: credit_account.clone(), + r#type: TransferType::kyc, + recurrent: true, + credit_amount: Amount::new(currency, 6, 0), + alg: PublicKeyAlg::EdDSA, + account_pub, + authorization_pub: auth_pub, + authorization_sig: EddsaSignature::ZEROED, + } + .signed(&key) + ) .await .assert_ok_json::<RegistrationResponse>(); } @@ -1026,16 +1051,16 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( let amount = amount(format!("{currency}:42")); let key_pair1 = Ed25519KeyPair::generate().unwrap(); let auth_pub1 = EddsaPublicKey::try_from(key_pair1.public_key().as_ref()).unwrap(); - let req = json!({ - "credit_account": credit_account, - "type": "reserve", - "recurrent": false, - "credit_amount": amount, - "alg": "EdDSA", - "account_pub": auth_pub1, - "authorization_pub": auth_pub1, - "authorization_sig": eddsa_sign(&key_pair1, auth_pub1.as_ref()), - }); + let req = RegistrationRequest { + credit_account: credit_account.clone(), + r#type: TransferType::reserve, + recurrent: false, + credit_amount: amount, + alg: PublicKeyAlg::EdDSA, + account_pub: auth_pub1, + authorization_pub: auth_pub1, + authorization_sig: EddsaSignature::ZEROED, + }; let register = async |auth_pub: &EddsaPublicKey| { wire_gateway @@ -1050,16 +1075,16 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( /* ----- Registration ----- */ let routine = async |ty: TransferType, - account_pub: &EddsaPublicKey, + account_pub: EddsaPublicKey, recurrent: bool, fmt: IncomingType| { - let req = json!(req + { - "type": ty, - "account_pub": account_pub, - "authorization_pub": auth_pub1, - "authorization_sig": eddsa_sign(&key_pair1, account_pub.as_ref()), - "recurrent": recurrent - }); + let req = RegistrationRequest { + r#type: ty, + account_pub, + recurrent, + ..req.clone() + } + .signed(&key_pair1); // Valid let res = prepared_transfer .post("/registration") @@ -1086,51 +1111,72 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( } }; for ty in [TransferType::reserve, TransferType::kyc] { - routine(ty, &auth_pub1, false, ty.into()).await; - routine(ty, &auth_pub1, true, IncomingType::map).await; + routine(ty, auth_pub1, false, ty.into()).await; + routine(ty, auth_pub1, true, IncomingType::map).await; } let acc_pub1 = EddsaPublicKey::rand(); for ty in [TransferType::reserve, TransferType::kyc] { - routine(ty, &acc_pub1, false, IncomingType::map).await; - routine(ty, &acc_pub1, true, IncomingType::map).await; + routine(ty, acc_pub1, false, IncomingType::map).await; + routine(ty, acc_pub1, true, IncomingType::map).await; } // Bad signature prepared_transfer .post("/registration") - .json(&json!(req + { "authorization_sig": eddsa_sign(&key_pair1, b"lol")})) + .json(&req) .await .assert_error(ErrorCode::BANK_BAD_SIGNATURE); // Unknown account prepared_transfer .post("/registration") - .json(&json!(req + { "credit_account": unknown_account })) + .json( + RegistrationRequest { + r#credit_account: unknown_account.clone(), + ..req.clone() + } + .signed(&key_pair1), + ) .await .assert_error(ErrorCode::BANK_UNKNOWN_CREDITOR); // Unsupported payto kind prepared_transfer .post("/registration") - .json(&json!(req + { "credit_account": UNKNOWN })) + .json( + RegistrationRequest { + r#credit_account: UNKNOWN.clone(), + ..req.clone() + } + .signed(&key_pair1), + ) .await .assert_error(ErrorCode::GENERIC_PAYTO_URI_MALFORMED); // Malformed payto prepared_transfer .post("/registration") - .json(&json!(req + {"credit_account": "http://email@test.com" })) + .json( + RegistrationRequest { + r#credit_account: unsafe { PaytoURI::from_raw("http://email@test.com") }, + ..req.clone() + } + .signed(&key_pair1), + ) .await .assert_error(ErrorCode::GENERIC_JSON_INVALID); // Reserve pub reuse prepared_transfer .post("/registration") - .json(&json!(req + { - "account_pub": acc_pub1, - "authorization_sig": eddsa_sign(&key_pair1, acc_pub1.as_ref()), - })) + .json( + RegistrationRequest { + account_pub: acc_pub1, + ..req.clone() + } + .signed(&key_pair1), + ) .await .assert_ok_json::<RegistrationResponse>(); { @@ -1138,11 +1184,14 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( let auth_pub = EddsaPublicKey::try_from(key_pair.public_key().as_ref()).unwrap(); prepared_transfer .post("/registration") - .json(&json!(req + { - "account_pub": acc_pub1, - "authorization_pub": auth_pub, - "authorization_sig": eddsa_sign(&key_pair, acc_pub1.as_ref()), - })) + .json( + RegistrationRequest { + account_pub: acc_pub1, + authorization_pub: auth_pub, + ..req.clone() + } + .signed(&key_pair), + ) .await .assert_error(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT); } @@ -1150,16 +1199,19 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( // Non recurrent accept one then bounce prepared_transfer .post("/registration") - .json(&json!(req + { - "account_pub": acc_pub1, - "authorization_sig": eddsa_sign(&key_pair1, acc_pub1.as_ref()), - })) + .json( + RegistrationRequest { + account_pub: acc_pub1, + ..req.clone() + } + .signed(&key_pair1), + ) .await .assert_ok_json::<RegistrationResponse>(); register(&auth_pub1) .await .assert_ok_json::<TransferResponse>(); - check_in(&[Reserve(acc_pub1.clone())]).await; + check_in(&[Reserve(acc_pub1)]).await; register(&auth_pub1) .await .assert_error(ErrorCode::BANK_TRANSFER_MAPPING_REUSED); @@ -1168,10 +1220,13 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( let acc_pub2 = EddsaPublicKey::rand(); prepared_transfer .post("/registration") - .json(&json!(req + { - "account_pub": acc_pub2, - "authorization_sig": eddsa_sign(&key_pair1, acc_pub2.as_ref()), - })) + .json( + RegistrationRequest { + account_pub: acc_pub2, + ..req.clone() + } + .signed(&key_pair1), + ) .await .assert_ok_json::<RegistrationResponse>(); wire_gateway @@ -1186,17 +1241,20 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( register(&auth_pub1) .await .assert_error(ErrorCode::BANK_TRANSFER_MAPPING_REUSED); - check_in(&[Reserve(acc_pub1.clone()), Reserve(acc_pub2.clone())]).await; + check_in(&[Reserve(acc_pub1), Reserve(acc_pub2)]).await; // Recurrent accept one and delay others let acc_pub3 = EddsaPublicKey::rand(); prepared_transfer .post("/registration") - .json(&json!(req + { - "account_pub": acc_pub3, - "authorization_sig": eddsa_sign(&key_pair1, acc_pub3.as_ref()), - "recurrent": true - })) + .json( + RegistrationRequest { + account_pub: acc_pub3, + recurrent: true, + ..req.clone() + } + .signed(&key_pair1), + ) .await .assert_ok_json::<RegistrationResponse>(); for _ in 0..5 { @@ -1205,9 +1263,9 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( .assert_ok_json::<TransferResponse>(); } check_in(&[ - Reserve(acc_pub1.clone()), - Reserve(acc_pub2.clone()), - Reserve(acc_pub3.clone()), + Reserve(acc_pub1), + Reserve(acc_pub2), + Reserve(acc_pub3), Pending, Pending, Pending, @@ -1219,29 +1277,35 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( let acc_pub4 = EddsaPublicKey::rand(); prepared_transfer .post("/registration") - .json(&json!(req + { - "type": "kyc", - "account_pub": acc_pub4, - "authorization_sig": eddsa_sign(&key_pair1, acc_pub4.as_ref()), - "recurrent": true - })) + .json( + RegistrationRequest { + r#type: TransferType::kyc, + account_pub: acc_pub4, + recurrent: true, + ..req.clone() + } + .signed(&key_pair1), + ) .await .assert_ok_json::<RegistrationResponse>(); prepared_transfer .post("/registration") - .json(&json!(req + { - "account_pub": acc_pub4, - "authorization_sig": eddsa_sign(&key_pair1, acc_pub4.as_ref()), - "recurrent": true - })) + .json( + RegistrationRequest { + account_pub: acc_pub4, + recurrent: true, + ..req.clone() + } + .signed(&key_pair1), + ) .await .assert_ok_json::<RegistrationResponse>(); check_in(&[ - Reserve(acc_pub1.clone()), - Reserve(acc_pub2.clone()), - Reserve(acc_pub3.clone()), - Kyc(acc_pub4.clone()), - Reserve(acc_pub4.clone()), + Reserve(acc_pub1), + Reserve(acc_pub2), + Reserve(acc_pub3), + Kyc(acc_pub4), + Reserve(acc_pub4), Pending, Pending, ]) @@ -1258,14 +1322,14 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( .await .assert_ok_json::<TransferResponse>(); check_in(&[ - Reserve(acc_pub1.clone()), - Reserve(acc_pub2.clone()), - Reserve(acc_pub3.clone()), - Kyc(acc_pub4.clone()), - Reserve(acc_pub4.clone()), + Reserve(acc_pub1), + Reserve(acc_pub2), + Reserve(acc_pub3), + Kyc(acc_pub4), + Reserve(acc_pub4), Pending, Pending, - Kyc(acc_pub4.clone()), + Kyc(acc_pub4), ]) .await; @@ -1274,12 +1338,15 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( let auth_pub2 = EddsaPublicKey::try_from(auth_pair.public_key().as_ref()).unwrap(); prepared_transfer .post("/registration") - .json(&json!(req + { - "account_pub": auth_pub2, - "authorization_pub": auth_pub2, - "authorization_sig": eddsa_sign(&auth_pair, auth_pub2.as_ref()), - "recurrent": true - })) + .json( + RegistrationRequest { + account_pub: auth_pub2, + authorization_pub: auth_pub2, + recurrent: true, + ..req.clone() + } + .signed(&auth_pair), + ) .await .assert_ok_json::<RegistrationResponse>(); for _ in 0..3 { @@ -1288,40 +1355,43 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( .assert_ok_json::<TransferResponse>(); } check_in(&[ - Reserve(acc_pub1.clone()), - Reserve(acc_pub2.clone()), - Reserve(acc_pub3.clone()), - Kyc(acc_pub4.clone()), - Reserve(acc_pub4.clone()), + Reserve(acc_pub1), + Reserve(acc_pub2), + Reserve(acc_pub3), + Kyc(acc_pub4), + Reserve(acc_pub4), Pending, Pending, - Kyc(acc_pub4.clone()), - Reserve(auth_pub2.clone()), + Kyc(acc_pub4), + Reserve(auth_pub2), Pending, Pending, ]) .await; prepared_transfer .post("/registration") - .json(&json!(req + { - "type": "kyc", - "account_pub": auth_pub2, - "authorization_pub": auth_pub2, - "authorization_sig": eddsa_sign(&auth_pair, auth_pub2.as_ref()), - "recurrent": false - })) + .json( + RegistrationRequest { + r#type: TransferType::kyc, + account_pub: auth_pub2, + authorization_pub: auth_pub2, + recurrent: false, + ..req.clone() + } + .signed(&auth_pair), + ) .await .assert_ok_json::<RegistrationResponse>(); check_in(&[ - Reserve(acc_pub1.clone()), - Reserve(acc_pub2.clone()), - Reserve(acc_pub3.clone()), - Kyc(acc_pub4.clone()), - Reserve(acc_pub4.clone()), + Reserve(acc_pub1), + Reserve(acc_pub2), + Reserve(acc_pub3), + Kyc(acc_pub4), + Reserve(acc_pub4), Pending, Pending, - Kyc(acc_pub4.clone()), - Reserve(auth_pub2.clone()), + Kyc(acc_pub4), + Reserve(auth_pub2), Bounced, Bounced, ]) @@ -1331,13 +1401,15 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( let acc_pub5 = EddsaPublicKey::rand(); prepared_transfer .post("/registration") - .json(&json!(req + { - "type": "reserve", - "account_pub": acc_pub5, - "authorization_pub": auth_pub2, - "authorization_sig": eddsa_sign(&auth_pair, acc_pub5.as_ref()), - "recurrent": true - })) + .json( + RegistrationRequest { + account_pub: acc_pub5, + authorization_pub: auth_pub2, + recurrent: true, + ..req.clone() + } + .signed(&auth_pair), + ) .await .assert_ok_json::<RegistrationResponse>(); wire_gateway @@ -1353,18 +1425,18 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( .await .assert_ok_json::<TransferResponse>(); check_in(&[ - Reserve(acc_pub1.clone()), - Reserve(acc_pub2.clone()), - Reserve(acc_pub3.clone()), - Kyc(acc_pub4.clone()), - Reserve(acc_pub4.clone()), + Reserve(acc_pub1), + Reserve(acc_pub2), + Reserve(acc_pub3), + Kyc(acc_pub4), + Reserve(acc_pub4), Pending, Pending, - Kyc(acc_pub4.clone()), - Reserve(auth_pub2.clone()), + Kyc(acc_pub4), + Reserve(auth_pub2), Bounced, Bounced, - Reserve(acc_pub5.clone()), + Reserve(acc_pub5), Pending, ]) .await; @@ -1372,24 +1444,29 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( // Recurrent kyc simple subject prepared_transfer .post("/registration") - .json(&json!(req + { - "type": "kyc", - "account_pub": acc_pub5, - "authorization_pub": auth_pub2, - "authorization_sig": eddsa_sign(&auth_pair, acc_pub5.as_ref()), - "recurrent": false - })) + .json( + RegistrationRequest { + r#type: TransferType::kyc, + account_pub: acc_pub5, + authorization_pub: auth_pub2, + ..req.clone() + } + .signed(&auth_pair), + ) .await .assert_ok_json::<RegistrationResponse>(); prepared_transfer .post("/registration") - .json(&json!(req + { - "type": "kyc", - "account_pub": acc_pub5, - "authorization_pub": auth_pub2, - "authorization_sig": eddsa_sign(&auth_pair, acc_pub5.as_ref()), - "recurrent": true - })) + .json( + RegistrationRequest { + r#type: TransferType::kyc, + account_pub: acc_pub5, + authorization_pub: auth_pub2, + recurrent: true, + ..req.clone() + } + .signed(&auth_pair), + ) .await .assert_ok_json::<RegistrationResponse>(); wire_gateway @@ -1407,56 +1484,56 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( .assert_ok_json::<TransferResponse>(); } check_in(&[ - Reserve(acc_pub1.clone()), - Reserve(acc_pub2.clone()), - Reserve(acc_pub3.clone()), - Kyc(acc_pub4.clone()), - Reserve(acc_pub4.clone()), + Reserve(acc_pub1), + Reserve(acc_pub2), + Reserve(acc_pub3), + Kyc(acc_pub4), + Reserve(acc_pub4), Pending, Pending, - Kyc(acc_pub4.clone()), - Reserve(auth_pub2.clone()), + Kyc(acc_pub4), + Reserve(auth_pub2), Bounced, Bounced, - Reserve(acc_pub5.clone()), + Reserve(acc_pub5), Bounced, - Kyc(acc_pub5.clone()), + Kyc(acc_pub5), Pending, Pending, ]) .await; /* ----- Unregistration ----- */ - let now = Timestamp::now().to_string(); - let un_req = json!({ - "timestamp": now, - "authorization_pub": auth_pub2, - "authorization_sig": eddsa_sign(&auth_pair, now.as_bytes()), - }); + let un_req = Unregistration { + timestamp: TalerTimestamp::Timestamp(Timestamp::now()), + authorization_pub: auth_pub2, + authorization_sig: EddsaSignature::ZEROED, + }; + let signed = un_req.clone().signed(&auth_pair); // Delete prepared_transfer .post("/unregistration") - .json(&un_req) + .json(&signed) .await .assert_no_content(); // Check bounce pending on deletion check_in(&[ - Reserve(acc_pub1.clone()), - Reserve(acc_pub2.clone()), - Reserve(acc_pub3.clone()), - Kyc(acc_pub4.clone()), - Reserve(acc_pub4.clone()), + Reserve(acc_pub1), + Reserve(acc_pub2), + Reserve(acc_pub3), + Kyc(acc_pub4), + Reserve(acc_pub4), Pending, Pending, - Kyc(acc_pub4.clone()), - Reserve(auth_pub2.clone()), + Kyc(acc_pub4), + Reserve(auth_pub2), Bounced, Bounced, - Reserve(acc_pub5.clone()), + Reserve(acc_pub5), Bounced, - Kyc(acc_pub5.clone()), + Kyc(acc_pub5), Bounced, Bounced, ]) @@ -1465,28 +1542,42 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( // Idempotent prepared_transfer .post("/unregistration") - .json(&un_req) + .json(&signed) .await .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); // Bad signature prepared_transfer .post("/unregistration") - .json(&json!(un_req + { - "authorization_sig": eddsa_sign(&auth_pair, b"lol"), - })) + .json(&un_req) .await .assert_error(ErrorCode::BANK_BAD_SIGNATURE); // Old timestamp - let now = (Timestamp::now() - SignedDuration::from_mins(10)).to_string(); prepared_transfer .post("/unregistration") - .json(json!({ - "timestamp": now, - "authorization_pub": auth_pub2, - "authorization_sig": eddsa_sign(&auth_pair, now.as_bytes()), - })) + .json( + Unregistration { + timestamp: TalerTimestamp::Timestamp( + Timestamp::now() - SignedDuration::from_mins(10), + ), + ..un_req.clone() + } + .signed(&auth_pair), + ) + .await + .assert_error(ErrorCode::BANK_OLD_TIMESTAMP); + + // Never timestamp + prepared_transfer + .post("/unregistration") + .json( + Unregistration { + timestamp: TalerTimestamp::Never, + ..un_req.clone() + } + .signed(&auth_pair), + ) .await .assert_error(ErrorCode::BANK_OLD_TIMESTAMP); @@ -1514,30 +1605,22 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>( .. } => (account_pub, authorization_pub, authorization_sig), }; - if let Some(auth_pub) = &auth_pub { - assert!(check_eddsa_signature( - auth_pub, - acc_pub.as_ref(), - &auth_sig.unwrap() - )); - } else { - assert!(auth_sig.is_none()) - } + assert_eq!(auth_pub.is_some(), auth_sig.is_some()); (acc_pub, auth_pub) }) .collect(); assert_eq!( history, [ - (acc_pub1.clone(), Some(auth_pub1.clone())), - (acc_pub2.clone(), None), - (acc_pub3.clone(), Some(auth_pub1.clone())), - (acc_pub4.clone(), Some(auth_pub1.clone())), - (acc_pub4.clone(), Some(auth_pub1.clone())), - (acc_pub4.clone(), None), - (auth_pub2.clone(), Some(auth_pub2.clone())), - (acc_pub5.clone(), None), - (acc_pub5.clone(), None), + (acc_pub1, Some(auth_pub1)), + (acc_pub2, None), + (acc_pub3, Some(auth_pub1)), + (acc_pub4, Some(auth_pub1)), + (acc_pub4, Some(auth_pub1)), + (acc_pub4, None), + (auth_pub2, Some(auth_pub2)), + (acc_pub5, None), + (acc_pub5, None), ] ) }