commit 88509824a6a6ae25be548da609b7f82583cd58ac
parent 604b3384f17025e7c59b7e2e875fb455b0c84ee6
Author: Antoine A <>
Date: Tue, 10 Feb 2026 10:26:01 +0100
common: better EddsaPublicKey key type
Diffstat:
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")