taler-rust

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

commit 77e9bb891753ecc6ef06660b7a339c9e07675ff4
parent 1a6b822592fb2b8879e1afa23fa41204ad65b1a2
Author: Antoine A <>
Date:   Sat, 11 Jan 2025 01:13:45 +0100

common: improve KYC subject parsing

Diffstat:
Mcommon/taler-api/Cargo.toml | 4++--
Mcommon/taler-api/benches/subject.rs | 10+++++-----
Mcommon/taler-api/src/subject.rs | 76+++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
3 files changed, 58 insertions(+), 32 deletions(-)

diff --git a/common/taler-api/Cargo.toml b/common/taler-api/Cargo.toml @@ -18,8 +18,8 @@ libdeflater = "1.22.0" ed25519-dalek = { version = "2.1.1", default-features = false } tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal"] } serde.workspace = true -tracing.workspace= true -tracing-subscriber.workspace= true +tracing.workspace = true +tracing-subscriber.workspace = true serde_json.workspace = true axum.workspace = true url.workspace = true diff --git a/common/taler-api/benches/subject.rs b/common/taler-api/benches/subject.rs @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2024 Taler Systems SA + Copyright (C) 2024-2025 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 @@ -74,10 +74,10 @@ fn parser(c: &mut Criterion) { }); } - bench(c, "real_simple", real_simple); - bench(c, "real_splitted", real_splitted); - bench(c, "rng", &randoms); - bench(c, "chunks", &chunks); + bench(c, "subject_real_simple", real_simple); + bench(c, "subject_real_splitted", real_splitted); + bench(c, "subject_rng", &randoms); + bench(c, "subject_chunks", &chunks); } criterion_group!(benches, parser); diff --git a/common/taler-api/src/subject.rs b/common/taler-api/src/subject.rs @@ -124,13 +124,24 @@ pub fn parse_outgoing(subject: &str) -> Result<OutgoingSubject, OutgoingSubjectE pub fn parse_incoming_unstructured(subject: &str) -> Option<IncomingSubjectResult> { // We expect subject to be less than 65KB assert!(subject.len() <= u16::MAX as usize); + + const KEY_SIZE: usize = 52; + const KYC_SIZE: usize = KEY_SIZE + 3; + /** Parse an incoming subject */ + #[inline] fn parse_single(str: &str) -> Option<Candidate> { // Check key type - let (is_kyc, raw) = if let Some(key) = str.strip_prefix("KYC:") { - (true, key) - } else { - (false, str) + let (is_kyc, raw) = match str.len() { + KEY_SIZE => (false, str), + KYC_SIZE => { + if let Some(key) = str.strip_prefix("KYC") { + (true, key) + } else { + return None; + } + } + _ => return None, }; // Check key validity @@ -156,7 +167,7 @@ pub fn parse_incoming_unstructured(subject: &str) -> Option<IncomingSubjectResul let mut concatenated = String::new(); let mut part = None; for (i, b) in subject.as_bytes().iter().enumerate() { - if matches!(b, b'0'..=b':' | b'A'..=b'Z' | b'a'..=b'z') { + if b.is_ascii_alphanumeric() { if part.is_none() { part = Some(i); } @@ -185,37 +196,36 @@ pub fn parse_incoming_unstructured(subject: &str) -> Option<IncomingSubjectResul for end in parts[i..].iter() { let range = start.start..end.end; // Until they are to long to be a key - if range.len() > 56 { + if range.len() > KYC_SIZE { break; } - // If the slice is the right size for a key (56B with prefix else 54B) - if range.len() == 52 || range.len() == 56 { - // Parse the concatenated parts - let slice = - unsafe { &concatenated.get_unchecked(start.start as usize..end.end as usize) }; - if let Some(other) = parse_single(slice) { - // On success update best candidate - match &mut best { - Some(best) => { - if other.quality > best.quality // We prefer high quality keys + + // Parse the concatenated parts + // SAFETY: we now end.end <= concatenated.len + let slice = + unsafe { &concatenated.get_unchecked(start.start as usize..end.end as usize) }; + if let Some(other) = parse_single(slice) { + // On success update best candidate + match &mut best { + Some(best) => { + if other.quality > best.quality // We prefer high quality keys || matches!( // We prefer prefixed keys over reserve keys (&best.subject.ty(), &other.subject.ty()), (IncomingType::reserve, IncomingType::kyc | IncomingType::wad) ) - { - *best = other - } else if best.subject.key() != other.subject.key() // If keys are different + { + *best = other + } else if best.subject.key() != other.subject.key() // If keys are different && best.quality == other.quality // Of same quality && !matches!( // And prefixing is diferent (&best.subject.ty(), &other.subject.ty()), (IncomingType::kyc | IncomingType::wad, IncomingType::reserve) ) - { - return Some(IncomingSubjectResult::Ambiguous); - } + { + return Some(IncomingSubjectResult::Ambiguous); } - None => best = Some(other), } + None => best = Some(other), } } } @@ -234,7 +244,7 @@ fn parse() { for ty in [IncomingType::reserve, IncomingType::kyc] { let prefix = match ty { IncomingType::reserve => "", - IncomingType::kyc => "KYC:", + IncomingType::kyc => "KYC", IncomingType::wad => unreachable!(), }; let standard = &format!("{prefix}{key}"); @@ -346,7 +356,7 @@ fn parse() { #[test] /** Test parsing logic using real cases */ fn real() { - // Good case + // Good reserve case for (subject, key) in [ ( "Taler TEGY6d9mh9pgwvwpgs0z0095z854xegfy7j j202yd0esp8p0za60", @@ -360,6 +370,10 @@ fn real() { "Taler NDDCAM9XN4HJZFTBD8V6FNE2FJE8G Y734PJ5AGQMY06C8D4HB3Z0", "NDDCAM9XN4HJZFTBD8V6FNE2FJE8GY734PJ5AGQMY06C8D4HB3Z0", ), + ( + "KYCVEEXTBXBEMCS5R64C24GFNQVWBN5R2F9QSQ7PN8QXAP1NG4NG", + "KYCVEEXTBXBEMCS5R64C24GFNQVWBN5R2F9QSQ7PN8QXAP1NG4NG", + ), ] { assert_eq!( Some(IncomingSubjectResult::Success(IncomingSubject::Reserve( @@ -368,4 +382,16 @@ fn real() { parse_incoming_unstructured(subject) ) } + // Good kyc case + for (subject, key) in [( + "KYC JW398X85FWPKKMS0EYB6TQ1799RMY5DDXTZ FPW4YC3WJ2DWSJT70", + "JW398X85FWPKKMS0EYB6TQ1799RMY5DDXTZFPW4YC3WJ2DWSJT70", + )] { + assert_eq!( + Some(IncomingSubjectResult::Success(IncomingSubject::Kyc( + EddsaPublicKey::from_str(key).unwrap(), + ))), + parse_incoming_unstructured(subject) + ) + } }