libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

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             ('&', "&amp;"),
     72             ('<', "&lt;"),
     73             ('>', "&gt;"),
     74             ('\r', "&#xD;"),
     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             ('&', "&amp;"),
     86             ('<', "&lt;"),
     87             ('"', "&quot;"),
     88             ('\t', "&#x9;"),
     89             ('\n', "&#xA;"),
     90             ('\r', "&#xD;"),
     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&#13;
    329 hUBX3Y+0PahH+XeOnPGuUYdiOy0/FydtG2E1oQELNRojWhxxJMKPpN6jO9Y3j8QmS31oAWUiLjgA&#13;
    330 S//AU924Wh0rIwA8L3riSzGZDAgf6c0Wg+loPk581AD9QtzMiDi6onLVQvlKYtlVJNheTIreG54i&#13;
    331 a6vPTIqlMWB5iA5ZqoE6zO+VWr4sxTPswlHD29dDar7B4YJ1vYLLTzFHc0yJaDjWaURQNr0mDqUC&#13;
    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 }