xml_sign.rs (25005B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2026 Taler Systems S.A. 4 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 use std::{collections::HashSet, io::Write, rc::Rc}; 21 22 use aws_lc_rs::{ 23 digest::{Context, Digest}, 24 error::Unspecified, 25 rand::SystemRandom, 26 rsa::PublicKey, 27 signature::{RSA_PKCS1_SHA256, RsaKeyPair}, 28 }; 29 use roxmltree::{Document, Node}; 30 use taler_common::encoding::base64; 31 use thiserror::Error; 32 33 use crate::xml::{self, Xml, XmlAccess as _}; 34 35 fn escape( 36 mut text: &str, 37 replacements: &[(char, &str)], 38 out: &mut impl Write, 39 ) -> std::io::Result<()> { 40 // Find the next character that needs escaping 41 while let Some(pos) = text.find(|c| replacements.iter().any(|&(r, _)| r == c)) { 42 // Write the safe chunk of text before the escaped character 43 if pos > 0 { 44 out.write_all(&text.as_bytes()[..pos])?; 45 } 46 47 // Get the character that triggered the match 48 let ch = text[pos..].chars().next().unwrap(); 49 50 // Find and write its replacement 51 let escaped = replacements.iter().find(|&&(r, _)| r == ch).unwrap().1; 52 out.write_all(escaped.as_bytes())?; 53 54 // Advance the text slice past the character we just escaped 55 text = &text[pos + ch.len_utf8()..]; 56 } 57 58 // Write any remaining unescaped tail 59 if !text.is_empty() { 60 out.write_all(text.as_bytes())?; 61 } 62 63 Ok(()) 64 } 65 66 // C14N requires specific escaping for Text nodes 67 fn escape_text(text: &str, out: &mut impl Write) -> std::io::Result<()> { 68 escape( 69 text, 70 &[ 71 ('&', "&"), 72 ('<', "<"), 73 ('>', ">"), 74 ('\r', "
"), 75 ], 76 out, 77 ) 78 } 79 80 // C14N requires specific escaping for Attributes 81 fn escape_attr(text: &str, out: &mut impl Write) -> std::io::Result<()> { 82 escape( 83 text, 84 &[ 85 ('&', "&"), 86 ('<', "<"), 87 ('"', """), 88 ('\t', "	"), 89 ('\n', "
"), 90 ('\r', "
"), 91 ], 92 out, 93 ) 94 } 95 96 /// C14N normalization context 97 struct C14N<'a> { 98 active_namespaces: Rc<HashSet<(&'a str, &'a str)>>, 99 sorted: Vec<(&'a str, &'a str)>, 100 } 101 102 impl<'a> C14N<'a> { 103 pub fn new() -> Self { 104 Self { 105 active_namespaces: Rc::new(HashSet::new()), 106 sorted: Vec::new(), 107 } 108 } 109 110 /// C14N inclusive normalization 111 pub fn c14n_inclusive( 112 &mut self, 113 node: Node<'a, 'a>, 114 out: &mut impl Write, 115 ) -> std::io::Result<()> { 116 let Self { 117 active_namespaces, 118 sorted, 119 } = self; 120 Rc::make_mut(active_namespaces).clear(); 121 fn inner<'a, W: Write>( 122 node: Node<'a, 'a>, 123 mut active_namespaces: Rc<HashSet<(&'a str, &'a str)>>, 124 sorted: &mut Vec<(&'a str, &'a str)>, 125 out: &mut W, 126 ) -> std::io::Result<()> { 127 if node.is_text() { 128 if let Some(text) = node.text() { 129 escape_text(text, out)?; 130 } 131 } else if node.is_element() { 132 let prefix = node 133 .tag_name() 134 .namespace() 135 .and_then(|uri| node.lookup_prefix(uri)); 136 let tag_name = std::fmt::from_fn(|f| { 137 if let Some(ns) = prefix { 138 write!(f, "{ns}:")?; 139 }; 140 write!(f, "{}", node.tag_name().name()) 141 }); 142 143 // Open element 144 write!(out, "<{tag_name}")?; 145 146 // Write sorted missing namespaces 147 sorted.clear(); 148 sorted.extend(node.namespaces().filter_map(|ns| { 149 let value = (ns.name().unwrap_or_default(), ns.uri()); 150 if active_namespaces.contains(&value) { 151 None 152 } else { 153 Rc::make_mut(&mut active_namespaces).insert(value); 154 Some(value) 155 } 156 })); 157 sorted.sort_unstable(); 158 for (prefix, uri) in &*sorted { 159 write!(out, " xmlns")?; 160 if !prefix.is_empty() { 161 write!(out, ":{prefix}")?; 162 } 163 write!(out, "=\"{uri}\"")?; 164 } 165 166 // Write sorted attributes 167 sorted.clear(); 168 sorted.extend(node.attributes().map(|it| (it.name(), it.value()))); 169 sorted.sort_unstable(); 170 for (k, v) in &*sorted { 171 write!(out, " {k}=\"")?; 172 escape_attr(v, out)?; 173 write!(out, "\"")?; 174 } 175 176 write!(out, ">")?; 177 178 for child in node.children() { 179 // Pass the cloned active_namespaces down to children 180 inner(child, active_namespaces.clone(), sorted, out)?; 181 } 182 183 write!(out, "</{tag_name}>")?; 184 } 185 Ok(()) 186 } 187 inner(node, self.active_namespaces.clone(), sorted, out) 188 } 189 } 190 191 /// Digest wrapper to implement std::io::Write 192 struct DigestWriter<'a>(&'a mut Context); 193 194 impl<'a> Write for DigestWriter<'a> { 195 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { 196 self.0.update(buf); 197 Ok(buf.len()) 198 } 199 200 fn flush(&mut self) -> std::io::Result<()> { 201 Ok(()) 202 } 203 } 204 205 /// Digest of all authenticated node 206 fn digest_authenticated<'a>(doc: &'a Document, c14n: &mut C14N<'a>) -> Digest { 207 fn inner<'a>(node: Node<'a, 'a>, c14n: &mut C14N<'a>, out: &mut Context) { 208 if node.attribute("authenticate") == Some("true") { 209 c14n.c14n_inclusive(node, &mut DigestWriter(out)) 210 .expect("digest never fails"); 211 } else { 212 for child in node.children().filter(|n| n.is_element()) { 213 inner(child, c14n, out); 214 } 215 } 216 } 217 218 let mut out = aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256); 219 inner(doc.root(), c14n, &mut out); 220 out.finish() 221 } 222 223 const C14N_ALG: &str = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"; 224 const SIG_ALG: &str = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; 225 const DIGEST_ALG: &str = "http://www.w3.org/2001/04/xmlenc#sha256"; 226 const DSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#"; 227 228 #[derive(Debug, Error)] 229 pub enum SignError { 230 #[error(transparent)] 231 Xml(#[from] roxmltree::Error), 232 #[error("Must contain an empty AuthSignature element")] 233 NoAuthSignature, 234 #[error("bad signature")] 235 Signature(#[from] Unspecified), 236 } 237 238 /** Sign an EBICS document */ 239 pub fn sign_ebics(mut xml: String, key: &RsaKeyPair) -> Result<String, SignError> { 240 let doc = Document::parse(&xml)?; 241 let mut c14n = C14N::new(); 242 243 let digest = digest_authenticated(&doc, &mut c14n); 244 let digest = base64::encode(digest.as_ref()); 245 246 // Wrap signed info for signature in a canonical form 247 let default_namespace = doc.root_element().default_namespace().unwrap_or_default(); 248 let signed_info = format!( 249 r##"<ds:SignedInfo xmlns="{default_namespace}" xmlns:ds="{DSIG_NS}"><ds:CanonicalizationMethod Algorithm="{C14N_ALG}"></ds:CanonicalizationMethod><ds:SignatureMethod Algorithm="{SIG_ALG}"></ds:SignatureMethod><ds:Reference URI="#xpointer(//*[@authenticate='true'])"><ds:Transforms><ds:Transform Algorithm="{C14N_ALG}"></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="{DIGEST_ALG}"></ds:DigestMethod><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo>"## 250 ); 251 let mut sig = vec![0u8; key.public_modulus_len()]; 252 key.sign( 253 &RSA_PKCS1_SHA256, 254 &SystemRandom::new(), 255 signed_info.as_bytes(), 256 &mut sig, 257 )?; 258 let sig = base64::encode(sig); 259 let signature = format!( 260 r##"<AuthSignature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="{C14N_ALG}"/><ds:SignatureMethod Algorithm="{SIG_ALG}"/><ds:Reference URI="#xpointer(//*[@authenticate='true'])"><ds:Transforms><ds:Transform Algorithm="{C14N_ALG}"/></ds:Transforms><ds:DigestMethod Algorithm="{DIGEST_ALG}"/><ds:DigestValue>{digest}</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>{sig}</ds:SignatureValue></AuthSignature>"## 261 ); 262 let pattern = "<AuthSignature/>"; 263 let start = xml.find(pattern).ok_or(SignError::NoAuthSignature)?; 264 xml.replace_range(start..start + pattern.len(), &signature); 265 Ok(xml) 266 } 267 268 #[derive(Debug, Error)] 269 pub enum VerifyError { 270 #[error(transparent)] 271 Utf8(#[from] std::str::Utf8Error), 272 #[error(transparent)] 273 Xml(#[from] roxmltree::Error), 274 #[error(transparent)] 275 Schema(#[from] xml::Error), 276 #[error("bad digest")] 277 Digest, 278 #[error("bad signature")] 279 Signature(#[from] Unspecified), 280 } 281 282 /** Verify an EBICS document */ 283 pub fn verify_ebics<'a>(xml: &'a [u8], key: &PublicKey) -> Result<Document<'a>, VerifyError> { 284 let str = std::str::from_utf8(xml)?; 285 let doc = Document::parse(str)?; 286 let mut c14n = C14N::new(); 287 288 let auth_signature = Xml { 289 node: doc.root_element(), 290 } 291 .one("AuthSignature")?; 292 let signed_info = auth_signature.one("SignedInfo")?; 293 let signature = auth_signature.one("SignatureValue").b64()?; 294 295 // Check digest 296 let digest = signed_info.one("Reference").one("DigestValue").b64()?; 297 if digest_authenticated(&doc, &mut c14n).as_ref() != digest { 298 return Err(VerifyError::Digest); 299 } 300 301 // Check signature 302 let mut out = aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256); 303 c14n.c14n_inclusive(signed_info.node, &mut DigestWriter(&mut out)) 304 .expect("digest never fails"); 305 let digest = out.finish(); 306 aws_lc_rs::signature::UnparsedPublicKey::new( 307 &aws_lc_rs::signature::RSA_PKCS1_2048_8192_SHA256, 308 key, 309 ) 310 .verify_digest(&digest, &signature)?; 311 312 Ok(doc) 313 } 314 315 #[cfg(test)] 316 mod test { 317 use aws_lc_rs::{ 318 rsa::PublicKey, 319 signature::{KeyPair, RsaKeyPair}, 320 }; 321 use roxmltree::Document; 322 use taler_common::encoding::{base32, base64}; 323 324 use crate::xml_sign::{C14N, digest_authenticated, sign_ebics, verify_ebics}; 325 326 #[test] 327 fn canonicalize() { 328 let xml = r##"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><ebicsNoPubKeyDigestsRequest xmlns="urn:org:ebics:H005" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Revision="1" Version="H005"><header authenticate="true"><static><HostID>PFEBICS</HostID><Nonce>BC750C641453F93EBF236A9B25F6B70A</Nonce><Timestamp>2026-02-14T18:10:31.125926573Z</Timestamp><PartnerID>PFC00563</PartnerID><UserID>PFC00563</UserID><OrderDetails><AdminOrderType>HPB</AdminOrderType></OrderDetails><SecurityMedium>0000</SecurityMedium></static><mutable/></header><AuthSignature><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#xpointer(//*[@authenticate='true'])"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>ws6QyiLpZVu+CbpqlhQ11PGwCdHSgmtmL7FvwrqZqmU=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>RvVxaDRsgtyZITf3C/UfmWGLERFRWZFxbwb5yhoJBOu5f6KsythhNvF28gznE1VN7E+5oP+nRkba 329 hUBX3Y+0PahH+XeOnPGuUYdiOy0/FydtG2E1oQELNRojWhxxJMKPpN6jO9Y3j8QmS31oAWUiLjgA 330 S//AU924Wh0rIwA8L3riSzGZDAgf6c0Wg+loPk581AD9QtzMiDi6onLVQvlKYtlVJNheTIreG54i 331 a6vPTIqlMWB5iA5ZqoE6zO+VWr4sxTPswlHD29dDar7B4YJ1vYLLTzFHc0yJaDjWaURQNr0mDqUC 332 kJMyqsK/0dKW+4n3JgWuVGK8YdoUuvmYooqgFw==</ds:SignatureValue></AuthSignature><body/></ebicsNoPubKeyDigestsRequest> 333 "##; 334 335 let doc = Document::parse(xml).unwrap(); 336 let mut c14n = C14N::new(); 337 let res = digest_authenticated(&doc, &mut c14n); 338 let hex = base64::encode(res); 339 assert_eq!(hex, "ws6QyiLpZVu+CbpqlhQ11PGwCdHSgmtmL7FvwrqZqmU="); 340 341 let xml = r##"<?xml version="1.0" encoding="UTF-8"?> 342 <ebicsResponse xmlns="urn:org:ebics:H005" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Version="H005" Revision="1" xsi:schemaLocation="urn:org:ebics:H005 ebics_response_H005.xsd"> 343 <header authenticate="true"> 344 <static> 345 <TransactionID>7FD993238073A6ADAE3B0E5C2A8010E6</TransactionID> 346 </static> 347 <mutable> 348 <TransactionPhase>Initialisation</TransactionPhase> 349 <OrderID>N0NU</OrderID> 350 <ReturnCode>000000</ReturnCode> 351 <ReportText>[EBICS_OK] OK</ReportText> 352 </mutable> 353 </header> 354 <AuthSignature> 355 <ds:SignedInfo> 356 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/> 357 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/> 358 <ds:Reference URI="#xpointer(//*[@authenticate='true'])"> 359 <ds:Transforms> 360 <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/> 361 </ds:Transforms> 362 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/> 363 <ds:DigestValue>WJz3HUYjV3HMK0Cy+69XCnAcmiD21mJ5BRiQPwsi1VI=</ds:DigestValue> 364 </ds:Reference> 365 </ds:SignedInfo> 366 <ds:SignatureValue>Ug6LWlR5FCrOjKjqa37Y6D/vYdYxDp3FcLnj/SEJU5kCGpqd+MrEJDg0/q726ozlxkw50hEbK+Kh+MDxRPTztxOdc78V9PuAK9mzo41+G6cv26SKZqX3wtCIrcaFhsEfzIqe9m8NwlnQ3aATMxEevjVPLE+TzSd+Tb6vFybt3a6Qi3iHmjTTeNVPTcAte91A2wqI/k+aPbg2ndRio/stGjuvVYDXNy9YuXvg8XEtgkbDtkx90O5shexaUMI/W5YqY49kd7aY4gSY6jf1/rfkWHU556mtPjuYBLg0TL9nOQWIrzw3eIWpVB0xoPvdPfzRtYvT7KEuk5LtSwEfHiLqgw==</ds:SignatureValue> 367 </AuthSignature> 368 <body> 369 <ReturnCode authenticate="true">000000</ReturnCode> 370 <TimestampBankParameter authenticate="true">2020-11-25T19:03:45.693Z</TimestampBankParameter> 371 </body> 372 </ebicsResponse> 373 "##; 374 let doc = Document::parse(xml).unwrap(); 375 let res = digest_authenticated(&doc, &mut c14n); 376 assert_eq!( 377 base64::encode(res), 378 "WJz3HUYjV3HMK0Cy+69XCnAcmiD21mJ5BRiQPwsi1VI=" 379 ); 380 } 381 382 #[test] 383 fn sign() { 384 let key = "62109F820403038614N8CJ46YW6G20810M0090G4MWR84153080G00M2040G18GPPFA8VSJD6G0ENC3SH2ET37BXQ1RTRAX162GWVTW33BX14FBAC0N7DFJBKKNS0EJ4DY07TABNKDHGX0EX7XWV47P456NXX8DP33MVZ010X2F248GDXQK3WWYZKAYMA61KYNTJ4QD1BAZWES5GBA8F9WD9EM9WN9ZYQHFGNWVDQ2BEE54CQAGF82AFR0NJEJWFT4QKNVR8QB79VESG623W5NZPFQM1ZSJ20ZDYCJC7KJ1Q23TDJA0C1SY3KWRM97R60BZXKQ9Q9FM1AFQ8CAYDG0FJSQQM0D5W8QQENK79W8VZ0605A1FKYZYBFQX34FBFJKTCSDEZWSYDQM4HD9JF8F86HXW3F2GE9A7H7JHZG7441MJ91H24ND5M8YK4VXYAB7RX8JAXSS8K33BQXF5QBRR20C0G0082G80G0079GETRHX75MR929BDEYWFKTXE82F0QV71AHY3MKKQXNK545W4XSGHGZARZTH5TGABDTW2SJHKXQ787X2CQFY8ZDDHN5WEMXT5VMBZK5B3TJW5XM98GRREWWPA8AJPA8QZ30Q9RYX49228NHSAM8F2DEH40KAXMFT8HB1PDVCVQRQV5HKYBD1Z6Z1ZZZXXJ114X0E4X6R3FMVWQGANAKYRT2S75FKCG00C96EBM1B8RBZPBQZ7FVA9ZB8F64PYG4KRFHT4CYQZ5D6HP1K42PKYB7TA45SZKRDXE3PYTZE6XASJSP9H1EH1JM0ZPMC1Y2FBWPQ8SR2233J55HX6PXWSJ2ZJYTC8V9FM9F442SQM5EGNYK59RCSEGWMZ0WSF98WX4QVDX4CVN3E1GQPNH9K8ERG82G60G1M763Z4MZSJG1MJZQV32HYNMBEV26J8JKC6E53GWKY101RCB1JXN08H8P64P8QTDV9WRS3EQV30557E7QJAYG1K5A4D76DYTNE0GBC7KE5FD6S8ADSJV9XWVVQHVV1BRQVZ4ZWWSK5M9VEVZNRXXBJVZPKR26XEBT6ANVEBTSQ538K1BZQGBQTKWVMDFMG91A4ATXQM2B5J1MC183080RTHA7FVF7SN90B4XQ87GST3Z50H6T6CRQGR0PDDV51HGH1JE76AAWP1VCEWGASWZ2C9KVB8Z5GH71SCW0MF89A02NFNGVQ9D3QV3PMZQSGM6RC35DDBD33J8N5G2V2SN5MCNB3RA7SMJVVG5DG7QHCJ83QX11G5P8KHQ8MFEV69HGXSS7DTHBV71EWP78PPEMSXP7C7ASYZC20M1G02CCQEFM8PYF0M5DPZBEYG56RRD8JYK9PDADYXM7P1SGSZT2J07705TZNKF0Y34W9T28FZ340PRAA5HSDX8SP3EFSFMNV8RC109HKRW0ZP4WRE1E8QJVGS19CZVPAKMRQA17VF9H4HK3FVXYCA2B3FAZTMRVABA9D03P01BYNERVDEJMQ2173562N24FEBYB18EYF94WD3GVX82G60G15KVSJV527Q6723V93NANH5CYPN6H8JE8CTXXA030S1EEBEDT4KYEDZE1BVJ2A42GPCS60BA448VKT83A793QEW5A7EDKFCKWFQYPSZTSCBWRS4ZQTYG00Z5T2G4JMY6PWPS92D6YNJCYK0EGNNFF7QGDXXYWN33MM0296SQ1MK0R0F45EXAQT5S2Q39SXAA8KHR32A8BC0HG418300KMYBR5ZKNTX7SANSJB5SSYGP9RZVHN23RC1J29QRH5XFSMABMDA582GJH5JAE0D31AH5PTAHP04Y61KNCSKNC748N1PRN503VZY7EA1C4G75C0F13K04TE8RVP63K0TQ693E2XB23WSHYERKSZ6AKCTR3E15N1AF70HEKK13E4QCCY2JN896YEWWT9B8CVW50D8C87S75EK3G"; 385 386 let key: RsaKeyPair = 387 RsaKeyPair::from_pkcs8(&base32::decode(key.as_bytes()).unwrap()).unwrap(); 388 let tmp = r##"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><ebicsNoPubKeyDigestsRequest xmlns="urn:org:ebics:H005" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Revision="1" Version="H005"><header authenticate="true"><static><HostID>PFEBICS</HostID><Nonce>6BC48C9C2576ABD00295788E56DFCD0A</Nonce><Timestamp>2026-02-21T17:01:53.186561035Z</Timestamp><PartnerID>PFC00563</PartnerID><UserID>PFC00563</UserID><OrderDetails><AdminOrderType>HPB</AdminOrderType></OrderDetails><SecurityMedium>0000</SecurityMedium></static><mutable/></header><AuthSignature/><body/></ebicsNoPubKeyDigestsRequest>"##; 389 let xml = tmp.to_owned(); 390 let signed = sign_ebics(xml, &key).unwrap(); 391 let doc = verify_ebics(signed.as_bytes(), key.public_key()).unwrap(); 392 let signature = doc 393 .descendants() 394 .find(|it| it.has_tag_name("SignatureValue")) 395 .unwrap() 396 .text() 397 .unwrap(); 398 assert_eq!( 399 signature, 400 "eYyb1v/dGVOPndpMhXZlVQM2q9H9BJP77nYOWaa7jjoeLef7/8HjKIv8oq6Kaf6Z9mAfh/Pcip3a75gkdKpz7ocl1YdsaD+CcQkO1J/n4NwY821ccSh0Ahm2PBE168hyEMzPJrDeDtJrYqs+J/+nC8ek0hbo4/WPsH4UoxVu+ANsHR+BnQFQW3k9BFv+XKZbrBltIY62SN73tYwU8QzRtINJLzjhNB3T6S101n4CYwycXpL5b/oXXOUxxfDnn9EmIFt4DIgjxxqDYdQEBytULLORdkIdf563aw2wDaN12OQV2TB9gAs4Uu203FkUbmIagarMhbKKlqa1NkOteZ13Xw==" 401 ); 402 } 403 404 #[test] 405 fn verify() { 406 let xml = r##"<?xml version="1.0" encoding="UTF-8"?> 407 <ebicsResponse xmlns="urn:org:ebics:H005" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Version="H005" Revision="1" xsi:schemaLocation="urn:org:ebics:H005 ebics_response_H005.xsd"> 408 <header authenticate="true"> 409 <static> 410 <TransactionID>846BEEFDCBB4293BA764FAC72CD99B4E</TransactionID> 411 <NumSegments>1</NumSegments> 412 </static> 413 <mutable> 414 <TransactionPhase>Initialisation</TransactionPhase> 415 <SegmentNumber lastSegment="true">1</SegmentNumber> 416 <ReturnCode>000000</ReturnCode> 417 <ReportText>[EBICS_OK] OK</ReportText> 418 </mutable> 419 </header> 420 <AuthSignature> 421 <ds:SignedInfo> 422 <ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/> 423 <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/> 424 <ds:Reference URI="#xpointer(//*[@authenticate='true'])"> 425 <ds:Transforms> 426 <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/> 427 </ds:Transforms> 428 <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/> 429 <ds:DigestValue>9ifuDmTeB7+ZR0qAksWBqcuS2SgO+cIZrMCSrwmGfVw=</ds:DigestValue> 430 </ds:Reference> 431 </ds:SignedInfo> 432 <ds:SignatureValue>eeMBzXQTshVSBH5ryz2ducFgTegLyZhkZhGom5o6Se2lujaD3ixVVxnNlzbOVvWL8yvv2zfki877qTe5b+u4ejYfrR1rc6l1jB/lS9pqT9wyB0pD52LXNfrxhAkOsljv8V858Wjj7KSuUT0+7v8q6BRRg55BrIIJBWqPwWKY2a7AmFWTkYck/fLxawtQvrBOTxBMufQ9d060Ad8Ndewjzjt2lLSCH7K3mM2+E2AMmpwgXnYrlDr5GU5/Qt2KBG8XYJqe354+MKfp0FLqRq9tXnucjFOaRqQVzMNfFBkfFpvwNnoAC1FAgOQeXPyr4jOW6qlUBuBqM/jTCOyJe5RMvA==</ds:SignatureValue> 433 </AuthSignature> 434 <body> 435 <DataTransfer> 436 <DataEncryptionInfo authenticate="true"> 437 <EncryptionPubKeyDigest Version="E002" Algorithm="http://www.w3.org/2001/04/xmlenc#sha256">ujHlW/uumNiBAls2bUCmMFdYUOoxe8QMjNCPKlLF5ow=</EncryptionPubKeyDigest> 438 <TransactionKey>PpGWjNbu4bx1TjV/5OgEOAQF92NNQ5LaW61lKGI6/2/ycYmI1usfNC4x1bq/dlusCw6E/fOnHzDFyaD1C/ef0fZO2Ir++6s/ZK0e87+KBU7bhkL4ftoWulkRpdZlI5GgNlDxP5II4ZZ3KhlLqgv34bhJuL4Thi3mZz0ajR21PP4cV+NeWVpCsDyPQlRRwBy8dNAY+QrZxXvOmu3V9UZyRhRZLjStEeOFBwsEyGwmd6LIE/jJjpFY4PaxXL/TtjvE3qPS2SKQ0W1uVwWvMLMuNYgHHt02bUS5LZt4tJdHmNkMqQZbv262a9xRPxwVa7QhY+5GIMpSY4ZMoFPRlTHaqw==</TransactionKey> 439 </DataEncryptionInfo> 440 <OrderData>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</OrderData> 441 </DataTransfer> 442 <ReturnCode authenticate="true">000000</ReturnCode> 443 <TimestampBankParameter authenticate="true">2020-11-25T19:03:45.693Z</TimestampBankParameter> 444 </body> 445 </ebicsResponse> 446 "##; 447 let doc = Document::parse(xml).unwrap(); 448 let mut c14n = C14N::new(); 449 let res = digest_authenticated(&doc, &mut c14n); 450 assert_eq!( 451 base64::encode(res), 452 "9ifuDmTeB7+ZR0qAksWBqcuS2SgO+cIZrMCSrwmGfVw=" 453 ); 454 let key = "621028HG1M30JAM6923FE381040GA003G80GY01GG80GM0M2040G1EACATA11EF5SVKNBNBYF1S3WSKQ2A2R9VZ7RW2HRX00293JPZ7VQ780RFRVYTQKKDDNJAQGBH4659GT9QYBMJCG1RKZEH1WDJ0GAAY7B7NBMW6FWXCKFYRMZQME0WBGZ1AAMY2VBQ5XAFV8216EFNF2EPG6M5ZGHG9RG6EGED56TK9JESQ02Q7AAVBRAAARVBN9NHCN64KQ3SRRHYXB8RWRK4TSSC93XG8RWMQH4ZDJSBYDCEXFY6G3AWTZ0EZNCJJAYB98T4GNFWZMN81AVYCQHXT1APX81AXCAYNK7J9XETF5CN1J1WV0BVA2BYG4VAMAW123REPN67JF1TNWPTADBMHS17N2V1GFYT8JRWX4TGM2996NXTEPMA8C2CDDE0CRY2A6HT8C5H2D6C62YGRSCF820C0G008"; 455 let key = PublicKey::from_der(&base32::decode(key).unwrap()).unwrap(); 456 verify_ebics(xml.as_bytes(), &key).unwrap(); 457 } 458 }