taler-rust

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

commit 88509824a6a6ae25be548da609b7f82583cd58ac
parent 604b3384f17025e7c59b7e2e875fb455b0c84ee6
Author: Antoine A <>
Date:   Tue, 10 Feb 2026 10:26:01 +0100

common: better EddsaPublicKey key type

Diffstat:
Mcommon/taler-api/src/db.rs | 7+++++--
Mcommon/taler-api/src/subject.rs | 8++------
Mcommon/taler-api/tests/common/db.rs | 8++++----
Mcommon/taler-common/src/api_common.rs | 99++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mcommon/taler-common/src/types/base32.rs | 7++++---
Mtaler-cyclos/src/bin/cyclos-harness.rs | 6+++---
Mtaler-cyclos/src/db.rs | 4++--
Mtaler-magnet-bank/src/bin/magnet-bank-harness.rs | 6+++---
Mtaler-magnet-bank/src/db.rs | 4++--
9 files changed, 113 insertions(+), 36 deletions(-)

diff --git a/common/taler-api/src/db.rs b/common/taler-api/src/db.rs @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2024-2025 Taler Systems SA + Copyright (C) 2024, 2025, 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 @@ -27,7 +27,7 @@ use sqlx::{ }; use sqlx::{Postgres, Row}; use taler_common::{ - api_common::SafeU64, + api_common::{EddsaPublicKey, SafeU64}, api_params::{History, Page}, types::{ amount::{Amount, Currency, Decimal}, @@ -194,6 +194,9 @@ pub trait TypeHelper { ) -> sqlx::Result<Base32<L>> { self.try_get_map(index, |slice: &[u8]| Base32::try_from(slice)) } + fn try_get_eddsa<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<EddsaPublicKey> { + self.try_get_map(index, |slice: &[u8]| EddsaPublicKey::try_from(slice)) + } fn try_get_url<I: sqlx::ColumnIndex<Self>>(&self, index: I) -> sqlx::Result<Url> { self.try_get_parse(index) } diff --git a/common/taler-api/src/subject.rs b/common/taler-api/src/subject.rs @@ -14,9 +14,8 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::{fmt::Debug, ops::Deref, str::FromStr}; +use std::{fmt::Debug, str::FromStr}; -use aws_lc_rs::signature::ParsedPublicKey; use taler_common::{ api_common::{EddsaPublicKey, ShortHashCode}, types::base32::{Base32Error, CROCKFORD_ALPHABET}, @@ -41,7 +40,7 @@ impl IncomingSubject { pub fn key(&self) -> &[u8] { match self { - IncomingSubject::Kyc(key) | IncomingSubject::Reserve(key) => key.deref(), + IncomingSubject::Kyc(key) | IncomingSubject::Reserve(key) => key.as_ref().as_ref(), } } } @@ -155,9 +154,6 @@ pub fn parse_incoming_unstructured( // Check key validity let key = EddsaPublicKey::from_str(raw).ok()?; - if ParsedPublicKey::new(&aws_lc_rs::signature::ED25519, key.as_slice()).is_err() { - return None; - } let quality = Base32Quality::measure(raw); Some(Candidate { diff --git a/common/taler-api/tests/common/db.rs b/common/taler-api/tests/common/db.rs @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2024-2025 Taler Systems SA + Copyright (C) 2024, 2025, 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 @@ -223,7 +223,7 @@ pub async fn add_incoming( .bind(subject) .bind(debit_account.raw()) .bind(kind) - .bind(key.as_slice()) + .bind(key.as_ref().as_slice()) .bind_timestamp(timestamp) .try_map(|r: PgRow| { Ok(if r.try_get("out_reserve_pub_reuse")? { @@ -274,14 +274,14 @@ pub async fn incoming_history( date: r.try_get_timestamp("created_at")?.into(), amount: r.try_get_amount("amount", currency)?, debit_account: r.try_get_payto("debit_payto")?, - reserve_pub: r.try_get_base32("metadata")?, + reserve_pub: r.try_get_eddsa("metadata")?, }, IncomingType::kyc => IncomingBankTransaction::Kyc { row_id: r.try_get_safeu64("tx_in_id")?, date: r.try_get_timestamp("created_at")?.into(), amount: r.try_get_amount("amount", currency)?, debit_account: r.try_get_payto("debit_payto")?, - account_pub: r.try_get_base32("metadata")?, + account_pub: r.try_get_eddsa("metadata")?, }, IncomingType::wad => IncomingBankTransaction::Wad { row_id: r.try_get_safeu64("tx_in_id")?, diff --git a/common/taler-common/src/api_common.rs b/common/taler-common/src/api_common.rs @@ -14,13 +14,16 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::{fmt::Display, ops::Deref}; +use std::{borrow::Cow, fmt::Display, ops::Deref, str::FromStr}; -use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair}; +use aws_lc_rs::{ + error::KeyRejected, + signature::{self, Ed25519KeyPair, KeyPair, ParsedPublicKey}, +}; use serde::{Deserialize, Deserializer, Serialize, de::Error}; use serde_json::value::RawValue; -use crate::types::base32::Base32; +use crate::types::base32::{Base32, Base32Error}; /// <https://docs.taler.net/core/api-common.html#tsref-type-ErrorDetail> #[derive(Debug, Clone, Serialize, Deserialize)] @@ -110,18 +113,92 @@ impl Display for SafeU64 { } } -/// EdDSA and ECDHE public keys always point on Curve25519 -/// and represented using the standard 256 bits Ed25519 compact format, -/// converted to Crockford Base32. -pub type EddsaPublicKey = Base32<32>; /// 64-byte hash code pub type HashCode = Base32<64>; /// 32-bytes hash code pub type ShortHashCode = Base32<32>; pub type WadId = Base32<24>; -pub fn rand_edsa_pub_key() -> EddsaPublicKey { - let signing_key = Ed25519KeyPair::generate().unwrap(); - let bytes: [u8; 32] = signing_key.public_key().as_ref().try_into().unwrap(); - Base32::from(bytes) +/// EdDSA and ECDHE public keys always point on Curve25519 +/// and represented using the standard 256 bits Ed25519 compact format, +/// converted to Crockford Base32. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EddsaPublicKey(Base32<32>); + +impl AsRef<Base32<32>> for EddsaPublicKey { + fn as_ref(&self) -> &Base32<32> { + &self.0 + } +} + +impl Serialize for EddsaPublicKey { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for EddsaPublicKey { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let raw = Cow::<str>::deserialize(deserializer)?; + Self::from_str(&raw).map_err(D::Error::custom) + } +} + +impl Display for EddsaPublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum EddsaPublicKeyError { + #[error(transparent)] + Base32(#[from] Base32Error<32>), + #[error(transparent)] + Invalid(#[from] KeyRejected), +} + +impl FromStr for EddsaPublicKey { + type Err = EddsaPublicKeyError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let encoded = Base32::<32>::from_str(s)?; + Self::try_from(encoded) + } +} + +impl TryFrom<&[u8]> for EddsaPublicKey { + type Error = EddsaPublicKeyError; + + fn try_from(value: &[u8]) -> Result<Self, Self::Error> { + let encoded = Base32::try_from(value)?; + Self::try_from(encoded) + } +} + +impl TryFrom<Base32<32>> for EddsaPublicKey { + type Error = EddsaPublicKeyError; + + fn try_from(value: Base32<32>) -> Result<Self, Self::Error> { + ParsedPublicKey::new(&signature::ED25519, value.as_ref())?; + Ok(Self(value)) + } +} + +impl EddsaPublicKey { + pub fn slice(&self) -> &[u8; 32] { + self.0.deref() + } + + pub fn rand() -> EddsaPublicKey { + let signing_key = Ed25519KeyPair::generate().unwrap(); + let bytes: [u8; 32] = signing_key.public_key().as_ref().try_into().unwrap(); + Self(Base32::from(bytes)) + } } diff --git a/common/taler-common/src/types/base32.rs b/common/taler-common/src/types/base32.rs @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2024-2025 Taler Systems SA + Copyright (C) 2024, 2025, 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 @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::{fmt::Display, ops::Deref, str::FromStr}; +use std::{borrow::Cow, fmt::Display, ops::Deref, str::FromStr}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; @@ -207,7 +207,8 @@ impl<'de, const L: usize> Deserialize<'de> for Base32<L> { where D: Deserializer<'de>, { - Base32::from_str(&String::deserialize(deserializer)?).map_err(D::Error::custom) + let raw = Cow::<str>::deserialize(deserializer)?; + Self::from_str(&raw).map_err(D::Error::custom) } } diff --git a/taler-cyclos/src/bin/cyclos-harness.rs b/taler-cyclos/src/bin/cyclos-harness.rs @@ -26,7 +26,7 @@ use taler_api::db::TypeHelper as _; use taler_build::long_version; use taler_common::{ CommonArgs, - api_common::{EddsaPublicKey, HashCode, ShortHashCode, rand_edsa_pub_key}, + api_common::{EddsaPublicKey, HashCode, ShortHashCode}, api_params::{History, Page}, api_wire::{IncomingBankTransaction, TransferState}, config::Config, @@ -355,7 +355,7 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { step("Test incoming talerable transaction"); // Send talerable transaction - let reserve_pub = rand_edsa_pub_key(); + let reserve_pub = EddsaPublicKey::rand(); let amount = decimal("3.3"); harness .client_send(&format!("Taler {reserve_pub}"), amount) @@ -580,7 +580,7 @@ async fn online_harness(config: &Config, reset: bool) -> anyhow::Result<()> { step("Test incoming transactions"); let taler_amount = decimal("3"); let malformed_amount = decimal("4"); - let reserve_pub = rand_edsa_pub_key(); + let reserve_pub = EddsaPublicKey::rand(); harness .client_send(&format!("Taler {reserve_pub}"), taler_amount) .await; diff --git a/taler-cyclos/src/db.rs b/taler-cyclos/src/db.rs @@ -558,14 +558,14 @@ pub async fn incoming_history( amount: r.try_get_amount_i(2, currency)?, debit_account: r.try_get_cyclos_fullpaytouri(4, 5, root)?, date: r.try_get_timestamp(6)?.into(), - reserve_pub: r.try_get_base32(7)?, + reserve_pub: r.try_get_eddsa(7)?, }, IncomingType::kyc => IncomingBankTransaction::Kyc { row_id: r.try_get_safeu64(1)?, amount: r.try_get_amount_i(2, currency)?, debit_account: r.try_get_cyclos_fullpaytouri(4, 5, root)?, date: r.try_get_timestamp(6)?.into(), - account_pub: r.try_get_base32(7)?, + account_pub: r.try_get_eddsa(7)?, }, IncomingType::wad => { unimplemented!("WAD is not yet supported") diff --git a/taler-magnet-bank/src/bin/magnet-bank-harness.rs b/taler-magnet-bank/src/bin/magnet-bank-harness.rs @@ -25,7 +25,7 @@ use sqlx::PgPool; use taler_build::long_version; use taler_common::{ CommonArgs, - api_common::{EddsaPublicKey, HashCode, ShortHashCode, rand_edsa_pub_key}, + api_common::{EddsaPublicKey, HashCode, ShortHashCode}, api_params::{History, Page}, api_wire::{IncomingBankTransaction, TransferState}, config::Config, @@ -348,7 +348,7 @@ async fn logic_harness(cfg: &Config, reset: bool) -> anyhow::Result<()> { step("Test incoming talerable transaction"); // Send talerable transaction - let reserve_pub = rand_edsa_pub_key(); + let reserve_pub = EddsaPublicKey::rand(); harness .client_send(&format!("Taler {reserve_pub}"), 33) .await; @@ -536,7 +536,7 @@ async fn online_harness(config: &Config, reset: bool) -> anyhow::Result<()> { let balance = &mut Balances::new(&harness).await; step("Test incoming transactions"); - let reserve_pub = rand_edsa_pub_key(); + let reserve_pub = EddsaPublicKey::rand(); harness .client_send(&format!("Taler {reserve_pub}"), 3) .await; diff --git a/taler-magnet-bank/src/db.rs b/taler-magnet-bank/src/db.rs @@ -573,14 +573,14 @@ pub async fn incoming_history( amount: r.try_get_amount_i(2, &CURRENCY)?, debit_account: r.try_get_iban(4)?.as_full_payto(r.try_get(5)?), date: r.try_get_timestamp(6)?.into(), - reserve_pub: r.try_get_base32(7)?, + reserve_pub: r.try_get_eddsa(7)?, }, IncomingType::kyc => IncomingBankTransaction::Kyc { row_id: r.try_get_safeu64(1)?, amount: r.try_get_amount_i(2, &CURRENCY)?, debit_account: r.try_get_iban(4)?.as_full_payto(r.try_get(5)?), date: r.try_get_timestamp(6)?.into(), - account_pub: r.try_get_base32(7)?, + account_pub: r.try_get_eddsa(7)?, }, IncomingType::wad => { unimplemented!("WAD is not yet supported")