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 }