libeufin

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

pdf.rs (5982B)


      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 aws_lc_rs::{encoding::AsDer as _, rsa::PublicKey, signature::KeyPair};
     21 use jiff::Zoned;
     22 use pdf_writer::{Content, Finish, Name, Pdf, Rect, Ref, Str};
     23 
     24 use crate::{
     25     config::EbicsHostCfg, crypto::ebics_pub_key_hash, keys::ClientKeys, utils::hex_chunk_by_two,
     26 };
     27 pub fn generate_keys_pdf(keys: &ClientKeys, cfg: &EbicsHostCfg) -> anyhow::Result<Vec<u8>> {
     28     let date = Zoned::now().date().to_string();
     29 
     30     // Assign structural IDs directly
     31     let catalog_id = Ref::new(1);
     32     let page_tree_id = Ref::new(2);
     33     let font_reg_id = Ref::new(3);
     34     let font_bld_id = Ref::new(4);
     35 
     36     // Static mapping for the 3 EBICS pages
     37     let page_ids = [Ref::new(5), Ref::new(6), Ref::new(7)];
     38     let content_ids = [Ref::new(8), Ref::new(9), Ref::new(10)];
     39 
     40     let mut pdf = Pdf::new();
     41 
     42     // Catalog & Page Tree
     43     pdf.catalog(catalog_id).pages(page_tree_id);
     44     pdf.pages(page_tree_id)
     45         .count(3)
     46         .kids(page_ids.iter().copied());
     47 
     48     // Register built-in fonts
     49     pdf.type1_font(font_reg_id)
     50         .base_font(Name(b"Helvetica"))
     51         .encoding_predefined(Name(b"WinAnsiEncoding"));
     52 
     53     pdf.type1_font(font_bld_id)
     54         .base_font(Name(b"Helvetica-Bold"))
     55         .encoding_predefined(Name(b"WinAnsiEncoding"));
     56 
     57     let f_reg = Name(b"F1");
     58     let f_bld = Name(b"F2");
     59 
     60     let pages_data = [
     61         (
     62             "Signaturschlüssel",
     63             "Öffentlicher Schlüssel (Public signature key)",
     64             keys.sign.public_key(),
     65         ),
     66         (
     67             "Authentifikationsschlüssel",
     68             "Öffentlicher Schlüssel (Public identification and authentication key)",
     69             keys.auth.public_key(),
     70         ),
     71         (
     72             "Verschlüsselungsschlüssel",
     73             "Öffentlicher Schlüssel (Public encryption key)",
     74             &PublicKey::from_der(keys.enc.public_key().as_der()?.as_ref())?,
     75         ),
     76     ];
     77 
     78     // Conversion factor for Millimeters to Points
     79     const fn mm(v: f32) -> f32 {
     80         v * 2.834_65
     81     }
     82 
     83     struct Page {
     84         y: f32,
     85         content: Content,
     86     }
     87 
     88     impl Page {
     89         pub fn new() -> Self {
     90             Self {
     91                 y: 270.0,
     92                 content: Content::new(),
     93             }
     94         }
     95 
     96         pub fn line(&mut self, font: Name, size: f32, text: &str) {
     97             let encoded: Vec<u8> = text
     98                 .chars()
     99                 .map(|c| match c {
    100                     'Ö' => 0xD6,
    101                     'ü' => 0xFC,
    102                     'ä' => 0xE4,
    103                     'ß' => 0xDF,
    104                     _ => c as u8,
    105                 })
    106                 .collect();
    107 
    108             self.content.begin_text();
    109             self.content.set_font(font, size);
    110             self.content
    111                 .set_text_matrix([1.0, 0.0, 0.0, 1.0, mm(15.0), mm(self.y)]);
    112             self.content.show(Str(&encoded));
    113             self.content.end_text();
    114 
    115             self.y -= size * 0.45;
    116         }
    117     }
    118 
    119     for (i, (title, label, pub_key)) in pages_data.into_iter().enumerate() {
    120         let mut page = pdf.page(page_ids[i]);
    121         page.parent(page_tree_id);
    122         page.media_box(Rect::new(0.0, 0.0, mm(210.0), mm(297.0)));
    123         page.contents(content_ids[i]);
    124 
    125         // Map Fonts to Page Resources
    126         let mut res = page.resources();
    127         let mut fonts = res.fonts();
    128         fonts.insert(f_reg).primitive(font_reg_id);
    129         fonts.insert(f_bld).primitive(font_bld_id);
    130         fonts.finish();
    131         res.finish();
    132         page.finish();
    133 
    134         let mut p = Page::new();
    135 
    136         // Write Header
    137         p.line(f_bld, 24.0, title);
    138         p.y -= 2.2;
    139         for header in [
    140             format!("Datum: {date}").as_str(),
    141             &format!("Host-ID: {}", cfg.host_id),
    142             &format!("User-ID: {}", cfg.user_id),
    143             &format!("Partner-ID: {}", cfg.partner_id),
    144             "ES version: A006",
    145         ] {
    146             p.line(f_reg, 13.0, header);
    147         }
    148 
    149         p.y -= 5.0;
    150         p.line(f_bld, 13.0, label);
    151         p.y -= 5.0;
    152 
    153         // Write Public Key Fields
    154         for (hdr, bytes) in [
    155             (
    156                 "Exponent:",
    157                 pub_key.exponent().big_endian_without_leading_zero(),
    158             ),
    159             (
    160                 "Modulus:",
    161                 pub_key.modulus().big_endian_without_leading_zero(),
    162             ),
    163             ("SHA-256 hash:", ebics_pub_key_hash(pub_key).as_ref()),
    164         ] {
    165             p.line(f_bld, 13.0, hdr);
    166             for chunk in bytes.chunks(16) {
    167                 p.line(f_reg, 11.0, &hex_chunk_by_two(chunk).to_string());
    168             }
    169             p.y -= 5.0;
    170         }
    171 
    172         // Write Footer
    173         p.line(f_reg, 13.0, "Ort / Datum: ________________");
    174         p.line(f_reg, 13.0, "Firma / Name: ________________");
    175         p.line(f_reg, 13.0, "Unterschrift: ________________");
    176 
    177         pdf.stream(content_ids[i], &p.content.finish());
    178     }
    179 
    180     Ok(pdf.finish())
    181 }
    182 
    183 #[test]
    184 fn test() {
    185     generate_keys_pdf(
    186         &ClientKeys::generate().unwrap(),
    187         &EbicsHostCfg {
    188             base_url: "https://isotest.postfinance.ch/ebicsweb/ebicsweb",
    189             unix_path: None,
    190             host_id: "PFEBICS",
    191             user_id: "PFC00563",
    192             partner_id: "PFC00563",
    193         },
    194     )
    195     .unwrap();
    196 }