From 28905fedc1e994bfd2ec16e50711639aa1247c28 Mon Sep 17 00:00:00 2001 From: Ana Gelez Date: Thu, 8 Aug 2024 15:11:15 +0200 Subject: [PATCH] WIP: digital signatures --- Cargo.lock | 369 +++++++++++++++++++++++++++++- Cargo.toml | 9 + crates/typst-pdf/Cargo.toml | 7 +- crates/typst-pdf/src/catalog.rs | 17 +- crates/typst-pdf/src/lib.rs | 6 +- crates/typst-pdf/src/signature.rs | 168 ++++++++++++++ 6 files changed, 565 insertions(+), 11 deletions(-) create mode 100644 crates/typst-pdf/src/signature.rs diff --git a/Cargo.lock b/Cargo.lock index 4c2e4d386..7dddab6a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.11" @@ -141,6 +152,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "biblatex" version = "0.9.3" @@ -205,6 +222,24 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -229,6 +264,15 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.0.106" @@ -303,6 +347,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "citationberg" version = "0.3.1" @@ -372,6 +426,18 @@ dependencies = [ "roff", ] +[[package]] +name = "cms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" +dependencies = [ + "const-oid", + "der", + "spki", + "x509-cert", +] + [[package]] name = "cobs" version = "0.2.3" @@ -423,6 +489,12 @@ dependencies = [ "syn", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation" version = "0.9.4" @@ -448,6 +520,15 @@ dependencies = [ "libm", ] +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -497,6 +578,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "csv" version = "1.3.0" @@ -524,6 +615,30 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.3.11" @@ -544,6 +659,18 @@ dependencies = [ "syn", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + [[package]] name = "dirs" version = "5.0.1" @@ -701,6 +828,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "flagset" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" + [[package]] name = "flate2" version = "1.0.30" @@ -791,6 +924,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getopts" version = "0.2.21" @@ -873,6 +1016,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "hypher" version = "0.1.5" @@ -1136,6 +1288,16 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -1239,6 +1401,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "libc" version = "0.2.155" @@ -1449,6 +1620,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1475,6 +1663,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1482,6 +1681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1647,11 +1847,19 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pdf-writer" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af6a7882fda7808481d43c51cadfc3ec934c6af72612a1fe6985ce329a2f0469" dependencies = [ "bitflags 2.6.0", "itoa", @@ -1659,6 +1867,15 @@ dependencies = [ "ryu", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1722,6 +1939,44 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "pkcs5", + "rand_core", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -1859,6 +2114,8 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha", "rand_core", ] @@ -1877,6 +2134,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] name = "rayon" @@ -1999,6 +2259,26 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2055,6 +2335,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -2079,6 +2368,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "security-framework" version = "2.11.0" @@ -2184,12 +2484,33 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shell-escape" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -2238,6 +2559,16 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2310,6 +2641,12 @@ name = "subsetter" version = "0.11.0" source = "git+https://github.com/typst/subsetter?rev=4e0058b#4e0058b4b9a0948a5f79894111948d95e59ba350" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "svg2pdf" version = "0.11.0" @@ -2588,6 +2925,12 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "typst" version = "0.11.0" @@ -2777,6 +3120,7 @@ dependencies = [ "arrayvec", "base64", "bytemuck", + "cms", "comemo", "ecow", "image", @@ -2784,6 +3128,10 @@ dependencies = [ "miniz_oxide", "once_cell", "pdf-writer", + "pkcs8", + "rand", + "rsa", + "sha2", "subsetter", "svg2pdf", "ttf-parser", @@ -3406,6 +3754,17 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", +] + [[package]] name = "xattr" version = "1.3.1" @@ -3528,6 +3887,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerotrie" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index 773544ebb..474e56c6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ ciborium = "0.2.1" clap = { version = "4.4", features = ["derive", "env"] } clap_complete = "4.2.1" clap_mangen = "0.2.10" +cms = "0.2.3" ctrlc = "3.4.1" codespan-reporting = "0.11" comemo = "0.4" @@ -78,16 +79,19 @@ pathdiff = "0.2" pdf-writer = "0.10.0" phf = { version = "0.11", features = ["macros"] } pixglyph = "0.4" +pkcs8 = { version = "0.10.2", features = ["pkcs5", "encryption"] } png = "0.17" portable-atomic = "1.6" proc-macro2 = "1" pulldown-cmark = "0.9" quote = "1" qcms = "0.3.0" +rand = "0.8.5" rayon = "1.7.0" regex = "1" resvg = { version = "0.42", default-features = false, features = ["raster-images"] } roxmltree = "0.20" +rsa = "0.9.6" rustybuzz = "0.14" same-file = "1" self-replace = "1.3.7" @@ -95,6 +99,7 @@ semver = "1" serde = { version = "1.0.184", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" +sha2 = { version = "0.10.8", featurs = ["oid"] } shell-escape = "0.1.5" siphasher = "1" smallvec = { version = "1.11.1", features = ["union", "const_generics", "const_new"] } @@ -126,6 +131,7 @@ xmlparser = "0.13.5" xmlwriter = "0.1.0" xmp-writer = "0.2" xz2 = { version = "0.1", features = ["static"] } +x509-cert = "0.2.5" yaml-front-matter = "0.1" zip = { version = "2", default-features = false, features = ["deflate"] } @@ -142,3 +148,6 @@ strip = true [workspace.lints.clippy] uninlined_format_args = "warn" blocks_in_conditions = "allow" + +[patch.crates-io] +pdf-writer = { path = "../pdf-writer" } diff --git a/crates/typst-pdf/Cargo.toml b/crates/typst-pdf/Cargo.toml index cdd65e828..8c53493bf 100644 --- a/crates/typst-pdf/Cargo.toml +++ b/crates/typst-pdf/Cargo.toml @@ -17,8 +17,10 @@ typst = { workspace = true } typst-assets = { workspace = true } typst-macros = { workspace = true } typst-timing = { workspace = true } +arrayvec = { workspace = true } base64 = { workspace = true } bytemuck = { workspace = true } +cms = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } image = { workspace = true } @@ -26,7 +28,10 @@ indexmap = { workspace = true } miniz_oxide = { workspace = true } once_cell = { workspace = true } pdf-writer = { workspace = true } -arrayvec = { workspace = true } +pkcs8 = { workspace = true, features = ["pkcs5", "encryption"] } +rand = { workspace = true } +rsa = { workspace = true } +sha2 = { workspace = true, features = ["oid"] } subsetter = { workspace = true } svg2pdf = { workspace = true } ttf-parser = { workspace = true } diff --git a/crates/typst-pdf/src/catalog.rs b/crates/typst-pdf/src/catalog.rs index 7d52cc58d..517d9ee18 100644 --- a/crates/typst-pdf/src/catalog.rs +++ b/crates/typst-pdf/src/catalog.rs @@ -10,17 +10,17 @@ use typst::foundations::{Datetime, Smart}; use typst::layout::Dir; use typst::text::Lang; -use crate::WithEverything; use crate::{hash_base64, outline, page::PdfPageLabel}; +use crate::{signature, WithEverything}; /// Write the document catalog. pub fn write_catalog( ctx: WithEverything, ident: Smart<&str>, timestamp: Option, - pdf: &mut Pdf, + mut pdf: Pdf, alloc: &mut Ref, -) { +) -> Vec { let lang = ctx .resources .languages @@ -35,10 +35,10 @@ pub fn write_catalog( }; // Write the outline tree. - let outline_root_id = outline::write_outline(pdf, alloc, &ctx); + let outline_root_id = outline::write_outline(&mut pdf, alloc, &ctx); // Write the page labels. - let page_labels = write_page_labels(pdf, alloc, &ctx); + let page_labels = write_page_labels(&mut pdf, alloc, &ctx); // Write the document information. let info_ref = alloc.bump(); @@ -132,6 +132,9 @@ pub fn write_catalog( .pair(Name(b"Type"), Name(b"Metadata")) .pair(Name(b"Subtype"), Name(b"XML")); + // Prepare digital signatures + let (signature_range, signature_form_ref) = signature::prepare(alloc, &mut pdf); + // Write the document catalog. let catalog_ref = alloc.bump(); let mut catalog = pdf.catalog(catalog_ref); @@ -167,7 +170,11 @@ pub fn write_catalog( catalog.lang(TextStr(lang.as_str())); } + catalog.insert(Name(b"AcroForm")).primitive(signature_form_ref); + catalog.finish(); + + signature::write(signature_range, pdf.finish()) } /// Write the page labels. diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index 9af830bd3..ea8f6648f 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -13,6 +13,7 @@ mod outline; mod page; mod pattern; mod resources; +mod signature; use std::collections::HashMap; use std::hash::Hash; @@ -348,10 +349,9 @@ impl PdfBuilder { process: P, ) -> Vec where - P: Fn(S, Smart<&str>, Option, &mut Pdf, &mut Ref), + P: Fn(S, Smart<&str>, Option, Pdf, &mut Ref) -> Vec, { - process(self.state, ident, timestamp, &mut self.pdf, &mut self.alloc); - self.pdf.finish() + process(self.state, ident, timestamp, self.pdf, &mut self.alloc) } } diff --git a/crates/typst-pdf/src/signature.rs b/crates/typst-pdf/src/signature.rs new file mode 100644 index 000000000..92c2c6749 --- /dev/null +++ b/crates/typst-pdf/src/signature.rs @@ -0,0 +1,168 @@ +use pkcs8::DecodePrivateKey; +use sha2::{ + digest::const_oid::db::rfc5912::{ID_SHA_512, SHA_512_WITH_RSA_ENCRYPTION}, + Digest, +}; +use std::ops::Range; + +use cms::{ + cert::{ + x509::{ + der::{ + asn1::{OctetString, SetOfVec}, + oid::db::rfc5911::{ID_DATA, ID_SIGNED_DATA}, + Any, AnyRef, Encode, + }, + spki::AlgorithmIdentifier, + Certificate, + }, + CertificateChoices, IssuerAndSerialNumber, + }, + content_info::{CmsVersion, ContentInfo}, + signed_data::{ + CertificateSet, EncapsulatedContentInfo, SignedData, SignerIdentifier, + SignerInfo, SignerInfos, + }, +}; +use pdf_writer::{ + types::{FieldType, SigFlags}, + writers::{Field, Form}, + Finish, Name, Pdf, Primitive, Ref, Str, +}; +use rsa::{traits::SignatureScheme, Pkcs1v15Sign, RsaPrivateKey}; +use sha2::Sha512; + +const SIG_SIZE: usize = 1024 * 4; + +pub fn prepare(alloc: &mut Ref, pdf: &mut Pdf) -> (Range, Ref) { + let form_ref = alloc.bump(); + let signature_field_ref = alloc.bump(); + + let mut signature_field: Field = pdf.indirect(signature_field_ref).start(); + signature_field.field_type(FieldType::Signature); + let mut signature_dict = signature_field.insert(Name(b"V")).dict(); + signature_dict.pair(Name(b"Type"), Name(b"Sig")); + signature_dict.pair(Name(b"Filter"), Name(b"Adobe.PPKLite")); + signature_dict.pair(Name(b"SubFilter"), Name(b"adbe.pkcs7.detached")); + let mut placeholder = [0; SIG_SIZE]; + placeholder[0] = 255; // Make sure pdf-writer writes this array as binary + let sig_end = signature_dict + .pair(Name(b"Contents"), Str(&placeholder)) + .current_len(); + let sig_start = sig_end + - SIG_SIZE * 2 // 2 chars to write each byte + - 2; // take < and > into account; + signature_dict + .insert(Name(b"ByteRange")) + .array() + .items([0, sig_start as i32, sig_end as i32]) + .item(Str(b"typst-document-size")); + signature_dict.finish(); + signature_field.finish(); + + let mut form: Form = pdf.indirect(form_ref).start(); + form.fields([signature_field_ref]); + form.sig_flags(SigFlags::SIGNATURES_EXIST); + + (sig_start..sig_end, form_ref) +} + +pub fn write(range: Range, mut bytes: Vec) -> Vec { + let needle = b"(typst-document-size)"; + let doc_size_start = bytes[range.end..] + .windows(needle.len()) + .position(|x| x == needle) + .unwrap(); + let doc_size_range = doc_size_start..(doc_size_start + needle.len()); + dbg!(&range, &doc_size_range); + let mut actual_size = Vec::new(); + ::write(bytes.len() as i32, &mut actual_size); + actual_size.extend(std::iter::repeat(b' ').take(needle.len() - actual_size.len())); + bytes.splice( + doc_size_range.start + range.end..doc_size_range.end + range.end, + actual_size, + ); + + let mut hasher = Sha512::new(); + hasher.update(&bytes[0..range.start]); + hasher.update(&bytes[range.end..]); + let hashed = hasher.finalize(); + + let priv_key = + RsaPrivateKey::from_pkcs8_encrypted_pem(include_str!("../../../key.pem"), "abcd") + .unwrap(); + let signer = Pkcs1v15Sign::new::(); + let signature = + signer.sign(Some(&mut rand::rngs::OsRng), &priv_key, &hashed).unwrap(); + + let pem_chain = + Certificate::load_pem_chain(include_bytes!("../../../cert.pem")).unwrap(); + let sig_data = ContentInfo { + content_type: ID_SIGNED_DATA, + content: Any::from( + AnyRef::try_from( + SignedData { + version: CmsVersion::V0, + digest_algorithms: SetOfVec::try_from(vec![AlgorithmIdentifier { + oid: SHA_512_WITH_RSA_ENCRYPTION, + parameters: None, + }]) + .unwrap(), + encap_content_info: EncapsulatedContentInfo { + econtent_type: ID_DATA, + econtent: None, + }, + certificates: Some(CertificateSet( + SetOfVec::from_iter( + pem_chain + .clone() + .into_iter() + .map(CertificateChoices::Certificate), + ) + .unwrap(), + )), + crls: None, + signer_infos: SignerInfos( + SetOfVec::from_iter(pem_chain.into_iter().map(|cert| { + SignerInfo { + version: CmsVersion::V1, + sid: SignerIdentifier::IssuerAndSerialNumber( + IssuerAndSerialNumber { + issuer: cert.tbs_certificate.issuer, + serial_number: cert.tbs_certificate.serial_number, + }, + ), + digest_alg: AlgorithmIdentifier { + oid: ID_SHA_512, + parameters: None, + }, + signed_attrs: None, // TODO: should contain revocation information (see section 12.8.3.3.2) + signature_algorithm: AlgorithmIdentifier { + oid: SHA_512_WITH_RSA_ENCRYPTION, + parameters: None, + }, + signature: OctetString::new(&signature[..]).unwrap(), + unsigned_attrs: None, // TODO: should contain timestamp + } + })) + .unwrap(), + ), + } + .to_der() + .unwrap() + .as_slice(), + ) + .unwrap(), + ), + }; + let mut sig = sig_data.to_der().unwrap(); + // pad with 0 to keep the ranges correct + sig.extend(std::iter::repeat(0).take(SIG_SIZE - sig.len())); + let mut encoded_sig = Vec::with_capacity(sig.len() * 2); + Str(&sig).write(&mut encoded_sig); + + dbg!(range.len(), encoded_sig.len()); + bytes.splice(range, encoded_sig); + + bytes +}