openpgp-cert-d-0.3.1/.cargo_vcs_info.json0000644000000001540000000000100136610ustar { "git": { "sha1": "c20712add373f5643a32380b740638b7ba9075e4" }, "path_in_vcs": "openpgp-cert-d" }openpgp-cert-d-0.3.1/Cargo.lock0000644000000441510000000000100116410ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] name = "anstyle" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anyhow" version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "assert_fs" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f070617a68e5c2ed5d06ee8dd620ee18fb72b99f6c094bed34cf8ab07c875b48" dependencies = [ "anstyle", "doc-comment", "globwalk", "predicates", "predicates-core", "predicates-tree", "tempfile", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "bstr" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" dependencies = [ "memchr", "serde", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "crossbeam-deque" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" dependencies = [ "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", ] [[package]] name = "crossbeam-utils" version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" dependencies = [ "cfg-if", ] [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", "redox_users", "windows-sys 0.48.0", ] [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fd-lock" version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b93f7a0db71c99f68398f80653ed05afb0b00e062e1a20c7ff849c4edfabbbcc" dependencies = [ "cfg-if", "rustix", "windows-sys 0.52.0", ] [[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 = "getrandom" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "globset" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ "aho-corasick", "bstr", "log", "regex-automata", "regex-syntax", ] [[package]] name = "globwalk" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" dependencies = [ "bitflags 1.3.2", "ignore", "walkdir", ] [[package]] name = "ignore" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060" dependencies = [ "crossbeam-deque", "globset", "log", "memchr", "regex-automata", "same-file", "walkdir", "winapi-util", ] [[package]] name = "itertools" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] [[package]] name = "libc" version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libredox" version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ "bitflags 2.4.1", "libc", "redox_syscall", ] [[package]] name = "linux-raw-sys" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memoffset" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] [[package]] name = "openpgp-cert-d" version = "0.3.1" dependencies = [ "anyhow", "assert_fs", "dirs", "fd-lock", "libc", "predicates", "sha1collisiondetection", "tempfile", "thiserror", "walkdir", ] [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "predicates" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0" dependencies = [ "anstyle", "difflib", "itertools", "predicates-core", ] [[package]] name = "predicates-core" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" [[package]] name = "predicates-tree" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" dependencies = [ "predicates-core", "termtree", ] [[package]] name = "proc-macro2" version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_users" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ "getrandom", "libredox", "thiserror", ] [[package]] name = "regex-automata" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rustix" version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "serde" version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "sha1collisiondetection" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31c0b86a052106b16741199985c9ec2bf501f619f70c48fa479b44b093ad9a68" dependencies = [ "generic-array", ] [[package]] name = "syn" version = "2.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", "windows-sys 0.48.0", ] [[package]] name = "termtree" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.0", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ "windows_aarch64_gnullvm 0.52.0", "windows_aarch64_msvc 0.52.0", "windows_i686_gnu 0.52.0", "windows_i686_msvc 0.52.0", "windows_x86_64_gnu 0.52.0", "windows_x86_64_gnullvm 0.52.0", "windows_x86_64_msvc 0.52.0", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" openpgp-cert-d-0.3.1/Cargo.toml0000644000000033770000000000100116710ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.70" name = "openpgp-cert-d" version = "0.3.1" authors = [ "Justus Winter ", "Nora Widdecke ", "Neal H. Walfield ", ] description = "Shared OpenPGP Certificate Directory" documentation = "https://docs.rs/openpgp-cert-d" readme = "README.md" keywords = [ "cryptography", "openpgp", "pgp", "certificate", "cert", ] categories = [ "cryptography", "authentication", "filesystem", "data-structures", "caching", ] license = "MIT" repository = "https://gitlab.com/sequoia-pgp/pgp-cert-d" [dependencies.anyhow] version = "1" features = ["std"] default-features = false [dependencies.dirs] version = "5" default-features = false [dependencies.fd-lock] version = ">=3, <5" default-features = false [dependencies.sha1collisiondetection] version = "0.3" default-features = false [dependencies.tempfile] version = "3.2" default-features = false [dependencies.thiserror] version = "1" default-features = false [dependencies.walkdir] version = "2" default-features = false [dev-dependencies.assert_fs] version = "1" default-features = false [dev-dependencies.predicates] version = "3" default-features = false [target."cfg(unix)".dependencies.libc] version = "0.2" openpgp-cert-d-0.3.1/Cargo.toml.orig000064400000000000000000000023161046102023000153420ustar 00000000000000[package] name = "openpgp-cert-d" description = "Shared OpenPGP Certificate Directory" version = "0.3.1" authors = [ "Justus Winter ", "Nora Widdecke ", "Neal H. Walfield ", ] documentation = "https://docs.rs/openpgp-cert-d" repository = "https://gitlab.com/sequoia-pgp/pgp-cert-d" readme = "README.md" keywords = ["cryptography", "openpgp", "pgp", "certificate", "cert"] categories = ["cryptography", "authentication", "filesystem", "data-structures", "caching"] license = "MIT" edition = "2021" rust-version = "1.70" [dependencies] anyhow = { version = "1", default-features = false, features = ["std"] } dirs = { version = "5", default-features = false } fd-lock = { version = ">=3, <5", default-features = false } sha1collisiondetection = { version = "0.3", default-features = false } tempfile = { version = "3.2", default-features = false } thiserror = { version = "1", default-features = false } walkdir = { version = "2", default-features = false } [target.'cfg(unix)'.dependencies] libc = "0.2" [dev-dependencies] assert_fs = { version = "1", default-features = false } predicates = { version = "3", default-features = false } openpgp-cert-d-0.3.1/LICENSE.txt000064400000000000000000000017771046102023000143100ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. openpgp-cert-d-0.3.1/README.md000064400000000000000000000010431046102023000137260ustar 00000000000000# Shared OpenPGP Certificate Directory This crate implements a generic [OpenPGP certificate store] that can be shared between implementations. It also defines a way to root trust, and a way to associate pet names with certificates. Sharing certificates and trust decisions increases security by enabling more applications to take advantage of OpenPGP. It also improves privacy by reducing the required certificate discoveries that go out to the network. [OpenPGP certificate store]: https://datatracker.ietf.org/doc/draft-nwjw-openpgp-cert-d/ openpgp-cert-d-0.3.1/examples/certd-tag.rs000064400000000000000000000106311046102023000165100ustar 00000000000000use std::time::SystemTime; use openpgp_cert_d as cert_d; use cert_d::CertD; use cert_d::Tag; fn main() -> std::result::Result<(), Box> { let args = std::env::args().collect::>(); let certd = if args.len() == 1 { CertD::new()? } else if args.len() == 2 { CertD::with_base_dir(&args[1])? } else { eprintln!("Usage: {} [CERTD]", args[0]); return Err("Invalid arguments".into()); }; let iter_fingerprint = certd.fingerprints(); let fingerprints = iter_fingerprint.collect::, _>>()?; eprintln!("Have {} certificates", fingerprints.len()); // Number of trials. const N: usize = 1000; let mut stat_one_trials: [u128; N] = [0; N]; let mut stat_one_tags: [Tag; N] = [Tag(0); N]; let mut tag_readdir_std_trials: [u128; N] = [0; N]; let mut tag_readdir_std_tags: [Tag; N] = [Tag(0); N]; let mut tag_probe_std_trials: [u128; N] = [0; N]; let mut tag_probe_std_tags: [Tag; N] = [Tag(0); N]; #[cfg(unix)] let mut tag_readdir_unix_trials: [u128; N] = [0; N]; #[cfg(unix)] let mut tag_readdir_unix_tags: [Tag; N] = [Tag(0); N]; #[cfg(unix)] let mut tag_probe_unix_trials: [u128; N] = [0; N]; #[cfg(unix)] let mut tag_probe_unix_tags: [Tag; N] = [Tag(0); N]; eprintln!("Running {} trials", N); for trial in 0..N { let start = SystemTime::now(); if let Ok(meta_data) = std::fs::metadata(certd.base_dir()) { stat_one_tags[trial] = Tag::try_from(meta_data).unwrap(); } let end = SystemTime::now(); stat_one_trials[trial] = end.duration_since(start).unwrap().as_nanos(); let start = SystemTime::now(); tag_readdir_std_tags[trial] = certd.tag_readdir_std(); let end = SystemTime::now(); tag_readdir_std_trials[trial] = end.duration_since(start).unwrap().as_nanos(); let start = SystemTime::now(); tag_probe_std_tags[trial] = certd.tag_probe_std(None); let end = SystemTime::now(); tag_probe_std_trials[trial] = end.duration_since(start).unwrap().as_nanos(); #[cfg(unix)] { let start = SystemTime::now(); tag_readdir_unix_tags[trial] = certd.tag_readdir_unix(); let end = SystemTime::now(); tag_readdir_unix_trials[trial] = end.duration_since(start).unwrap().as_nanos(); } #[cfg(unix)] { let start = SystemTime::now(); tag_probe_unix_tags[trial] = certd.tag_probe_unix(None); let end = SystemTime::now(); tag_probe_unix_trials[trial] = end.duration_since(start).unwrap().as_nanos(); } } let stat_one_mean: u128 = stat_one_trials.iter().sum::() / (N as u128); let summarize = |desc, mut trials: [u128; N]| { // Add thousand separators. let ts = |n: u128| { n.to_string() .as_bytes() .rchunks(3) .rev() .map(String::from_utf8_lossy) .collect::>() .join(",") }; eprintln!("{}:", desc); let mean: u128 = trials.iter().sum::() / (N as u128); trials.sort(); //eprintln!(" min: {} ns", ts(trials[0])); eprintln!(" 10%: {} ns", ts(trials[1 * N / 10])); eprintln!(" mean: {} ns", ts(mean)); eprintln!(" median: {} ns", ts(trials[5 * N / 10])); eprintln!(" 90%: {} ns", ts(trials[9 * N / 10])); //eprintln!(" max: {} ns", ts(trials[N - 1])); eprintln!("stat factor: {}", mean / stat_one_mean); }; summarize("stats_one", stat_one_trials); summarize("CertD::tag_readdir_std", tag_readdir_std_trials); summarize("CertD::tag_probe_std", tag_probe_std_trials); // Make sure the two implementations compute the same // tags. assert_eq!(tag_readdir_std_tags, tag_probe_std_tags); #[cfg(unix)] { summarize("CertD::tag_readdir_unix", tag_readdir_unix_trials); // Make sure the two implementations compute the same // tags. assert_eq!(tag_readdir_std_tags, tag_readdir_unix_tags); } #[cfg(unix)] { summarize("CertD::tag_probe_unix", tag_probe_unix_trials); assert_eq!(tag_readdir_std_tags, tag_probe_unix_tags); } Ok(()) } openpgp-cert-d-0.3.1/examples/entropy.rs000064400000000000000000000076111046102023000163420ustar 00000000000000use std::time::Duration; use std::time::UNIX_EPOCH; use openpgp_cert_d as cert_d; use cert_d::CertD; use cert_d::Tag; fn main() -> std::result::Result<(), Box> { let args = std::env::args().collect::>(); let certd = if args.len() == 1 { CertD::new()? } else if args.len() == 2 { CertD::with_base_dir(&args[1])? } else { eprintln!("Usage: {} [CERTD]", args[0]); return Err("Invalid arguments".into()); }; let mut size_one_count = [0; 64]; let mut secs_one_count = [0; 64]; let mut nanos_one_count = [0; 64]; let mut tag_one_count = [0; 64]; let mut count = 0; let count_ones = |one_count: &mut [usize; 64], mut value: u64| { for i in 0..64 { if value % 2 == 1 { one_count[i] += 1; } value >>= 1; } }; for item in certd.iter_files() { let Ok((fpr, file)) = item else { continue }; let m = match file.metadata() { Ok(m) => m, Err(err) => { eprintln!("Failed to stat {}: {}", fpr, err); continue; } }; let tag = match Tag::try_from(&m) { Ok(m) => m, Err(err) => { eprintln!("Failed to compute tag for {}: {}", fpr, err); continue; } }; count_ones(&mut size_one_count, m.len()); let mtime = m.modified()? .duration_since(UNIX_EPOCH) .unwrap_or_else(|_| Duration::new(0, 0)); count_ones(&mut secs_one_count, mtime.as_secs()); count_ones(&mut nanos_one_count, mtime.subsec_nanos() as u64); count_ones(&mut tag_one_count, tag.0); count += 1; } // Estimate the entropy using a Maximum Likelihood estimator // (i.e., shannon entropy). // // https://en.wikipedia.org/wiki/Entropy_estimation // https://strimmerlab.github.io/publications/lecture-notes/MATH20802/from-entropy-to-maximum-likelihood.html let mut size_entropy = 0f64; let mut secs_entropy = 0f64; let mut nanos_entropy = 0f64; let mut tag_entropy = 0f64; let mut max_entropy = 0f64; // Entropy of the probability (`p` = 0..1) of a binary event. let entropy = |p: f64| -> f64 { assert!(0. <= p); assert!(p <= 1.); // log(0) is NaN, so is 0 * log(0). But we need 0 * log(0) to // be 0. if p < 0.0001 || p > 0.9999 { 0. } else { -p * p.log2() - (1. - p) * (1. - p).log2() } }; let p = |one_count: usize| -> f64 { f64::from(one_count as u32) / f64::from(count) }; for i in 0..64 { let size_prob = p(size_one_count[i]); let secs_prob = p(secs_one_count[i]); let nanos_prob = p(nanos_one_count[i]); let tag_prob = p(tag_one_count[i]); size_entropy += entropy(size_prob); secs_entropy += entropy(secs_prob); nanos_entropy += entropy(nanos_prob); tag_entropy += entropy(tag_prob); // Sanity check. max_entropy += entropy(0.5); eprintln!("{:2}: size: {:3.0}% ({:5}); secs: {:3.0}% ({:5}); \ nanos: {:3.0}% ({:5}); tag: {:3.0}% ({:5})", i, size_prob * 100., size_one_count[i], secs_prob * 100., secs_one_count[i], nanos_prob * 100., nanos_one_count[i], tag_prob * 100., tag_one_count[i]); } eprintln!("Maximum-likelihood estimate of entropy (max: {} bits):", max_entropy); eprintln!(" size: {:.2} bits", size_entropy); eprintln!(" secs: {:.2} bits", secs_entropy); eprintln!(" nanos: {:.2} bits", nanos_entropy); eprintln!(" max empirical entropy: {:.2} bits", size_entropy + secs_entropy + nanos_entropy); eprintln!(" tag: {:.2} bits", tag_entropy); Ok(()) } openpgp-cert-d-0.3.1/src/certd.rs000064400000000000000000001732041046102023000147160ustar 00000000000000use std::{ borrow::Cow, convert::TryInto, env, fs::{self, File}, io::{self, Read, Write}, path::{Path, PathBuf}, }; use fd_lock::RwLock; use tempfile::NamedTempFile; use walkdir::WalkDir; use crate::SPECIAL_NAMES; use crate::{pgp, InternalError}; use crate::{Error, Result, Tag}; const PATH_PREFIX_LEN: usize = 2; const TRACE: bool = false; /// The data type returned by the merge callback. /// /// See, for instance, [`CertD::insert`]. pub enum MergeResult<'a> { /// Keep the on-disk version. Keep, /// Use the specified version. /// /// This is usually a merged version of the on-disk version and the /// new version of the certificate. DataRef(&'a [u8]), /// Use the specified version. /// /// This is usually a merged version of the on-disk version and the /// new version of the certificate. Data(Vec), } impl<'a> From<&'a [u8]> for MergeResult<'a> { fn from(data: &'a [u8]) -> Self { MergeResult::DataRef(data) } } impl From> for MergeResult<'_> { fn from(data: Vec) -> Self { MergeResult::Data(data) } } // Used by `CertD::tag`. // // One tag for each directory. struct CertDTag([Tag; 256]); impl CertDTag { fn null() -> Self { Self([Tag(676149182_1608123167); 256]) } fn compress(&self) -> Tag { // This is pretty naive. A hash would be better. So would a // few more bits. let mut composite: u64 = 0; for (i, tag) in self.0.iter().enumerate() { let mut tag = tag.0; tag = tag.rotate_right(i as u32); composite ^= tag } Tag(composite) } } /// A certificate store. /// /// This is a handle to an on-disk certificate store that can be used /// to lookup and insert certificates. /// /// A certificate store contains certificates. Its main role is to /// hold certificates indexed by their fingerprint. (Note: /// certificates are not indexed by their subkey fingerprints.) But, /// it can also store certificates under [special names]. Currently, /// the specification defines one special name, `trust-root`, which /// holds the user's local trust root. Non-standard special names are /// possible. These MUST start with an underscore, which SHOULD be /// immediately followed by the vendor's name, e.g., /// `_sequoia_some_special.pgp`. /// /// [special names]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names #[derive(Debug)] pub struct CertD { base: PathBuf, } impl CertD { /// Opens the default certificate store. /// /// If not explicitly requested otherwise, an application SHOULD /// use the [default store]. To use a store with a different /// location, use [`CertD::with_base_dir`]. /// /// [default store]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-default-stores-location pub fn new() -> Result { CertD::with_base_dir(Self::user_configured_store_path()?) } /// Returns the location of the user-configured store. /// /// If set, this is the value of the environment variable /// `PGP_CERT_D`. Otherwise, it is the default store's path as /// returned by [`CertD::default_store_path`]. pub fn user_configured_store_path() -> Result { if let Some(path) = env::var_os("PGP_CERT_D") { Ok(PathBuf::from(path)) } else { CertD::default_store_path() } } /// Returns the location of the default store. /// /// [The location of the default store] is platform specific. /// This returns an error on unsupported platforms. /// /// [The location of the default store]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#platform-specifics pub fn default_store_path() -> Result { Ok(dirs::data_dir() .ok_or(Error::UnsupportedPlatform( "Default store's path".into()))? .join("pgp.cert.d")) } /// Opens a store with an explicit location. /// /// Note: Most applications should use the [default store], which /// is shared. The default store can be opened using /// [`CertD::new`]. /// /// [default store]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-default-stores-location pub fn with_base_dir>(base: P) -> Result { Ok(CertD { base: base.as_ref().into(), }) } /// Get the this Certd's base path. pub fn base_dir(&self) -> &Path { &self.base } /// Computes the certificate directory's tag. /// /// Modulo collisions in the hashing algorithm, this tag will /// change whenever a certificate indexed by its fingerprint is /// added, updated, or removed. The tag is designed to not change /// when a certificate indexed by a special name, or some other /// unrelated data in the certd directory is added, updated, or /// removed. /// /// The tag should be stable in the sense that you can serialize /// it to disk, read it in again later, and compare it to the /// current value. However, how the tag is computed may change. /// In this case, you may observe a spurious update. pub fn tag(&self) -> Tag { let revert_to_readdir = Some(4); platform! { unix => self.tag_probe_unix(revert_to_readdir), windows => self.tag_probe_std(revert_to_readdir), } } /// Never call this function directly! /// /// This function is not part of the semver contract; It is only /// exported to facilitate testing and benchmarking. /// /// This implements a variant of [`CertD::tag`], which uses /// `readdir`, and is implemented using functionality from Rust's /// standard library. #[doc(hidden)] pub fn tag_readdir_std(&self) -> Tag { tracer!(TRACE, "CertD::tag_readdir_std"); let mut composite = CertDTag::null(); let dir = std::fs::read_dir(&self.base); if let Ok(dir) = dir { 'entry: for e in dir { let e = if let Ok(e) = e { e } else { continue; }; // Calling file_type is normally free. // // https://doc.rust-lang.org/std/fs/struct.DirEntry.html#method.file_type // // As we are only interested in directories, filter out // anything as early as possible. if let Ok(file_type) = e.file_type() { if file_type.is_dir() { // Check! } else { continue; } } else { continue; } let filename = e.file_name(); t!("Examining {:?}", filename); let filename: &[u8] = platform! { unix => { use std::os::unix::ffi::OsStrExt; filename.as_bytes() }, windows => { if let Some(filename) = filename.to_str() { filename.as_bytes() } else { t!("Can't convert to a str."); continue; } } }; if filename.len() != 2 { t!("Wrong length."); continue; } let mut nibbles: [u8; 2] = [0; 2]; for i in 0..2usize { let v = filename[i]; nibbles[i] = match v { b'0'..=b'9' => v - b'0', b'a'..=b'f' => 10 + v - b'a', _ => { t!("{}: contains non-lower-hex characters.", String::from_utf8_lossy(filename)); continue 'entry; } }; } let i = ((nibbles[0] << 4) + nibbles[1]) as usize; // On Windows this is free. On Unix this requires a // system call. let metadata = if let Ok(metadata) = e.metadata() { metadata } else { t!("{:02x}: Can't read meta-data.", i); continue; }; let tag = if let Ok(tag) = Tag::try_from(metadata) { tag } else { t!("Can't compute tag."); continue; }; t!("{:02x} => Tag({:x})", i, tag.0); composite.0[i] = tag; } } composite.compress() } /// Never call this function directly! /// /// This function is not part of the semver contract; It is only /// exported to facilitate testing and benchmarking. /// /// This implements a variant of [`CertD::tag`], which uses /// `readdir`, and is specialized for Unix platforms. #[cfg(unix)] #[doc(hidden)] pub fn tag_readdir_unix(&self) -> Tag { use crate::unixdir::Dir; tracer!(TRACE, "CertD::tag_readdir_unix"); let mut composite = CertDTag::null(); let dir = Dir::open(&self.base); if let Ok(mut dir) = dir { 'entry: while let Some(e) = dir.readdir() { let file_type = e.file_type(); if file_type.is_dir() || file_type.is_unknown() { // Plausible. } else { continue; } let filename = e.file_name(); t!("Examining {:?}", filename); if filename.len() != 2 { t!("Wrong length."); continue; } let mut nibbles: [u8; 2] = [0; 2]; for i in 0..2usize { let v = filename[i]; nibbles[i] = match v { b'0'..=b'9' => v - b'0', b'a'..=b'f' => 10 + v - b'a', _ => { t!("{}: contains non-lower-hex characters.", String::from_utf8_lossy(filename)); continue 'entry; } }; } let i = ((nibbles[0] << 4) + nibbles[1]) as usize; let metadata = if let Ok(metadata) = e.metadata() { metadata } else { t!("{:02x}: Can't read meta-data.", i); continue; }; // Double check as the type in the directory entry is // not definitive (it could be unknown). if ! metadata.is_dir() { t!("{:02x}: Not a directory."); continue; } let tag = Tag::from(metadata); t!("{:02x} => Tag({:x})", i, tag.0); composite.0[i] = tag; } } composite.compress() } /// Never call this function directly! /// /// This function is not part of the semver contract; It is only /// exported to facilitate testing and benchmarking. /// /// This implements a variant of [`CertD::tag`], which `stats` all /// of the expected subdirectories using the Rust standard /// library. #[doc(hidden)] pub fn tag_probe_std(&self, revert_to_readir: Option) -> Tag { tracer!(TRACE, "CertD::tag_probe_std"); const FILENAMES: [&str; 256] = [ "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "0e", "0f", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", "1e", "1f", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2a", "2b", "2c", "2d", "2e", "2f", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3a", "3b", "3c", "3d", "3e", "3f", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4a", "4b", "4c", "4d", "4e", "4f", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5a", "5b", "5c", "5d", "5e", "5f", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6a", "6b", "6c", "6d", "6e", "6f", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7a", "7b", "7c", "7d", "7e", "7f", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8a", "8b", "8c", "8d", "8e", "8f", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9a", "9b", "9c", "9d", "9e", "9f", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "aa", "ab", "ac", "ad", "ae", "af", "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "ba", "bb", "bc", "bd", "be", "bf", "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf", "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", "dc", "dd", "de", "df", "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef", "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff", ]; let mut misses = 0; let mut hits = 0; let mut composite = CertDTag::null(); let base = self.base_dir(); for (i, filename) in FILENAMES.iter().enumerate() { let mut path = base.to_path_buf(); path.push(filename); let metadata = if let Ok(metadata) = std::fs::metadata(path) { hits += 1; metadata } else { t!("{:02x}: No such file or directory.", i); misses += 1; if let Some(revert_to_readir) = revert_to_readir { if revert_to_readir == misses && hits <= 1 { t!("Too many misses; switching to readir implementation."); return self.tag_readdir_std(); } } continue; }; if ! metadata.file_type().is_dir() { t!("{:02x}: Not a directory."); continue; } let tag = match Tag::try_from(&metadata) { Ok(tag) => tag, Err(err) => { t!("{:02x}: Can't compute tag: {}.", i, err); continue; } }; t!("{:02x} => Tag({:x})", i, tag.0); composite.0[i] = tag; } composite.compress() } /// Never call this function directly! /// /// This function is not part of the semver contract; It is only /// exported to facilitate testing and benchmarking. /// /// This implements a variant of [`CertD::tag`], which `stats` all /// of the expected subdirectories, and is specialized for Unix /// platforms. #[doc(hidden)] #[cfg(unix)] pub fn tag_probe_unix(&self, revert_to_readir: Option) -> Tag { use crate::unixdir::Dir; tracer!(TRACE, "CertD::tag_probe_unix"); const FILENAMES: [&str; 256] = [ "00\0", "01\0", "02\0", "03\0", "04\0", "05\0", "06\0", "07\0", "08\0", "09\0", "0a\0", "0b\0", "0c\0", "0d\0", "0e\0", "0f\0", "10\0", "11\0", "12\0", "13\0", "14\0", "15\0", "16\0", "17\0", "18\0", "19\0", "1a\0", "1b\0", "1c\0", "1d\0", "1e\0", "1f\0", "20\0", "21\0", "22\0", "23\0", "24\0", "25\0", "26\0", "27\0", "28\0", "29\0", "2a\0", "2b\0", "2c\0", "2d\0", "2e\0", "2f\0", "30\0", "31\0", "32\0", "33\0", "34\0", "35\0", "36\0", "37\0", "38\0", "39\0", "3a\0", "3b\0", "3c\0", "3d\0", "3e\0", "3f\0", "40\0", "41\0", "42\0", "43\0", "44\0", "45\0", "46\0", "47\0", "48\0", "49\0", "4a\0", "4b\0", "4c\0", "4d\0", "4e\0", "4f\0", "50\0", "51\0", "52\0", "53\0", "54\0", "55\0", "56\0", "57\0", "58\0", "59\0", "5a\0", "5b\0", "5c\0", "5d\0", "5e\0", "5f\0", "60\0", "61\0", "62\0", "63\0", "64\0", "65\0", "66\0", "67\0", "68\0", "69\0", "6a\0", "6b\0", "6c\0", "6d\0", "6e\0", "6f\0", "70\0", "71\0", "72\0", "73\0", "74\0", "75\0", "76\0", "77\0", "78\0", "79\0", "7a\0", "7b\0", "7c\0", "7d\0", "7e\0", "7f\0", "80\0", "81\0", "82\0", "83\0", "84\0", "85\0", "86\0", "87\0", "88\0", "89\0", "8a\0", "8b\0", "8c\0", "8d\0", "8e\0", "8f\0", "90\0", "91\0", "92\0", "93\0", "94\0", "95\0", "96\0", "97\0", "98\0", "99\0", "9a\0", "9b\0", "9c\0", "9d\0", "9e\0", "9f\0", "a0\0", "a1\0", "a2\0", "a3\0", "a4\0", "a5\0", "a6\0", "a7\0", "a8\0", "a9\0", "aa\0", "ab\0", "ac\0", "ad\0", "ae\0", "af\0", "b0\0", "b1\0", "b2\0", "b3\0", "b4\0", "b5\0", "b6\0", "b7\0", "b8\0", "b9\0", "ba\0", "bb\0", "bc\0", "bd\0", "be\0", "bf\0", "c0\0", "c1\0", "c2\0", "c3\0", "c4\0", "c5\0", "c6\0", "c7\0", "c8\0", "c9\0", "ca\0", "cb\0", "cc\0", "cd\0", "ce\0", "cf\0", "d0\0", "d1\0", "d2\0", "d3\0", "d4\0", "d5\0", "d6\0", "d7\0", "d8\0", "d9\0", "da\0", "db\0", "dc\0", "dd\0", "de\0", "df\0", "e0\0", "e1\0", "e2\0", "e3\0", "e4\0", "e5\0", "e6\0", "e7\0", "e8\0", "e9\0", "ea\0", "eb\0", "ec\0", "ed\0", "ee\0", "ef\0", "f0\0", "f1\0", "f2\0", "f3\0", "f4\0", "f5\0", "f6\0", "f7\0", "f8\0", "f9\0", "fa\0", "fb\0", "fc\0", "fd\0", "fe\0", "ff\0", ]; let mut misses = 0; let mut hits = 0; let mut composite = CertDTag::null(); let base = self.base_dir(); let dir = Dir::open(base); if let Ok(mut dir) = dir { for (i, filename) in FILENAMES.iter().enumerate() { let metadata = if let Ok(metadata) = dir.fstat(filename.as_bytes()) { hits += 1; metadata } else { t!("{:02x}: No such file or directory.", i); misses += 1; if let Some(revert_to_readir) = revert_to_readir { if revert_to_readir == misses && hits <= 1 { t!("Too many misses; switching to readir implementation."); return self.tag_readdir_unix(); } } continue; }; if ! metadata.is_dir() { t!("{:02x}: Not a directory."); continue; } let tag = Tag::from(&metadata); t!("{:02x} => Tag({:x})", i, tag.0); composite.0[i] = tag } } composite.compress() } /// Turns a fingerprint into a path in the store. /// /// [The transformation from a fingerprint to a path] is defined /// by the standard. /// /// [The transformation from a fingerprint to a path]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#section-3.2.1 pub fn get_path_by_fingerprint(&self, fingerprint: &str) -> Result { if fingerprint.len() != 40 { return Err(Error::BadName); } if fingerprint.chars().any(|c| !c.is_ascii_hexdigit()) { return Err(Error::BadName); } let fingerprint = fingerprint.to_ascii_lowercase(); Ok(self.base.join(&fingerprint[..2]).join(&fingerprint[2..])) } /// Turns a path in the store into a fingerprint, if it conforms to the cert-d /// specification. fn get_fingerprint_by_path( &self, path: &Path, ) -> std::result::Result { let path = if path.is_absolute() { path.strip_prefix(&self.base) .map_err(|_| InternalError::PathNotInStore)? } else { path }; if !self.base.join(path).is_file() { return Err(InternalError::BadFingerprintPath); } if path.components().count() != 2 { return Err(InternalError::BadFingerprintPath); } let components = path.components().map(|c| c.as_os_str()).collect::>(); if components.iter().any(|c| !c.is_ascii()) { return Err(InternalError::BadFingerprintPath); } let head = components[0].to_string_lossy(); if head.len() != PATH_PREFIX_LEN { return Err(InternalError::BadFingerprintPath); } let tail = components[1].to_string_lossy(); if tail.len() != pgp::FINGERPRINT_LEN_CHARS_V4 - PATH_PREFIX_LEN && tail.len() != pgp::FINGERPRINT_LEN_CHARS_V6 - PATH_PREFIX_LEN { return Err(InternalError::BadFingerprintPath); } Ok(head.to_string() + &tail) } /// Turns a special name into a path in the store. /// /// The specification currently defines one [special name]. /// Non-standard special names are allowed, but they must MUST /// start with an underscore, which SHOULD be immediately followed /// by the vendor's name, e.g., `_sequoia_some_special.pgp`. /// Other names cause this function to return [`Error::BadName`]. /// /// [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names pub fn get_path_by_special(&self, special: &str) -> Result { Self::get_relative_path_by_special(special).map(|special| { self.base.join(special) }) } /// Returns whether the special name is valid. /// /// The specification currently defines one [special name]. /// Non-standard special names are allowed, but they must MUST /// start with an underscore, which SHOULD be immediately followed /// by the vendor's name, e.g., `_sequoia_some_special.pgp`. /// Other names cause this function to return [`Error::BadName`]. /// /// [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names pub fn is_special(special: &str) -> Result<()> { Self::get_relative_path_by_special(special).map(|_| ()) } // The meat behind `CertD::get_path_by_special` and // `CertD::is_special`. See them for documentation. fn get_relative_path_by_special(special: &str) -> Result { if let Some('_') = special.chars().next() { let special = PathBuf::from(special); if special.components().count() != 1 { Err(Error::BadName) } else { Ok(special) } } else if SPECIAL_NAMES.binary_search(&special).is_ok() { Ok(PathBuf::from(special)) } else { Err(Error::BadName) } } /// Looks up a certificate in the store by name, i.e., by /// fingerprint or by special name. /// /// The specification currently defines one [special name]. /// Non-standard special names are allowed, but they must MUST /// start with an underscore, which SHOULD be immediately followed /// by the vendor's name, e.g., `_sequoia_some_special.pgp`. /// Other names cause this function to return [`Error::BadName`]. /// /// [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names /// /// If the certificate exists, this function computes the tag, /// reads in the certificate data, and returns `Ok(Some((tag, /// cert_data)))`. See [`Tag`] for how the tag can be used to /// cache lookups. /// /// If the certificate does not exist, this function returns `Ok(None)`. /// /// If an I/O error occurs, or the name was invalid, this function returns /// an [`Error`]. /// /// [`CertD::get_file`] is often more efficient if you don't /// necessarily need the tag or the file's contents. pub fn get(&self, name: &str) -> Result)>> { if let Some(mut fp) = self.get_file(name)? { let tag = Tag::try_from(&fp)?; let mut buf = Vec::new(); fp.read_to_end(&mut buf)?; Ok(Some((tag, buf))) } else { Ok(None) } } /// Looks up a certificate in the store by name, i.e., by /// fingerprint or by special name. /// /// The specification currently defines one [special name]. /// Non-standard special names are allowed, but they must MUST /// start with an underscore, which SHOULD be immediately followed /// by the vendor's name, e.g., `_sequoia_some_special.pgp`. /// Other names cause this function to return [`Error::BadName`]. /// /// [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names /// /// If the certificate exists, this function returns /// `Ok(Some([std::fs::File]))`. If the certificate does not /// exist, it returns `Ok(None)`. If an I/O error occurs, or the /// name was invalid, this function returns an [`Error`]. /// /// You can get the file's [`Tag`] by doing: /// `Tag::try_from(&file)`. See [`Tag`] for how the tag can be /// used to cache lookups. pub fn get_file(&self, name: &str) -> Result> { let path = self.get_path(name)?; match fs::File::open(path) { Ok(f) => Ok(Some(f)), Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None), Err(e) => Err(e.into()), } } /// Conditionally looks up a certificate in the store by an name, /// i.e. a fingerprint or a special name. /// /// The specification currently defines one [special name]. /// Non-standard special names are allowed, but they must MUST /// start with an underscore, which SHOULD be immediately followed /// by the vendor's name, e.g., `_sequoia_some_special.pgp`. /// Other names cause this function to return [`Error::BadName`]. /// /// [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names /// /// If the certificate has changed, i.e., the provided tag does /// not match the current tag, this function returns /// `Ok(Some((cert, tag)))`. The tag can be used in subsequent /// calls to this function. /// /// If the certificate has not changed, i.e., the provided tag /// matches the current tag, or the certificate does not exist, /// this function returns `Ok(None)`. /// /// If an I/O error occurs, or the name was invalid, this function returns /// an [`Error`]. pub fn get_if_changed( &self, since: Tag, name: &str, ) -> Result)>> { let path = self.get_path(name)?; match fs::File::open(path) { Ok(mut f) => { let tag = f.metadata()?.try_into()?; if since == tag { Ok(None) // Not modified. } else { let mut buf = Vec::new(); f.read_to_end(&mut buf)?; Ok(Some((tag, buf))) } } Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None), Err(e) => Err(e.into()), } } /// Get the path to a certificate by fingerprint or special name. /// /// The specification currently defines one [special name]. /// Non-standard special names are allowed, but they must MUST /// start with an underscore, which SHOULD be immediately followed /// by the vendor's name, e.g., `_sequoia_some_special.pgp`. /// Other names cause this function to return [`Error::BadName`]. /// /// [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names pub fn get_path(&self, name: &str) -> Result { // Try to convert the name to a path, first as a fingerprint and // if that fails as a special name. // If the errors get more insightful than just Error::BadName, prefer // returning the one from fingerprint_to_path. self.get_path_by_fingerprint(name) .or_else(|_| self.get_path_by_special(name)) } /// Inserts or updates a cert. /// /// Requires the fingerprint and a callback function. The /// callback is passed `data`, and an `Option>`, which /// contains the existing cert data, if any. The callback is /// expected to merge the two copies of the certificate together. /// The returned data is written to the store. Note: The callback /// may decide to omit (parts of) the existing data, but this /// should be done with great care as not to lose any vital /// information. /// /// The new [`Tag`] is returned, and if `return_inserted` is true, /// the data written to the store is also returned. /// /// This function locks store, which may block the current thread. /// Use [`CertD::try_insert`] to avoid blocking. pub fn insert<'a, D, M>(&self, fingerprint: &str, data: D, return_inserted: bool, merge: M) -> Result<(Tag, Option>)> where M: FnOnce(D, Option<&[u8]>) -> Result>, { let blocking = true; self.insert_impl( fingerprint, true, data, return_inserted, merge, blocking) } /// Inserts or updates a cert, non-blocking variant. /// /// Requires the fingerprint and a callback function. The /// callback is passed `data`, and an `Option>`, which /// contains the existing cert data, if any. The callback is /// expected to merge the two copies of the certificate together. /// The returned data is written to the store. Note: The callback /// may decide to omit (parts of) the existing data, but this /// should be done with great care as not to lose any vital /// information. /// /// The new [`Tag`] is returned, and if `return_inserted` is true, /// the data written to the store is also returned. /// /// This function attempts to lock the store. If the store is /// already locked, then it instead returns [`Error::IoError`] /// with an [`std::io::ErrorKind::WouldBlock`]. pub fn try_insert<'a, D, M>(&self, fingerprint: &str, data: D, return_inserted: bool, merge: M) -> Result<(Tag, Option>)> where M: FnOnce(D, Option<&[u8]>) -> Result>, { let blocking = false; self.insert_impl( fingerprint, true, data, return_inserted, merge, blocking) } /// Inserts or updates a cert. /// /// Requires the new certificate data, and a callback function. /// The fingerprint is extracted from `data`. The callback is /// passed `data`, and an `Option>`, which contains the /// existing cert data, if any. The callback is expected to merge /// the two copies of the certificate together. The returned data /// is written to the store. Note: The callback may decide to /// omit (parts of) the existing data, but this should be done /// with great care as not to lose any vital information. /// /// The new [`Tag`] is returned, and if `return_inserted` is true, /// the data written to the store is also returned. /// /// This function locks store, which may block the current thread. /// Use [`CertD::try_insert_data`] to avoid blocking. pub fn insert_data<'a, M>(&self, data: &'a [u8], return_inserted: bool, merge: M) -> Result<(Tag, Option>)> where M: FnOnce(&'a [u8], Option<&[u8]>) -> Result>, { let blocking = true; let fingerprint = pgp::fingerprint(data)?; self.insert_impl( &fingerprint, true, data, return_inserted, merge, blocking) } /// Inserts or updates a cert, non-blocking variant. /// /// Requires the new certificate data, and a callback function. /// The fingerprint is extracted from `data`. The callback is /// passed `data`, and an `Option>`, which contains the /// existing cert data, if any. The callback is expected to merge /// the two copies of the certificate together. The returned data /// is written to the store. Note: The callback may decide to /// omit (parts of) the existing data, but this should be done /// with great care as not to lose any vital information. /// /// The new [`Tag`] is returned, and if `return_inserted` is true, /// the data written to the store is also returned. /// /// This function attempts to lock the store. If the store is /// already locked, then it instead returns [`Error::IoError`] /// with an [`std::io::ErrorKind::WouldBlock`]. pub fn try_insert_data<'a, M>(&self, data: &'a [u8], return_inserted: bool, merge: M) -> Result<(Tag, Option>)> where M: FnOnce(&'a [u8], Option<&[u8]>) -> Result>, { let blocking = false; pgp::plausible_tsk_or_tpk(data)?; let fingerprint = pgp::fingerprint(data)?; self.insert_impl( &fingerprint, true, data, return_inserted, merge, blocking) } /// Inserts or updates the cert or key stored under a special name. /// /// Requires the special name, the cert or key in binary format /// and a callback function. The callback is invoked with an /// `Option>` of the existing data (if any), and is /// expected to merge the two copies together. The returned /// `Vec` is written to the store under the special name. /// (Note: The function may decide to omit (parts of) the existing /// data, but this should be done with great care as not to lose /// any vital information.) The new [`Tag`] is returned, and if /// `return_inserted` is true, the data written to the store is /// also returned. Otherwise, `None` is returned. /// /// This function locks store, which may block the current thread. /// Use [`CertD::try_insert_special`] to avoid blocking. /// /// The specification currently defines one [special name]. /// Non-standard special names are allowed, but they must MUST /// start with an underscore, which SHOULD be immediately followed /// by the vendor's name, e.g., `_sequoia_some_special.pgp`. /// Other names cause this function to return [`Error::BadName`]. /// /// [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names pub fn insert_special<'a, D, M>( &self, special_name: &str, data: D, return_inserted: bool, merge: M, ) -> Result<(Tag, Option>)> where M: FnOnce(D, Option<&[u8]>) -> Result>, { let blocking = true; self.insert_impl( special_name, false, data, return_inserted, merge, blocking) } /// Inserts or updates the cert or key stored under a special /// name, non-blocking variant. /// /// Requires the special name, the cert or key in binary format /// and a callback function. The callback is invoked with an /// `Option>` of the existing data (if any), and is /// expected to merge the two copies together. The returned /// `Vec` is written to the store under the special name. /// (Note: The function may decide to omit (parts of) the existing /// data, but this should be done with great care as not to lose /// any vital information.) The new [`Tag`] is returned, and if /// `return_inserted` is true, the data written to the store is /// also returned. Otherwise, `None` is returned. /// /// This function attempts to lock the store. If the store is /// already locked, then it instead returns [`Error::IoError`] /// with an [`std::io::ErrorKind::WouldBlock`]. /// /// The specification currently defines one [special name]. /// Non-standard special names are allowed, but they must MUST /// start with an underscore, which SHOULD be immediately followed /// by the vendor's name, e.g., `_sequoia_some_special.pgp`. /// Other names cause this function to return [`Error::BadName`]. /// /// [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names pub fn try_insert_special<'a, D, M>( &self, special_name: &str, data: D, return_inserted: bool, merge: M, ) -> Result<(Tag, Option>)> where M: FnOnce(D, Option<&[u8]>) -> Result>, { let blocking = false; self.insert_impl( special_name, false, data, return_inserted, merge, blocking) } fn insert_impl<'a, D, M>( &self, name: &str, name_is_fingerprint: bool, data: D, return_inserted: bool, merge: M, blocking: bool, ) -> Result<(Tag, Option>)> where M: FnOnce(D, Option<&[u8]>) -> Result>, { let name = if name_is_fingerprint { pgp::canonicalize_fingerprint(name)? } else { Cow::Borrowed(name) }; let target_path = self.get_path(&name)?; // Make sure the directory exists. fs::create_dir_all(target_path.parent().expect("at least one leg"))?; let mut lf = RwLock::new(self.idempotent_create_lockfile()?); // Lock exclusively let lock = if blocking { lf.write()? } else { lf.try_write()? }; let old_cert = self.get(&name)?.map(|(_, cert)| cert); let old_cert = old_cert.as_deref(); let merge_result = merge(data, old_cert)?; let new_cert = match merge_result { MergeResult::Keep => old_cert.unwrap_or(&[]), MergeResult::DataRef(data) => data, MergeResult::Data(ref data) => data, }; pgp::plausible_tsk_or_tpk(new_cert)?; if name_is_fingerprint { let fingerprint = pgp::fingerprint(new_cert)?; if fingerprint != name { return Err(Error::BadData(pgp::Error::WrongCertificate( name.to_string(), fingerprint.to_string()))); } } if let MergeResult::Keep = merge_result { // There's nothing to do. } else { let mut tmp = NamedTempFile::new_in(&self.base)?; tmp.write_all(new_cert.as_ref())?; tmp.persist(&target_path).map_err(|e| e.error)?; } let tag = fs::File::open(&target_path)?.metadata()?.try_into()?; drop(lock); Ok((tag, if return_inserted { Some(new_cert.to_vec()) } else { None })) } /// Iterates over the certs in the store returning their fingerprints. /// /// Note: this only considers certificates that are stored under /// their fingerprint; it does not include certificates stored /// under a special name. pub fn fingerprints(&self) -> impl Iterator> + '_ { WalkDir::new(&self.base) // take only subdirs of depth 2 .max_depth(2) .min_depth(2) .into_iter() // Convert the paths to fingerprints. The store is a shared // directory, writable by anyone, so there may be files that don't // correspond to a fingerprint. Filter them out. .filter_map(move |e| match e { Ok(entry) => match self.get_fingerprint_by_path(entry.path()) { Ok(fingerprint) => Some(Ok(fingerprint)), Err(_) => None, }, Err(err) => { if let Some(std::io::ErrorKind::NotFound) = err.io_error().map(|err| err.kind()) { // Ignore file not found. None } else { Some(Err(err.into())) } } }) } /// Iterates over the certs in the store. /// /// Iterates over the certs in the store returning the /// fingerprint, and a file handle for each cert. /// /// Note: this only considers certificates that are stored under /// their fingerprint; it does not include certificates stored /// under a special name. pub fn iter_files( &self, ) -> impl Iterator> + '_ { // Helper function analogous to get, with the fingerprint included in // the output. let get_with_fingerprint = move |fingerprint: &str| -> Result<(String, File)> { match self.get_file(fingerprint)? { None => Err(Error::IoError(io::Error::new( io::ErrorKind::Other, // The file was found when fingerprints() walked over the // directory, but wasn't found for reading now. format!("The file for {} disappeared.", fingerprint), ))), Some(file) => Ok((fingerprint.to_owned(), file)), } }; self.fingerprints() .map(move |fingerprint_result| { fingerprint_result.and_then(|fingerprint| { get_with_fingerprint(&fingerprint) }) }) } /// Iterates over the certs in the store. /// /// Iterates over the certs in the store returning the /// fingerprint, the tag, and the data for each cert. /// /// Note: this only considers certificates that are stored under /// their fingerprint; it does not include certificates stored /// under a special name. pub fn iter( &self, ) -> impl Iterator)>> + '_ { self.iter_files() .map(|r| { let (fingerprint, mut fp) = r?; let tag = Tag::try_from(&fp)?; let mut data = Vec::new(); fp.read_to_end(&mut data)?; Ok((fingerprint, tag, data)) }) } fn idempotent_create_lockfile(&self) -> Result { let lock_path = self.base.join("writelock"); // Open the lockfile for writing, and create it if it does not exist yet. std::fs::OpenOptions::new() .write(true) .create(true) .open(lock_path) .map_err(Into::into) } } #[cfg(test)] mod tests { use super::*; use assert_fs::prelude::*; use predicates::prelude::*; use crate::TRUST_ROOT; fn test_base() -> assert_fs::TempDir { let base = assert_fs::TempDir::new().unwrap(); match std::env::var_os("CERTD_TEST_PERSIST") { Some(_) => { eprintln!("Test base dir: {}", &base.path().to_string_lossy()); base.into_persistent() } None => base, } } struct Testdata<'a> { data: &'a [u8], fingerprint: &'a str, } impl Testdata<'_> { fn path(&self) -> String { [&self.fingerprint[..2], &self.fingerprint[2..]].join("/") } fn add_to_certd(&self, base: &assert_fs::TempDir) { base.child(self.path()).write_binary(self.data).unwrap(); } } static ALICE: Testdata = Testdata { fingerprint: "eb85bb5fa33a75e15e944e63f231550c4f47e38e", data: include_bytes!("../../testdata/alice.pgp"), }; static BOB: Testdata = Testdata { fingerprint: "d1a66e1a23b182c9980f788cfbfcc82a015e7330", data: include_bytes!("../../testdata/bob.pgp"), }; static TESTY: Testdata = Testdata { fingerprint: "39d100ab67d5bd8c04010205fb3751f1587daef1", data: include_bytes!("../../testdata/testy-new.pgp"), }; fn setup_testdir( testdata: &[&Testdata], ) -> Result<(assert_fs::TempDir, CertD)> { let base = test_base(); for t in testdata.iter() { t.add_to_certd(&base); } let trust_root_data = include_bytes!("../../testdata/sender.pgp"); base.child("trust-root") .write_binary(trust_root_data) .unwrap(); let certd = CertD::with_base_dir(&base)?; Ok((base, certd)) } #[test] fn get_fingerprint() -> std::result::Result<(), Box> { let data = include_bytes!("../../testdata/testy-new.pgp"); let base = test_base(); base.child("39/d100ab67d5bd8c04010205fb3751f1587daef1") .write_binary(data) .unwrap(); let certd = CertD::with_base_dir(&base)?; let (tag, cert) = certd .get("39d100ab67d5bd8c04010205fb3751f1587daef1")? .unwrap(); assert_eq!(cert, data); assert!(certd .get_if_changed(tag, "39d100ab67d5bd8c04010205fb3751f1587daef1")? .is_none()); let mut fp = certd .get_file("39d100ab67d5bd8c04010205fb3751f1587daef1")? .unwrap(); let tag = Tag::try_from(&fp)?; let mut data = Vec::new(); fp.read_to_end(&mut data)?; assert_eq!(cert, data); assert!(certd .get_if_changed(tag, "39d100ab67d5bd8c04010205fb3751f1587daef1")? .is_none()); base.close().unwrap(); Ok(()) } #[test] fn get_special() -> std::result::Result<(), Box> { let data = include_bytes!("../../testdata/sender.pgp"); let base = test_base(); base.child("trust-root").write_binary(data).unwrap(); let certd = CertD::with_base_dir(&base)?; let (tag, cert) = certd.get(TRUST_ROOT)?.unwrap(); assert_eq!(cert, data); assert!(certd.get_if_changed(tag, TRUST_ROOT)?.is_none()); base.close().unwrap(); Ok(()) } #[test] fn get_not_found() -> Result<()> { let base = test_base(); let certd = CertD::with_base_dir(&base)?; let result = certd.get("39d100ab67d5bd8c04010205fb3751f1587daef1"); assert!(matches!(result, Ok(None))); Ok(()) } #[test] fn insert_locked() -> Result<()> { let data = include_bytes!("../../testdata/testy-new.pgp"); let base = test_base(); let file = base.child("39/d100ab67d5bd8c04010205fb3751f1587daef1"); file.assert(predicate::path::missing()); let certd = CertD::with_base_dir(&base)?; // Lock the lockfile before we try to insert let mut lf = RwLock::new(certd.idempotent_create_lockfile()?); // Lock exclusively let _lock = lf.write()?; let result = certd.try_insert_data( data, false, |new: &[u8], old: Option<&[u8]>| { assert!(old.is_none()); Ok(MergeResult::DataRef(new)) }); match result.unwrap_err() { Error::IoError(e) if e.kind() == io::ErrorKind::WouldBlock => { Ok(()) } e => Err(e), } } #[test] fn insert_special_locked() -> Result<()> { let data = include_bytes!("../../testdata/sender.pgp"); let base = test_base(); let file = base.child("trust-root"); file.assert(predicate::path::missing()); let certd = CertD::with_base_dir(&base)?; // Lock the lockfile before we try to insert let mut lock = RwLock::new(certd.idempotent_create_lockfile()?); // Lock exclusively let _lock = lock.write()?; let result = certd.try_insert_special( TRUST_ROOT, &data[..], false, |new: &[u8], old: Option<&[u8]>| { assert!(old.is_none()); Ok(MergeResult::DataRef(new)) }, ); match result.unwrap_err() { Error::IoError(e) if e.kind() == io::ErrorKind::WouldBlock => { Ok(()) } e => Err(e), } } #[test] fn insert_new() -> Result<()> { let data = include_bytes!("../../testdata/testy-new.pgp"); let data = &data[..]; let base = test_base(); let file = base.child("39/d100ab67d5bd8c04010205fb3751f1587daef1"); file.assert(predicate::path::missing()); let certd = CertD::with_base_dir(&base)?; let (_, inserted) = certd.insert_data( data, true, |new: &[u8], old: Option<&[u8]>| { assert!(old.is_none()); Ok(MergeResult::DataRef(new)) })?; file.assert(data); assert_eq!(inserted.as_deref(), Some(data)); Ok(()) } #[test] fn insert_special_new() -> Result<()> { let data = include_bytes!("../../testdata/sender.pgp"); let data = &data[..]; let base = test_base(); let file = base.child("trust-root"); file.assert(predicate::path::missing()); let certd = CertD::with_base_dir(&base)?; let (_, inserted) = certd.insert_special( "trust-root", data, true, |new: &[u8], old: Option<&[u8]>| { assert!(old.is_none()); Ok(MergeResult::DataRef(new)) }, )?; file.assert(data); assert_eq!(inserted.as_deref(), Some(data)); Ok(()) } #[test] fn insert_update() -> std::result::Result<(), Box> { let data = include_bytes!("../../testdata/testy-new.pgp"); let data = &data[..]; let base = test_base(); let file = base.child("39/d100ab67d5bd8c04010205fb3751f1587daef1"); file.touch().unwrap(); file.assert(predicate::str::is_empty()); let certd = CertD::with_base_dir(&base)?; let (_, inserted) = certd.insert_data( data, true, |new: &[u8], old: Option<&[u8]>| { assert!(old.is_some()); Ok(MergeResult::DataRef(new)) })?; file.assert(data); assert_eq!(inserted.as_deref(), Some(data)); Ok(()) } #[test] fn insert_special_update( ) -> std::result::Result<(), Box> { let data = include_bytes!("../../testdata/sender.pgp"); let data = &data[..]; let base = test_base(); let file = base.child("trust-root"); file.touch().unwrap(); file.assert(predicate::str::is_empty()); let certd = CertD::with_base_dir(&base)?; let (_, inserted) = certd.insert_special( TRUST_ROOT, data, true, |new: &[u8], old: Option<&[u8]>| { assert!(old.is_some()); Ok(MergeResult::DataRef(new)) }, )?; file.assert(data); assert_eq!(inserted.as_deref(), Some(data)); Ok(()) } #[test] fn insert_get() -> std::result::Result<(), Box> { let data = include_bytes!("../../testdata/testy-new.pgp"); let data = &data[..]; let base = test_base(); let certd = CertD::with_base_dir(&base)?; certd.insert_data( data, false, |new: &[u8], old: Option<&[u8]>| { assert!(old.is_none()); Ok(MergeResult::DataRef(new)) })?; let (_, cert) = certd .get("39d100ab67d5bd8c04010205fb3751f1587daef1")? .unwrap(); assert_eq!(cert, data); Ok(()) } #[test] fn get_path_by_fingerprint() -> Result<()> { let base = test_base(); let certd = CertD::with_base_dir(&base)?; let expected = base .path() .join("39") .join("d100ab67d5bd8c04010205fb3751f1587daef1"); let fingerprint = "39d100ab67d5bd8c04010205fb3751f1587daef1"; assert_eq!(certd.get_path_by_fingerprint(fingerprint)?, expected); let fingerprint = "39D100AB67D5BD8C04010205FB3751F1587DAEF1"; assert_eq!(certd.get_path_by_fingerprint(fingerprint)?, expected); let fingerprint = "39D100ab67D5bD8C04010205FB3751f1587DAeF1"; assert_eq!(certd.get_path_by_fingerprint(fingerprint)?, expected); Ok(()) } #[test] fn get_path_by_fingerprint_negative() -> Result<()> { let base = test_base(); let certd = CertD::with_base_dir(&base)?; // empty let fingerprint = ""; let result = certd.get_path_by_fingerprint(fingerprint); assert!(matches!(result.unwrap_err(), Error::BadName)); // too short let fingerprint = "39d100ab67d5bd8c04010205fb3751f1587daef"; let result = certd.get_path_by_fingerprint(fingerprint); assert!(matches!(result.unwrap_err(), Error::BadName)); // not ascii hex let fingerprint = "peter"; let result = certd.get_path_by_fingerprint(fingerprint); assert!(matches!(result.unwrap_err(), Error::BadName)); Ok(()) } #[test] fn get_path_by_special() -> Result<()> { let base = test_base(); let certd = CertD::with_base_dir(&base)?; let expected = base.path().join(TRUST_ROOT); let name = "trust-root"; assert_eq!(certd.get_path_by_special(name)?, expected); let name = "_sequoia"; assert_eq!(certd.get_path_by_special(name)?, base.path().join(name)); let name = "_sequoia_foo"; assert_eq!(certd.get_path_by_special(name)?, base.path().join(name)); Ok(()) } #[test] fn get_path_by_special_negative() -> Result<()> { let base = test_base(); let certd = CertD::with_base_dir(&base)?; // empty let name = ""; let result = certd.get_path_by_special(name); assert!(matches!(result.unwrap_err(), Error::BadName)); // unknown let name = "mySpecialName"; let result = certd.get_path_by_special(name); assert!(matches!(result.unwrap_err(), Error::BadName)); // Case matters. let name = "TRUST-ROOT"; let result = certd.get_path_by_special(name); assert!(matches!(result.unwrap_err(), Error::BadName)); let name = "TrUsT-RooT"; let result = certd.get_path_by_special(name); assert!(matches!(result.unwrap_err(), Error::BadName)); // Directories are not currently allowed. let name = "_sequoia_foo/bar"; let result = certd.get_path_by_special(name); assert!(matches!(result.unwrap_err(), Error::BadName)); Ok(()) } #[test] fn is_special() -> Result<()> { assert!(CertD::is_special("trust-root").is_ok()); assert!(CertD::is_special("TRUST-ROOT").is_err()); assert!(CertD::is_special("_special_foo_bar").is_ok()); assert!(CertD::is_special("special_foo_bar").is_err()); assert!(CertD::is_special("8f17777118a33dda9ba48e62aacb3243630052d9").is_err()); Ok(()) } #[test] fn fingerprints() -> Result<()> { use std::collections::HashSet; let (_base, certd) = setup_testdir(&[&ALICE, &BOB, &TESTY])?; let iter_fingerprint = certd.fingerprints(); let fingerprints = iter_fingerprint.collect::>>()?; let expected: HashSet<_> = [ALICE.fingerprint, BOB.fingerprint, TESTY.fingerprint] .iter() .map(|&s| s.to_owned()) .collect(); assert_eq!(expected, fingerprints); Ok(()) } #[test] fn fingerprints_empty() -> Result<()> { use std::collections::HashSet; let base = test_base(); let certd = CertD::with_base_dir(&base)?; let iter_fingerprint = certd.fingerprints(); let fingerprints = iter_fingerprint.collect::>>()?; assert!(fingerprints.is_empty()); Ok(()) } #[test] fn fingerprints_junk() -> Result<()> { use std::collections::HashSet; let (base, certd) = setup_testdir(&[&ALICE, &BOB, &TESTY])?; base.child("some_file").write_str("some_text").unwrap(); base.child("aa/some_file").write_str("some_text").unwrap(); base.child("aa/aa/some_file") .write_str("some_text") .unwrap(); let iter_fingerprint = certd.fingerprints(); let fingerprints = iter_fingerprint.collect::>>()?; let expected: HashSet<_> = [ALICE.fingerprint, BOB.fingerprint, TESTY.fingerprint] .iter() .map(|&s| s.to_owned()) .collect(); assert_eq!(expected, fingerprints); Ok(()) } #[test] fn iter() -> Result<()> { use std::collections::HashSet; let (_base, certd) = setup_testdir(&[&ALICE, &BOB, &TESTY])?; let mut expected: HashSet<_> = [&ALICE, &BOB, &TESTY] .iter() .map(|&s| { ( s.fingerprint.to_owned(), certd.get(s.fingerprint).unwrap().unwrap().0, s.data.to_vec(), ) }) .collect(); for item in certd.iter() { let item = item?; assert!(expected.contains(&item)); expected.remove(&item); } assert!(expected.is_empty()); Ok(()) } #[test] fn iter_files() -> Result<()> { use std::collections::HashSet; let (_base, certd) = setup_testdir(&[&ALICE, &BOB, &TESTY])?; let mut expected: HashSet<_> = [&ALICE, &BOB, &TESTY] .iter() .map(|&s| { ( s.fingerprint.to_owned(), certd.get(s.fingerprint).unwrap().unwrap().0, s.data.to_vec().into_boxed_slice(), ) }) .collect(); for item in certd.iter_files() { let (fingerprint, mut fp) = item?; let tag = Tag::try_from(&fp)?; let mut cert = Vec::new(); fp.read_to_end(&mut cert)?; let item = (fingerprint, tag, cert.into()); assert!(expected.contains(&item)); expected.remove(&item); } assert!(expected.is_empty()); Ok(()) } #[test] fn base_path() -> Result<()> { let base = assert_fs::TempDir::new().unwrap(); let certd = CertD::with_base_dir(&base)?; assert_eq!(certd.base_dir(), base.path()); Ok(()) } #[test] fn default_store_path() { assert!(CertD::default_store_path().is_ok(), "The default store's path is not defined for this platform."); } #[test] fn certd_does_not_exist() -> Result<()> { let mut base = assert_fs::TempDir::new().unwrap().path().to_path_buf(); base.push("asdflkj"); // std::fs::try_exists would be better, but it is still // experimental. assert!(std::fs::metadata(&base).is_err()); let certd = CertD::with_base_dir(&base)?; // fingerprints shouldn't fail even if the cert directory did // not exist. let fingerprints = certd.fingerprints().collect::>>()?; assert_eq!(fingerprints.len(), 0); Ok(()) } #[test] fn certd_tag() -> Result<()> { let (_base, certd) = setup_testdir(&[&ALICE])?; let certd_tag = || -> Tag { let tag = certd.tag(); let tag_readdir_std = certd.tag_readdir_std(); assert_eq!(tag, tag_readdir_std); #[cfg(unix)] { let tag_readdir_unix = certd.tag_readdir_unix(); assert_eq!(tag, tag_readdir_unix); } let tag_probe_std = certd.tag_probe_std(None); assert_eq!(tag, tag_probe_std); #[cfg(unix)] { let tag_probe_unix = certd.tag_probe_unix(None); assert_eq!(tag, tag_probe_unix); } tag }; let iter_fingerprint = certd.fingerprints(); let fingerprints = iter_fingerprint.collect::>>()?; assert_eq!(fingerprints.len(), 1); let tag0 = certd_tag(); eprintln!("tag0: {:x}", tag0.0); // Insert a new certificate. This should change the tag. eprintln!("Inserting BOB"); certd.insert_data( &BOB.data, false, |new: &[u8], old: Option<&[u8]>| { assert!(old.is_none()); Ok(MergeResult::DataRef(new)) })?; let tag1 = certd_tag(); eprintln!("tag1: {:x}", tag1.0); assert_ne!(tag0, tag1); // Adding a special shouldn't change the certd's tag. eprintln!("Inserting special _bob"); certd.insert_special( "_bob", BOB.data, false, |new: &[u8], old: Option<&[u8]>| { assert!(old.is_none()); Ok(MergeResult::DataRef(new)) })?; let tag2 = certd_tag(); eprintln!("tag2: {:x}", tag2.0); assert_eq!(tag1, tag2); // Insert a new certificate. This should change the tag. eprintln!("Inserting TESTY"); certd.insert_data( TESTY.data, false, |new: &[u8], old: Option<&[u8]>| { assert!(old.is_none()); Ok(MergeResult::DataRef(new)) })?; let tag3 = certd_tag(); eprintln!("tag3: {:x}", tag3.0); assert_ne!(tag2, tag3); Ok(()) } } openpgp-cert-d-0.3.1/src/error.rs000064400000000000000000000042141046102023000147400ustar 00000000000000use crate::pgp; /// Result specialization. pub type Result = std::result::Result; /// OpenPGP-Cert-D errors. /// /// Errors defined by the [Shared PGP Certificate Directory]. /// /// [Shared PGP Certificate Directory]: https://sequoia-pgp.gitlab.io/pgp-cert-d/#section-6 #[derive(thiserror::Error, Debug)] pub enum Error { /// The name was neither a valid fingerprint, nor a known special name. #[error("The name is not valid fingerprint or a known special name")] BadName, /// The base directory cannot possibly contain a store. #[error("Base directory is not a store")] NotAStore, /// Error computing the fingerprint. /// /// This means that the certificate data was malformed /// (e.g. because it was ASCII-Armored), or the certificate's /// OpenPGP version was not supported by this crate. #[error("The data was not valid OpenPGP cert or key in binary format")] BadData(#[from] pgp::Error), /// Unsupported platform. #[error("Functionality is not supported on this platform: {0}")] UnsupportedPlatform(String), /// An IO error occurred. #[error("IO error")] IoError(#[from] std::io::Error), /// Any other error. /// /// This is used to return arbitrary errors from /// [`crate::CertD::insert`], [`crate::CertD::try_insert`], /// [`crate::CertD::insert_special`], and /// [`crate::CertD::try_insert_special`]. #[error(transparent)] Other(#[from] Box), } impl From for Error { fn from(error: walkdir::Error) -> Self { Self::Other(std::io::Error::from(error).into()) } } impl From for Error { fn from(error: anyhow::Error) -> Self { Self::Other(>::from( error, )) } } #[derive(thiserror::Error, Debug)] pub(crate) enum InternalError { /// The path does not represent a fingerprnt. #[error("The path does not represent a fingerprint")] BadFingerprintPath, /// The path is not inside the store directory. #[error("The path is not in the store")] PathNotInStore, } openpgp-cert-d-0.3.1/src/lib.rs000064400000000000000000000232741046102023000143640ustar 00000000000000//! Shared OpenPGP Certificate Directory //! //! This crate implements a generic [OpenPGP certificate store] that can //! be shared between implementations. It also defines a way to root //! trust, and a way to associate pet names with certificates. //! Sharing certificates and trust decisions increases security by //! enabling more applications to take advantage of OpenPGP. It also //! improves privacy by reducing the required certificate discoveries //! that go out to the network. //! //! Note that this crate is only concerned with the low-level //! mechanics of the certificate directory and does not depend on an //! OpenPGP implementation. This is the reason that it works with //! bytes and not high-level OpenPGP data structures. Generally, it //! has to be combined with an OpenPGP implementation to be useful. //! //! [OpenPGP certificate store]: https://datatracker.ietf.org/doc/draft-nwjw-openpgp-cert-d/ use std::time::{Duration, UNIX_EPOCH}; #[macro_use] mod macros; mod certd; pub use certd::CertD; pub use certd::MergeResult; mod error; pub use error::*; mod pgp; #[cfg(unix)] mod unixdir; /// Special name of the trust root. /// /// This is the [special name] under which the [trust root] is stored. /// /// [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names /// [trust root]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-trust-root pub const TRUST_ROOT: &str = "trust-root"; // SPECIAL_NAMES must be sorted. This allows using // SPECIAL_NAMES.binary_search(needle).is_ok() (O(log(n))) instead of // SPECIAL_NAMES.contains(needle) (O(n))). const SPECIAL_NAMES: &[&str] = &[TRUST_ROOT]; /// Facilitates caching of derived data. /// /// Every time you look up a cert in the directory, the operation also /// returns a tag. This tag, which can be converted to a `u64`, is an /// opaque identifier that changes whenever the cert in the directory /// changes. /// /// To use it, store the tag with your cached data, and every time you /// re-do the lookup, compare the returned tag with the stored tag to /// see if your cached data is still up-to-date. /// /// # Examples /// /// This demonstrates how to use the tag to prevent useless /// recomputations. /// /// ``` /// # use openpgp_cert_d::*; /// # fn dosth(certd: &CertD) -> Result<()> { /// let fp = "eb85bb5fa33a75e15e944e63f231550c4f47e38e"; /// let (tag, cert) = certd.get(fp)?.expect("cert to exist"); /// // ... /// if let Some((new_cert, new_tag)) = certd.get_if_changed(tag, fp)? { /// // cert changed... /// } else { /// // cert didn't change... /// } /// # Ok(()) } /// ``` #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] pub struct Tag(pub u64); impl Tag { fn new(mtime_secs: u64, mtime_nsec: u32, size: u64, is_dir: bool) -> Self { // We estimate the entropy of the parameters as follows: // // - mtime_secs: log2(age of least recently updated certificate) // // Assuming the least recently updated certificate was // updated a year ago and a uniform distribution of // insertions and updates, there are 31536000 possible // values, which corresponds to 25 bits of entropy. // // - mtime_nsecs: log2(1 billion) = just under 30 bits of entropy. // // Since all values are equally likely, this represents // // - size: log2(large certificate size) // // Assuming certificates can grow to be about 1 MB large and // their size is uniformly distributed, we'd have log2(1 M) // = 20 bits of entropy. In practice, we expect clustering // around different typical sizes. // // We can empirically estimate entropy using a maximum // likelihood estimator for each bit, and summing them. This // is done in the `entropy` example. On my certificate store // with 3285 certificates, I see: // // ``` // $ cargo run --release --example entropy // 0: size: 50% ( 1639); secs: 51% ( 1664); nanos: 50% ( 1641) // 1: size: 50% ( 1654); secs: 48% ( 1563); nanos: 50% ( 1654) // 2: size: 51% ( 1663); secs: 49% ( 1625); nanos: 50% ( 1641) // 3: size: 50% ( 1640); secs: 55% ( 1810); nanos: 50% ( 1656) // 4: size: 50% ( 1632); secs: 45% ( 1466); nanos: 50% ( 1647) // 5: size: 48% ( 1577); secs: 55% ( 1812); nanos: 50% ( 1631) // 6: size: 53% ( 1730); secs: 62% ( 2039); nanos: 50% ( 1635) // 7: size: 51% ( 1671); secs: 63% ( 2056); nanos: 52% ( 1694) // 8: size: 49% ( 1618); secs: 38% ( 1246); nanos: 49% ( 1619) // 9: size: 49% ( 1594); secs: 63% ( 2060); nanos: 50% ( 1636) // 10: size: 47% ( 1536); secs: 63% ( 2055); nanos: 50% ( 1656) // 11: size: 53% ( 1741); secs: 63% ( 2068); nanos: 51% ( 1660) // 12: size: 39% ( 1274); secs: 37% ( 1218); nanos: 50% ( 1641) // 13: size: 28% ( 935); secs: 38% ( 1254); nanos: 50% ( 1649) // 14: size: 21% ( 701); secs: 37% ( 1225); nanos: 51% ( 1664) // 15: size: 12% ( 381); secs: 65% ( 2124); nanos: 50% ( 1630) // 16: size: 6% ( 210); secs: 60% ( 1972); nanos: 50% ( 1640) // 17: size: 3% ( 97); secs: 38% ( 1240); nanos: 50% ( 1649) // 18: size: 1% ( 33); secs: 35% ( 1144); nanos: 49% ( 1610) // 19: size: 0% ( 7); secs: 67% ( 2216); nanos: 50% ( 1638) // 20: size: 0% ( 3); secs: 47% ( 1534); nanos: 49% ( 1608) // 21: size: 0% ( 1); secs: 50% ( 1641); nanos: 50% ( 1656) // 22: size: 0% ( 0); secs: 73% ( 2405); nanos: 49% ( 1598) // 23: size: 0% ( 0); secs: 98% ( 3218); nanos: 50% ( 1634) // 24: size: 0% ( 0); secs: 1% ( 38); nanos: 50% ( 1628) // 25: size: 0% ( 0); secs: 0% ( 0); nanos: 51% ( 1659) // 26: size: 0% ( 0); secs: 100% ( 3284); nanos: 47% ( 1538) // 27: size: 0% ( 0); secs: 0% ( 0); nanos: 46% ( 1500) // 28: size: 0% ( 0); secs: 0% ( 0); nanos: 44% ( 1460) // 29: size: 0% ( 0); secs: 100% ( 3284); nanos: 47% ( 1540) // 30: size: 0% ( 0); secs: 100% ( 3284); nanos: 0% ( 0) // 31: size: 0% ( 0); secs: 0% ( 0); nanos: 0% ( 0) // ... // Maximum-likelihood estimate of entropy (max: 64 bits): // size: 15.73 bits // secs: 22.34 bits // nanos: 29.98 bits // max empirical entropy: 68.05 bits // ``` // // So for me, size has a bit less than 16 bits of entropy, // most of which appears to be concentrated in bit 0 through // bit 11. // // For mtime_secs, we expect less significant bits to have // more entropy than higher bits, and that matches what we // observe. The amount of entropy through bit 20 is pretty // good. That is followed by a steep drop in the amount of // entropy. After bit 25, there is no entropy. // // For nano seconds, we expect the bits needs to represent the // range of values to have nearly full entropy, and that is // also what we see, i.e., just under 30 bits of entropy. // // Empirically we observed 68 bits of entropy, which is close // to our intuition. // // Based on the above analysis, we mix the values by xoring // the parameters as follows: // // - mtime_secs as is, // - mtime_nsec left shifted by 34, and // - size left rotated by 22. // // Using this algorithm on my current certificate directory, I // observe the tags have nearly maximum entropy: 63.23 bits // out of a maximum of 64 bits of entropy: // // ``` // $ cargo run --release --example tag-entropy /tmp/test-certd // ... // Maximum-likelihood estimate of entropy (max: 64 bits): // tag: 63.23 bits // ``` // // In conclusion, it's unclear that using a cryptographic hash // function will add much. // // Note: as directory sizes are not constant across file // systems, and we want to support synchronization, we don't // consider the size parameter for directories. Tag(mtime_secs ^ ((mtime_nsec as u64) << 34) ^ if is_dir { 0 } else { size.rotate_left(22) }) } } impl std::convert::TryFrom<&std::fs::File> for Tag { type Error = std::io::Error; /// Compute a `Tag` from file metadata. fn try_from(fp: &std::fs::File) -> std::io::Result { Tag::try_from(fp.metadata()?) } } impl std::convert::TryFrom<&std::fs::Metadata> for Tag { type Error = std::io::Error; /// Compute a `Tag` from file metadata. fn try_from(m: &std::fs::Metadata) -> std::io::Result { let d = m .modified()? .duration_since(UNIX_EPOCH) .unwrap_or_else(|_| Duration::new(0, 0)); Ok(Tag::new(d.as_secs(), d.subsec_nanos(), m.len(), m.is_dir())) } } impl std::convert::TryFrom for Tag { type Error = std::io::Error; /// Compute a `Tag` from file metadata. fn try_from(m: std::fs::Metadata) -> std::io::Result { Tag::try_from(&m) } } impl From for Tag { fn from(t: u64) -> Self { Tag(t) } } impl From for u64 { fn from(t: Tag) -> Self { t.0 } } #[cfg(test)] mod tests { use super::*; #[test] fn special_names_is_sorted() { let mut sn = SPECIAL_NAMES.to_vec(); sn.sort_unstable(); assert_eq!(sn, SPECIAL_NAMES); } } openpgp-cert-d-0.3.1/src/macros.rs000064400000000000000000000075351046102023000151040ustar 00000000000000//! A collection of useful macros. /// Platform abstraction. /// /// Using this macro makes sure that missing support for new platform /// is a compile-time error. macro_rules! platform { { unix => $unix:expr, windows => $windows:expr $(,)? } => { if cfg!(unix) { #[cfg(unix)] { $unix } #[cfg(not(unix))] { unreachable!() } } else if cfg!(windows) { #[cfg(windows)] { $windows } #[cfg(not(windows))] { unreachable!() } } else { #[cfg(not(any(unix, windows)))] compile_error!("Unsupported platform"); unreachable!() } } } #[allow(unused_macros)] macro_rules! trace { ( $TRACE:expr, $fmt:expr, $($pargs:expr),* ) => { if $TRACE { eprintln!($fmt, $($pargs),*); } }; ( $TRACE:expr, $fmt:expr ) => { trace!($TRACE, $fmt, ); }; } // Converts an indentation level to whitespace. #[allow(unused)] pub(crate) fn indent(i: isize) -> &'static str { let s = " "; &s[0..std::cmp::min(usize::try_from(i).unwrap_or(0), s.len())] } #[allow(unused_macros)] macro_rules! tracer { ( $TRACE:expr, $func:expr ) => { tracer!($TRACE, $func, 0) }; ( $TRACE:expr, $func:expr, $indent:expr ) => { // Currently, Rust doesn't support $( ... ) in a nested // macro's definition. See: // https://users.rust-lang.org/t/nested-macros-issue/8348/2 #[allow(unused_macros)] macro_rules! t { ( $fmt:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, $fmt) }; ( $fmt:expr, $a:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a)) }; ( $fmt:expr, $a:expr, $b:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h, $i)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h, $i, $j)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr, $k:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k)) }; } } } openpgp-cert-d-0.3.1/src/pgp.rs000064400000000000000000000247611046102023000144060ustar 00000000000000use sha1collisiondetection::{Output, Sha1CD}; use std::{ borrow::Cow, convert::{TryFrom, TryInto}, }; pub(crate) const FINGERPRINT_LEN_CHARS_V4: usize = 40; pub(crate) const FINGERPRINT_LEN_CHARS_V6: usize = 64; // Bit 6 of the packet header's first octet denotes the packet format (see 4.2) const MASK_PACKET_FORMAT: u8 = 0b0100_0000; // The high bit (bit 7) of the packet header's first octet must be one. const MASK_HIGH_BIT: u8 = 0b1000_0000; // In the new packet format, bits 5-0 hold the packet tag. const MASK_TAG_NEW: u8 = 0b0011_1111; // In the old packet format, bits 5-2 of the packet header's first octet hold // the packet tag. const MASK_TAG_OLD: u8 = 0b0011_1100; // In the old packet format, after masking, shift to get the tag. const SHIFT_TAG_OLD: usize = 2; // In the old packet format, bits 1-0 of the packet header's first octet hold // the length type. const MASK_LENGTH_OLD: u8 = 0b0000_0011; // Old packet format's length types: const LENGTH_TYPE_OLD_ONE_OCTET: u8 = 0; const LENGTH_TYPE_OLD_TWO_OCTETS: u8 = 1; const LENGTH_TYPE_OLD_FOUR_OCTETS: u8 = 2; const LENGTH_TYPE_OLD_INDETERMINATE: u8 = 3; // Packet tag that denotes a Secret-Key packet (5.5.1.1) const PACKET_TAG_SECRET_KEY: u8 = 5; // Packet tag that denotes a Public-Key packet (5.5.1.1) const PACKET_TAG_PUBLIC_KEY: u8 = 6; // We only compute fingerprints for V4 Public-Key packets const KEY_PACKET_VERSION_FOUR: u8 = 4; const ARMOR_HEADER_PUBLIC_KEY: &[u8] = b"-----BEGIN PGP PUBLIC KEY BLOCK-----"; // Data from the packet header, only what is necessary for further processing. struct HeaderData { header_len: usize, body_len: u32, packet_tag: u8, } /// Compute the fingerprint of an OpenPGP TPK. /// This minimal implementation keeps close to the RFC. /// /// The TPK's fingerprint is the fingerprint of its public key packet, which /// must also be the first packet. /// Subkey fingerprints are not supported. // // We do not compute fingerprints of TSKs, it's not possible in the general // case without algorithm specific handling. // See https://gitlab.com/openpgp-wg/rfc4880bis/-/issues/43 pub(crate) fn fingerprint(bytes: &[u8]) -> Result { let header_data = parse_header(bytes)?; if header_data.packet_tag != PACKET_TAG_PUBLIC_KEY { return Err(Error::UnsupportedPacketForFingerprint(format!( "{}", header_data.packet_tag ))); } compute_fingerprint(bytes, header_data.header_len, header_data.body_len) } // Extract information from the packet header: header length, body length and // the packet tag. fn parse_header(bytes: &[u8]) -> Result { // Rough heuristic: The public key material needs to be at least 32 bytes // long, plus at least two bytes packet header. if bytes.len() < 32 + 2 { return Err(Error::NotEnoughData); }; // The high bit of the CTB must be one. If it is not, the data may be ascii, // so we can check if it is armored. if bytes[0] & MASK_HIGH_BIT == 0 { if bytes.starts_with(ARMOR_HEADER_PUBLIC_KEY) { return Err(Error::UnsupportedArmor); } else { return Err(Error::UnsupportedData); } } let is_new_ctb = bytes[0] & MASK_PACKET_FORMAT != 0; let header_data = if is_new_ctb { let packet_tag = bytes[0] & MASK_TAG_NEW; // interpret length encoding according to 4.2.2 let (header_len, body_len) = match bytes[1] { 0..=191 => (2, bytes[1] as u32), 192..=223 => ( 3, u16::from_be_bytes([bytes[1] - 192, bytes[2]]) as u32 + 192, ), 255 => ( 6, u32::from_be_bytes((&bytes[2..=5]).try_into().unwrap()), ), 224..=254 => { // do not handle partial length encoding return Err(Error::UnsupportedLengthEncoding); } }; HeaderData { header_len, body_len, packet_tag, } } else { let packet_tag = (bytes[0] & MASK_TAG_OLD) >> SHIFT_TAG_OLD; // interpret length encoding according to 4.2.1 let (header_len, body_len) = match bytes[0] & MASK_LENGTH_OLD { LENGTH_TYPE_OLD_ONE_OCTET => (2, bytes[1] as u32), LENGTH_TYPE_OLD_TWO_OCTETS => ( 3, u16::from_be_bytes((&bytes[1..=2]).try_into().unwrap()) as u32, ), LENGTH_TYPE_OLD_FOUR_OCTETS => { (5, u32::from_be_bytes((&bytes[1..=4]).try_into().unwrap())) } LENGTH_TYPE_OLD_INDETERMINATE => { // do not handle indeterminate length encoding return Err(Error::UnsupportedLengthEncoding); } _ => unreachable!(), }; HeaderData { header_len, body_len, packet_tag, } }; Ok(header_data) } // Computes the fingerprint of a Public-Key packet according to RFC 4880, 12.2. // Makes no effort to assert that the bytes really are a Public-Key packet. fn compute_fingerprint( bytes: &[u8], header_len: usize, body_len: u32, ) -> Result { let body = &bytes .get(header_len..(header_len + body_len as usize)) .ok_or(Error::NotEnoughData)?; // First byte is the packet version. let version = body[0]; if version != KEY_PACKET_VERSION_FOUR { return Err(Error::UnsupportedKeyVersion(version)); } let mut hasher = Sha1CD::default(); // RFC 4880, 12.2: // A V4 fingerprint is the 160-bit SHA-1 hash of the octet 0x99, hasher.update([0x99u8]); // followed by the two-octet packet length let length = ::try_from(body.len()) .map_err(|_| Error::PublicKeyPacketTooLong)?; hasher.update(length.to_be_bytes()); // followed by the entire Public-Key packet starting with the version field. hasher.update(body); let mut result = Output::default(); let _ = hasher.finalize_into_dirty_cd(&mut result); Ok(format_fingerprint(&result)) } fn format_fingerprint(bytes: &[u8]) -> String { use std::fmt::Write; bytes .iter() .fold( String::with_capacity(40), |mut s, b| { write!(&mut s, "{:02x}", b).unwrap(); s }) } // Canonicalizes a fingerprint. // // Note: the input may not contain spaces. pub(crate) fn canonicalize_fingerprint(fpr: &str) -> Result> { if (fpr.len() == FINGERPRINT_LEN_CHARS_V4 || fpr.len() == FINGERPRINT_LEN_CHARS_V6) && fpr.chars().all(|c| { c.is_ascii_hexdigit() }) { if fpr.chars().all(|c| c.is_ascii_lowercase()) { Ok(Cow::Borrowed(fpr)) } else { let mut fpr = fpr.to_string(); fpr.make_ascii_lowercase(); Ok(Cow::Owned(fpr)) } } else { Err(Error::InvalidFingerprint( format!("{} is not a valid fingerprint", fpr))) } } // Check if the given data may plausibly be a TSK or TPK, i.e. starts with a // Public-Key or Secret-Key packet. pub(crate) fn plausible_tsk_or_tpk(bytes: &[u8]) -> Result<()> { let header_data = parse_header(bytes)?; if header_data.header_len + header_data.body_len as usize > bytes.len() { return Err(Error::NotEnoughData); } if header_data.packet_tag == PACKET_TAG_PUBLIC_KEY || header_data.packet_tag == PACKET_TAG_SECRET_KEY { Ok(()) } else { Err(Error::UnsupportedPacket) } } /// Result specialization for this module. pub type Result = std::result::Result; #[derive(thiserror::Error, Debug)] pub enum Error { /// Expected to read more data. #[error("Not enough data")] NotEnoughData, /// Public key packet too long for fingerprint calculation. #[error("Public key packet too long")] PublicKeyPacketTooLong, /// Unhandled packet type for fingerprint. #[error("Unsupported packet type for fingerprint computation, found {0}")] UnsupportedPacketForFingerprint(String), /// Unhandled packet type for other uses. #[error("Unsupported packet type")] UnsupportedPacket, /// Unsupported length encoding. #[error("Unsupported length encoding")] UnsupportedLengthEncoding, /// Unsupported key version. #[error("Unsupported key version: {0}")] UnsupportedKeyVersion(u8), /// Not PGP data. #[error("Not a PGP packet")] UnsupportedData, /// Armored data. #[error("Armored data unsupported")] UnsupportedArmor, /// Invalid fingerprint. #[error("{0} is not a valid fingerprint")] InvalidFingerprint(String), /// Wrong certificate. #[error("Expected a certificate for {0}, found a certificate for {1}")] WrongCertificate(String, String), } #[cfg(test)] mod tests { use super::*; struct Testdata<'a> { data: &'a [u8], fingerprint: &'a str, } static ALICE: Testdata = Testdata { fingerprint: "eb85bb5fa33a75e15e944e63f231550c4f47e38e", data: include_bytes!("../../testdata/alice.pgp"), }; static SENDER_PUBLIC: Testdata = Testdata { fingerprint: "c9cecc00208658e6183da1c6ab27f5772e0e7843", data: include_bytes!("../../testdata/sender_public.pgp"), }; #[test] fn compute_fingerprint() { // old ctb assert_eq!(fingerprint(ALICE.data).unwrap(), ALICE.fingerprint); // new ctb assert_eq!( fingerprint(SENDER_PUBLIC.data).unwrap(), SENDER_PUBLIC.fingerprint ); } #[test] fn error_unsupported_armor() { let data = include_bytes!("../../testdata/alice.asc"); assert!(matches!( fingerprint(data).unwrap_err(), Error::UnsupportedArmor )); } #[test] fn error_not_enough_data() { let data = &[17u8; 17]; assert!(matches!( fingerprint(data).unwrap_err(), Error::NotEnoughData )); } #[test] fn error_unsupported_packet_for_fingerprint() { // Secret keys (packet tag 5) are unsupported let data = include_bytes!("../../testdata/sender.pgp"); assert!(matches!( fingerprint(data).unwrap_err(), Error::UnsupportedPacketForFingerprint(p) if p == "5".to_string() )); } #[test] fn error_unsupported_data() { let data = b"Herr von Ribbeck auf Ribbeck im Havelland, Ein Birnbaum in seinem Garten stand,"; assert!(matches!( fingerprint(data).unwrap_err(), Error::UnsupportedData )); } } openpgp-cert-d-0.3.1/src/unixdir.rs000064400000000000000000000210771046102023000152770ustar 00000000000000use std::cell::OnceCell; use std::ffi::CStr; use std::ffi::c_char; use std::os::unix::ffi::OsStringExt; use std::path::Path; use crate::Tag; type Result = std::result::Result; pub(crate) struct FileType(u8); impl FileType { /// Whether a file is a directory. /// /// According to glibc's documentation: /// /// Currently, only some filesystems (among them: Btrfs, ext2, /// ext3, and ext4) have full support for returning the file type /// in d_type. All applications must properly handle a re‐ turn /// of DT_UNKNOWN. pub fn is_dir(&self) -> bool { self.0 == libc::DT_DIR } /// Whether a file's type is not known. pub fn is_unknown(&self) -> bool { self.0 == libc::DT_UNKNOWN } } // Not all Unix platforms have 64-bit variants of stat64, etc. Rust's // libc doesn't define a nice way to figure out if those functions are // available. Eventually, we can use something like: // // #[cfg_accessible(libc::stat64)] // // (See https://github.com/rust-lang/rust/issues/64797), but that // hasn't been stabilized yet. // // For now, we copy Rust's libc's strategy: // // https://github.com/rust-lang/rust/blob/96df4943409564a187894018f15f795426dc2e1f/library/std/src/sys/unix/fs.rs#L67 #[cfg(any(all(target_os = "linux", not(target_env = "musl")), target_os = "hurd"))] mod libc64 { pub(super) use libc::stat64 as stat; pub(super) use libc::fstatat64 as fstatat; pub(super) use libc::dirent64 as dirent; pub(super) use libc::readdir64 as readdir; } #[cfg(not(any(all(target_os = "linux", not(target_env = "musl")), target_os = "hurd")))] mod libc64 { pub(super) use libc::stat; pub(super) use libc::fstatat; pub(super) use libc::dirent; pub(super) use libc::readdir; } /// A thin wrapper around a `libc::stat64`. pub(crate) struct Metadata(libc64::stat); impl Metadata { /// The size. pub fn size(&self) -> u64 { self.0.st_size as u64 } /// The modification time as the time since the Unix epoch. pub fn modified(&self) -> std::time::Duration { std::time::Duration::new( self.0.st_mtime as u64, self.0.st_mtime_nsec as u32) } /// Whether a file is a directory. pub fn is_dir(&self) -> bool { (self.0.st_mode & libc::S_IFMT) == libc::S_IFDIR } } impl std::convert::From<&crate::unixdir::Metadata> for Tag { fn from(m: &Metadata) -> Self { let d = m.modified(); let size = m.size(); Tag::new(d.as_secs(), d.subsec_nanos(), size, m.is_dir()) } } impl Metadata { fn fstat(dir: *mut libc::DIR, nul_terminated_filename: &[u8]) -> Result { // The last character must be a NUL, i.e., this has to be a c string. assert_eq!(nul_terminated_filename[nul_terminated_filename.len() - 1], 0); let dirfd = unsafe { libc::dirfd(dir) }; if dirfd == -1 { return Err(std::io::Error::last_os_error()); } let mut statbuf = std::mem::MaybeUninit::::uninit(); let result = unsafe { libc64::fstatat( dirfd, nul_terminated_filename.as_ptr() as *const c_char, statbuf.as_mut_ptr(), libc::AT_SYMLINK_NOFOLLOW, ) }; if result == -1 { return Err(std::io::Error::last_os_error()); } Ok(Metadata(unsafe { statbuf.assume_init() })) } } /// A thin wrapper for a `libc::dirent64`. /// /// [`libc::dirent64`](https://docs.rs/libc/latest/libc/struct.dirent64.html) pub(crate) struct DirEntry { dir: *mut libc::DIR, entry: *mut libc64::dirent, name_len: OnceCell, // We save the metadata inline to avoid a heap allocation. metadata: OnceCell>, } impl DirEntry { /// Returns the file's type, as recorded in the directory. pub fn file_type(&self) -> FileType { FileType(unsafe { *self.entry }.d_type) } /// Returns the filename. /// /// Note: this is not NUL terminated. pub fn file_name(&self) -> &[u8] { unsafe { let name = (*self.entry).d_name.as_ptr() as *const c_char; let name_len = *self.name_len.get_or_init(|| { // According to the Single Unix Specification: // // The character array d_name is of unspecified // size, but the number of bytes preceding the // terminating null byte will not exceed {NAME_MAX}. // // https://pubs.opengroup.org/onlinepubs/007908799/xsh/dirent.h.html // // All platforms that I check use 256 bytes. Don't // hard code that (but do sanity check it). let max_len = std::mem::size_of_val(&(*self.entry).d_name); assert!(max_len >= 128); libc::strnlen(name, max_len) }); std::slice::from_raw_parts( name as *const u8, name_len) } } /// Stats the file. /// /// To avoid a heap allocation, the data struct is stored inline. /// To avoid races, the lifetime is bound to self. pub fn metadata(&self) -> Result<&Metadata> { // Rewrite this to use OnceCell::get_or_try_init once that has // stabilized. Until then we do a little dance with the // Result. let result = self.metadata.get_or_init(|| { let dirfd = unsafe { libc::dirfd(self.dir) }; if dirfd == -1 { return Err(std::io::Error::last_os_error()); } let mut statbuf = std::mem::MaybeUninit::::uninit(); let result = unsafe { libc64::fstatat( dirfd, (*self.entry).d_name.as_ptr() as *const c_char, statbuf.as_mut_ptr(), libc::AT_SYMLINK_NOFOLLOW, ) }; if result == -1 { return Err(std::io::Error::last_os_error()); } Ok(Metadata(unsafe { statbuf.assume_init() })) }); match result { Ok(metadata) => Ok(metadata), Err(err) => { if let Some(underlying) = err.get_ref() { // We can't clone the error, so we clone the error // kind and turn the error into a string. It's // not great, but its good enough for us. Err(std::io::Error::new( err.kind(), underlying.to_string())) } else { Err(std::io::Error::from(err.kind())) } }, } } } pub(crate) struct Dir { dir: Option<*mut libc::DIR>, entry: Option, } impl Drop for Dir { fn drop(&mut self) { if let Some(dir) = self.dir.take() { unsafe { libc::closedir(dir) }; } } } impl Dir { pub fn open(dir: &Path) -> Result { let mut dir = dir.as_os_str().to_os_string().into_vec(); // NUL-terminate it. dir.push(0); let dir = unsafe { CStr::from_ptr(dir.as_ptr() as *const c_char) }; let dir = unsafe { libc::opendir(dir.as_ptr().cast()) }; if dir.is_null() { return Err(std::io::Error::last_os_error()); } let dir = Dir { dir: Some(dir), entry: None, }; Ok(dir) } /// Get the next directory entry. /// /// Returns None, if the end of directory has been reached. /// /// DirEntry is deallocated when the directory pointer is /// advanced. Hence, the lifetime of the returned DirEntry is /// tied to the lifetime of the &mut to self. pub fn readdir(&mut self) -> Option<&mut DirEntry> { let dir = self.dir?; let entry = unsafe { libc64::readdir(dir) }; if entry.is_null() { unsafe { libc::closedir(dir) }; self.dir = None; return None; } self.entry = Some(DirEntry { dir, entry, name_len: OnceCell::default(), metadata: OnceCell::default(), }); self.entry.as_mut() } /// Stat an entry in the directory. pub fn fstat(&mut self, nul_terminated_filename: &[u8]) -> Result { let dir = self.dir.ok_or_else(|| { std::io::Error::new(std::io::ErrorKind::Other, "Directory closed") })?; Metadata::fstat(dir, nul_terminated_filename) } }