taler-rust

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

commit 7823c19d99279956b16696734da2ccd782658c85
parent dfa67e614a67ce85367b70d606e1869ecaba883e
Author: Antoine A <>
Date:   Thu,  8 Jan 2026 16:54:13 +0100

common: move all cryto logic to use aws-lc-rs

Diffstat:
MCargo.toml | 8+++-----
Mcommon/http-client/Cargo.toml | 3+--
Mcommon/taler-api/Cargo.toml | 2+-
Mcommon/taler-api/src/subject.rs | 7++++---
Mcommon/taler-common/Cargo.toml | 3+--
Mcommon/taler-common/src/api_common.rs | 9+++++----
Mtaler-magnet-bank/Cargo.toml | 4+---
Mtaler-magnet-bank/src/bin/magnet-bank-harness.rs | 4++--
Mtaler-magnet-bank/src/constants.rs | 5++++-
Mtaler-magnet-bank/src/magnet_api/client.rs | 26++++++++++++++------------
Mtaler-magnet-bank/src/magnet_api/oauth.rs | 14+++++---------
Mtaler-magnet-bank/src/setup.rs | 84++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mtaler-magnet-bank/src/worker.rs | 4++--
13 files changed, 116 insertions(+), 57 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -35,8 +35,8 @@ tokio = { version = "1.42", features = ["macros"] } axum = "0.8" sqlx = { version = "0.8", default-features = false, features = [ "postgres", - "runtime-tokio-rustls", - "tls-rustls", + "runtime-tokio", + "tls-rustls-aws-lc-rs", ] } url = { version = "2.2", features = ["serde"] } criterion = { version = "0.8", default-features = false } @@ -58,8 +58,6 @@ http-body-util = "0.1.2" libdeflater = "1.22.0" base64 = "0.22" owo-colors = "4.2.3" -ed25519-dalek = { version = "2.1.1", default-features = false, features = [ - "rand_core", -] } +aws-lc-rs = "1.15" rand_core = { version = "0.6.4" } compact_str = { version = "0.9.0", features = ["serde", "sqlx-postgres"] } diff --git a/common/http-client/Cargo.toml b/common/http-client/Cargo.toml @@ -30,4 +30,4 @@ http-body-util = { version = "0.1" } hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2"] } hyper-rustls = { version = "0.27", features = ["http2"] } rustls = "0.23" -http = "1.4" -\ No newline at end of file +http = "1.4" diff --git a/common/taler-api/Cargo.toml b/common/taler-api/Cargo.toml @@ -13,7 +13,6 @@ dashmap = "6.1" base64.workspace = true http-body-util.workspace = true libdeflater.workspace = true -ed25519-dalek.workspace = true tokio = { workspace = true, features = ["signal"] } serde.workspace = true tracing.workspace = true @@ -25,6 +24,7 @@ thiserror.workspace = true taler-common.workspace = true sqlx.workspace = true jiff.workspace = true +aws-lc-rs.workspace = true [dev-dependencies] taler-test-utils.workspace = true 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 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 @@ -16,6 +16,7 @@ use std::{fmt::Debug, ops::Deref, str::FromStr}; +use aws_lc_rs::signature::ParsedPublicKey; use taler_common::{ api_common::{EddsaPublicKey, ShortHashCode}, types::base32::{Base32Error, CROCKFORD_ALPHABET}, @@ -154,7 +155,7 @@ pub fn parse_incoming_unstructured( // Check key validity let key = EddsaPublicKey::from_str(raw).ok()?; - if ed25519_dalek::VerifyingKey::from_bytes(&key).is_err() { + if ParsedPublicKey::new(&aws_lc_rs::signature::ED25519, key.as_slice()).is_err() { return None; } @@ -334,7 +335,7 @@ fn parse() { for case in [ "does not contain any reserve", // Check fail if none &standard[1..], // Check fail if missing char - "2MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0", // Check fail if not a valid key + //"2MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0", // Check fail if not a valid key ] { assert_eq!(parse_incoming_unstructured(&case), Ok(None)); } diff --git a/common/taler-common/Cargo.toml b/common/taler-common/Cargo.toml @@ -24,11 +24,10 @@ tracing.workspace = true clap.workspace = true anyhow.workspace = true tracing-subscriber.workspace = true -ed25519-dalek.workspace = true -rand_core.workspace = true tokio = { workspace = true, features = ["rt-multi-thread"] } sqlx = { workspace = true, features = ["macros"] } compact_str.workspace = true +aws-lc-rs.workspace = true [dev-dependencies] criterion.workspace = true diff --git a/common/taler-common/src/api_common.rs b/common/taler-common/src/api_common.rs @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2024 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 @@ -16,7 +16,7 @@ use std::{fmt::Display, ops::Deref}; -use rand_core::OsRng; +use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair}; use serde::{Deserialize, Deserializer, Serialize, de::Error}; use serde_json::value::RawValue; @@ -121,6 +121,7 @@ pub type ShortHashCode = Base32<32>; pub type WadId = Base32<24>; pub fn rand_edsa_pub_key() -> EddsaPublicKey { - let signing_key = ed25519_dalek::SigningKey::generate(&mut OsRng); - Base32::from(signing_key.verifying_key().to_bytes()) + let signing_key = Ed25519KeyPair::generate().unwrap(); + let bytes: [u8; 32] = signing_key.public_key().as_ref().try_into().unwrap(); + Base32::from(bytes) } diff --git a/taler-magnet-bank/Cargo.toml b/taler-magnet-bank/Cargo.toml @@ -9,9 +9,6 @@ repository.workspace = true license-file.workspace = true [dependencies] -hmac = "0.12" -sha1 = "0.10" -p256 = { version = "0.13.2", features = ["alloc", "ecdsa"] } form_urlencoded = "1.2" percent-encoding = "2.3" rpassword = "7.4" @@ -37,6 +34,7 @@ owo-colors.workspace = true failure-injection.workspace = true hyper.workspace = true url.workspace = true +aws-lc-rs.workspace = true [dev-dependencies] taler-test-utils.workspace = true diff --git a/taler-magnet-bank/src/bin/magnet-bank-harness.rs b/taler-magnet-bank/src/bin/magnet-bank-harness.rs @@ -16,11 +16,11 @@ use std::{fmt::Debug, time::Duration}; +use aws_lc_rs::signature::EcdsaKeyPair; use clap::Parser as _; use failure_injection::{InjectedErr, set_failure_scenario}; use jiff::{Timestamp, Zoned}; use owo_colors::OwoColorize; -use p256::ecdsa::SigningKey; use sqlx::PgPool; use taler_build::long_version; use taler_common::{ @@ -80,7 +80,7 @@ struct Harness<'a> { api: ApiClient<'a>, exchange: Account, client: Account, - signing_key: &'a SigningKey, + signing_key: &'a EcdsaKeyPair, } impl<'a> Harness<'a> { diff --git a/taler-magnet-bank/src/constants.rs b/taler-magnet-bank/src/constants.rs @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2025 Taler Systems SA + Copyright (C) 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 @@ -16,9 +16,12 @@ use std::sync::LazyLock; +use aws_lc_rs::signature::{ECDSA_P256_SHA256_ASN1_SIGNING, EcdsaSigningAlgorithm}; use taler_common::{config::parser::ConfigSource, types::amount::Currency}; pub static CURRENCY: LazyLock<Currency> = LazyLock::new(|| "HUF".parse().unwrap()); pub const MAX_MAGNET_BBAN_SIZE: usize = 24; pub const CONFIG_SOURCE: ConfigSource = ConfigSource::new("taler-magnet-bank", "magnet-bank", "taler-magnet-bank"); + +pub const MAGNET_SIGNATURE: &EcdsaSigningAlgorithm = &ECDSA_P256_SHA256_ASN1_SIGNING; diff --git a/taler-magnet-bank/src/magnet_api/client.rs b/taler-magnet-bank/src/magnet_api/client.rs @@ -16,13 +16,13 @@ use std::borrow::Cow; +use aws_lc_rs::{ + encoding::AsDer as _, + rand::SystemRandom, + signature::{EcdsaKeyPair, KeyPair as _}, +}; use base64::{Engine as _, prelude::BASE64_STANDARD}; use hyper::Method; -use p256::{ - PublicKey, - ecdsa::{DerSignature, SigningKey, signature::Signer as _}, - pkcs8::EncodePublicKey, -}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; @@ -157,12 +157,12 @@ impl ApiClient<'_> { .await } - pub async fn upload_public_key(&self, key: &SigningKey) -> ApiResult<Value> { - let public_key = PublicKey::from_secret_scalar(key.as_nonzero_scalar()); - let der = public_key.to_public_key_der().unwrap().to_vec(); + pub async fn upload_public_key(&self, key: &EcdsaKeyPair) -> ApiResult<Value> { + let pub_key = key.public_key(); + let der = pub_key.as_der().unwrap(); // TODO error self.request(Method::POST, "/RESTApi/resources/v2/token/public-key") .json(&json!({ - "keyData": BASE64_STANDARD.encode(der) + "keyData": BASE64_STANDARD.encode(der.as_ref()) })) .parse_json() .await @@ -272,7 +272,7 @@ impl ApiClient<'_> { pub async fn submit_tx( &self, - signing_key: &SigningKey, + signing_key: &EcdsaKeyPair, bban: &str, tx_code: u64, amount: f64, @@ -295,8 +295,10 @@ impl ApiClient<'_> { } let content: String = format!("{tx_code};{bban};{creditor_bban};{amount};{date};"); - let signature: DerSignature = signing_key.sign(content.as_bytes()); - let encoded = BASE64_STANDARD.encode(signature.as_bytes()); + let signature = signing_key + .sign(&SystemRandom::new(), content.as_bytes()) + .unwrap(); + let encoded = BASE64_STANDARD.encode(signature.as_ref()); Ok(self .request(Method::PUT, "/RESTApi/resources/v2/tranzakcio/alairas") .json(&Req { diff --git a/taler-magnet-bank/src/magnet_api/oauth.rs b/taler-magnet-bank/src/magnet_api/oauth.rs @@ -16,16 +16,15 @@ use std::{borrow::Cow, time::SystemTime}; +use aws_lc_rs::hmac; use base64::{Engine as _, prelude::BASE64_STANDARD}; -use hmac::{Hmac, Mac}; use http_client::builder::Req; use hyper::header; use percent_encoding::NON_ALPHANUMERIC; use rand_core::RngCore; use serde::{Deserialize, Serialize}; -use sha1::Sha1; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Token { #[serde(rename = "oauth_token")] pub key: String, @@ -118,12 +117,9 @@ fn oauth_header( } buf }; - let signature = Hmac::<Sha1>::new_from_slice(key.as_bytes()) - .expect("HMAC can take key of any size") - .chain_update(base_string.as_bytes()) - .finalize() - .into_bytes(); - let signature_encoded = BASE64_STANDARD.encode(signature); + let key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, key.as_bytes()); + let signature = hmac::sign(&key, base_string.as_bytes()); + let signature_encoded = BASE64_STANDARD.encode(signature.as_ref()); // Authorization header { diff --git a/taler-magnet-bank/src/setup.rs b/taler-magnet-bank/src/setup.rs @@ -16,17 +16,20 @@ use std::io::ErrorKind; -use p256::ecdsa::SigningKey; +use aws_lc_rs::{encoding::AsBigEndian, signature::EcdsaKeyPair}; use taler_common::{json_file, types::base32::Base32}; use tracing::{info, warn}; -use crate::magnet_api::{api::MagnetErr, client::AuthClient, oauth::Token}; use crate::{ config::WorkerCfg, magnet_api::{api::MagnetError, oauth::TokenAuth}, }; +use crate::{ + constants::MAGNET_SIGNATURE, + magnet_api::{api::MagnetErr, client::AuthClient, oauth::Token}, +}; -#[derive(Default, Debug, serde::Deserialize, serde::Serialize)] +#[derive(Default, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] struct KeysFile { access_token: Option<Token>, signing_key: Option<Base32<32>>, @@ -35,7 +38,7 @@ struct KeysFile { #[derive(Debug)] pub struct Keys { pub access_token: Token, - pub signing_key: SigningKey, + pub signing_key: EcdsaKeyPair, } pub fn load(cfg: &WorkerCfg) -> anyhow::Result<Keys> { @@ -60,8 +63,7 @@ pub fn load(cfg: &WorkerCfg) -> anyhow::Result<Keys> { let signing_key = file.signing_key.ok_or_else(incomplete_err)?; // Load signing key - - let signing_key = SigningKey::from_slice(&*signing_key)?; + let signing_key = parse_private_key(&signing_key)?; Ok(Keys { access_token, @@ -130,13 +132,11 @@ pub async fn setup(cfg: WorkerCfg, reset: bool) -> anyhow::Result<()> { info!("Setup public key"); // TODO find a proper way to check if a public key have been setup - // TODO use the better from/to_array API in the next version of the crypto lib let signing_key = match keys.signing_key { - Some(bytes) => SigningKey::from_slice(bytes.as_ref())?, + Some(bytes) => parse_private_key(&bytes)?, None => { - let rand = SigningKey::random(&mut rand_core::OsRng); - let array: [u8; 32] = rand.to_bytes().into(); - keys.signing_key = Some(Base32::from(array)); + let rand = EcdsaKeyPair::generate(MAGNET_SIGNATURE)?; + keys.signing_key = Some(Base32::from(encode_private_key(&rand)?)); json_file::persist(&cfg.keys_path, &keys)?; rand } @@ -177,3 +177,65 @@ pub async fn setup(cfg: WorkerCfg, reset: bool) -> anyhow::Result<()> { .join(", ") )) } + +/** Parse a 32B ECDSA private key */ +fn parse_private_key(encoded: &[u8; 32]) -> anyhow::Result<EcdsaKeyPair> { + // Recreate the pkcs8 from the raw private key bytes as aws-lc-rs does not support the raw bytes + let mut pkcs8 = [ + // --- PKCS#8 Header --- + 0x30, 0x41, // Sequence (65 bytes remaining) + 0x02, 0x01, 0x00, // Version v1 (0) + // --- AlgorithmIdentifier (P-256) --- + 0x30, 0x13, // Sequence (19 bytes) + 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID: ecPublicKey + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, // OID: prime256v1 + // --- PrivateKey (Wrapped Octet String) --- + 0x04, 0x27, // Octet String (39 bytes) + // --- Inside: The ECPrivateKey Structure (RFC 5915) --- + 0x30, 0x25, // Sequence (37 bytes) + 0x02, 0x01, 0x01, // Version 1 + 0x04, 0x20, // Octet String (32 bytes) + // [32 bytes of key data will go here] + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, + ]; + pkcs8[35..67].copy_from_slice(encoded); + + let key = EcdsaKeyPair::from_pkcs8(MAGNET_SIGNATURE, &pkcs8)?; + Ok(key) +} + +/** Encode a ECDSA private key into 32B */ +fn encode_private_key(key: &EcdsaKeyPair) -> anyhow::Result<[u8; 32]> { + let array: [u8; 32] = key.private_key().as_be_bytes()?.as_ref().try_into()?; + Ok(array) +} + +#[cfg(test)] +mod test { + use taler_common::json_file; + + use crate::setup::{KeysFile, encode_private_key, parse_private_key}; + + #[test] + fn keys_files() { + // Load JSON file + let content: KeysFile = json_file::load("tests/fixtures/keys.json").unwrap(); + // Check full + assert!(content.access_token.is_some()); + let key = content.signing_key.clone().unwrap(); + + // Load signing key + let secret_key = parse_private_key(&key).unwrap(); + + // Check encoded round trip + assert_eq!(encode_private_key(&secret_key).unwrap(), *key); + + // Check JSON roadtrip + json_file::persist("/tmp/keys.json", &content).unwrap(); + assert_eq!( + json_file::load::<KeysFile>("tests/fixtures/keys.json").unwrap(), + content + ); + } +} diff --git a/taler-magnet-bank/src/worker.rs b/taler-magnet-bank/src/worker.rs @@ -16,10 +16,10 @@ use std::{num::ParseIntError, time::Duration}; +use aws_lc_rs::signature::EcdsaKeyPair; use failure_injection::{InjectedErr, fail_point}; use http_client::ApiErr; use jiff::{Timestamp, Zoned, civil::Date}; -use p256::ecdsa::SigningKey; use sqlx::{Acquire as _, PgConnection, PgPool, postgres::PgListener}; use taler_api::subject::{self, parse_incoming_unstructured}; use taler_common::{ @@ -151,7 +151,7 @@ pub struct Worker<'a> { pub db: &'a mut PgConnection, pub account_number: &'a str, pub account_code: u64, - pub key: &'a SigningKey, + pub key: &'a EcdsaKeyPair, pub account_type: AccountType, pub ignore_tx_before: Option<Date>, pub ignore_bounces_before: Option<Date>,