sequoia-cert-store-0.7.1/.cargo/config.toml000064400000000000000000000002141046102023000166740ustar 00000000000000[target.'cfg(all())'] # Note: if the RUSTFLAGS environment variable is set, this will be # ignored. rustflags = [ # "-Dwarnings", ]sequoia-cert-store-0.7.1/.cargo_vcs_info.json0000644000000001360000000000100145740ustar { "git": { "sha1": "a5659d94ede742c5be18119f8f2a4e0e09cd68c4" }, "path_in_vcs": "" }sequoia-cert-store-0.7.1/.ci/all_commits.sh000075500000000000000000000022711046102023000167010ustar 00000000000000#!/usr/bin/env bash # Test all commits on this branch but the last one. # # Used in the all_commits ci job to ensure all commits build # and tests pass at least for the sequoia-openpgp crate. # NOTE: under gitlab's Settings, "CI/CD", General Pipelines ensure # that the "git shallow clone" setting is set to 0. Otherwise other # branch are not fetched. set -e set -x # Use dummy identity to make git rebase happy. git config user.name "C.I. McTestface" git config user.email "ci.mctestface@example.com" # Make sure the gitlab project is configured. if ! git describe --all origin/main then echo "origin/main is not present. Configure the gitlab project (see .ci/all_commits.sh)." exit 1 fi # If the previous commit already is on main we're done. git merge-base --is-ancestor HEAD~ origin/main && echo "All commits tested already" && exit 0 # Leave out the last commit - it has already been checked. git checkout HEAD~ git status git rebase origin/main \ --exec 'echo ===; echo ===; echo ===; git log -n 1;' \ --exec 'cargo test --all' && echo "All commits passed tests" && exit 0 # The rebase failed - probably because a test failed. git rebase --abort; exit 1 sequoia-cert-store-0.7.1/.codespellrc000064400000000000000000000004111046102023000156600ustar 00000000000000[codespell] skip = Cargo.lock,*.bin,*.gpg,*.pgp,./.git,data,highlight.js,*/target,Makefile,*.html,*/cargo,*.xml,*.xmlv2, ignore-words-list = crate,ede,iff,mut,nd,te,uint,KeyServer,keyserver,Keyserver,keyservers,Keyservers,keypair,keypairs,KeyPair,fpr,dedup,ba,ned, sequoia-cert-store-0.7.1/.gitignore000064400000000000000000000000141046102023000153470ustar 00000000000000/target/ *~ sequoia-cert-store-0.7.1/.gitlab-ci.yml000064400000000000000000000002441046102023000160200ustar 00000000000000stages: - pre-check - build - test include: - component: "gitlab.com/sequoia-pgp/common-ci/sequoia-pipeline@main" variables: NO_DEFAULT_FEATURES: false sequoia-cert-store-0.7.1/CONTRIBUTING.md000064400000000000000000000033071046102023000156200ustar 00000000000000Sequoia PGP is owned by the [p≡p foundation] and licensed under the terms of the LGPLv2+. [p≡p foundation]: https://pep.foundation/ To finance its mission, privacy by default, the [p≡p foundation] allows third parties (currently only [p≡p security]) to relicense its software. Consistent with the rules of a foundation, the money collected by the foundation in this manner is fully reinvested in the foundation's mission, which includes further development of Sequoia PGP. [p≡p security]: https://www.pep.security/ To do this, the [p≡p foundation] needs permission from all contributors to relicense their changes. In return, the [p≡p foundation] guarantees that *all* releases of Sequoia PGP (and any other software it owns) will also be released under a GNU-approved license. That is, even if Foo Corp is granted a license to use Sequoia PGP in a proprietary product, the exact code that Foo Corp uses will also be licensed under a GNU-approved license. If you want to contribute to Sequoia PGP, and you agree to the above, please sign the [p≡p foundation]'s [CLA]. This is an electronic assignment; no paper work is required. You'll need to provide a valid email address. After clicking on a link to verify your email address, you'll receive a second email, which contains the contract between you and the [p≡p foundation]. Be sure to keep it for future reference. The maintainers of Sequoia PGP will also receive a notification. At that point, we can merge patches from you into Sequoia PGP. [CLA]: https://contribution.pep.foundation/contribute/ Please direct questions regarding the CLA to [contribution@pep.foundation]. [contribution@pep.foundation]: mailto:contribution@pep.foundation sequoia-cert-store-0.7.1/Cargo.lock0000644000002453610000000000100125620ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aead" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", "generic-array", ] [[package]] name = "aes" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", "cpufeatures", ] [[package]] name = "aes-gcm" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead", "aes", "cipher", "ctr", "ghash", "subtle", ] [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "argon2" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", "cpufeatures", "password-hash", ] [[package]] name = "ascii-canvas" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" dependencies = [ "term", ] [[package]] name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bindgen" version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ "bitflags", "cexpr", "clang-sys", "itertools 0.13.0", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "syn", ] [[package]] name = "bit-set" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "blake2" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ "digest", ] [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "block-padding" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ "generic-array", ] [[package]] name = "buffered-reader" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db26bf1f092fd5e05b5ab3be2f290915aeb6f3f20c4e9f86ce0f07f336c2412f" dependencies = [ "libc", ] [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" version = "1.2.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-link 0.2.1", ] [[package]] name = "cipher" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", "zeroize", ] [[package]] name = "clang-sys" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", "libloading", ] [[package]] name = "cmac" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" dependencies = [ "cipher", "dbl", "digest", ] [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crossbeam" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", "crossbeam-queue", "crossbeam-utils", ] [[package]] name = "crossbeam-channel" version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "rand_core", "typenum", ] [[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ "cipher", ] [[package]] name = "curve25519-dalek" version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", "digest", "fiat-crypto", "rustc_version", "subtle", "zeroize", ] [[package]] name = "curve25519-dalek-derive" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "data-encoding" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "dbl" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" dependencies = [ "generic-array", ] [[package]] name = "der" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "zeroize", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "const-oid", "crypto-common", "subtle", ] [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-next" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ "cfg-if", "dirs-sys-next", ] [[package]] name = "dirs-sys" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users 0.5.2", "windows-sys 0.60.2", ] [[package]] name = "dirs-sys-next" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users 0.4.6", "winapi", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "doc-comment" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" [[package]] name = "dyn-clone" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "eax" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28" dependencies = [ "aead", "cipher", "cmac", "ctr", "subtle", ] [[package]] name = "ed25519" version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", "signature", ] [[package]] name = "ed25519-dalek" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", "rand_core", "serde", "sha2", "subtle", "zeroize", ] [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "ena" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" dependencies = [ "log", ] [[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "endian-type" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" [[package]] name = "enum-as-inner" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "fallible-iterator" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "fallible-streaming-iterator" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fd-lock" version = "4.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix", "windows-sys 0.52.0", ] [[package]] name = "fiat-crypto" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-macro", "futures-task", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", ] [[package]] name = "gethostname" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ "rustix", "windows-link 0.2.1", ] [[package]] name = "getrandom" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", ] [[package]] name = "ghash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", "polyval", ] [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "h2" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", "http", "indexmap", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", ] [[package]] name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ "hashbrown 0.15.5", ] [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hickory-client" version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "156579a5cd8d1fc6f0df87cc21b6ee870db978a163a1ba484acd98a4eff5a6de" dependencies = [ "cfg-if", "data-encoding", "futures-channel", "futures-util", "hickory-proto", "once_cell", "radix_trie", "rand", "thiserror 1.0.69", "tokio", "tracing", ] [[package]] name = "hickory-proto" version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" dependencies = [ "async-trait", "cfg-if", "data-encoding", "enum-as-inner", "futures-channel", "futures-io", "futures-util", "idna", "ipnet", "once_cell", "openssl", "rand", "thiserror 1.0.69", "tinyvec", "tokio", "tracing", "url", ] [[package]] name = "hickory-resolver" version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" dependencies = [ "cfg-if", "futures-util", "hickory-proto", "ipconfig", "lru-cache", "once_cell", "parking_lot", "rand", "resolv-conf", "smallvec", "thiserror 1.0.69", "tokio", "tracing", ] [[package]] name = "hkdf" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac", ] [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", ] [[package]] name = "http-body-util" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", "http", "http-body", "pin-project-lite", ] [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", "pin-utils", "smallvec", "tokio", "want", ] [[package]] name = "hyper-rustls" version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http", "hyper", "hyper-util", "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", ] [[package]] name = "hyper-tls" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", "hyper", "hyper-util", "native-tls", "tokio", "tokio-native-tls", "tower-service", ] [[package]] name = "hyper-util" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64", "bytes", "futures-channel", "futures-core", "futures-util", "http", "http-body", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", "socket2 0.6.1", "system-configuration", "tokio", "tower-service", "tracing", "windows-registry", ] [[package]] name = "iana-time-zone" version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "icu_collections" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_normalizer" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", "writeable", "yoke", "zerofrom", "zerotrie", "zerovec", ] [[package]] name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "indexmap" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", ] [[package]] name = "inout" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", "generic-array", ] [[package]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ "socket2 0.5.10", "widestring", "windows-sys 0.48.0", "winreg", ] [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", ] [[package]] name = "itertools" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] [[package]] name = "itertools" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "lalrpop" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" dependencies = [ "ascii-canvas", "bit-set", "ena", "itertools 0.11.0", "lalrpop-util", "petgraph", "regex", "regex-syntax", "string_cache", "term", "tiny-keccak", "unicode-xid", "walkdir", ] [[package]] name = "lalrpop-util" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ "regex-automata", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ "spin", ] [[package]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", "windows-link 0.2.1", ] [[package]] name = "libm" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags", "libc", ] [[package]] name = "libsqlite3-sys" version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" dependencies = [ "cc", "pkg-config", "vcpkg", ] [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru-cache" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" dependencies = [ "linked-hash-map", ] [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memsec" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492" [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "wasi", "windows-sys 0.61.2", ] [[package]] name = "native-tls" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "nettle" version = "7.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44e6ff4a94e5d34a1fd5abbd39418074646e2fa51b257198701330f22fcd6936" dependencies = [ "getrandom 0.2.16", "libc", "nettle-sys", "thiserror 1.0.69", "typenum", ] [[package]] name = "nettle-sys" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61a3f5406064d310d59b1a219d3c5c9a49caf4047b6496032e3f930876488c34" dependencies = [ "bindgen", "cc", "libc", "pkg-config", "tempfile", "vcpkg", ] [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nibble_vec" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" dependencies = [ "smallvec", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "num-bigint-dig" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82c79c15c05d4bf82b6f5ef163104cc81a760d8e874d38ac50ab67c8877b647b" dependencies = [ "lazy_static", "libm", "num-integer", "num-iter", "num-traits", "smallvec", ] [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] [[package]] name = "num-iter" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "ocb3" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c196e0276c471c843dd5777e7543a36a298a4be942a2a688d8111cd43390dedb" dependencies = [ "aead", "cipher", "ctr", "subtle", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "opaque-debug" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openpgp-cert-d" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd47b0b6df1022ca8a9a06791261c3153028abef191fe53aa326b7f443f2d6" dependencies = [ "anyhow", "dirs", "fd-lock", "libc", "sha1collisiondetection", "sha2", "tempfile", "thiserror 2.0.17", "walkdir", ] [[package]] name = "openssl" version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-link 0.2.1", ] [[package]] name = "password-hash" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", "rand_core", "subtle", ] [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", "indexmap", ] [[package]] name = "phf_shared" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", "spki", ] [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "polyval" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", "cpufeatures", "opaque-debug", "universal-hash", ] [[package]] name = "potential_utf" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "precomputed-hash" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radix_trie" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" dependencies = [ "endian-type", "nibble_vec", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.16", ] [[package]] name = "rayon" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "redox_users" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.16", "libredox", "thiserror 1.0.69", ] [[package]] name = "redox_users" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", "thiserror 2.0.17", ] [[package]] name = "regex" version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64", "bytes", "encoding_rs", "futures-core", "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", "hyper-tls", "hyper-util", "js-sys", "log", "mime", "native-tls", "percent-encoding", "pin-project-lite", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", "tokio-native-tls", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", ] [[package]] name = "resolv-conf" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" [[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rusqlite" version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" dependencies = [ "bitflags", "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", "smallvec", ] [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rustls" version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-pki-types" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error", "tempfile", "wait-timeout", ] [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[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 = "schannel" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "sequoia-cert-store" version = "0.7.1" dependencies = [ "anyhow", "crossbeam", "dirs", "gethostname", "num_cpus", "openpgp-cert-d", "rand", "rayon", "rusqlite", "rusty-fork", "sequoia-net", "sequoia-openpgp", "smallvec", "tempfile", "thiserror 1.0.69", "tokio", "url", ] [[package]] name = "sequoia-net" version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "956ef5d37e41f53259cd3c6caac5f135351ee92f76f3ac6ee9cf771ee6e33925" dependencies = [ "anyhow", "base64", "futures-util", "hickory-client", "hickory-resolver", "http", "hyper", "hyper-tls", "libc", "percent-encoding", "reqwest", "sequoia-openpgp", "thiserror 1.0.69", "tokio", "url", "z-base-32", ] [[package]] name = "sequoia-openpgp" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0e334ce3ec5b9b47d86a80563b3ecec435f59acf37e86058b3b686a42c5a2ba" dependencies = [ "aes-gcm", "anyhow", "argon2", "base64", "buffered-reader", "chrono", "cipher", "dyn-clone", "eax", "ed25519", "ed25519-dalek", "getrandom 0.2.16", "hkdf", "idna", "lalrpop", "lalrpop-util", "libc", "memsec", "nettle", "num-bigint-dig", "ocb3", "rand_core", "regex", "regex-syntax", "sha1collisiondetection", "sha2", "thiserror 2.0.17", "win-crypto-ng", "winapi", "xxhash-rust", ] [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", "serde_core", ] [[package]] name = "serde_urlencoded" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", "itoa", "ryu", "serde", ] [[package]] name = "sha1collisiondetection" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f606421e4a6012877e893c399822a4ed4b089164c5969424e1b9d1e66e6964b" dependencies = [ "digest", "generic-array", ] [[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signature" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "rand_core", ] [[package]] name = "siphasher" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "socket2" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", "windows-sys 0.60.2", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "spki" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", ] [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "string_cache" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", "parking_lot", "phf_shared", "precomputed-hash", ] [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] [[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "system-configuration" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "tempfile" version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.52.0", ] [[package]] name = "term" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" dependencies = [ "dirs-next", "rustversion", "winapi", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl 2.0.17", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thiserror-impl" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tiny-keccak" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" dependencies = [ "crunchy", ] [[package]] name = "tinystr" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tinyvec" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", "socket2 0.6.1", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", ] [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", ] [[package]] name = "tokio-util" version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "tower" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", "sync_wrapper", "tokio", "tower-layer", "tower-service", ] [[package]] name = "tower-http" version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", ] [[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "universal-hash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common", "subtle", ] [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", ] [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wait-timeout" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "try-lock", ] [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "widestring" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "win-crypto-ng" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99abfb435a71e54ab2971d8d8c32f1a7e006cdbf527f71743b1d45b93517bb92" dependencies = [ "cipher", "doc-comment", "rand_core", "winapi", "zeroize", ] [[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.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys 0.48.0", ] [[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-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", ] [[package]] name = "windows-implement" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-link" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", ] [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link 0.1.3", ] [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link 0.2.1", ] [[package]] name = "windows-strings" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link 0.1.3", ] [[package]] name = "windows-strings" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link 0.2.1", ] [[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.6", ] [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets 0.53.5", ] [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link 0.2.1", ] [[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.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", "windows_i686_gnullvm 0.53.1", "windows_i686_msvc 0.53.1", "windows_x86_64_gnu 0.53.1", "windows_x86_64_gnullvm 0.53.1", "windows_x86_64_msvc 0.53.1", ] [[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.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[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.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[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.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[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.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[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.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[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.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[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.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winreg" version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", "windows-sys 0.48.0", ] [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xxhash-rust" version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "yoke" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "z-base-32" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bf7b4a78668416e1e8a332334e26fb2f377afe707f0c6feaf6ed5f9100133b" [[package]] name = "zerocopy" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn", "synstructure", ] [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", "zerofrom", ] [[package]] name = "zerovec" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", "syn", ] sequoia-cert-store-0.7.1/Cargo.toml0000644000000056610000000000100126020ustar # 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.79" name = "sequoia-cert-store" version = "0.7.1" authors = [ "Neal H. Walfield ", "Justus Winter ", ] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "A certificate database interface." homepage = "https://sequoia-pgp.org/" readme = "README.md" keywords = [ "cryptography", "openpgp", "keyring", "database", ] categories = [ "cryptography", "authentication", "email", "command-line-utilities", ] license = "LGPL-2.0-or-later" repository = "https://gitlab.com/sequoia-pgp/sequoia-cert-store" [package.metadata.docs.rs] features = ["sequoia-openpgp/default"] [badges.maintenance] status = "actively-developed" [features] default = ["keyserver"] keyserver = [ "dep:sequoia-net", "dep:tokio", ] [lib] name = "sequoia_cert_store" path = "src/lib.rs" [[test]] name = "keyring" path = "tests/keyring.rs" [dependencies.anyhow] version = "1.0.18" [dependencies.crossbeam] version = "0.8.1" [dependencies.dirs] version = ">=5, <7" [dependencies.gethostname] version = ">=0.4, <2" [dependencies.num_cpus] version = "1" [dependencies.openpgp-cert-d] version = "0.3.4" default-features = false [dependencies.rayon] version = "1" [dependencies.rusqlite] version = ">=0.29, <0.38" features = [ "collation", "blob", "trace", ] [dependencies.sequoia-net] version = "0.30" optional = true default-features = false [dependencies.sequoia-openpgp] version = "2" default-features = false [dependencies.smallvec] version = "1.1" [dependencies.thiserror] version = ">=1, <3" [dependencies.tokio] version = "1.13" features = ["rt"] optional = true [dependencies.url] version = "2.5" [dev-dependencies.rand] version = ">=0.8, <0.10" [dev-dependencies.rusqlite] version = ">=0.29, <0.38" features = [ "collation", "blob", "trace", "modern_sqlite", ] [dev-dependencies.rusty-fork] version = "0.3.0" [dev-dependencies.tempfile] version = "3" [target."cfg(not(windows))".dev-dependencies.sequoia-openpgp] version = "2" features = [ "crypto-nettle", "__implicit-crypto-backend-for-tests", ] default-features = false [target."cfg(windows)".dependencies.rusqlite] version = ">=0.29, <0.38" features = ["bundled"] [target."cfg(windows)".dev-dependencies.sequoia-openpgp] version = "2" features = [ "crypto-cng", "__implicit-crypto-backend-for-tests", ] default-features = false sequoia-cert-store-0.7.1/Cargo.toml.orig000064400000000000000000000042561046102023000162620ustar 00000000000000[package] name = "sequoia-cert-store" description = "A certificate database interface." version = "0.7.1" authors = ["Neal H. Walfield ", "Justus Winter "] homepage = "https://sequoia-pgp.org/" repository = "https://gitlab.com/sequoia-pgp/sequoia-cert-store" readme = "README.md" keywords = ["cryptography", "openpgp", "keyring", "database"] categories = ["cryptography", "authentication", "email", "command-line-utilities"] license = "LGPL-2.0-or-later" edition = "2021" rust-version = "1.79" [badges] maintenance = { status = "actively-developed" } [dependencies] anyhow = "1.0.18" crossbeam = "0.8.1" dirs = { version = ">=5, <7" } gethostname = { version = ">=0.4, <2" } num_cpus = "1" openpgp-cert-d = { version = "0.3.4", default-features = false } rayon = "1" sequoia-openpgp = { version = "2", default-features = false } sequoia-net = { version = "0.30", default-features = false, optional = true } smallvec = "1.1" thiserror = { version = ">=1, <3" } tokio = { version = "1.13", features = [ "rt" ], optional = true } url = "2.5" [dependencies.rusqlite] version = ">=0.29, <0.38" features = ["collation", "blob", "trace"] # Use the bundled sqlite on Windows. [target.'cfg(windows)'.dependencies] rusqlite = { version = ">=0.29, <0.38", features = ["bundled"] } [dev-dependencies] rand = ">=0.8, <0.10" rusty-fork = "0.3.0" tempfile = "3" [dev-dependencies.rusqlite] version = ">=0.29, <0.38" features = ["collation", "blob", "trace", "modern_sqlite"] [lib] name = "sequoia_cert_store" path = "src/lib.rs" [features] default = ["keyserver"] keyserver = ["dep:sequoia-net", "dep:tokio"] [target.'cfg(not(windows))'.dev-dependencies] # Enables a crypto backend for the tests: sequoia-openpgp = { version = "2", default-features = false, features = ["crypto-nettle", "__implicit-crypto-backend-for-tests"] } [target.'cfg(windows)'.dev-dependencies] # Enables a crypto backend for the tests: sequoia-openpgp = { version = "2", default-features = false, features = ["crypto-cng", "__implicit-crypto-backend-for-tests"] } [package.metadata.docs.rs] # Enables a crypto backend for the docs.rs generation: features = ["sequoia-openpgp/default"] sequoia-cert-store-0.7.1/LICENSE.txt000064400000000000000000000627341046102023000152230ustar 00000000000000Sequoia PGP is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Sequoia PGP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. --- GNU LIBRARY GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the library GPL. It is numbered 2 because it goes with version 2 of the ordinary GPL.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Library General Public License, applies to some specially designated Free Software Foundation software, and to any other libraries whose authors decide to use it. You can use it for your libraries, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library, or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link a program with the library, you must provide complete object files to the recipients so that they can relink them with the library, after making changes to the library and recompiling it. And you must show them these terms so they know their rights. Our method of protecting your rights has two steps: (1) copyright the library, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the library. Also, for each distributor's protection, we want to make certain that everyone understands that there is no warranty for this free library. If the library is modified by someone else and passed on, we want its recipients to know that what they have is not the original version, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that companies distributing free software will individually obtain patent licenses, thus in effect transforming the program into proprietary software. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License, which was designed for utility programs. This license, the GNU Library General Public License, applies to certain designated libraries. This license is quite different from the ordinary one; be sure to read it in full, and don't assume that anything in it is the same as in the ordinary license. The reason we have a separate public license for some libraries is that they blur the distinction we usually make between modifying or adding to a program and simply using it. Linking a program with a library, without changing the library, is in some sense simply using the library, and is analogous to running a utility program or application program. However, in a textual and legal sense, the linked executable is a combined work, a derivative of the original library, and the ordinary General Public License treats it as such. Because of this blurred distinction, using the ordinary General Public License for libraries did not effectively promote software sharing, because most developers did not use the libraries. We concluded that weaker conditions might promote sharing better. However, unrestricted linking of non-free programs would deprive the users of those programs of all benefit from the free status of the libraries themselves. This Library General Public License is intended to permit developers of non-free programs to use free libraries, while preserving your freedom as a user of such programs to change the free libraries that are incorporated in them. (We have not seen how to achieve this as regards changes in header files, but we have achieved it as regards changes in the actual functions of the Library.) The hope is that this will lead to faster development of free libraries. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, while the latter only works together with the library. Note that it is possible for a library to be covered by the ordinary General Public License rather than by this special one. GNU LIBRARY GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Library General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also compile or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. c) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. d) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Library General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! sequoia-cert-store-0.7.1/README.md000064400000000000000000000047601046102023000146520ustar 00000000000000An OpenPGP certificate store abstraction and implementation. This crates provides a unified, high-level API for different certificate stores via the `Store` and `StoreUpdate` traits. It also provides a number of helper functions and data structures, like `UserIDIndex` to help implement this functionality. Finally, the `CertStore` data structure combines multiple certificate backends in a transparent way to users. This crate supports multiple backends: `CertD` uses an [OpenPGP Certificate Directory]. `Certs` manages a bunch of certificates in-memory. It can be loaded with certificates from a keyring, a keybox, a database, etc. It can also be used as the basis for a new backend, which actually writes changes back to the underlying store. `Pep` provides access to a [pEp] certificate store. Finally, there is a key server backend, which can fetch certificates via HKPS and WKD. [OpenPGP Certificate Directory]: https://crates.io/crates/openpgp-cert-d [pEp]: https://gitea.pep.foundation/pEp.foundation/pEpEngine ## Usage To use `sequoia-cert-store` from your project, you should add the following to your crate's `Cargo.toml`: ```toml [dependencies] sequoia-cert-store = "0.7" sequoia-openpgp = { version = "2", default-features = false } ``` To compile your crate you would then run: ``` $ cargo build --release --features sequoia-openpgp/crypto-default $ cargo test --features sequoia-openpgp/crypto-default $ cargo doc --no-deps --features sequoia-openpgp/crypto-default ``` If you do not disable the use of `sequoia-openpgp`'s default features, then `sequoia-openpgp` will select the default cryptographic backend, and your users won't be able to easily compile your crate with a different cryptographic backend. `sequoia-openpgp` currently uses Nettle as its default cryptographic backend. `sequoia-openpgp` also supports OpenSSL (`sequoia-openpgp/crypto-openssl`), Botan (`sequoia-openpgp/crypto-botan`), Windows CNG (`sequoia-openpgp/crypto-cng`), Rust Crypto (`sequoia-openpgp/crypto-rust`). For more information about building `sequoia-openpgp`, please refer to [`sequoia-openpgp`'s README]. This also includes information about the different backends' [build requirements]. [`sequoia-openpgp`'s README]: https://gitlab.com/sequoia-pgp/sequoia#features [build requirements]: https://gitlab.com/sequoia-pgp/sequoia#building-sequoia # License sequoia-cert-store is distributed under the terms of LGPL 2.0 or later. See [LICENSE.txt](LICENSE.txt) and [CONTRIBUTING.md](CONTRIBUTING.md) for details. sequoia-cert-store-0.7.1/deny.toml000064400000000000000000000045361046102023000152300ustar 00000000000000# This file is maintained in https://gitlab.com/sequoia-pgp/common-ci. # You can fetch it as follows: # # $ wget 'https://gitlab.com/sequoia-pgp/common-ci/-/raw/main/deny.toml?ref_type=heads&inline=false' -O deny.toml # # You should add that file as is to your project. # # You should also consider adding the Makefile # # $ wget 'https://gitlab.com/sequoia-pgp/common-ci/-/raw/main/Makefile?ref_type=heads&inline=false' -O Makefile # # Which makes it easy to keep that file up to date by doing: # # $ make [advisories] ignore = [ # These are due to sequoia-tpm's dependency on structopt. # sequoia-keystore crate actually use those. So we're fine. Remove # these once sequoia-tpm no longer users structopt. "RUSTSEC-2021-0139", "RUSTSEC-2021-0145", # Unfixable (as of rsa 0.9.6) marvin attack. "RUSTSEC-2023-0071", "RUSTSEC-2020-0159", "RUSTSEC-2020-0071", # chrono not affected by time 0.1 issue # fehler is unmaintained. # # fehler is used by subplot and thus an indirect dependency. Remove # when a new version subplot is released without fehler. See # https://gitlab.com/subplot/subplot/-/issues/340. "RUSTSEC-2023-0067", # yaml-rust is unmaintained. # # yaml-rust is used by subplot/roadmap/serde_yaml thus an indirect # dependency. Remove when a new version of roadmap is released that # uses a newer version of serde_yaml. See # https://gitlab.com/larswirzenius/roadmap/-/issues/13 "RUSTSEC-2024-0320", # instant is unmaintained. # # instant is used by indicatif and thus an indirect dependency. # Remove when a new version of indicatif is released that drops the # dependency. "RUSTSEC-2024-0384", # paste is unmaintained as of 2025-03-07 "RUSTSEC-2024-0436", # humantime is unmaintained. "RUSTSEC-2025-0014", ] yanked = "deny" [bans] multiple-versions = "allow" deny = [ # does not have responsible disclosure policy: # https://github.com/briansmith/ring#bug-reporting {name = "ring"}, ] [licenses] allow = [ "Apache-2.0", "Apache-2.0 WITH LLVM-exception", "BSD-3-Clause", "BSD-2-Clause", "BSL-1.0", "CC0-1.0", "CC-BY-4.0", "GPL-2.0", "GPL-3.0", "ISC", "LGPL-2.0", "LGPL-2.0-or-later", "LGPL-3.0", # For Nettle. See https://gitlab.com/sequoia-pgp/nettle-rs/-/issues/43 . "LGPL-3.0-only", "MIT", "MIT-0", "MPL-2.0", "Unicode-DFS-2016", "Unicode-3.0", "Zlib", ] sequoia-cert-store-0.7.1/openpgp-policy.toml000064400000000000000000003606611046102023000172420ustar 00000000000000version = 0 commit_goodlist = [] [authorization.dvn] sign_commit = true keyring = """ -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: 0CC0 11A7 C3DC 6EB4 7567 9BC9 AC0A BA31 866A 7E76 Comment: Devan Carpenter Comment: dvn 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 /g== =nP6D -----END PGP PUBLIC KEY BLOCK----- """ [authorization.justus] sign_commit = true keyring = """ -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: D2F2 C5D4 5BE9 FDE6 A4EE 0AAF 3185 5247 6038 31FD Comment: Justus Winter (Code Signing Key) Comment: Justus Winter Comment: Justus Winter Comment: Justus Winter Comment: Justus Winter Comment: Justus Winter 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 GNLnNS2c13ota/HZo1jjyOOIhJ3wkyUY5s8ugtxIwO4gE34rgdxXe1w7w2dy3DKq zWOHKs7wca8m6ufk75MQGk7TPL6nXZM+1F6OBo+moEaW/X8hn4meXikLE/6p4PxG wclL3/+FbwlJpMjPunedyDa+CVL5yCf7vww8mW52YiIWHNvmSJ5szWgc276uMk0T 1+ckzQ1KdXN0dXMgV2ludGVywsHhBBMBCgCZBYJofiMXBYkQ8B+PAwsJBwkQm33U M/JUkEpHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jndceh oMOXCdcGEK5NieaTCfHICdd30Ek4k7Ypxe002Z0FFQoOCAwCFgACF4ACGwECHgkW IQTLzY8DBYhlPu3X4mWbfdQz8lSQSg0nCQIJAQkDBwIHAQcDAABY9g/gnj/02oHg NDdYg7P3fWZAkABu8Gj0uH68dpFCIL8wCc5AL9l6WVSBmdxFmoNMD7TsKfiBRVg1 9IUWitxb0bcmm/OzzkvGgk1SG8gcJE46oA4sumfNh/7CnHUAvKL4ALIPMMXUDkz3 KhmUaqnZdzpQYsGRvujgOaKCia0xVNCSh2t1wGPPyIP0QLCrC6Ntcm/HD0tEQlU7 0GxLMNAzsD6ag4iqZI8X1/qilbbtiIYdoiT+bU9CWuv/fBx/nK43O4+t/G168vfh mseTAhI/q8ObbrQJB9aJJW7gbF2Qs2wC6yrtb8BYaKpfSzNXAlKH+YQbFdouq8m1 NPNoUbU27dMEG9KPeQXAiv74N4upkNpUju1IieJ80fat4QWx3pstlbCU2dfS7/8n FqbW74QFjy6eR/Mt81exw1+rxW4CMRVLnGeTxvMYWVJvj34F/dHOjGk/CNkeMvDK T+PggTHT1O/54wEgdURUKD9L0VOaBlVFIawmqfmKdXd3qks2mtakPv29vR6CnKPQ M59DSU3hE7XjWXndgjjT70d2GYcDEybBB37sy6h4dh0kc8+BWTwJztMDn2ffhUD7 XwKlTpFBhHwPcS3q+SbDApKxrOhYoSmdzez92lwG7748Cu/WZIB2u65kvzm+l4ov vuUuGcyOf7rSalLjGz+J4n7IywWz/cLB4QQTAQoAmQWCZ9gwBgWJDwBWzQMLCQcJ EJt91DPyVJBKRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9y 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 tHKhU4JgfYfj3/ZkxE7B8rTAgDZ/l7ax+ppDmWU17OqhIVx7RFBxLEEg90569zT4 cvYROmoHhr7CweEEEwEKAJkFgmfYMAoFiQ8AVs0DCwkHCRCbfdQz8lSQSkcUAAAA AAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdVuzGVhHnLLB+JWAgd MQStKDJtRcLwGgcPL1i8xDel+QUVCg4IDAIWAAIXgAIbAQIeCRYhBMvNjwMFiGU+ 7dfiZZt91DPyVJBKDScJAgkBCQMHAgcBBwMAANgKD90amXbCcheJIIlYxu6w+CI8 u+fBK5wzvAsBus+lPiiHIMwwcOiWM5cSY1zHWmrHT7UvIJ64TEJbyyIYtvy5/LPV O0b+m95MeVHWTEyqY0TylIqyjKBJeQdUmZiJERWXOCOhqOCxT+VSZ15k3KYP/jIH byldOvjpsWRcTwaJq2lvTPBUbJ9hzPNkuTdIFrmqPMJypgfBm+HwsIbH1XAEUKEq GpofsAh5NSGDZVWOT8ZHtpHiA8fAN1iTsM8hjd6EQ9UzR2BuRv964e+5cVFLmSnU kCHxUM1vyjciFq6pv6Zxhzdb8DEBvcABBwtyihFvKdNd8sKUyjKvB7Fo3Qaz/8xC dEJSHdqCJGJCO1DWHP3L+L59D4g2MKu3YAlOQDx2izEafJfRVQhwYLvlswzaeV93 aaM5gNEHRIayHaOOOR2xcZR27Vc/iL7z6vHUSw8GTUcQjm9uA5VWegR1dHIDC4ok is43nNBinP1lp1o4jNe5cQSpOalRrgprr8ow7YuEe07MmqvC1q6udYGNf3YZVgfW KiBWSU2N0JEVCCjD4Ae2+0d6qph6BWwjbKQtTQ8Rv5OMFJtlKKHVMXLBYfkI2gMy YPgTT0Vc0D3wqrqeSHPxwGIeWF83e5dMIE3Wnme9NOChQkFCWAH40YMhNutrG3dR AJM8WExkMyAL0gmzwsGNBBMBCgA7AhsBCAsJCAcNDAsKBRUKCQgLAh4BAheAFiEE y82PAwWIZT7t1+Jlm33UM/JUkEoFAmaOrBwFCQ8AVs0ACgkQm33UM/JUkEr2rA/g sXbm0JtEbE/gzxoLlI71WhZFvXWwZehWpNnxWuBgnxHyCAiB6Ulf0wqlfTyRUirI o6pcL9Gadj6y4V5sxa8NLH6Zvv2KeLelNjMTSx3VPAbSprhzLQXvO25wubmfI8wP 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 wocIA3yDQ/POwE0EWW+JnwEIALljGLPt+4QNsRcqLgzAVXsgTpDeYQmtdZh3otmM vMU76/cM+Ez9iBA50jvD7x5imuwVpQ+1gAsXLhtH/lQUaP0zTRGTe2CVXd0LTlCI cbNpEaAbVbTgfmFxC4bUOJNcyuABfOVklTBceRplK0iWRs1zXf/K8Qrz/Ujv7W5p w+WckeFKH30Pg8wlMt/yLZZTHTsxUhGh4gP35oXvfmFxb2si7obPUfSywmSzVk/U 8WVhx7IMJPMb9iJnPCIB/AecM+mUC8rIyhjHQsZB22/5akyiGFpJ10ve92MOKrzU fessmj4YK1rjY1wDSsMFf9bPt4sNZ8o96ajZWma+zMn9lhcAEQEAAcLC9gQYAQoA eAWCaH4jlwWJEPAfNwkQm33UM/JUkEpHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu c2VxdW9pYS1wZ3Aub3Jnw2NNWQrZUBaUYYLV8LDmEeEG76wn6pJcMlxZVZw5Y7oC GwIWIQTLzY8DBYhlPu3X4mWbfdQz8lSQSgE2wHQgBBkBCgAdFiEEJWpOVeSnLZet JGjniNx+MzhfeR0FAllviZ8ACgkQiNx+MzhfeR3XZgf/ViFPa4G4HdY7X9GrinjD /GYvQnHeQl/NsY4lIEzR5/8GptxbeMTF0kjGGX6snU+oCshPSUibcd09eBIWZ+B1 KJcUosvxvvEIuegA+t8Yb61QWzj+GDzc8az/+atr8fM4a6Y6Y3KQojCCy2+ouZ9D LZRHLNHpZx5uwBN6/zzr+CuNCkLIY5XWZYIKPaoscRCuXqxEYCkO9zZFvvHSZatt s8sBZJkpgVlT7GdGwpqw55phJGZi/qSFXi3U52BCli5t/r4jJ2KfpO/lnCkfupSQ 5nIJA8+TlsOksz0DY3Mlti9PtLV9YbgNkOO2eSOcl3mmrgJXy2jh+0qOGG28WpkE AJSnD+CKo2jppMFmS0YmIT6UVVsoYvWnUmDJ5SCSnJR/GMBwSfKD/SfWxQAPLWzn 5onVRlyFCpBEaEqj3xb6d8Dr/3K0EZ6kdF89SW4spO/a3VDlfAJQ5oYNtlg+FW+k hUb6q4sEYp1tNUGfWwqDCjuSugqLfgpqZaO1nq55bMl3nEL3BndDp97GrSZx6I/P Cfzmwsuny/61tjCAY+NQBGAtc8zgiEM1xoIDJgBF27DDFRPpR9pwQLjlKro0AVhW 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 =N5EW -----END PGP PUBLIC KEY BLOCK----- """ [authorization.malte] sign_commit = true sign_tag = true sign_archive = true keyring = """ -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: 12E1 A36F 95BE 6C74 2355 C9E2 B84B 81FB 8504 CFD6 Comment: Comment: Malte Meiboom xjMEZhZeexYJKwYBBAHaRw8BAQdAFHXRDIvyWSzW7OmFE9dkUIqB1DdImqiN/rpe KgD1nSHCwBEEHxYKAIMFgmYWXnsFiQWkj70DCwkHCRC4S4H7hQTP1kcUAAAAAAAe ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmepskxeAz+yCOaHDI88hRCg atvbhvKU+C7YoBPXY1CpNQMVCggCmwECHgEWIQQS4aNvlb5sdCNVyeK4S4H7hQTP 1gAAuq0A/indXLJmOzn+tcmEotXiyxLheCOFOuWC4A5UnvLOIAM9AP49shPpyED7 DSMXxw7u6y/yI4QOBMSA2hul66AVxbPsDc0XPG1hbHRlQHNlcXVvaWEtcGdwLm9y Zz7CwBQEExYKAIYFgmYWXnsFiQWkj70DCwkHCRC4S4H7hQTP1kcUAAAAAAAeACBz YWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdUwkdEOlWZRe9uBvZ+ouHQZkYc TIPblRsgUjrkzUXSjgMVCggCmQECmwECHgEWIQQS4aNvlb5sdCNVyeK4S4H7hQTP 1gAAOdMBAI+eF3lIBns0xaWpqHLkUyGlLBZI9HbSjmjmYCr1XUvCAPwN0uxUNN/K le9Qb9LSxyyq0YqhshZDLUHeFL+QJF8/Bc0NTWFsdGUgTWVpYm9vbcLAEQQTFgoA gwWCZhZeewWJBaSPvQMLCQcJELhLgfuFBM/WRxQAAAAAAB4AIHNhbHRAbm90YXRp b25zLnNlcXVvaWEtcGdwLm9yZ/AVNOeWVsB2NSxus1vZ+ejjPZOjEKRlz+LktDow PumUAxUKCAKbAQIeARYhBBLho2+Vvmx0I1XJ4rhLgfuFBM/WAADgFQD/UaPn4AYR vhTQW1ylgMm9dpL8oXeGAQmoPooH+BV238cBAPoIQ6GYrmkkCIx7FjChFrnWNafI 0J05N6ceR8mAs/AKzjMEZhZeexYJKwYBBAHaRw8BAQdAaLIAzorb/m2dzsMwojCg MckTuhri4GAtCyUirWSkvfjCwMUEGBYKATcFgmYWXnsFiQWkj70JELhLgfuFBM/W RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZyAgQxt0rsE0 ukaEL8UTG3C9ZBHD9G/3rcmHpAbx1Zx6ApsCvqAEGRYKAG8FgmYWXnsJEKymHzqn Hm/URxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ0lhLysC ftYUq0OuLp+9WdZJhlYE0Qa2SIsdJHl8zWESFiEEF4ZP5qmyntDn94OIrKYfOqce b9QAAHkMAQDzCWA6uHQwbRcgS9VCrF8tbJjExummfi60++iED0Jl3gEAkF7j2Vue HlVRz66jdLLOdxFrvd9buaCGHG19SD6LDgwWIQQS4aNvlb5sdCNVyeK4S4H7hQTP 1gAARuwA/RlCdTyXtZh1dYlWWo08x0+f8brxVoq0H3/pYEJt1bdYAP9CDKArbaUP 25cktwwH4RSkp6dK6nD4KlQu36QVwbQRBw== =VQbl -----END PGP PUBLIC KEY BLOCK----- """ [authorization.neal] sign_commit = true keyring = """ -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: F717 3B3C 7C68 5CD9 ECC4 191B 74E4 45BA 0E15 C957 Comment: Neal H. Walfield (Code Signing Key) Comment: Neal H. Walfield Comment: Neal H. Walfield Comment: Neal H. Walfield Comment: Neal H. Walfield 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 XuwkEDh4MBNicSOdhoItEg89lEbzxQ== =VIUO -----END PGP PUBLIC KEY BLOCK----- """ sequoia-cert-store-0.7.1/src/cert_store.rs000064400000000000000000000521671046102023000167050ustar 00000000000000use std::path::Path; use std::sync::Arc; use anyhow::Context; use sequoia_openpgp as openpgp; use openpgp::cert::raw::RawCertParser; use openpgp::Fingerprint; use openpgp::KeyHandle; use openpgp::packet::UserID; use openpgp::parse::Parse; use openpgp::Result; use crate::LazyCert; use crate::store; use store::Certs; use store::MergeCerts; use store::Store; use store::StoreError; use store::StoreUpdate; use store::UserIDQueryParams; use crate::TRACE; #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum AccessMode { Always, OnMiss, } /// A unified interface to multiple certificate stores. /// /// When a certificate is looked up, the certificate is looked up in /// the primary cert-d, if any, and all the backends whose access mode /// is `AccessMode::Always`. The results are merged and returned. If /// no certificate is found, then the look up is also tried on the /// backends whose access mode is `AccessMode::OnMiss`. Finally, if a /// key server is configured, the key server is tried. /// /// In general, results are preferred to errors. That is, if a /// backend returns a positive result, and another backend returns an /// error, the error is ignored, even if it is something other than /// [`StoreError::NotFound`]. /// /// Results from the key server are either cached when /// [`CertStore::flush`] is called (or the [`CertStore`] is dropped) /// if there is a writable primary cert-d, or simply dropped /// otherwise. pub struct CertStore<'a> { certd: std::result::Result, store::Certs<'a>>, // Read-only backends. backends: Vec<(Box + Send + Sync + 'a>, AccessMode)>, keyserver: Option>>, } assert_send_and_sync!(CertStore<'_>); impl<'a> CertStore<'a> { /// Returns a CertStore, which does not have any configured backends. pub fn empty() -> Self { CertStore { certd: Err(store::Certs::empty()), backends: Vec::new(), keyserver: None, } } /// Returns a CertStore, which uses the default certificate /// directory. /// /// When a certificate is added or updated, it will be added to or /// updated in this certificate store. pub fn new() -> Result { Ok(CertStore { certd: Ok(store::CertD::open_default()?), backends: Vec::new(), keyserver: None, }) } /// Returns a CertStore, which uses the default certificate /// directory in read-only mode. pub fn readonly() -> Result { let mut cert_store = CertStore { certd: Err(store::Certs::empty()), backends: Vec::new(), keyserver: None, }; cert_store.add_default_certd()?; Ok(cert_store) } /// Returns a CertStore, which uses the specified certificate /// directory. /// /// When a certificate is added or updated, it will be added to or /// updated in this certificate store. pub fn open

(path: P) -> Result where P: AsRef { let path = path.as_ref(); Ok(CertStore { certd: Ok(store::CertD::open(path)?), backends: Vec::new(), keyserver: None, }) } /// Returns a CertStore, which uses the specified certificate /// directory in read-only mode. pub fn open_readonly

(path: P) -> Result where P: AsRef { let path = path.as_ref(); let mut cert_store = CertStore { certd: Err(store::Certs::empty()), backends: Vec::new(), keyserver: None, }; cert_store.add_certd(path)?; Ok(cert_store) } /// Add the specified backend to the CertStore. /// /// The backend is added to the collection of read-only backends. pub fn add_backend(&mut self, backend: Box + Send + Sync + 'a>, mode: AccessMode) -> &mut Self { self.backends.push((backend, mode)); self } /// Adds the specified cert-d to the CertStore. /// /// The cert-d is added in read-only mode, and its access mode is /// set to `AccessMode::Always`. pub fn add_certd

(&mut self, path: P) -> Result<&mut Self> where P: AsRef { let path = path.as_ref(); self.add_backend(Box::new(store::CertD::open(path)?), AccessMode::Always); Ok(self) } /// Adds the default cert-d to the CertStore. /// /// The cert-d is added in read-only mode, and its access mode is /// set to `AccessMode::Always`. pub fn add_default_certd(&mut self) -> Result<&mut Self> { self.add_backend(Box::new(store::CertD::open_default()?), AccessMode::Always); Ok(self) } /// Adds the specified keyring to the CertStore. /// /// The keyring is added in read-only mode, and its access mode is /// set to `AccessMode::Always`. pub fn add_keyring

(&mut self, path: P) -> Result<&mut Self> where P: AsRef { self.add_keyrings(std::iter::once(path))?; Ok(self) } /// Adds the specified keyrings to the CertStore. /// /// The keyrings are added in read-only mode, and their access /// mode is set to `AccessMode::Always`. pub fn add_keyrings(&mut self, filenames: I) -> Result<&mut Self> where P: AsRef, I: IntoIterator, { let keyring = Certs::empty(); let mut error = None; for filename in filenames { let filename = filename.as_ref(); let f = std::fs::File::open(filename) .with_context(|| format!("Open {:?}", filename))?; let parser = RawCertParser::from_reader(f) .with_context(|| format!("Parsing {:?}", filename))?; for cert in parser { match cert { Ok(cert) => { keyring.update(Arc::new(cert.into())) .expect("implementation doesn't fail"); } Err(err) => { eprint!("Parsing certificate in {:?}: {}", filename, err); error = Some(err); } } } } if let Some(err) = error { return Err(err).context("Parsing keyrings"); } self.add_backend( Box::new(keyring), AccessMode::Always); Ok(self) } /// Adds the specified keyserver to the CertStore. /// /// The keyserver is added in read-only mode, and its access mode /// is set to `AccessMode::OnMiss`. #[cfg(feature = "keyserver")] pub fn add_keyserver(&mut self, url: &str) -> Result<&mut Self> { self.keyserver = Some(Box::new(store::KeyServer::new(url)?)); Ok(self) } /// Adds the specified keyserver to the CertStore. /// /// The keyserver is added in read-only mode, and its access mode /// is set to `AccessMode::OnMiss`. /// /// A key server is treated specially from other backends: any /// results that it returns are written to the cert store (if it /// is open in read-write mode). #[cfg(feature = "keyserver")] pub fn add_keyserver_backend(&mut self, ks: store::KeyServer<'a>) -> Result<&mut Self> { self.keyserver = Some(Box::new(ks)); Ok(self) } /// Returns a reference to the certd store, if there is one. pub fn certd(&self) -> Option<&store::CertD<'a>> { self.certd.as_ref().ok() } /// Returns a mutable reference to the certd store, if there /// is one. pub fn certd_mut(&mut self) -> Option<&mut store::CertD<'a>> { self.certd.as_mut().ok() } } macro_rules! forward { ( $method:ident, append:$to_vec:expr, $self:expr, $term:expr, $($args:ident),* ) => {{ tracer!(TRACE, format!("{}({})", stringify!($method), $term)); let mut certs = Vec::new(); let mut err = None; match &$self.certd { Ok(certd) => { match certd.$method($($args),*) { Err(err2) => { if let Some(StoreError::NotFound(_)) = err2.downcast_ref::() { // Ignore NotFound. t!("certd returned nothing"); } else { t!("certd returned: {}", err2); err = Some(err2) } } Ok(c) => { let mut c = $to_vec(c); t!("certd returned {}", c.iter() .map(|cert| cert.fingerprint().to_string()) .collect::>() .join(", ")); certs.append(&mut c) } } } Err(in_memory) => { match in_memory.$method($($args),*) { Err(err2) => { if let Some(StoreError::NotFound(_)) = err2.downcast_ref::() { // Ignore NotFound. t!("in-memory returned nothing"); } else { t!("in-memory returned: {}", err2); err = Some(err2) } } Ok(c) => { let mut c = $to_vec(c); t!("in-memory returned {}", c.iter() .map(|cert| cert.fingerprint().to_string()) .collect::>() .join(", ")); certs.append(&mut c) } } } } for mode in [AccessMode::Always, AccessMode::OnMiss] { for (backend, _) in $self.backends.iter() .filter(|(_, m)| &mode == m) { match backend.$method($($args),*) { Err(err2) => { if let Some(StoreError::NotFound(_)) = err2.downcast_ref::() { // Ignore NotFound. t!("backend returned nothing"); } else { t!("backend returned: {}", err2); err = Some(err2) } } Ok(c) => { let mut c = $to_vec(c); t!("backend returned {}", c.iter() .map(|cert| cert.fingerprint().to_string()) .collect::>() .join(", ")); certs.append(&mut c) } } } // If we found a cert after the AccessMode::Always round, // don't consult the AccessMode::OnMiss backends. if mode == AccessMode::Always && ! certs.is_empty() { break; } } if certs.is_empty() { if let Some(ks) = $self.keyserver.as_ref() { if let Ok(c) = ks.$method($($args),*) { certs = $to_vec(c); t!("keyserver returned {}", certs.iter() .map(|cert| cert.fingerprint().to_string()) .collect::>() .join(", ")); } } } if certs.is_empty() { if let Some(err) = err { t!("query failed: {}", err); Err(err) } else { t!("query returned nothing"); Ok(certs) } } else { t!("query returned {}", certs.iter() .map(|cert| cert.fingerprint().to_string()) .collect::>() .join(", ")); Ok(certs) } }}; ( $method:ident, $self:expr, $($args:ident),* ) => {{ forward!($method, append:|c| c, $self, $($args),*) }} } fn merge<'a, 'b>(mut certs: Vec>>) -> Vec>> { certs.sort_by_key(|cert| cert.fingerprint()); certs.dedup_by(|a, b| { // If this returns true, a is dropped. So merge into b. if a.fingerprint() == b.fingerprint() { if let Ok(a2) = a.to_cert() { if let Ok(b2) = b.to_cert() { *b = Arc::new(LazyCert::from( b2.clone() .merge_public(a2.clone()) .expect("Same certificate"))); } else { // b is invalid, but a is valid. Just keep a. *b = Arc::new(LazyCert::from(a2.clone())); } } else { // a is invalid. By returning true, we drop a. // That's what we want. } true } else { false } }); certs } impl<'a> store::Store<'a> for CertStore<'a> { fn lookup_by_cert(&self, kh: &KeyHandle) -> Result>>> { let certs = forward!(lookup_by_cert, self, kh, kh)?; if certs.is_empty() { Err(StoreError::NotFound(kh.clone()).into()) } else { Ok(merge(certs)) } } fn lookup_by_cert_fpr(&self, fingerprint: &Fingerprint) -> Result>> { let certs = forward!(lookup_by_cert_fpr, append:|c| vec![c], self, fingerprint, fingerprint)?; // There may be multiple variants. Merge them. let certs = merge(certs); assert!(certs.len() <= 1); if let Some(cert) = certs.into_iter().next() { Ok(cert) } else { Err(StoreError::NotFound( KeyHandle::from(fingerprint.clone())).into()) } } fn lookup_by_cert_or_subkey(&self, kh: &KeyHandle) -> Result>>> { let certs = forward!(lookup_by_cert_or_subkey, self, kh, kh)?; if certs.is_empty() { Err(StoreError::NotFound(kh.clone()).into()) } else { Ok(merge(certs)) } } fn select_userid(&self, query: &UserIDQueryParams, pattern: &str) -> Result>>> { let certs = forward!(select_userid, self, pattern, query, pattern)?; if certs.is_empty() { Err(StoreError::NoMatches(pattern.to_string()).into()) } else { Ok(merge(certs)) } } fn lookup_by_userid(&self, userid: &UserID) -> Result>>> { let certs = forward!(lookup_by_userid, self, userid, userid)?; if certs.is_empty() { Err(StoreError::NoMatches( String::from_utf8_lossy(userid.value()).to_string()).into()) } else { Ok(merge(certs)) } } fn grep_userid(&self, pattern: &str) -> Result>>> { let certs = forward!(grep_userid, self, pattern, pattern)?; if certs.is_empty() { Err(StoreError::NoMatches(pattern.to_string()).into()) } else { Ok(merge(certs)) } } fn lookup_by_email(&self, email: &str) -> Result>>> { let certs = forward!(lookup_by_email, self, email, email)?; if certs.is_empty() { Err(StoreError::NoMatches(email.to_string()).into()) } else { Ok(merge(certs)) } } fn grep_email(&self, pattern: &str) -> Result>>> { let certs = forward!(grep_email, self, pattern, pattern)?; if certs.is_empty() { Err(StoreError::NoMatches(pattern.to_string()).into()) } else { Ok(merge(certs)) } } fn lookup_by_email_domain(&self, domain: &str) -> Result>>> { let certs = forward!(lookup_by_email_domain, self, domain, domain)?; if certs.is_empty() { Err(StoreError::NoMatches(domain.to_string()).into()) } else { Ok(merge(certs)) } } fn fingerprints(&self) -> Box + 'a> { let mut certs = Vec::new(); match self.certd.as_ref() { Ok(certd) => certs.extend(certd.fingerprints()), Err(in_memory) => certs.extend(in_memory.fingerprints()), }; for (backend, mode) in self.backends.iter() { if mode != &AccessMode::Always { continue; } certs.extend(backend.fingerprints()); } certs.sort(); certs.dedup(); Box::new(certs.into_iter()) } fn certs<'b>(&'b self) -> Box>> + 'b> where 'a: 'b { let mut certs = Vec::new(); match self.certd { Ok(ref certd) => certs.extend(certd.certs()), Err(ref in_memory) => certs.extend(in_memory.certs()), }; for (backend, mode) in self.backends.iter() { if mode != &AccessMode::Always { continue; } certs.extend(backend.certs()); } let certs = merge(certs); Box::new(certs.into_iter()) } fn prefetch_all(&self) { match self.certd.as_ref() { Ok(certd) => certd.prefetch_all(), Err(in_memory) => in_memory.prefetch_all(), }; for (backend, _mode) in self.backends.iter() { backend.prefetch_all(); } } fn prefetch_some(&self, certs: &[KeyHandle]) { match self.certd.as_ref() { Ok(certd) => certd.prefetch_some(certs), Err(in_memory) => in_memory.prefetch_some(certs), }; for (backend, _mode) in self.backends.iter() { backend.prefetch_some(certs); } } } impl<'a> store::StoreUpdate<'a> for CertStore<'a> { fn update_by(&self, cert: Arc>, merge_strategy: &dyn MergeCerts<'a>) -> Result>> { tracer!(TRACE, "CertStore::update_by"); match self.certd.as_ref() { Ok(certd) => { t!("Forwarding to underlying certd"); certd.update_by(cert, merge_strategy) } Err(in_memory) => { t!("Forwarding to underlying in-memory DB"); in_memory.update_by(cert, merge_strategy) } } } } impl<'a> CertStore<'a> { /// Flushes any modified certificates to the backing store. /// /// Currently, this flushes the key server cache to the underlying /// cert-d, if any. All other backends are currently expected to /// work in a write-through manner. /// /// Note: this is called automatically when the `CertStore` is /// dropped, but calling it explicitly allows for reporting of /// errors. pub fn flush(&mut self) -> Result<()> { // Sync the key server's cache to the backing store. tracer!(TRACE, "CertStore::flush"); t!("flushing"); let certd = if let Ok(certd) = self.certd.as_mut() { certd } else { // We don't have a writable backing store so we can't sync // anything to it. We're done. t!("no certd, can't sync"); return Ok(()); }; let ks = if let Some(ks) = self.keyserver.as_mut() { ks } else { // We don't have a key server. There is clearly nothing // to sync. t!("no keyserver, can't sync"); return Ok(()); }; let mut flushed_certs = Vec::new(); let mut result = Ok(()); for c in ks.certs() { let keyid = c.keyid(); if let Err(err) = certd.update(c.clone()) { t!("syncing {} to the cert-d: {}", keyid, err); if result.is_ok() { result = Err(err) .with_context(|| { format!("Flushing changes to {} to disk", keyid) }) } } else { flushed_certs.push(c); } } // Now purge the successfully flushed certs from the keyserver // cache, so that we will not try to flush them again next // time we're called. ks.delete_from_cache(flushed_certs.iter().map(Arc::clone)); t!("Flushed {} certificates", flushed_certs.len()); result } } impl<'a> Drop for CertStore<'a> { fn drop(&mut self) { let _ = self.flush(); } } sequoia-cert-store-0.7.1/src/lazy_cert.rs000064400000000000000000000242731046102023000165250ustar 00000000000000use std::borrow::Cow; use std::time::SystemTime; use std::sync::OnceLock; use sequoia_openpgp as openpgp; use openpgp::Cert; use openpgp::Fingerprint; use openpgp::KeyHandle; use openpgp::KeyID; use openpgp::Result; use openpgp::cert::raw::RawCert; use openpgp::cert::ValidCert; use openpgp::packet::Key; use openpgp::packet::UserID; use openpgp::packet::key; use openpgp::policy::Policy; use openpgp::serialize::SerializeInto; use super::TRACE; // Needed for the doc comments. #[allow(unused_imports)] use crate::store::Store; /// Stores either a `RawCert` or a parsed `Cert`. /// /// This allows the implementation to defer fully parsing and /// validating the certificate until it is actually needed. /// /// # Examples /// /// Functions like [`Store::lookup_by_cert`] return an /// `Arc`. Due to the [orphan rule], it is not possible for /// this library to provide an implementation of /// `TryFrom> for Cert`. So instead of using `TryFrom`, /// you have to do the conversion manually as follows: /// /// [orphan rule]: https://doc.rust-lang.org/book/ch10-02-traits.html#implementing-a-trait-on-a-type /// /// ``` /// # use std::sync::Arc; /// # /// # use sequoia_openpgp as openpgp; /// # use openpgp::cert::prelude::*; /// # use openpgp::Result; /// # /// # use sequoia_cert_store as cert_store; /// # use cert_store::LazyCert; /// # /// # fn main() -> Result<()> { /// # /// # let (cert, _) = /// # CertBuilder::general_purpose(Some("alice@example.org")) /// # .generate()?; /// // A LazyCert as returned by, e.g., `Store::lookup_by_cert`: /// let lc: Arc = Arc::new(LazyCert::from(cert)); /// /// // If you only need a reference: /// let cert: &Cert = lc.to_cert()?; /// /// // If you need an owned value: /// let cert: Cert = lc.to_cert()?.clone(); /// # Ok(()) } /// ``` #[derive(Clone)] pub struct LazyCert<'a> { // At least one of raw and cert are ever alive. Ideally, we'd put // them in an enum. To do that, the enum would have to be behind // a `RefCell`, but then we couldn't return bare references to the // `Cert`. raw: OnceLock>, cert: OnceLock>, } assert_send_and_sync!(LazyCert<'_>); impl<'a> std::fmt::Debug for LazyCert<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("LazyCert") .field("fingerprint", &self.fingerprint()) .field("subkeys", &self.subkeys().map(|k| k.fingerprint()) .collect::>()) .field("userids", &self.userids().collect::>()) .finish() } } impl<'a> LazyCert<'a> { /// Returns whether the cert has been parsed. pub(crate) fn is_parsed(&self) -> bool { self.cert.get().is_some() } /// Creates a `LazyCert` from a `Cert`. pub fn from_cert(cert: Cert) -> Self { tracer!(TRACE, "LazyCert::from_cert"); t!("Adding a parsed cert: {}", cert.fingerprint()); Self { raw: OnceLock::new(), cert: OnceLock::from(Cow::Owned(cert)), } } /// Creates a `LazyCert` from a `&Cert`. pub fn from_cert_ref(cert: &'a Cert) -> Self { tracer!(TRACE, "LazyCert::from_cert_ref"); t!("Adding a parsed cert: {}", cert.fingerprint()); Self { raw: OnceLock::new(), cert: OnceLock::from(Cow::Borrowed(cert)), } } /// Creates a `LazyCert` from a `RawCert`. pub fn from_raw_cert(raw: RawCert<'a>) -> Self { Self { raw: OnceLock::from(raw), cert: OnceLock::new(), } } /// Returns a reference to the raw cert, if any. /// /// If the cert only exists in parsed form, returns `None`. pub fn raw_cert(&self) -> Option<&RawCert<'a>> { self.raw.get() } /// Returns the RawCert, if any. /// /// If the cert exists only in parsed form, returns `Err(self)`. pub fn into_raw_cert(mut self) -> std::result::Result, Self> { match self.raw.take() { Some(raw) => Ok(raw), None => Err(self), } } /// Returns the certificate's fingerprint. pub fn fingerprint(&self) -> Fingerprint { if let Some(cert) = self.cert.get() { cert.fingerprint() } else if let Some(raw) = self.raw.get() { raw.fingerprint() } else { unreachable!("cert or raw must be set") } } /// Returns the certificate's Key ID. pub fn keyid(&self) -> KeyID { KeyID::from(self.fingerprint()) } /// Returns the certificate's Key Handle. pub fn key_handle(&self) -> KeyHandle { KeyHandle::from(self.fingerprint()) } /// Returns the user ids. pub fn userids(&self) -> impl Iterator + '_ { if let Some(cert) = self.cert.get() { Box::new(cert.userids().map(|ua| ua.userid().clone())) as Box + '_> } else if let Some(raw) = self.raw.get() { Box::new(raw.userids()) as Box + '_> } else { unreachable!("cert or raw must be set") } } /// Returns the keys. pub fn keys(&self) -> impl Iterator> + '_ { if let Some(cert) = self.cert.get() { Box::new(cert.keys().map(|ka| ka.key().clone())) as Box> + '_> } else if let Some(raw) = self.raw.get() { Box::new( raw .keys() // This is rather unsatisfying, but due to // lifetimes... .collect::>>() .into_iter()) as Box> + '_> } else { unreachable!("cert or raw must be set") } } /// Returns the primary key. pub fn primary_key(&self) -> Key { if let Some(cert) = self.cert.get() { cert.primary_key().key().clone() } else if let Some(raw) = self.raw.get() { raw.primary_key() } else { unreachable!("cert or raw must be set") } } /// Returns the subkeys. pub fn subkeys<'b>(&'b self) -> impl Iterator> + 'b { self.keys().skip(1) } /// Returns a reference to the parsed certificate. /// /// If the `LazyCert` is not yet parsed, parses now. pub fn to_cert(&self) -> Result<&Cert> { tracer!(TRACE, "LazyCert::to_cert"); if let Some(cert) = self.cert.get() { return Ok(cert); } if let Some(raw) = self.raw.get() { t!("Resolving {}", raw.fingerprint()); match Cert::try_from(raw) { Ok(cert) => { // The following will fail, if we lost a race to // parse the raw certificate. That's okay, // because parsing is deterministic. let _ = self.cert.set(Cow::Owned(cert)); } Err(err) => { return Err(err); } } } if let Some(cert) = self.cert.get() { return Ok(cert); } else { unreachable!("cert or raw must be set") } } /// Returns the parsed certificate. /// /// If the `LazyCert` is not yet parsed, parses now. pub fn into_cert(self) -> Result { let _ = self.to_cert()?; Ok(self.cert.into_inner().expect("valid").into_owned()) } /// Associates a policy and a reference time with the certificate. /// /// See [`Cert::with_policy`]. pub fn with_policy<'b, T>(&'b self, policy: &'b dyn Policy, time: T) -> Result> where T: Into>, { let cert = self.to_cert()?; cert.with_policy(policy, time) } /// Returns whether the certificate contains any secret key /// material. pub fn is_tsk(&self) -> bool { if let Some(cert) = self.cert.get() { cert.is_tsk() } else if let Some(raw) = self.raw.get() { raw.keys().any(|key| key.has_secret()) } else { unreachable!("cert or raw must be set") } } } impl<'a> From for LazyCert<'a> { fn from(cert: Cert) -> Self { LazyCert::from_cert(cert) } } impl<'a> From<&'a Cert> for LazyCert<'a> { fn from(cert: &'a Cert) -> Self { LazyCert::from_cert_ref(cert) } } impl<'a> From> for LazyCert<'a> { fn from(cert: RawCert<'a>) -> Self { LazyCert::from_raw_cert(cert) } } impl<'a> TryFrom> for Cert { type Error = anyhow::Error; fn try_from(lc: LazyCert<'a>) -> Result { lc.into_cert() } } impl<'a, 'b> TryFrom<&'b LazyCert<'a>> for &'b Cert { type Error = anyhow::Error; fn try_from(lc: &'b LazyCert<'a>) -> Result { lc.to_cert() } } // We can't implement openpgp::serialize::Marshal, because it is // sealed. So we fake what is used :/. impl<'a> LazyCert<'a> { /// Serializes the cert to a vector. pub fn to_vec(&self) -> Result> { if let Some(raw) = self.raw.get() { Ok(raw.as_bytes().to_vec()) } else if let Some(cert) = self.cert.get() { Ok(cert.to_vec()?) } else { unreachable!("raw or cert must be set"); } } /// Exports a serialized version of the cert to `o`. /// /// Non-exportable signatures are not exported, and any component /// bound merely by non-exportable signatures is not exported. pub fn export(&self, o: &mut dyn std::io::Write) -> Result<()> { use openpgp::serialize::Marshal; // We need to strip any local signatures. If we have a // RawCert, we could try to figure out if there are any local // signatures to avoid parsing and reserializing, but that is // error prone. let cert = self.to_cert()?; Ok(cert.export(o)?) } } sequoia-cert-store-0.7.1/src/lib.rs000064400000000000000000001563331046102023000153020ustar 00000000000000//! A certificate store abstraction. //! //! This crates provides a unified interface for different certificate //! stores via the [`Store`] and [`StoreUpdate`] traits. It also //! provides a number of helper functions and data structures, like //! [`UserIDIndex`] to help implement this functionality. //! //! [`UserIDIndex`]: store::UserIDIndex //! //! The [`CertStore`] data structure combines multiple certificate //! backends in a transparent way to users. //! //! # Examples //! //! We can store certificates in an in-memory store, and query the store: //! //! ```rust //! use std::sync::Arc; //! use sequoia_openpgp::cert::{Cert, CertBuilder}; //! use sequoia_cert_store::{CertStore, LazyCert, Store, StoreUpdate}; //! //! # fn main() -> anyhow::Result<()> { //! // Create an in-memory certificate store. To use the default //! // on-disk certificate store, use `CertStore::new`. //! let mut certs = CertStore::empty(); //! //! let (cert, _rev) = CertBuilder::new().generate()?; //! let fpr = cert.fingerprint(); //! //! // It's not in the cert store yet: //! assert!(certs.lookup_by_cert_fpr(&fpr).is_err()); //! //! // Insert a certificate. If using a backing store, it would //! // also be written to disk. //! certs.update(Arc::new(LazyCert::from(cert)))?; //! //! // Make sure it is there. //! let cert = certs.lookup_by_cert_fpr(&fpr).expect("present"); //! assert_eq!(cert.fingerprint(), fpr); //! //! // Resolve the `LazyCert` to a `Cert`. Certificates are stored //! // using `LazyCert` so that it is possible to work with `RawCert`s //! // and `Cert`s. This allows the implementation to defer fully parsing //! // and validating the certificate until it is actually needed. //! let cert: &Cert = cert.to_cert()?; //! # Ok(()) } //! ``` //! //! We can use the same API on a persistent certificate store, kept on the //! filesystem: //! //! ```rust //! use std::fs; //! # use tempfile; //! # //! # use std::sync::Arc; //! # //! # use sequoia_openpgp as openpgp; //! # use openpgp::Result; //! # use openpgp::cert::{Cert, CertBuilder}; //! # //! # use sequoia_cert_store::{CertStore, LazyCert, Store, StoreUpdate}; //! //! # fn main() -> Result<()> { //! // Make a certificate store on the file system, in a fresh empty directory. //! let directory = "/tmp/test-sequoia-certificate-directory"; //! # let directory_object = tempfile::tempdir()?; let directory = directory_object.path(); //! fs::create_dir_all(directory)?; //! let mut store = CertStore::open(directory)?; //! //! // Make a new certificate. //! let (cert, _rev) = CertBuilder::new().generate()?; //! let fpr = cert.fingerprint(); //! //! // The certificate of course will not be in the store yet. //! assert!(store.lookup_by_cert_fpr(&fpr).is_err()); //! //! // Add it. //! store.update(Arc::new(LazyCert::from(cert)))?; //! //! // Now the certificate can be found. //! let found = store.lookup_by_cert_fpr(&fpr).expect("present"); //! assert_eq!(found.fingerprint(), fpr); //! //! // Again, we can resolve the `LazyCert` to a `Cert`. //! let cert: &Cert = found.to_cert()?; //! # Ok(()) } //! ``` use std::str; use sequoia_openpgp as openpgp; use openpgp::Result; use openpgp::packet::UserID; #[macro_use] mod log; #[macro_use] mod macros; pub mod store; pub use store::Store; pub use store::StoreUpdate; mod cert_store; pub use cert_store::CertStore; pub use cert_store::AccessMode; mod lazy_cert; pub use lazy_cert::LazyCert; const TRACE: bool = false; /// Converts an email address to a User ID. /// /// If the email address is not valid, returns an error. /// /// The email address must be a bare email address. That is it must /// have the form `localpart@example.org`, and not be surrounded by /// angle brackets like ``. /// /// The email address is checked for validity. Specifically, it is /// checked to conform with [`RFC 2822`]'s [`addr-spec`] grammar. /// /// Returns a UserID containing the normalized User ID in angle /// brackets. /// /// [`RFC 2822`]: https://www.rfc-editor.org/rfc/rfc2822 /// [`addr-spec`]: https://www.rfc-editor.org/rfc/rfc2822#section-3.4.1 pub fn email_to_userid(email: &str) -> Result { let email_check = UserID::from(format!("<{}>", email)); match email_check.email() { Ok(Some(email_check)) => { if email != email_check { return Err(anyhow::anyhow!( "{:?} does not appear to be an email address", email)); } } Ok(None) => { return Err(anyhow::anyhow!( "{:?} does not appear to be an email address", email)); } Err(err) => { return Err(err.context(format!( "{:?} does not appear to be an email address", email))); } } let userid = UserID::from(&email[..]); match userid.email_normalized() { Err(err) => { Err(err.context(format!( "'{}' is not a valid email address", email))) } Ok(None) => { Err(anyhow::anyhow!("'{}' is not a valid email address", email)) } Ok(Some(_email)) => { Ok(userid) } } } #[cfg(test)] mod tests { use super::*; use std::path::PathBuf; use std::str; use std::sync::Arc; use anyhow::Context; /// Prints the error and causes, if any. pub fn print_error_chain(err: &anyhow::Error) { let _ = write_error_chain_into(&mut std::io::stderr(), err); } /// Prints the error and causes, if any. fn write_error_chain_into(sink: &mut dyn std::io::Write, err: &anyhow::Error) -> Result<()> { writeln!(sink, " {}", err)?; for cause in err.chain().skip(1) { writeln!(sink, " because: {}", cause)?; } Ok(()) } use openpgp::Fingerprint; use openpgp::KeyHandle; use openpgp::KeyID; use openpgp::Cert; use openpgp::parse::Parse; use openpgp::policy::StandardPolicy; use openpgp::serialize::Serialize; use openpgp_cert_d as cert_d; use store::Certs; use store::Pep; use store::StoreError; use store::UserIDQueryParams; fn certd_merge<'a>(new: &'a [u8], disk: Option<&[u8]>) -> cert_d::Result> { if let Some(disk) = disk { let new = Cert::from_bytes(&new).expect("valid"); let disk = Cert::from_bytes(&disk).expect("valid"); let merged = new.merge_public(disk).expect("valid"); let mut bytes = Vec::new(); merged.serialize(&mut bytes).expect("valid"); Ok(bytes.into()) } else { Ok(cert_d::MergeResult::DataRef(new)) } } include!("../tests/keyring.rs"); fn test_backend<'a, B>(backend: &B) where B: Store<'a> { // Check Store::list. { let mut got: Vec = backend.fingerprints().collect(); got.sort(); let mut expected: Vec = keyring::certs.iter() .map(|c| c.fingerprint.parse::().expect("valid")) .collect(); expected.sort(); expected.dedup(); assert_eq!(got.len(), expected.len()); assert_eq!(got, expected); } std::thread::yield_now(); // Check Store::iter. { let mut got: Vec = backend.certs().map(|c| c.fingerprint()).collect(); got.sort(); let mut expected: Vec = keyring::certs.iter() .map(|c| c.fingerprint.parse::().expect("valid")) .collect(); expected.sort(); expected.dedup(); assert_eq!(got.len(), expected.len()); assert_eq!(got, expected); } std::thread::yield_now(); // Iterate over the certificates in the keyring and check that // can look up the certificate by fingerprint, by key, by User // ID, and by email in various ways. for handle in keyring::certs.iter() { let fpr: Fingerprint = handle.fingerprint.parse().expect("valid"); let cert = handle.to_cert().expect("valid"); assert_eq!(fpr, cert.fingerprint(), "{}", handle.base); let keyid = KeyID::from(fpr.clone()); // Check by_cert_fpr. let got = backend.lookup_by_cert_fpr(&fpr).expect("present"); assert_eq!(got.fingerprint(), fpr, "{}, by_cert_fpr, primary", handle.base); // Look up by subkey and make sure we don't get cert. // Note: if a subkey is also a primary key (as is the case // for the ed certificate), then we'll get a certificate // back. for sk in cert.keys().subkeys() { match backend.lookup_by_cert_fpr(&sk.key().fingerprint()) { Ok(got) => { // Make sure what we got is what we looked up. assert_eq!(got.fingerprint(), sk.key().fingerprint()); // Make sure subkey is also a primary key. assert!( keyring::certs.iter().any(|c| { c.fingerprint.parse::().unwrap() == got.fingerprint() }), "{}, lookup_by_cert_fpr, subkey, unexpectedly got {}", handle.base, got.fingerprint()); } Err(err) => { match err.downcast_ref::() { Some(StoreError::NotFound(_)) => (), _ => panic!("Expected StoreError::NotFound, \ got: {}", err), } }, } std::thread::yield_now(); } // Check lookup_by_cert using key ids. let got = backend.lookup_by_cert(&KeyHandle::from(&keyid)) .expect("present"); // Make sure all the returned certificates match. assert!(got.iter() .all(|c| { c.keys().any(|k| k.keyid() == keyid) }), "{}, lookup_by_cert, keyid, primary", handle.base); // Make sure one of the returned certs is the cert we're // looking for. assert!(got.into_iter().any(|c| c.fingerprint() == fpr), "{}, lookup_by_cert, keyid, primary", handle.base); // Look up by subkey. This will only return something if // the subkey also happens to be a primary key. for sk in cert.keys().subkeys() { match backend.lookup_by_cert(&KeyHandle::from(sk.key().keyid())) { Ok(got) => { // We should never see an empty result. // Instead, the backend should return // StoreError::NotFound. assert!(! got.is_empty()); // Make sure we got what we looked up. for got in got.iter() { assert_eq!(got.keyid(), sk.key().keyid()); } // Make sure subkey is also a primary key. for got in got.into_iter() { assert!( keyring::certs.iter().any(|c| { c.fingerprint.parse::() .unwrap() == got.fingerprint() }), "{}, lookup_by_cert_fpr, subkey, \ unexpectedly got {}", handle.base, got.fingerprint()); } } Err(err) => { match err.downcast_ref::() { Some(StoreError::NotFound(_)) => (), _ => panic!("Expected StoreError::NotFound, \ got: {}", err), } }, } std::thread::yield_now(); } // Check lookup_by_cert_or_subkey using fingerprints. let got = backend.lookup_by_cert_or_subkey(&KeyHandle::from(fpr.clone())) .expect("present"); // Make sure all the returned certificates match. assert!(got.iter() .all(|c| { c.keys().any(|k| k.fingerprint() == fpr) }), "{}, lookup_by_cert_or_subkey, with fingerprint, primary", handle.base); // Make sure one of the returned certs is the cert we're // looking for. assert!(got.into_iter().any(|c| c.fingerprint() == fpr), "{}, lookup_by_cert_or_subkey, with fingerprint, primary", handle.base); // Look up by subkey and make sure we get cert. for sk in cert.keys().subkeys() { let got = backend.lookup_by_cert_or_subkey( &KeyHandle::from(sk.key().fingerprint())) .expect("present"); // Make sure all the returned certificates match. for got in got.iter() { assert!( got.keys().any(|k| k.fingerprint() == sk.key().fingerprint()), "{}, lookup_by_cert_or_subkey({}), with fingerprint, subkey", handle.base, sk.key().fingerprint()); } // Make sure one of the returned certs is the cert // we're looking for. assert!(got.into_iter().any(|c| c.fingerprint() == fpr), "{}, lookup_by_cert_or_subkey({}), with fingerprint, subkey", handle.base, sk.key().fingerprint()); std::thread::yield_now(); } // Check lookup_by_cert_or_subkey using keyids. let got = backend.lookup_by_cert_or_subkey(&KeyHandle::from(keyid.clone())) .expect("present"); // Make sure all the returned certificates match. for got in got.iter() { assert!( got.keys().any(|k| k.keyid() == keyid), "{}, lookup_by_cert_or_subkey({}), with keyid, primary", handle.base, keyid); } // Make sure one of the returned certs is the cert // we're looking for. assert!(got.into_iter().any(|c| c.fingerprint() == fpr), "{}, lookup_by_cert_or_subkey({}), with keyid, primary", handle.base, keyid); // Look up by subkey and make sure we get cert. for sk in cert.keys().subkeys() { let got = backend.lookup_by_cert_or_subkey(&KeyHandle::from(sk.key().keyid())) .expect("present"); // Make sure all the returned certificates match. for got in got.iter() { assert!( got.keys().any(|k| k.keyid() == sk.key().keyid()), "{}, lookup_by_cert_or_subkey({}), with keyid, subkey", handle.base, sk.key().keyid()); } // Make sure one of the returned certs is the cert // we're looking for. assert!(got.into_iter().any(|c| c.fingerprint() == fpr), "{}, lookup_by_cert_or_subkey({}), with keyid, subkey", handle.base, sk.key().keyid()); std::thread::yield_now(); } // Check look up by User ID address by querying for each // User ID, email, domain, etc. for ua in cert.userids() { let userid = ua.userid(); // Search by exact user id. let got = backend.lookup_by_userid(userid) .expect(&format!("{}, lookup_by_userid({:?})", handle.base, userid)); // Make sure all the returned certificates match. for got in got.iter() { assert!( got.userids().any(|u| &u == userid), "{}, lookup_by_userid({:?})", handle.base, userid); } // Make sure one of the returned certs is the cert // we're looking for. assert!(got.into_iter().any(|c| c.fingerprint() == fpr), "{}, lookup_by_userid({:?})", handle.base, userid); // Extract an interior substring (nor anchored at the // start or the end), and uppercase it. let pattern = str::from_utf8(userid.value()).expect("utf-8"); let pattern = &pattern[1..pattern.len() - 1]; let pattern = pattern.to_uppercase(); // grep removes all constraints so we should still // find it. let got = backend.grep_userid(&pattern) .expect(&format!("{}, grep_userid({:?})", handle.base, pattern)); // Make sure all the returned certificates match. let mut query = UserIDQueryParams::new(); query.set_email(false); query.set_anchor_start(false); query.set_anchor_end(false); query.set_ignore_case(true); for got in got.iter() { assert!( got.userids().any(|u| query.check(&u, &pattern)), "{}, grep_userid({:?})", handle.base, pattern); } // Make sure one of the returned certs is the cert // we're looking for. assert!(got.into_iter().any(|c| c.fingerprint() == fpr), "{}, grep_userid({:?})", handle.base, pattern); // Now use an anchor at the start, or the end, ignore // case, or not. The only one combination that should // return any results is no constraints, which we // tested above. for (start, end, ignore_case) in [(false, false, false), //(false, false, true), (false, true, false), (false, true, true), ( true, false, false), ( true, false, true), ( true, true, false), ( true, true, true)] { let result = backend.select_userid( UserIDQueryParams::new() .set_email(false) .set_anchor_start(start) .set_anchor_end(end) .set_ignore_case(ignore_case), &pattern); match result { Ok(got) => { panic!("{}, select_userid({:?}) -> {}", handle.base, pattern, got.into_iter() .map(|c| c.fingerprint().to_string()) .collect::>() .join(", ")); } Err(err) => { match err.downcast_ref::() { Some(StoreError::NoMatches(_)) => (), _ => panic!("{}, select_userid({:?}) -> {}", handle.base, pattern, err), } } } } // Search by exact email. let email = if let Ok(Some(email)) = userid.email() { email } else { // No email address. continue; }; // Search with the User ID using lookup_by_email. This will // fail: a User ID that contains an email address is // never a valid email address. assert!( backend.lookup_by_email( str::from_utf8(userid.value()).expect("valid utf-8")) .is_err(), "{}, lookup_by_email({:?})", handle.base, userid); // Search by email. let got = backend.lookup_by_email(&email) .expect(&format!("{}, lookup_by_email({:?})", handle.base, email)); // Make sure all the returned certificates match. let mut query = UserIDQueryParams::new(); query.set_email(true); query.set_anchor_start(true); query.set_anchor_end(true); query.set_ignore_case(false); for got in got.iter() { assert!( got.userids().any(|u| query.check(&u, &email)), "{}, lookup_by_email({:?})", handle.base, email); } // Make sure one of the returned certs is the cert // we're looking for. assert!(got.into_iter().any(|c| c.fingerprint() == fpr), "{}, lookup_by_email({:?})", handle.base, userid); // Extract an interior substring (nor anchored at the // start or the end), and uppercase it. let pattern = &email[1..email.len() - 1]; let pattern = pattern.to_uppercase(); // grep removes all constraints so we should still // find it. let got = backend.grep_email(&pattern) .expect(&format!("{}, grep_email({:?})", handle.base, pattern)); // Make sure all the returned certificates match. let mut query = UserIDQueryParams::new(); query.set_email(true); query.set_anchor_start(false); query.set_anchor_end(false); query.set_ignore_case(true); for got in got.iter() { assert!( got.userids().any(|u| query.check(&u, &pattern)), "{}, grep_email({:?})", handle.base, pattern); } // Make sure one of the returned certs is the cert // we're looking for. assert!(got.into_iter().any(|c| c.fingerprint() == fpr), "{}, grep_email({:?})", handle.base, pattern); // Now use an anchor at the start, or the end, ignore // case, or not. This should not return any results; // the only one that should return any results is no // constraints, which we tested above. for (start, end, ignore_case) in [(false, false, false), //(false, false, true), (false, true, false), (false, true, true), ( true, false, false), ( true, false, true), ( true, true, false), ( true, true, true)] { let result = backend.select_userid( UserIDQueryParams::new() .set_email(true) .set_anchor_start(start) .set_anchor_end(end) .set_ignore_case(ignore_case), &pattern); match result { Ok(got) => { panic!("{}, select_userid({:?}) -> {}", handle.base, pattern, got.into_iter() .map(|c| c.fingerprint().to_string()) .collect::>() .join(", ")); } Err(err) => { match err.downcast_ref::() { Some(StoreError::NoMatches(_)) => (), _ => panic!("{}, select_userid({:?}) -> {}", handle.base, pattern, err), } } } } // Search by domain. let domain = email.rsplit('@').next().expect("have an @"); // Search with the User ID using lookup_by_email_domain. // This will fail: a User ID that contains an email // address is never a valid email address. assert!( backend.lookup_by_email_domain( str::from_utf8(userid.value()).expect("valid utf-8")) .is_err(), "{}, lookup_by_email_domain({:?})", handle.base, userid); // Likewise with the email address. assert!( backend.lookup_by_email_domain(&email).is_err(), "{}, lookup_by_email_domain({:?})", handle.base, email); // Search by domain. We should find it. let got = backend.lookup_by_email_domain(&domain) .expect(&format!("{}, lookup_by_email_domain({:?})", handle.base, domain)); // Make sure all the returned certificates match. let mut query = UserIDQueryParams::new(); query.set_email(true); query.set_anchor_start(false); query.set_anchor_end(true); query.set_ignore_case(true); let at_domain = format!("@{}", domain); for got in got.iter() { assert!( got.userids().any(|u| query.check(&u, &at_domain)), "{}, lookup_by_email_domain({:?})", handle.base, domain); } // Make sure one of the returned certs is the cert // we're looking for. assert!(got.into_iter().any(|c| c.fingerprint() == fpr), "{}, lookup_by_email_domain({:?})", handle.base, userid); // Uppercase it. We should still find it. let pattern = domain.to_uppercase(); let got = backend.lookup_by_email_domain(&pattern) .expect(&format!("{}, lookup_by_email_domain({:?})", handle.base, pattern)); // Make sure all the returned certificates match. let mut query = UserIDQueryParams::new(); query.set_email(true); query.set_anchor_start(false); query.set_anchor_end(true); query.set_ignore_case(true); let at_domain = format!("@{}", pattern); for got in got.iter() { assert!( got.userids().any(|u| query.check(&u, &at_domain)), "{}, lookup_by_email_domain({:?})", handle.base, domain); } // Make sure one of the returned certs is the cert // we're looking for. assert!(got.into_iter().any(|c| c.fingerprint() == fpr), "{}, lookup_by_email_domain({:?})", handle.base, pattern); // Extract a substring that we shouldn't find. let pattern = &domain[1..pattern.len() - 1]; let result = backend.lookup_by_email_domain(pattern); match result { Ok(got) => { // We should never see an empty result. // Instead, the backend should return // StoreError::NotFound. assert!(! got.is_empty()); assert!( got.into_iter().all(|c| { c.fingerprint() != fpr }), "{}, lookup_by_email_domain({:?}, unexpectedly got {}", handle.base, pattern, fpr); } Err(err) => { match err.downcast_ref::() { Some(StoreError::NoMatches(_)) => (), _ => panic!("{}, lookup_by_email_domain({:?}) -> {}", handle.base, pattern, err), } } } std::thread::yield_now(); } } // So far, the tests have been generic in the sense that we // look up what is there. We now do some data set-specific // tests. let sort_vec = |mut v: Vec<_>| -> Vec<_> { v.sort(); v }; // alice and alice2 share a subkey. assert_eq!( sort_vec(backend.lookup_by_cert_or_subkey( &"5989D7BE9908AE24799DF6CFBE678043781349F1" .parse::().expect("valid")) .expect("present") .into_iter() .map(|c| c.fingerprint()) .collect::>()), sort_vec( vec![ keyring::alice.fingerprint .parse::().expect("valid"), keyring::alice2_adopted_alice.fingerprint .parse::().expect("valid"), ])); std::thread::yield_now(); // ed's primary is also a subkey on the same certificate. { let ed_fpr = "0C346B2B6241263F64E9C7CF1EA300797258A74E" .parse::().expect("valid"); let ed_kh = KeyHandle::from(&ed_fpr); assert_eq!( sort_vec(backend.lookup_by_cert_or_subkey(&ed_kh) .expect("present") .into_iter() .map(|c| c.fingerprint()) .collect::>()), sort_vec( vec![ keyring::ed.fingerprint .parse::().expect("valid"), ])); // Also test that the CertStore implementation is not // impacted. let cert = backend.lookup_by_cert_fpr(&ed_fpr) .expect("found cert"); assert_eq!(cert.fingerprint(), ed_fpr); // Make sure the implementation doesn't return the certificate // twice, once when matching on the primary key, and once when // matching on the subkey. let certs = backend.lookup_by_cert(&ed_kh) .expect("found cert"); assert_eq!(certs.len(), 1); assert_eq!(certs[0].fingerprint(), ed_fpr); let certs = backend.lookup_by_cert_or_subkey(&ed_kh) .expect("found cert"); assert_eq!(certs.len(), 1); assert_eq!(certs[0].fingerprint(), ed_fpr); let certs = backend.certs(); assert_eq!(certs.filter(|c| c.fingerprint() == ed_fpr).count(), 1); let fprs = backend.fingerprints(); assert_eq!(fprs.filter(|fpr| fpr == &ed_fpr).count(), 1); } // david has a subkey that doesn't have a binding signature, // but the backend is not supposed to check that. (That // subkey is bound to carol.) assert_eq!( sort_vec(backend.lookup_by_cert_or_subkey( &"CD22D4BD99FF10FDA11A83D4213DCB92C95346CE" .parse::().expect("valid")) .expect("present") .into_iter() .map(|c| c.fingerprint()) .collect::>()), sort_vec( vec![ keyring::carol.fingerprint .parse::().expect("valid"), keyring::david.fingerprint .parse::().expect("valid"), ])); // Try a key that is not present. match backend.lookup_by_cert_fpr( &"0123 4567 89AB CDEF 0123 4567 89AB CDEF 0123 4567" .parse::().expect("valid")) { Ok(cert) => panic!("lookup_by_cert_fpr(not present) -> {}", cert.fingerprint()), Err(err) => { match err.downcast_ref::() { Some(StoreError::NotFound(_)) => (), _ => panic!("lookup_by_cert(not present) -> {}", err), } } } match backend.lookup_by_cert_or_subkey( &"0123 4567 89AB CDEF 0123 4567 89AB CDEF 0123 4567" .parse::().expect("valid")) { Ok(certs) => panic!("lookup_by_cert_or_subkey(not present) -> {}", certs .into_iter() .map(|c| c.fingerprint().to_string()) .collect::>() .join(", ")), Err(err) => { match err.downcast_ref::() { Some(StoreError::NotFound(_)) => (), _ => panic!("lookup_by_cert(not present) -> {}", err), } } } std::thread::yield_now(); assert!( backend.lookup_by_cert_or_subkey( &"0123 4567 89AB CDEF 0123 4567 89AB CDEF" .parse::().expect("valid")) .is_err()); // Check puny code handling. // Look up the User ID using puny code. assert_eq!( backend.lookup_by_email("hans@xn--bcher-kva.tld") .expect("present") .len(), 1); // And without puny code. assert_eq!( backend.lookup_by_email("hans@bücher.tld") .expect("present") .into_iter() .map(|c| c.fingerprint()) .collect::>(), vec![ keyring::hans_puny_code.fingerprint .parse::().expect("valid") ]); // A substring shouldn't match. assert_eq!( backend.lookup_by_email("hans@bücher.tl") .unwrap_or(Vec::new()) .len(), 0); // The same, but just look up by domain. assert_eq!( backend.lookup_by_email_domain("xn--bcher-kva.tld") .expect("present") .into_iter() .map(|c| c.fingerprint()) .collect::>(), vec![ keyring::hans_puny_code.fingerprint .parse::().expect("valid") ]); // And without puny code. assert_eq!( backend.lookup_by_email_domain("bücher.tld") .expect("present") .into_iter() .map(|c| c.fingerprint()) .collect::>(), vec![ keyring::hans_puny_code.fingerprint .parse::().expect("valid") ]); std::thread::yield_now(); // Check that when looking up a subdomain, we don't get back // User IDs in a subdomain. assert_eq!( backend.lookup_by_email_domain("company.com") .expect("present") .into_iter() .map(|c| c.fingerprint()) .collect::>(), vec![ keyring::una.fingerprint .parse::().expect("valid") ]); assert_eq!( backend.lookup_by_email_domain("sub.company.com") .expect("present") .into_iter() .map(|c| c.fingerprint()) .collect::>(), vec![ keyring::steve.fingerprint .parse::().expect("valid") ]); // Check searching by domain. assert_eq!( sort_vec(backend.lookup_by_email_domain("verein.de") .expect("present") .into_iter() .map(|c| c.fingerprint()) .collect::>()), sort_vec( vec![ keyring::alice2_adopted_alice.fingerprint .parse::().expect("valid"), keyring::carol.fingerprint .parse::().expect("valid"), ])); // It should be case insenitive. assert_eq!( sort_vec(backend.lookup_by_email_domain("VEREIN.DE") .expect("present") .into_iter() .map(|c| c.fingerprint()) .collect::>()), sort_vec( vec![ keyring::alice2_adopted_alice.fingerprint .parse::().expect("valid"), keyring::carol.fingerprint .parse::().expect("valid"), ])); // Make sure the backend returns user IDs that are not self // signed. // // Peter's self-signed user ID is: 'Peter // '. Usa certificated a non-self signed // user ID: 'Dad ' assert_eq!( backend.lookup_by_userid( &UserID::from("Dad ")) .expect("present") .len(), 1); assert_eq!( backend.grep_userid("Dad") .expect("present") .len(), 1); assert_eq!( backend.lookup_by_email("peter@example.family") .expect("present") .len(), 1); assert_eq!( backend.grep_email("eter@example.family") .expect("present") .len(), 1); assert_eq!( backend.lookup_by_email_domain("example.family") .expect("present") .len(), 1); } #[test] fn certd() -> Result<()> { use std::io::Read; let path = tempfile::tempdir()?; let certd = cert_d::CertD::with_base_dir(&path) .map_err(|err| { let err = anyhow::Error::from(err) .context(format!("While opening the certd {:?}", path)); print_error_chain(&err); err })?; for cert in keyring::certs.iter() { let bytes = cert.bytes(); let mut reader = openpgp::armor::Reader::from_bytes( &bytes, openpgp::armor::ReaderMode::VeryTolerant); let mut bytes = Vec::new(); reader.read_to_end(&mut bytes) .expect(&format!("{}", cert.base)); certd .insert_data(&bytes, false, certd_merge) .with_context(|| { format!("{} ({})", cert.base, cert.fingerprint) }) .expect("can insert"); } drop (certd); let certd = store::certd::CertD::open(&path).expect("exists"); test_backend(&certd); Ok(()) } #[test] fn cert_store() -> Result<()> { use std::io::Read; let path = tempfile::tempdir()?; let certd = cert_d::CertD::with_base_dir(&path) .map_err(|err| { let err = anyhow::Error::from(err) .context(format!("While opening the certd {:?}", path)); print_error_chain(&err); err })?; for cert in keyring::certs.iter() { let bytes = cert.bytes(); let mut reader = openpgp::armor::Reader::from_bytes( &bytes, openpgp::armor::ReaderMode::VeryTolerant); let mut bytes = Vec::new(); reader.read_to_end(&mut bytes) .expect(&format!("{}", cert.base)); certd .insert_data(&bytes, false, certd_merge) .with_context(|| { format!("{} ({})", cert.base, cert.fingerprint) }) .expect("can insert"); } drop(certd); let cert_store = CertStore::open(&path).expect("exists"); test_backend(&cert_store); Ok(()) } #[test] fn cert_store_layered() -> Result<()> { use std::io::Read; // A certd for each certificate. let mut paths: Vec = Vec::new(); let mut cert_store = CertStore::empty(); for cert in keyring::certs.iter() { let path = tempfile::tempdir()?; let certd = cert_d::CertD::with_base_dir(&path) .map_err(|err| { let err = anyhow::Error::from(err) .context(format!("While opening the certd {:?}", path)); print_error_chain(&err); err })?; let bytes = cert.bytes(); let mut reader = openpgp::armor::Reader::from_bytes( &bytes, openpgp::armor::ReaderMode::VeryTolerant); let mut bytes = Vec::new(); reader.read_to_end(&mut bytes) .expect(&format!("{}", cert.base)); certd .insert_data(&bytes, false, certd_merge) .with_context(|| { format!("{} ({})", cert.base, cert.fingerprint) }) .expect("can insert"); drop(certd); let certd = store::CertD::open(&path).expect("valid"); cert_store.add_backend(Box::new(certd), AccessMode::Always); paths.push(path); } test_backend(&cert_store); Ok(()) } #[test] fn certs() -> Result<()> { use std::io::Read; let mut bytes = Vec::new(); for cert in keyring::certs.iter() { let binary = cert.bytes(); let mut reader = openpgp::armor::Reader::from_bytes( &binary, openpgp::armor::ReaderMode::VeryTolerant); reader.read_to_end(&mut bytes) .expect(&format!("{}", cert.base)); } let backend = store::Certs::from_bytes(&bytes) .expect("valid"); test_backend(&backend); Ok(()) } #[test] fn certd_with_prefetch() -> Result<()> { use std::io::Read; let path = tempfile::tempdir()?; let certd = cert_d::CertD::with_base_dir(&path) .map_err(|err| { let err = anyhow::Error::from(err) .context(format!("While opening the certd {:?}", path)); print_error_chain(&err); err })?; for cert in keyring::certs.iter() { let bytes = cert.bytes(); let mut reader = openpgp::armor::Reader::from_bytes( &bytes, openpgp::armor::ReaderMode::VeryTolerant); let mut bytes = Vec::new(); reader.read_to_end(&mut bytes) .expect(&format!("{}", cert.base)); certd .insert_data(&bytes, false, certd_merge) .with_context(|| { format!("{} ({})", cert.base, cert.fingerprint) }) .expect("can insert"); } drop (certd); let certd = store::CertD::open(&path).expect("exists"); certd.prefetch_all(); test_backend(&certd); Ok(()) } #[test] fn certs_with_prefetch() -> Result<()> { use std::io::Read; let mut bytes = Vec::new(); for cert in keyring::certs.iter() { let binary = cert.bytes(); let mut reader = openpgp::armor::Reader::from_bytes( &binary, openpgp::armor::ReaderMode::VeryTolerant); reader.read_to_end(&mut bytes) .expect(&format!("{}", cert.base)); } let backend = store::Certs::from_bytes(&bytes) .expect("valid"); backend.prefetch_all(); test_backend(&backend); Ok(()) } #[test] fn keyrings() -> Result<()> { let mut cert_store = CertStore::empty(); let mut base = PathBuf::from(env!("CARGO_MANIFEST_DIR")); base.push("tests"); cert_store.add_keyrings( keyring::certs.iter().map(|c| { PathBuf::from(&base).join(c.filename) }))?; test_backend(&cert_store); Ok(()) } #[test] fn certs_multithreaded() -> Result<()> { use std::io::Read; let mut bytes = Vec::new(); for cert in keyring::certs.iter() { let binary = cert.bytes(); let mut reader = openpgp::armor::Reader::from_bytes( &binary, openpgp::armor::ReaderMode::VeryTolerant); reader.read_to_end(&mut bytes) .expect(&format!("{}", cert.base)); } let backend = store::Certs::from_bytes(&bytes) .expect("valid"); std::thread::scope(|s| { let threads = (0..4usize) .map(|_| { s.spawn(|| { // Really make sure all threads start by sleeping // for 10ms. std::thread::sleep(std::time::Duration::new(0, 10)); test_backend(&backend); }) }) .collect::>(); threads.into_iter().for_each(|h| { h.join().expect("joined"); }); }); Ok(()) } #[test] fn pep() -> Result<()> { use std::io::Read; let mut bytes = Vec::new(); for cert in keyring::certs.iter() { let binary = cert.bytes(); let mut reader = openpgp::armor::Reader::from_bytes( &binary, openpgp::armor::ReaderMode::VeryTolerant); reader.read_to_end(&mut bytes) .expect(&format!("{}", cert.base)); } let backend = Pep::from_bytes(&bytes).expect("valid"); backend.prefetch_all(); test_backend(&backend); Ok(()) } // Make sure that when we update a certificate, we are able to // find any new components and we are still able to find the old // components. fn test_store_update<'a, B>(backend: B) -> Result<()> where B: store::StoreUpdate<'a> { let p = &StandardPolicy::new(); let signing_cert = Cert::from_bytes(&keyring::halfling_signing.bytes()) .expect("valid"); let fpr = signing_cert.fingerprint(); // We expect a primary and two subkeys. assert_eq!(signing_cert.keys().count(), 3); let signing_vc = signing_cert.with_policy(p, None).expect("ok"); let signing_fpr = signing_vc.keys().subkeys() .for_signing() .map(|ka| ka.key().fingerprint()) .collect::>(); assert_eq!(signing_fpr.len(), 1); let signing_fpr = KeyHandle::from( signing_fpr.into_iter().next().expect("have one")); let auth_fpr = signing_vc.keys().subkeys() .for_authentication() .map(|ka| ka.key().fingerprint()) .collect::>(); assert_eq!(auth_fpr.len(), 1); let auth_fpr = KeyHandle::from( auth_fpr.into_iter().next().expect("have one")); let encryption_cert = Cert::from_bytes(&keyring::halfling_encryption.bytes()) .expect("valid"); assert_eq!(fpr, encryption_cert.fingerprint()); // We expect a primary and two subkeys. assert_eq!(encryption_cert.keys().count(), 3); let encryption_vc = encryption_cert.with_policy(p, None).expect("ok"); let encryption_fpr = encryption_vc.keys().subkeys() .for_transport_encryption() .map(|ka| ka.key().fingerprint()) .collect::>(); assert_eq!(encryption_fpr.len(), 1); let encryption_fpr = KeyHandle::from( encryption_fpr.into_iter().next().expect("have one")); assert_ne!(signing_fpr, encryption_fpr); let auth2_fpr = encryption_vc.keys().subkeys() .for_authentication() .map(|ka| ka.key().fingerprint()) .collect::>(); assert_eq!(auth2_fpr.len(), 1); let auth2_fpr = KeyHandle::from( auth2_fpr.into_iter().next().expect("have one")); assert_eq!(auth_fpr, auth2_fpr); let merged_cert = signing_cert.clone() .merge_public(encryption_cert.clone()).expect("ok"); let check = |backend: &B, have_enc: bool, cert: &Cert| { let r = backend.lookup_by_cert(&KeyHandle::from(fpr.clone())).unwrap(); assert_eq!(r.len(), 1); assert_eq!(r[0].to_cert().expect("ok"), cert); let r = backend.lookup_by_cert_or_subkey(&signing_fpr).unwrap(); assert_eq!(r.len(), 1); assert_eq!(r[0].to_cert().expect("ok"), cert); let r = backend.lookup_by_cert_or_subkey(&auth_fpr).unwrap(); assert_eq!(r.len(), 1); assert_eq!(r[0].to_cert().expect("ok"), cert); if have_enc { let r = backend.lookup_by_cert_or_subkey(&encryption_fpr).unwrap(); assert_eq!(r.len(), 1); assert_eq!(r[0].to_cert().expect("ok"), cert); } else { assert!(backend.lookup_by_cert_or_subkey(&encryption_fpr).is_err()); } let r = backend.lookup_by_userid( &UserID::from("")).unwrap(); assert_eq!(r.len(), 1); assert_eq!(r[0].to_cert().expect("ok"), cert); let r = backend.lookup_by_userid( &UserID::from("Halfling ")).unwrap(); assert_eq!(r.len(), 1); assert_eq!(r[0].to_cert().expect("ok"), cert); if have_enc { let r = backend.lookup_by_userid( &UserID::from("Halfling ")) .unwrap(); assert_eq!(r.len(), 1); assert_eq!(r[0].to_cert().expect("ok"), cert); } else { assert!(backend.lookup_by_cert_or_subkey(&encryption_fpr).is_err()); } }; // Insert the signing certificate. backend.update(Arc::new(LazyCert::from(signing_cert.clone()))) .expect("ok"); check(&backend, false, &signing_cert); backend.update(Arc::new(LazyCert::from(encryption_cert.clone()))) .expect("ok"); check(&backend, true, &merged_cert); backend.update(Arc::new(LazyCert::from(signing_cert.clone()))) .expect("ok"); check(&backend, true, &merged_cert); Ok(()) } // Test StoreUpdate::update for CertStore. #[test] fn test_store_update_cert_store() -> Result<()> { let path = tempfile::tempdir()?; let cert_store = CertStore::open(&path).expect("exists"); test_store_update(cert_store) } /// Test StoreUpdate::update for CertD. #[test] fn test_store_update_certd() -> Result<()> { let path = tempfile::tempdir()?; let certs = store::CertD::open(&path)?; test_store_update(&certs)?; // We're the only user of the cert-d. Therefore, we should be // able to keep the persistent index and the in-memory index // in sync with the cert-d without having to do any hard // reloads. assert_eq!( certs.load_stats.in_memory_loads(), 0); assert_eq!( certs.load_stats.persistent_index_scans(), 1); Ok(()) } // Test StoreUpdate::update for Certs. #[test] fn test_store_update_certs() -> Result<()> { let certs = Certs::empty(); test_store_update(certs) } // Test StoreUpdate::update for Pep. #[test] fn test_store_update_pep() -> Result<()> { let certs = Pep::empty()?; test_store_update(certs) } #[test] fn test_store_multithreaded_update_cert_store() -> Result<()> { let path = tempfile::tempdir()?; let backend = CertStore::open(&path)?; test_store_multithreaded_update(backend) } #[test] fn test_store_multithreaded_update_certd() -> Result<()> { let path = tempfile::tempdir()?; let backend = store::CertD::open(&path)?; test_store_multithreaded_update(backend) } #[test] fn test_store_multithreaded_update_certs() -> Result<()> { let backend = store::Certs::empty(); test_store_multithreaded_update(backend) } #[test] fn test_store_multithreaded_update_pep() -> Result<()> { let backend = Pep::empty()?; test_store_multithreaded_update(backend) } fn test_store_multithreaded_update<'a, B>(backend: B) -> Result<()> where B: store::StoreUpdate<'a> + Sync { let mut certs = Vec::new(); for cert in keyring::certs.iter() { let cert = Cert::from_bytes(&cert.bytes()).expect("valid"); certs.push(cert); } let mut fprs: Vec = certs.iter().map(|cert| cert.fingerprint()).collect(); fprs.sort(); fprs.dedup(); std::thread::scope(|s| { let backend = &backend; let threads = certs.into_iter() .map(|cert| { s.spawn(move || { // Really make sure all threads start by sleeping // for 10ms. std::thread::sleep(std::time::Duration::new(0, 10)); let kh = cert.key_handle(); backend.update(Arc::new(LazyCert::from(cert))) .expect("ok"); assert!(backend.lookup_by_cert(&kh).is_ok()); assert!(backend.lookup_by_cert_or_subkey(&kh).is_ok()); }) }) .collect::>(); threads.into_iter().for_each(|h| { h.join().expect("joined"); }); }); assert_eq!(backend.certs().count(), fprs.len()); Ok(()) } } sequoia-cert-store-0.7.1/src/log.rs000064400000000000000000000073671046102023000153170ustar 00000000000000use std::cell::RefCell; // Like eprintln! macro_rules! log { ($dst:expr $(,)?) => ( eprintln!("{}", $dst) ); ($dst:expr, $($arg:tt)*) => ( eprintln!("{}", std::format!($dst, $($arg)*)) ); } // The indent level. It is increased with each call to tracer and // decremented when the tracer goes out of scope. thread_local! { pub static INDENT_LEVEL: RefCell = RefCell::new(0); } // Like eprintln!, but the first argument is a boolean, which // indicates if the string should actually be printed. macro_rules! trace { ( $TRACE:expr, $fmt:expr, $($pargs:expr),* ) => { if $TRACE { let indent_level = crate::log::INDENT_LEVEL.with(|i| { *i.borrow() }); let ws = " "; log!("{}{}", &ws[0..std::cmp::min(ws.len(), std::cmp::max(1, indent_level) - 1)], format!($fmt, $($pargs),*)); } }; ( $TRACE:expr, $fmt:expr ) => { trace!($TRACE, $fmt, ); }; } macro_rules! tracer { ( $TRACE:expr, $func: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)] macro_rules! t { ( $fmt:expr ) => { trace!($TRACE, "{}: {}", $func, $fmt) }; ( $fmt:expr, $a:expr ) => { trace!($TRACE, "{}: {}", $func, format!($fmt, $a)) }; ( $fmt:expr, $a:expr, $b:expr ) => { trace!($TRACE, "{}: {}", $func, format!($fmt, $a, $b)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr ) => { trace!($TRACE, "{}: {}", $func, format!($fmt, $a, $b, $c)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr ) => { trace!($TRACE, "{}: {}", $func, format!($fmt, $a, $b, $c, $d)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr ) => { trace!($TRACE, "{}: {}", $func, format!($fmt, $a, $b, $c, $d, $e)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr ) => { trace!($TRACE, "{}: {}", $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, "{}: {}", $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, "{}: {}", $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, "{}: {}", $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, "{}: {}", $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, "{}: {}", $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k)) }; } struct Indent {} impl Indent { fn init() -> Self { crate::log::INDENT_LEVEL.with(|i| { i.replace_with(|i| *i + 1); }); Indent {} } } impl Drop for Indent { fn drop(&mut self) { crate::log::INDENT_LEVEL.with(|i| { i.replace_with(|i| *i - 1); }); } } let _indent = Indent::init(); } } sequoia-cert-store-0.7.1/src/macros.rs000064400000000000000000000100721046102023000160050ustar 00000000000000/// A very simple profiling tool. /// /// Note: don't ever profile code that has not been compiled in /// release mode. There can be orders of magnitude difference in /// execution time between it and debug mode! /// /// This macro measures the wall time it takes to execute the block. /// If the time is at least $ms_threshold (in milli-seconds), then it /// displays the output on stderr. The output is prefixed with label, /// if it is provided. /// /// ```nocompile /// let result = time_it!("Some code", 10, { /// // Some code. /// 5 /// }); /// assert_eq!(result, 5); /// ``` #[allow(unused_macros)] macro_rules! time_it { ( $label:expr, $ms_threshold:expr, $body:expr ) => {{ use std::time::{SystemTime, Duration}; // We use drop so that code that uses non-local exits (e.g., // using break 'label) still works. struct Timer { start: SystemTime, } impl Drop for Timer { fn drop(&mut self) { let elapsed = self.start.elapsed(); if elapsed.clone().unwrap_or(Duration::from_millis($ms_threshold)) >= Duration::from_millis($ms_threshold) { if $label.len() > 0 { eprint!("{}:", $label); } eprintln!("{}:{}: {:?}", file!(), line!(), elapsed); } } } let _start = Timer { start: SystemTime::now() }; $body }}; ( $label:expr, $body:expr ) => { time_it!($label, 0, $body) }; ( $body:expr ) => { time_it!("", $body) }; } #[allow(dead_code)] pub(crate) trait Sendable : Send {} #[allow(dead_code)] pub(crate) trait Syncable : Sync {} /// A simple shortcut for ensuring a type is send and sync. /// /// For most types just call it after defining the type: /// /// ```ignore /// pub struct MyStruct {} /// assert_send_and_sync!(MyStruct); /// ``` /// /// For types with lifetimes, use the anonymous lifetime: /// /// ```ignore /// pub struct WithLifetime<'a> { _p: std::marker::PhantomData<&'a ()> } /// assert_send_and_sync!(WithLifetime<'_>); /// ``` /// /// For a type generic over another type `W`, /// pass the type `W` as a where clause /// including a trait bound when needed: /// /// ```ignore /// pub struct MyWriter { _p: std::marker::PhantomData } /// assert_send_and_sync!(MyWriter where W: std::io::Write); /// ``` /// /// This will assert that `MyWriterStruct` is `Send` and `Sync` /// if `W` is `Send` and `Sync`. /// /// You can also combine the two and be generic over multiple types. /// Just make sure to list all the types - even those without additional /// trait bounds: /// /// ```ignore /// pub struct MyWriterWithLifetime<'a, C, W: std::io::Write> { /// _p: std::marker::PhantomData<&'a (C, W)>, /// } /// assert_send_and_sync!(MyWriterWithLifetime<'_, C, W> where C, W: std::io::Write); /// ``` /// /// If you need multiple additional trait bounds on a single type /// you can add them separated by `+` like in normal where clauses. /// However you have to make sure they are `Identifiers` like `Write`. /// In macro patterns `Paths` (like `std::io::Write`) may not be followed /// by `+` characters. // This is copied from sequoia-openpgp/src/macros.rs. macro_rules! assert_send_and_sync { ( $x:ty where $( $g:ident$( : $a:path )? $(,)?)*) => { impl<$( $g ),*> crate::macros::Sendable for $x where $( $g: Send + Sync $( + $a )? ),* {} impl<$( $g ),*> crate::macros::Syncable for $x where $( $g: Send + Sync $( + $a )? ),* {} }; ( $x:ty where $( $g:ident$( : $a:ident $( + $b:ident )* )? $(,)?)*) => { impl<$( $g ),*> crate::macros::Sendable for $x where $( $g: Send + Sync $( + $a $( + $b )* )? ),* {} impl<$( $g ),*> crate::macros::Syncable for $x where $( $g: Send + Sync $( + $a $( + $b )* )? ),* {} }; ( $x:ty ) => { impl crate::macros::Sendable for $x {} impl crate::macros::Syncable for $x {} }; } sequoia-cert-store-0.7.1/src/store/certd/cache.rs000064400000000000000000000747651046102023000200440ustar 00000000000000use std::ffi::OsString; use std::path::Path; use std::path::PathBuf; use std::sync::Mutex; use std::time::SystemTime; use std::time::UNIX_EPOCH; use rusqlite::{ Connection, OpenFlags, Transaction, TransactionBehavior, params, types::ValueRef, }; use sequoia_openpgp as openpgp; use openpgp::packet::signature::cache::SignatureVerificationCache; use crate::Result; const TRACE: bool = cfg!(test) || super::TRACE; /// We save a low precision (one day) timestamp to minimize the amount /// of on-disk churn. const TICK_SECONDS: u64 = 24 * 60 * 60; fn ticks_since_unix_epoch() -> u64 { SystemTime::now().duration_since(UNIX_EPOCH) .map(|t| t.as_secs()) .unwrap_or(0) / TICK_SECONDS } /// We evict cache entries that haven't been accessed in this much /// time (in ticks). const EVICTION_THRESHOLD: u64 = 30; /// Some information about the cache at the time it was restored. struct RestoredState { /// The time that the cache was restored as the number of ticks /// since the UNIX epoch. (A tick is defined as /// `TICK_SECONDS` seconds.) restored_at: i64, /// Observation: Ticks are relatively long. As such, even if an /// entry was accessed, we may not have to write it out to the /// database if the last access time did not change. In many /// cases, this means that we don't have to update the database at /// all as the same set of entries are usually accessed over and /// over again. /// /// Optimization: When we load the cache, we record what entries /// were already accessing during the current (`restored_at`) tick /// here. When we later save the cache, if the tick hasn't /// changed (i.e., the current tick is still `restored_at`), then /// these entries don't need to be written out: the new access /// tick is the same as the old one! /// /// This vector is sorted so that we can use binary_search. accessed_near_restore: Vec>, /// The oldest entry that we restored from the cache as the number /// of ticks since the UNIX epoch. (A tick is defined as /// `TICK_SECONDS` seconds.) We use this to decide whether we /// need to evict any entries. least_recently_accessed: i64, } /// Whether the cache was restored. If we didn't restore (or finish /// restoring!) the cache, we don't write it out. static RESTORED_STATE: Mutex> = Mutex::new(None); /// A handle to the signature verification cache. /// /// When this handle is dropped, the cache is saved to disk. pub struct CertdSignatureVerificationCache { /// The database. filename: PathBuf, /// At least when using the OpenSSL crypto backend, we can't use /// OpenSSL after the main thread exits. As such, if we load the /// cache from a separate thread, then we join the thread in the /// drop handler. thread_handle: Option>, } impl CertdSignatureVerificationCache { const DATABASE_VERSION: usize = 1; const DATABASE_ID: &'static str = "sequoia signature verification cache v1"; /// Initializes the database. /// /// Any existing content is lost. fn initialize_v1(tx: &Transaction) -> std::result::Result<(), rusqlite::Error> { tx.execute_batch("\ -- A table identifying the version and a human-readable magic. DROP TABLE IF EXISTS version; CREATE TABLE version ( id INTEGER PRIMARY KEY, version INTEGER NOT NULL, comment TEXT NOT NULL );")?; tx.execute("\ -- Record the schema version. INSERT OR IGNORE INTO version VALUES (0, ?1, ?2); ", (Self::DATABASE_VERSION, Self::DATABASE_ID))?; tx.execute("\ -- The signature verification cache with the last access time. The last -- access time is the number of days since the UNIX epoch, i.e., the -- number of seconds divided by 24 * 60 * 60. CREATE TABLE IF NOT EXISTS entries ( value BLOB PRIMARY KEY, last_accessed INTEGER ) WITHOUT ROWID", ())?; Ok(()) } /// Returns the database version. fn db_version(tx: &Transaction) -> rusqlite::Result { let mut stmt = tx.prepare_cached( "SELECT version FROM version WHERE id == 0")?; let r = stmt.query([])?.mapped(|r| r.get(0)).next().unwrap_or(Ok(0))?; Ok(r) } /// Returns a connection to the database. /// /// If `fail_fast` is set, then we'd rather fail then wait too /// long for the database lock. fn open(filename: &Path, fail_fast: bool) -> Result { tracer!(TRACE, "CertdSignatureVerificationCache::open"); t!("Opening {}", filename.display()); let open = || -> std::result::Result { let mut conn = Connection::open_with_flags( filename, OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_PRIVATE_CACHE)?; // Use WAL logging // // https://www.sqlite.org/wal.html conn.execute_batch("PRAGMA journal_mode=WAL")?; // Disable synchronous mode. This mode is more dangerous, but // since this database is only a cache, a corrupted database // will not result in data loss. // // https://www.sqlite.org/pragma.html#pragma_synchronous conn.execute_batch("PRAGMA synchronous=OFF")?; // See which database version we're dealing with. let mut transaction_mode = TransactionBehavior::Deferred; let (mut retries, wait) = if cfg!(test) { // When running tests, retry to exhaustion. This is // needed by the unit tests (in particular, see // test_signature_cache), which tests that the cache // is updated even under contention. (usize::MAX, std::time::Duration::from_millis(1)) } else if fail_fast { // 4 * 1 = 4ms. (4, std::time::Duration::from_millis(1)) } else { // 32 * 10 = 320ms. (32, std::time::Duration::from_millis(10)) }; loop { let tx = Transaction::new(&mut conn, transaction_mode)?; match Self::db_version(&tx) { Ok(1) => { tx.rollback()?; break; }, Ok(n) => { t!("Expected version 1 DB, got version {} DB. \ Re-initializing.", n); if let Err(err) = Self::initialize_v1(&tx) { t!("Failed to initialize database: {}", err); return Err(err); } tx.commit()?; break; }, Err(rusqlite::Error::SqliteFailure(e, _)) if e.code == rusqlite::ErrorCode::Unknown => { match Self::initialize_v1(&tx) { // Initializing the database, may fail if // another process has lock the database // (e.g., it is also initializing the // database). Sleep a bit and try again. Err(rusqlite::Error::SqliteFailure(e, _)) if retries > 0 && e.code == rusqlite::ErrorCode::DatabaseLocked => { retries -= 1; transaction_mode = TransactionBehavior::Immediate; tx.rollback()?; std::thread::sleep(wait); continue; }, other => other?, } tx.commit()?; return Ok(conn); }, Err(rusqlite::Error::SqliteFailure(e, _)) if retries > 0 && (e.code == rusqlite::ErrorCode::DatabaseBusy || e.code == rusqlite::ErrorCode::DatabaseLocked) => { // When trying to initialize the database, we // may find that it is locked. If so, sleep a // bit and then retry. retries -= 1; transaction_mode = TransactionBehavior::Immediate; tx.rollback()?; std::thread::sleep(wait); continue; } Err(err) => { t!("Can't open sqlite DB: {:?}", err); return Err(err.into()); } } } Ok(conn) }; match open() { Ok(conn) => Ok(conn), Err(err) => { t!("Error opening database: {}", err); if let rusqlite::Error::SqliteFailure(e, _) = err { t!("Error code returned from sqlite: {:?}", e.code); if e.code == rusqlite::ErrorCode::Unknown || e.code == rusqlite::ErrorCode::DatabaseCorrupt || e.code == rusqlite::ErrorCode::SchemaChanged || e.code == rusqlite::ErrorCode::NotADatabase { // The database is corrupted. Remove the file // and try again. // // We need to do this indirectly. // `std::fs::remove_file` doesn't guarantee // that the file is removed immediately // // "Note that there is no guarantee that the // file is immediately deleted (e.g., // depending on platform, other open file // descriptors may prevent immediate // removal)." // // https://doc.rust-lang.org/stable/std/fs/fn.remove_file.html // // If that happens (and it is the behavior // that we observe on Windows), then we can't // create a new database. // // To work around this, we rename the file, // and then remove the file using its new // name. t!("Corrupted database ({:?}), \ removing and recreating {}", err, filename.display()); let mut tmp = filename.to_path_buf(); tmp.set_extension("~"); if let Err(err) = std::fs::rename(filename, &tmp) { t!("Couldn't remove file or move it \ out of the way: {}", err); // Renaming failed. Just try and remove // the original file. tmp = filename.to_path_buf(); } else { t!("Renamed {} to {}", filename.display(), tmp.display()); } match std::fs::remove_file(&tmp) { Ok(()) => { let result = open(); if let Err(err) = result.as_ref() { t!("Failed to recreate database: {}", err); if let rusqlite::Error::SqliteFailure(e, _) = err { t!("Error code returned from sqlite: {:?}", e.code); } } else { t!("Successfully recreated database."); } return Ok(result?); } Err(err) => { t!("Removing {}: {}", tmp.display(), err); } } } } Err(err.into()) } } } /// Returns the cache's filename for the given certd. pub fn cache_file

(certd: P) -> Result where P: AsRef { let certd = certd.as_ref(); std::fs::create_dir_all(certd)?; let mut filename = OsString::from("_sequoia_signature_verification_cache_v1_on_"); filename.push(&gethostname::gethostname()); filename.push(".sqlite"); Ok(certd.join(filename)) } /// Loads the signature verification cache from disk. /// /// This returns a `CertdSignatureVerificationCache`. When the /// `CertdSignatureVerificationCache` is dropped, it automatically /// saves the cache to disk. /// /// To avoid blocking the thread, this function spawns a separate /// thread to do the actual load. pub fn load(filename: PathBuf) -> CertdSignatureVerificationCache { tracer!(TRACE, "CertdSignatureVerificationCache::load"); let filename_copy = filename.clone(); let handle = std::thread::spawn(move || { if let Err(err) = Self::load_internal(filename_copy.clone()) { t!("Error loading signature verification cache from {}: {}", filename_copy.display(), err); } }); CertdSignatureVerificationCache { filename, thread_handle: Some(handle), } } /// Loads the signature verification cache from disk. /// /// Returns a `CertdSignatureVerificationCache`, and an optional /// error. When the `CertdSignatureVerificationCache` is dropped, /// it automatically saves the cache to disk. /// /// If an error occurs, it is returned separately. An error means /// that the signature verification cache wasn't restored. But, /// that doesn't mean that the signature verification cache can't /// be saved later. /// /// Unlike [`CertdSignatureVerificationCache::load`], this /// function blocks the current thread until the cache is /// restored. #[allow(dead_code)] pub fn load_blocking(filename: PathBuf) -> (CertdSignatureVerificationCache, Option) { let result = Self::load_internal(filename.clone()); ( CertdSignatureVerificationCache { filename, thread_handle: None, }, result.err(), ) } /// Loads the signature verification cache from disk. /// /// On success, this returns a `CertdSignatureVerificationCache`. /// When the `CertdSignatureVerificationCache` is dropped, it /// automatically saves the cache to disk. fn load_internal(filename: PathBuf) -> Result<()> { tracer!(TRACE, "CertdSignatureVerificationCache::load"); let mut conn = Self::open(&filename, false)?; let tx = Transaction::new( &mut conn, TransactionBehavior::Deferred)?; // Load the cache. let restored_at = ticks_since_unix_epoch() as i64; let mut least_recently_accessed = restored_at; let mut accessed_near_restore = Vec::new(); let mut stmt = tx.prepare("SELECT value, last_accessed FROM entries")?; let rows = stmt.query_map([], |row| { let value: Vec = row.get(0)?; let last_accessed = row.get_ref(1)?; if let ValueRef::Integer(last_accessed) = last_accessed { if last_accessed == restored_at { accessed_near_restore.push(value.clone()); } else { least_recently_accessed = least_recently_accessed.min(last_accessed); } } Ok(value) })?; let entries = rows.collect::>(); drop(stmt); drop(tx); t!("Loading {} entries", entries.len()); accessed_near_restore.sort(); SignatureVerificationCache::restore( entries.into_iter().filter_map(|e| e.ok()), move || { let mut restored_state = RESTORED_STATE.lock().unwrap(); if restored_state.is_none() { *restored_state = Some(RestoredState { restored_at, accessed_near_restore, least_recently_accessed, }); } t!("Finished restoring the cache."); }); drop(conn); Ok(()) } /// Save the signature verification cache to the database. /// /// This is normally called when `CertdSignatureVerificationCache` /// is dropped. fn save(filename: &Path) -> Result<()> { tracer!(TRACE, "CertdSignatureVerificationCache::save"); // Normally save is called synchronously at program exit. If // we can't lock the database fast, then we should just not // bother to save the cache. let mut conn = Self::open(filename, true)?; let tx = Transaction::new( &mut conn, TransactionBehavior::Immediate)?; // Save the cache. let now = ticks_since_unix_epoch(); let restored_state = RESTORED_STATE.lock().unwrap(); // If an entry's last access time is the same as the last // access time in the db, then that entry is already up to // date. let mut ignore = &Vec::new(); // The on-disk entry with the oldest last access time at the // time of the restore. If this entry isn't old enough to be // considered for deletion, then no entries are old enough. let mut least_recently_accessed = 0; if let Some(restored_state) = restored_state.as_ref() { let restored_at = restored_state.restored_at; t!("{} ticks since restore", restored_at - (now as i64)); if restored_at == now as i64 { ignore = &restored_state.accessed_near_restore; } least_recently_accessed = restored_state.least_recently_accessed as u64; } else { t!("Didn't restore the cache."); } t!("ignore contains {} entries", ignore.len()); // Add the list of entries that need to be updated to an // in-memory table, and then do a single insert. Using an // INSERT per update takes about five times as long. tx.execute("ATTACH ':memory:' as in_memory", ())?; tx.execute("CREATE TABLE in_memory.accessed \ (value BLOB PRIMARY KEY) \ WITHOUT ROWID", ())?; let mut accessed_stmt = tx.prepare( "INSERT OR IGNORE INTO in_memory.accessed (value) VALUES (?1)")?; // Number of entries that were inserted. let mut inserted = 0; // Number of entries that were accessed. let mut accessed = 0; // Number of entries that were accessed and need their // last_access_time field updated. let mut accessed_old = 0; for entry in SignatureVerificationCache::dump() { let entry_inserted = entry.inserted(); let entry_accessed = entry.accessed(); if entry_inserted { inserted += 1; accessed_stmt.execute(params!(entry.value()))?; } else if entry_accessed { accessed += 1; if ignore.binary_search_by(|probe| { probe[..].cmp(entry.value()) }).is_err() { accessed_old += 1; accessed_stmt.execute(params!(entry.value()))?; } } } t!("{} new entries, {} accessed old ({} accessed total)", inserted, accessed_old, accessed); if inserted > 0 || accessed_old > 0 { tx.execute( "INSERT OR REPLACE INTO entries \ SELECT value, ?1 FROM in_memory.accessed", params!(now))?; } drop(accessed_stmt); if now > EVICTION_THRESHOLD && least_recently_accessed < now - EVICTION_THRESHOLD { tx.execute( "DELETE FROM entries WHERE last_accessed < ?1", params!(now - EVICTION_THRESHOLD))?; } tx.commit()?; Ok(()) } } impl Drop for CertdSignatureVerificationCache { fn drop(&mut self) { tracer!(TRACE, "CertdSignatureVerificationCache::drop"); t!("saving cache"); if let Some(handle) = self.thread_handle.take() { let _ = handle.join(); } match CertdSignatureVerificationCache::save(&self.filename) { Ok(()) => { t!("Saved signature cache to {}.", self.filename.display()); } Err(err) => { t!("Error saving signature cache to {}: {}", self.filename.display(), err); } } } } #[cfg(test)] mod tests { use super::*; use std::io::Write; // The signature verification cache is a global singleton. For // the following tests to be reliable, they need to run in their // own process. use rusty_fork::rusty_fork_test; use openpgp::cert::CertBuilder; use openpgp::policy::StandardPolicy; const P: &StandardPolicy = &StandardPolicy::new(); fn test_signature_cache(prefix: &str, filename: &Path, can_update: bool) -> Result<()> { eprintln!("{}1. Restore the signature cache", prefix); let (cache, err) = CertdSignatureVerificationCache::load_blocking( filename.to_path_buf()); if let Some(err) = err { eprintln!("{}1. loading cache: {}", prefix, err); } eprintln!("{}2. Save the signature cache", prefix); drop(cache); eprintln!("{}3. Restore the signature cache (2)", prefix); let (cache, err) = CertdSignatureVerificationCache::load_blocking( filename.to_path_buf()); if let Some(err) = err { eprintln!("{}3. loading cache: {}", prefix, err); } eprintln!("{}4. Count the number of entries in the signature cache database", prefix); let conn = Connection::open_with_flags( filename, OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_PRIVATE_CACHE)?; // Count the number of cache entries that we already have. let mut count_stmt = conn.prepare_cached("SELECT count(*) FROM entries")?; let precount = count_stmt .query([])? .mapped(|r| r.get(0)).next().unwrap_or(Ok(0))?; eprintln!("{}5. Have {} entries in db", prefix, precount); // Create a certificate, and turn it into a verified // certificate. This should create a few new cache entries. eprintln!("{}6. Generate a certificate", prefix); let (alice, _rev) = CertBuilder::general_purpose(Some("xxx")) .generate() .expect("can generate certificate"); let _vc = alice.with_policy(P, None).expect("is valid"); eprintln!("{}7. Save the signature cache (2)", prefix); drop(cache); // Count the number of cache entries that we have now. eprintln!("{}8. Count the number of entries in the signature cache database", prefix); let postcount = count_stmt .query([])? .mapped(|r| r.get(0)).next().unwrap_or(Ok(0))?; eprintln!("{}9. Have {} entries in db", prefix, postcount); if can_update { // Make sure that we have more cache entries than before. assert!(postcount > precount, "{}: assertion failed: postcount ({}) > precount ({})", prefix, postcount, precount); } else { assert_eq!(postcount, precount); } Ok(()) } rusty_fork_test! { #[test] fn signature_cache_good() { let tempfile = tempfile::NamedTempFile::new() .expect("Can create temp files"); if let Err(err) = test_signature_cache("", tempfile.path(), true) { panic!("test_signature_cache(\"\", {}, true) failed: {}", tempfile.path().display(), err); } } #[test] fn signature_cache_corrupted() { let tempfile = tempfile::NamedTempFile::new() .expect("Can create temp files"); std::fs::write(tempfile.path(), "this is a corrupted sqlite db.") .expect("can write"); // Make sure sqlite fails to use the database. let result = Connection::open_with_flags( tempfile.path(), OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_PRIVATE_CACHE) .and_then(|conn| { let mut count_stmt = conn.prepare_cached("SELECT count(*) FROM entries")?; let _precount = count_stmt .query([])? .mapped(|r| r.get(0)).next().unwrap_or(Ok(0))?; Ok(()) }); assert!(result.is_err()); // Make sure the file descriptor is closed. drop(result); // This should work, because we should replace the corrupted // database. if let Err(err) = test_signature_cache("", tempfile.path(), true) { panic!("test_signature_cache(\"\", {}, true) failed: {}", tempfile.path().display(), err); } } #[test] fn signature_cache_readonly() { let dir = tempfile::tempdir().expect("can create temp directory"); let tempfile = dir.path().join("cache.sqlite"); eprintln!("1. Loading signature cache (1)"); let cache = CertdSignatureVerificationCache::load_blocking( tempfile.as_path().to_path_buf()); eprintln!("2. Saving signature cache (1)"); drop(cache); assert!(tempfile.exists()); let metadata = tempfile.metadata().expect("can get metadata"); eprintln!("{}'s metadata: {:?}", tempfile.display(), metadata); eprintln!("3. Making cache file readonly"); let mut permissions = metadata.permissions(); permissions.set_readonly(true); std::fs::set_permissions(&tempfile, permissions) .expect("can make read only"); eprintln!("{}'s metadata after making read only: {:?}", tempfile.display(), tempfile.metadata().unwrap()); let result = std::fs::OpenOptions::new().write(true).open(&tempfile) .and_then(|mut file| { file.write_all(b"foo") }); if result.is_ok() { eprintln!("Read-only permission ignored (are you \ running as root). Skipping test."); return; } eprintln!("4. Loading signature cache (2)"); let cache = CertdSignatureVerificationCache::load_blocking( tempfile.as_path().to_path_buf()); eprintln!("5. Saving signature cache (2)"); eprintln!("metadata: {:?}", tempfile.metadata().unwrap()); drop(cache); eprintln!("6. Running signature cache test"); if let Err(err) = test_signature_cache("", tempfile.as_path(), false) { panic!("test_signature_cache(\"\", {}, false) failed: {}", tempfile.as_path().display(), err); } } #[test] fn signature_cache_readonly_directory() { let tmp_dir = tempfile::tempdir().expect("can create temp directory"); let dir = tmp_dir.path(); let tempfile = dir.join("cache.sqlite"); assert!(!tempfile.exists()); let metadata = dir.metadata().expect("can get metadata"); let mut permissions = metadata.permissions(); permissions.set_readonly(true); std::fs::set_permissions(&dir, permissions) .expect("can make read only"); // If readonly is not respected (e.g., when running as // root), skip the test: if std::fs::write(&tempfile, "hi").is_ok() { eprintln!("Read-only permission ignored (are you \ running as root). Skipping test."); return; } let cache = CertdSignatureVerificationCache::load_blocking( tempfile.to_path_buf()); drop(cache); } // Have a lot of threads load the same cache to check that the // code doesn't panic. #[test] fn signature_cache_hammer_time() { let tempfile = tempfile::NamedTempFile::new() .expect("Can create temp files"); let file = tempfile.path(); let threads: Vec<_> = (0..2) .map(|thread| { let file = file.to_path_buf(); std::thread::Builder::new() .name(format!("thread number {}", thread)) .spawn(move || { for i in 0..10 { let prefix = format!("{}.{}. ", thread, i); if let Err(err) = test_signature_cache( &prefix[..], &file, true) { panic!("Thread {}: test_signature_cache: {}", thread, err); } } }) .unwrap() }) .collect(); for t in threads.into_iter() { t.join().expect("ok"); } } } } sequoia-cert-store-0.7.1/src/store/certd/shadow_ca.rs000064400000000000000000001345201046102023000207130ustar 00000000000000use std::borrow::Cow; use std::collections::btree_map; use std::iter; use std::str; use std::sync::Arc; use std::time::{Duration, SystemTime}; use anyhow::Context; use sequoia_openpgp as openpgp; use openpgp::Packet; use openpgp::Result; use openpgp::cert::prelude::*; use openpgp::packet::UserID; use openpgp::packet::signature::SignatureBuilder; use openpgp::parse::Parse; use openpgp::serialize::SerializeInto; use openpgp::types::KeyFlags; use openpgp::types::SignatureType; use openpgp_cert_d as cert_d; use crate::LazyCert; use crate::store::Store; use crate::store::StoreUpdate; use crate::TRACE; use super::CertD; /// The creation time for the trust root and intermediate CAs. /// /// We use a creation time in the past (Feb 2002) so that it is still /// possible to use the CA when the reference time is in the past. fn ca_creation_time() -> SystemTime { SystemTime::UNIX_EPOCH + Duration::new(1014235320, 0) } pub(super) struct CA<'a> { // The tag of the file stored under the special name. special_tag: cert_d::Tag, // The cert as stored under the special name. special: Cert, // The cert as stored under its fingerprint. public: Arc>, // The merged certificate. If special and public are up to date, // then we can avoid the merge. merged: Arc>, } assert_send_and_sync!(CA<'_>); impl<'a> CertD<'a> { /// Returns the certificate directory's local trust root, and /// whether it was just created. /// /// If the local trust root does not yet exist, it is created. pub fn trust_root(&self) -> Result<(Arc>, bool)> { self.get_ca(cert_d::TRUST_ROOT, true, &UserID::from_static_bytes(b"Local Trust Root"), None) } /// Converts a shadow CA's name to a certificate directory's /// special name. /// /// For instance, this converts "keys.openpgp.org" to its special /// name. fn shadow_ca_special(name: &str) -> Cow { if name == cert_d::TRUST_ROOT { Cow::Borrowed(name) } else { Cow::Owned(format!("_sequoia_ca_{}.pgp", name)) } } // '/', '\\', ':', and whitespace are invalid. fn valid_name(name: &str) -> bool { name.chars().all(Self::valid_char) } /// Returns whether `c` may be used in names. fn valid_char(c: char) -> bool { // The list of reserved characters on Windows are documented // here: // // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file ! (c.is_whitespace() || c == '<' || c == '>' || c == ':' || c == '"' || c == '/' || c == '\\' || c == '|' || c == '?' || c == '*') } /// Returns a shadow CA's key, and whether it was just created. /// /// # Introduction /// /// Shadow CAs are used to encode evidence of a binding's veracity /// in standard web of trust data structures so that the /// information can be automatically incorporated into web of /// trust calculations. /// /// Consider [`keys.openpgp.org`], which is a verifying keyserver. /// `keys.openpgp.org` checks whether a user ID should be /// associated with a certificate using a challenge-response /// authentication scheme. When someone uploads a certificate, /// `keys.openpgp.org` iterates over the user IDs, and if a user /// ID contains an email address, sends an email to it containing /// a secret link. The owner of the email address can visit the /// link to confirm to `keys.openpgp.org` that the email address /// should be associated with the certificate. /// /// Although `keys.openpgp.org` verifies User IDs, it does not /// issue third-party certifications attesting to that fact. But, /// clients can infer this from `keys.openpgp.org` behavior: when /// `keys.openpgp.org` returns a certificate, it only returns user /// IDs that it has verified as belonging to the certificate. /// /// An OpenPGP client could record the fact that /// `keys.openpgp.org` returned a user ID in a database. The main /// question is then how to combine that information with other /// information in a coherent manner. Alternatively, we can /// encode the assertion as a certification using a so-called /// shadow CA. By encoding this information as a normal OpenPGP /// artifact, it can be directly integrated into web of trust /// calculations; another, parallel system to combine evidence is /// not needed. /// /// [keys.openpgp.org]: https://keys.openpgp.org /// /// # Usage /// /// `name` is the shadow CA's name. This is converted to the /// special name `_sequoia_ca_ + name + .pgp`. Names must not /// contain white space (` ` or `\n`), path separators (`\` or /// `/`), or colons (`:`). /// /// If the shadow CA doesn't exist and `create` is true, it is /// created. In this case, the user ID is set to `userid`, and if /// `trust_amount` is greater than 0, it is designed a trusted /// introducer by the last `CA` listed in `intermediaries`, or the /// local trust root, if the list is empty. The certification's /// trust amount is set to `trust_amount`. /// /// `intermediaries` is a list of tuples consisting of a name, a /// user ID, and a trust amount. Each tuple corresponds to an /// intermediary shadow CA. It may be empty; the local trust root /// is always the trust anchor and shouldn't be listed explicitly. /// /// This function works from the trust root towards the shadow CA. /// If an intermediate shadow CA does not exist and `create` is /// true, it is created, and certified by its parent CA. /// /// On success, the shadow CA is returned, and whether the shadow /// CA was just created. /// /// # Examples /// /// The following code looks up the shadow CA for /// `keys.openpgp.org`. It is not directly certified by the trust /// root, but by a partially trusted intermediary named /// `public_directories`, which acts as a type of resistor for all /// public directories. Note: if you are actually looking up the /// shadow CA for `keys.openpgp.org`, you should use /// [`CertD::shadow_ca_keys_openpgp_org`] instead. /// /// The local key for `keys.openpgp.org` is then used to certify /// the user IDs on a certificate, which was presumably returned /// by `keys.openpgp.org`. /// /// ```rust /// use std::sync::Arc; /// /// use anyhow::Context; /// /// use sequoia_openpgp as openpgp; /// use openpgp::cert::CertBuilder; /// use openpgp::Cert; /// use openpgp::packet::Packet; /// use openpgp::packet::UserID; /// use openpgp::packet::signature::SignatureBuilder; /// use openpgp::types::SignatureType; /// /// use sequoia_cert_store as cert_store; /// use cert_store::LazyCert; /// use cert_store::store::certd::CertD; /// use cert_store::store::StoreUpdate; /// /// # fn main() -> openpgp::Result<()> { /// # let certd_path = tempfile::tempdir()?; /// let mut certd = CertD::open(&certd_path)?; /// /// let (koo, _created) = certd.shadow_ca( /// "keys.openpgp.org", /// true, /// "Downloaded from keys.openpgp.org", /// 1, /// &[ /// ( "public_directories", UserID::from("Public Directories"), 40), /// ])?; /// /// // Pretend the following certificate was fetched from keys.openpgp.org. /// let alice_userid = ""; /// let (alice, _rev) = CertBuilder::general_purpose(Some(alice_userid)) /// .generate()?; /// /// // Certify the binding using the shadow CA. /// let mut koo_signer = koo.to_cert()?.primary_key().key().parts_as_secret() /// .context("CA is not certification capable.")? /// .clone().into_keypair() /// .context("CA is not certification capable.")?; /// /// let sig = SignatureBuilder::new(SignatureType::GenericCertification) /// .set_exportable_certification(false)? /// .sign_userid_binding( /// &mut koo_signer, /// alice.primary_key().key(), /// &UserID::from(alice_userid)) /// .context("Signing binding")?; /// /// let alice = alice.insert_packets([ /// Packet::from(UserID::from(alice_userid)), /// Packet::from(sig), /// ])?.0; /// /// certd.update(Arc::new(LazyCert::from(alice))); /// # Ok(()) } /// ``` pub fn shadow_ca(&self, name: &str, create: bool, userid: U, trust_amount: u8, intermediaries: &[(&str, UserID, u8)], ) -> Result<(Arc>, bool)> where U: Into, { let userid = userid.into(); tracer!(TRACE, "CertD::shadow_ca"); t!("{}, create: {}, userid: {:?}, {}, {} intermediaries", name, create, String::from_utf8_lossy(userid.value()), trust_amount, intermediaries.len()); let name = CertD::shadow_ca_special(name); // If the CA is cached in memory, short-circuit everything. if let Ok(result) = self.get_ca(&name, false, &userid, None) { t!("Returning cached certificate {} for {}", result.0.fingerprint(), name); return Ok(result); } // Iterate from the trust root towards the shadow CA. let mut ca: (Arc, bool) = self.get_ca( cert_d::TRUST_ROOT, create, &UserID::from_static_bytes(b"Local Trust Root"), None)?; for (name, userid, trust_amount) in intermediaries .into_iter() .map(|(name, userid, amount)| { (CertD::shadow_ca_special(name), userid, *amount) }) .chain(iter::once((name, &userid, trust_amount))) { if ! Self::valid_name(&name) { return Err(cert_d::Error::BadName.into()); } t!("Getting {} (create: {}, userid: {:?}, {})", name, create, String::from_utf8_lossy(userid.value()), trust_amount); ca = self.get_ca(&name, create, &userid, Some((ca.0, trust_amount)))?; t!("{} -> {}", name, ca.0.fingerprint()); } Ok(ca) } /// Returns the specified CA, optionally creating and certifying /// it if it doesn't exist. /// /// See [`CertD::shadow_ca`] for details about shadow CAs. /// /// This function first checks if the specified CA is in the /// in-memory cache. If so, it checks that the cached version is /// up to date, and then returns the result. /// /// If the CA is not in the in-memory cache, it is loaded from /// disk. If it doesn't exist on disk and `create` is `true`, the /// shadow CA is created. See [`CertD::load_ca`] for details of /// what is exactly done. fn get_ca(&self, special_name: &str, create: bool, userid: &UserID, parent: Option<(Arc>, u8)>) -> Result<(Arc>, bool)> { tracer!(TRACE, "CertD::get_ca"); let mut cas = self.cas.lock().unwrap(); let get_if_changed = |tag, name| -> Result> { match self.certd.get_if_changed(tag, name) { Ok(None) => { // The certificate on disk did not change. return Ok(None); }, Ok(Some((new_tag, bytes))) => { // The certificate on disk changed. Reparse it. t!("{} changed on disk, reloading.", name); match Cert::from_bytes(&bytes) { Ok(cert) => { return Ok(Some((cert, new_tag))); } Err(err) => { // An error occurred while parsing the // data. That's not great. We just keep // using what we have. t!("While reparsing {}: {}", name, err); return Ok(None); } } } // An error occurred while reading the file. That's // not great. We just keep using what we have. Err(err) => { t!("While rereading {}: {}", name, err); return Ok(None); } } }; if let Some(CA { special_tag, special, public, merged }) = cas.get_mut(special_name) { // The certificate is cached in memory. Make sure the // in-memory version is up to date. t!("{} -> {} is cached in memory", special_name, special.fingerprint()); let mut changed = false; if let Some((cert, tag)) = get_if_changed(*special_tag, special_name)? { *special = cert; *special_tag = tag; changed = true; } if let Ok(certs) = self.lookup_by_cert(&special.key_handle()) { // Since we looked up by fingerprint, certs contains 0 // or 1 elements. if let Some(cert) = certs.into_iter().next() { // Did it change? if ! Arc::ptr_eq(&cert, &public) { // Yup, it changed. *public = cert; changed = true; } } } if changed { *merged = Arc::new(LazyCert::from( public.to_cert()? .clone() .merge_public_and_secret(special.clone())?)); } Ok((Arc::clone(merged), false)) } else { // The certificate is not cached in memory; we need to // read it from disk, or generate it. let (ca, created) = self.load_ca(special_name, create, userid, parent)?; let merged = Arc::clone(&ca.merged); t!("Loaded {} ({}) from disk, caching in memory", special_name, merged.fingerprint()); match cas.entry(special_name.to_string()) { btree_map::Entry::Occupied(mut oe) => { oe.insert(ca); } btree_map::Entry::Vacant(ve) => { ve.insert(ca); } } Ok((merged, created)) } } /// Loads a CA's certificate from the certificate directory, or /// optionally generates one if it does not exist. /// /// Loads the certificate stored under the specified special name /// from the certificate directory. If the special name exists, /// this also reads and merges in the certificate data stored /// under the fingerprint. /// /// If `create` is `false` and the certificate does not exist, /// returns `std::io::ErrorKind::NotFound`. /// /// If `create` is `true`, and we need to generate a certificate, /// we are careful to so while we hold the certificate directory's /// lock to avoid races. /// /// If we create a certificate, and `parent` is not `None`, then /// we certify the certificate using `parent` as a trusted /// introducer for the specified trust amount. Note: if the CA is /// not created, this function does **not** check that the parent /// has certified the CA. /// /// If we create a certificate, we write it to disk under both its /// special name, and its fingerprint. fn load_ca(&self, name: &str, create: bool, userid: &UserID, parent: Option<(Arc>, u8)>) -> Result<(CA<'a>, bool)> { tracer!(TRACE, "CertD::load_ca"); let certd = self.certd(); // Exclusively lock the certificate directory by inserting the // trust root. We may discover that the trust already exists. // In that case, we won't generate a new one, but just load // the existing one. let mut ca = Err(anyhow::anyhow!("merge callback not invoked")); let mut created = false; let (special_tag, _) = certd.insert_special( name, (), false, |(), disk| { if let Some(disk) = disk { // We have one. ca = Cert::from_bytes(&disk); Ok(cert_d::MergeResult::Keep) } else if create { // We don't have one, and we should create one. let key = Self::generate_ca_key(userid)?; t!("Created {} for {}", key.fingerprint(), name); let bytes = key.as_tsk().to_vec()?; ca = Ok(key); created = true; Ok(cert_d::MergeResult::Data(bytes)) } else { // We don't have one, and we shouldn't create one. Err(cert_d::Error::Other( std::io::Error::new( std::io::ErrorKind::NotFound, format!("{} not found", name)).into())) } })?; let mut ca = ca?; let mut certify = |parent_cert: Arc, trust_amount| -> Result<()> { t!("Using {} to make {} a trusted introducer", parent_cert.fingerprint(), ca.fingerprint()); let mut signer = parent_cert .to_cert()? .primary_key() .key() .parts_as_secret() .context("Trust root can't be used for signing.")? .clone() .into_keypair() .context("Trust root can't be used for signing.")?; let sig = SignatureBuilder::new(SignatureType::GenericCertification) .set_exportable_certification(false)? .set_trust_signature(255, trust_amount)? .set_signature_creation_time(ca_creation_time())? .sign_userid_binding( &mut signer, ca.primary_key().key(), &userid) .with_context(|| { format!("Creating certification for {} {:?}", ca.fingerprint(), String::from_utf8_lossy(userid.value())) })?; ca = ca.clone().insert_packets([ Packet::from(userid.clone()), Packet::from(sig), ])?.0; Ok(()) }; let (public, merged) = if created { // We just created the CA. Make it a trusted introducer. if let Some((parent_cert, trust_amount)) = parent { if let Err(err) = certify(parent_cert, trust_amount) { // XXX: What should we do with the error? t!("Failed to authorize CA, {:?}: {}", userid, err); } } // Insert the public bits under the fingerprint. let merged = Arc::new(LazyCert::from(ca.clone())); let public = self.update_by( Arc::clone(&merged), &mut ())?; (public, merged) } else { // We didn't create it. Read in the public version and // merge it. let certs = self.lookup_by_cert(&ca.key_handle())?; // Since we looked up by fingerprint, certs contains 0 or // 1 elements. if let Some(public) = certs.into_iter().next() { let merged = Arc::new(LazyCert::from( public.to_cert()? .clone() .merge_public_and_secret(ca.clone())?)); (public, merged) } else { // The certificate is saved under the special name, // but not its fingerprint. It seems something went // wrong in the past, but we can fix it by writing out // the public version now. let merged = Arc::new(LazyCert::from(ca.clone())); let public = self.update_by( Arc::clone(&merged), &mut ())?; (public, merged) } }; let ca = CA { special: ca, special_tag, public, merged, }; Ok((ca, created)) } /// Returns a new key, which is appropriate for a CA. /// /// That is, it has just a certification capable primary and no /// subkeys. fn generate_ca_key(userid: &UserID) -> Result { let (root, _) = CertBuilder::new() .set_primary_key_flags(KeyFlags::empty().set_certification()) .set_exportable(false) .set_creation_time(ca_creation_time()) // CAs should *not* expire. .set_validity_period(None) .add_userid(userid.clone()) .generate()?; Ok(root) } const PUBLIC_DIRECTORY_NAME: &'static str = "public_directories"; const PUBLIC_DIRECTORY_USERID: UserID = UserID::from_static_bytes(b"Public Directories"); const PUBLIC_DIRECTORY_TRUST_AMOUNT: u8 = 40; /// A local, intermediate CA used to authorize the shadow CAs for /// public directories. /// /// Shadow CAs for public directories like /// `hkps://keys.openpgp.org` and WKD are not directly certified /// by the local trust root, but by a local, intermediate CA. /// This CA acts as a resistor, which limits the combined trust /// amount of public directories. pub fn public_directory_ca(&self) -> Result<(Arc>, bool)> { self.shadow_ca( Self::PUBLIC_DIRECTORY_NAME, true, Self::PUBLIC_DIRECTORY_USERID, Self::PUBLIC_DIRECTORY_TRUST_AMOUNT, &[]) } fn via_public_directory_ca(&self, name: &str, userid: &str) -> Result<(Arc>, bool)> { self.shadow_ca( name, true, userid, // A public directory has the smallest, positive trust // amount: 1 out of 120. 1, &[ // The public directory CA acts as a resistor and // limits the combined trust amount of public // directories. (Self::PUBLIC_DIRECTORY_NAME, Self::PUBLIC_DIRECTORY_USERID, Self::PUBLIC_DIRECTORY_TRUST_AMOUNT), ]) } /// Returns the shadow CA for keys.openpgp.org. /// /// See [`CertD::shadow_ca`] for more information about shadow /// CAs. pub fn shadow_ca_keys_openpgp_org(&self) -> Result<(Arc>, bool)> { self.via_public_directory_ca( "keys.openpgp.org", "Downloaded from keys.openpgp.org") } /// Returns the shadow CA for Proton. /// /// See [`CertD::shadow_ca`] for more information about shadow /// CAs. pub fn shadow_ca_proton_me(&self) -> Result<(Arc>, bool)> { self.via_public_directory_ca( "proton.me", "Downloaded from Proton Mail") } /// Returns the shadow CA for keys.mailvelope.com. /// /// See [`CertD::shadow_ca`] for more information about shadow /// CAs. pub fn shadow_ca_keys_mailvelope_com(&self) -> Result<(Arc>, bool)> { self.via_public_directory_ca( "keys.mailvelope.com", "Downloaded from keys.mailvelope.com") } /// Returns the shadow CA for WKDs. /// /// See [`CertD::shadow_ca`] for more information about shadow /// CAs. pub fn shadow_ca_wkd(&self) -> Result<(Arc>, bool)> { self.via_public_directory_ca( "wkd", "Downloaded from a Web Key Directory (WKD)") } /// Returns the shadow CA for DANE. /// /// See [`CertD::shadow_ca`] for more information about shadow /// CAs. pub fn shadow_ca_dane(&self) -> Result<(Arc>, bool)> { self.via_public_directory_ca( "dane", "Downloaded from DANE") } const WEB_NAME: &'static str = "web"; const WEB_USERID_STR: &'static str = "Downloaded from the Web"; const WEB_USERID: UserID = UserID::from_static_bytes(b"Downloaded from the Web"); const WEB_TRUST_AMOUNT: u8 = 1; /// Returns the shadow CA for the Web. /// /// See [`CertD::shadow_ca`] for more information about shadow /// CAs. pub fn shadow_ca_web(&self) -> Result<(Arc>, bool)> { debug_assert_eq!(Self::WEB_USERID_STR.as_bytes(), Self::WEB_USERID.value()); self.via_public_directory_ca( Self::WEB_NAME, Self::WEB_USERID_STR) } /// Returns the shadow CA for the give URL. /// /// See [`CertD::shadow_ca`] for more information about shadow /// CAs. pub fn shadow_ca_for_url(&self, url: &str) -> Result>, bool)>> { debug_assert_eq!(Self::WEB_USERID_STR.as_bytes(), Self::WEB_USERID.value()); // First, parse and sanitize the URL. let mut url = url::Url::parse(url)?; // We only certify the certificate if the transport was // encrypted and authenticated. if url.scheme() != "https" { return Ok(None); } // Drop sensitive and irrelevant information. url.set_username("").expect("https? supports authentication"); url.set_password(None).expect("https? supports authentication"); url.set_fragment(None); // Percent-encode characters not valid in names. let mut name = String::from("shadow_of_url_"); for c in url.as_str().chars() { if Self::valid_char(c) && c != '%' { name.push(c); } else { // Note: all invalid chars are ASCII characters, so // u8::try_from will succeed. debug_assert_eq!(c.len_utf8(), 1); name.push_str( &format!("%{:02X}", u8::try_from(c).unwrap_or(0))); } } self.shadow_ca( &name, true, format!("Downloaded from {}", url.as_str()), 1, &[ (Self::PUBLIC_DIRECTORY_NAME, Self::PUBLIC_DIRECTORY_USERID, Self::PUBLIC_DIRECTORY_TRUST_AMOUNT), (Self::WEB_NAME, Self::WEB_USERID, Self::WEB_TRUST_AMOUNT), ]) .map(Some) } /// Returns a shadow CA for the specified keyserver. /// /// If a keyserver is not known to be a verifying keyserver, then /// this returns `Ok(None)`. /// /// The URI should be of the form `hkps://server.example.com`. /// Other protocols are not supported. The server name is matched /// case insensitively. /// /// See [`CertD::shadow_ca`] for more information about shadow /// CAs. pub fn shadow_ca_keyserver(&self, uri: &str) -> Result>, bool)>> { let uri = uri.to_ascii_lowercase(); // We only certify the certificate if the transport was // encrypted and authenticated. let server = if let Some(server) = uri.strip_prefix("hkps://") { server } else { return Ok(None); }; let server = server.strip_suffix("/").unwrap_or(server); // A basic sanity check on the name, which we are about to use // as a filename: it can't start with a dot, and no // whitespace, no slashes, and no colons are allowed. if server.chars().next() == Some('.') || ! Self::valid_name(server) { return Ok(None); } // The known verifying key servers. match &server[..] { "keys.openpgp.org" | "keys.openpgp.io" => self.shadow_ca_keys_openpgp_org().map(Some), "keys.mailvelope.com" => self.shadow_ca_keys_mailvelope_com().map(Some), "mail-api.proton.me" | "api.protonmail.ch" => self.shadow_ca_proton_me().map(Some), _ => Ok(None), } } const AUTOCRYPT_NAME: &'static str = "autocrypt"; const AUTOCRYPT_USERID: UserID = UserID::from_static_bytes(b"Imported from Autocrypt"); const AUTOCRYPT_TRUST_AMOUNT: u8 = 40; /// Returns the shadow CA for Autocrypt. /// /// See [`CertD::shadow_ca`] for more information about shadow /// CAs. pub fn shadow_ca_autocrypt(&self) -> Result<(Arc>, bool)> { self.shadow_ca( Self::AUTOCRYPT_NAME, true, Self::AUTOCRYPT_USERID, Self::AUTOCRYPT_TRUST_AMOUNT, &[]) } const AUTOCRYPT_GOSSIP_NAME: &'static str = "autocrypt-gossip"; const AUTOCRYPT_GOSSIP_USERID: UserID = UserID::from_static_bytes(b"Imported from Autocrypt Gossip"); const AUTOCRYPT_GOSSIP_TRUST_AMOUNT: u8 = 40; /// Returns the shadow CA for Autocrypt Gossip. /// /// See [`CertD::shadow_ca`] for more information about shadow /// CAs. pub fn shadow_ca_autocrypt_gossip(&self) -> Result<(Arc>, bool)> { self.shadow_ca( Self::AUTOCRYPT_GOSSIP_NAME, true, Self::AUTOCRYPT_GOSSIP_USERID, Self::AUTOCRYPT_GOSSIP_TRUST_AMOUNT, &[ (Self::AUTOCRYPT_NAME, Self::AUTOCRYPT_USERID, Self::AUTOCRYPT_TRUST_AMOUNT), ]) } /// Returns the shadow CA for recording Autocrypt Gossip in the /// name of the given cert and sender address. /// /// If you observe an Autocrypt mail from `addr` signed by `cert`, /// and the message contains Autocrypt Gossip headers in the /// encrypted payload, use this function to get a shadow CA to /// turn the Autocrypt Gossip into OpenPGP certifications. /// /// See [`CertD::shadow_ca`] for more information about shadow /// CAs. pub fn shadow_ca_autocrypt_gossip_for(&self, cert: &Cert, addr: &str) -> Result<(Arc>, bool)> { // Sanity-check the address. let u = UserID::from_address( "Autocrypt Gossip from", None, addr)?; self.shadow_ca( &format!("shadow_of_{:x}", cert.fingerprint()), true, u, 1, &[ (Self::AUTOCRYPT_NAME, Self::AUTOCRYPT_USERID, Self::AUTOCRYPT_TRUST_AMOUNT), (Self::AUTOCRYPT_GOSSIP_NAME, Self::AUTOCRYPT_GOSSIP_USERID, Self::AUTOCRYPT_GOSSIP_TRUST_AMOUNT), ]) } } #[cfg(test)] mod tests { use super::*; use openpgp::Fingerprint; use openpgp::packet::UserID; use openpgp::policy::StandardPolicy; use crate::store::Store; #[test] fn shadow_ca() -> Result<()> { tracer!(true, "shadow_ca"); let path = tempfile::tempdir()?; let certd = CertD::open(&path)?; // Check that the special exists, and includes secret key // material, and that there is a corresponding entry under its // fingerprint, which doesn't include any secret key material. let check = |name: &str| { t!("check({})", name); assert!(cert_d::CertD::is_special(name).is_ok()); let (_tag, ca) = certd.certd() .get(name) .unwrap() .expect("exists"); let ca = Cert::from_bytes(&ca).expect("valid"); assert!(ca.is_tsk()); let (_tag, cert) = certd.certd() .get(&ca.fingerprint().to_string()) .unwrap() .expect("exists"); let cert = Cert::from_bytes(&cert).expect("valid"); assert!(! cert.is_tsk()); assert_eq!(ca.fingerprint(), cert.fingerprint()); ca }; let (koo, _tag) = certd.shadow_ca( "keys.openpgp.org", true, UserID::from_static_bytes(b"Downloaded from keys.openpgp.org"), 1, &[ ( "public_directories", UserID::from_static_bytes(b"Public Directories"), 10, ), ])?; let cert = check(&CertD::shadow_ca_special("keys.openpgp.org")); assert_eq!(cert.fingerprint(), koo.fingerprint()); let public_directories = check(&CertD::shadow_ca_special("public_directories")); let trust_root = check(&CertD::shadow_ca_special(cert_d::TRUST_ROOT)); let (dane, _tag) = certd.shadow_ca( "dane", true, UserID::from_static_bytes(b"Downloaded from DANE"), 1, &[ ( "public_directories", UserID::from_static_bytes(b"Public Directories"), 10), ])?; let cert = check(&CertD::shadow_ca_special("dane")); assert_eq!(cert.fingerprint(), dane.fingerprint()); let cert = check(&CertD::shadow_ca_special("keys.openpgp.org")); assert_eq!(cert.fingerprint(), koo.fingerprint()); let cert = check(&CertD::shadow_ca_special("public_directories")); assert_eq!(cert, public_directories); let cert = check(&CertD::shadow_ca_special(cert_d::TRUST_ROOT)); assert_eq!(cert, trust_root); let (tofu, _tag) = certd.shadow_ca( "tofu", true, UserID::from_static_bytes(b"Trust on First Use (TOFU)"), 120, &[])?; let cert = check(&CertD::shadow_ca_special("tofu")); assert_eq!(cert.fingerprint(), tofu.fingerprint()); let cert = check(&CertD::shadow_ca_special("dane")); assert_eq!(cert.fingerprint(), dane.fingerprint()); let cert = check(&CertD::shadow_ca_special("keys.openpgp.org")); assert_eq!(cert.fingerprint(), koo.fingerprint()); let cert = check(&CertD::shadow_ca_special("public_directories")); assert_eq!(cert, public_directories); let cert = check(&CertD::shadow_ca_special(cert_d::TRUST_ROOT)); assert_eq!(cert, trust_root); Ok(()) } #[test] fn shadow_ca_keyserver() -> Result<()> { tracer!(true, "keyserver_shadow_ca"); let path = tempfile::tempdir()?; let test = |certd: &CertD, may_create: bool| { let (koo, created) = certd.shadow_ca_keys_openpgp_org().unwrap(); if ! may_create { assert!(! created); } let koo = koo.fingerprint(); let (mailvelope, created) = certd.shadow_ca_keys_mailvelope_com().unwrap(); if ! may_create { assert!(! created); } let mailvelope = mailvelope.fingerprint(); let lookup = |uri, expected: Option<&Fingerprint>| { let result = certd.shadow_ca_keyserver(uri).unwrap(); if let Some(fingerprint) = expected { let (ca, created) = result.expect("valid URI"); if ! may_create { assert!(! created); } assert_eq!(fingerprint, &ca.fingerprint()); } else { assert!(result.is_none()); } }; lookup("hkps://keys.openpgp.org", Some(&koo)); lookup("HKPS://KEYS.OPENPGP.ORG", Some(&koo)); lookup("HKPS://KEYS.OPENPGP.io", Some(&koo)); // hkp is not considered secure. lookup("hkp://keys.openpgp.org", None); // https is not the right protocol. lookup("https://keys.openpgp.org", None); // Not a verifying keyserver. lookup("hkps://keyserver.ubuntu.com", None); lookup("hkps://keys.mailvelope.com", Some(&mailvelope)); }; // The first time through we create the trust root and CAs. t!("Creating CAs"); let certd = CertD::open(&path)?; test(&certd, true); // The second time through we load them from disk. t!("Loading CAs"); let certd2 = CertD::open(&path)?; test(&certd2, false); Ok(()) } fn check_certifiation(certd: &CertD, certifier: &Cert, cert: Fingerprint, count: usize) { const P: &StandardPolicy = &StandardPolicy::new(); let cert: Arc = certd.lookup_by_cert_fpr(&cert).expect("exists"); let cert: &Cert = cert.to_cert().expect("valid cert"); let userids: Vec = cert.userids().collect(); assert_eq!(userids.len(), 1); let certifications = userids[0].valid_certifications_by_key( P, None, certifier.primary_key().key()); assert_eq!(certifications.count(), count); } #[test] fn shadow_ca_cerified() -> Result<()> { // Check that the shadow CA and intermediate CA are actually // certified. tracer!(true, "keyserver_shadow_certified"); let path = tempfile::tempdir()?; // The first time through we create the trust root and CAs. t!("Creating CAs"); let certd = CertD::open(&path)?; let (koo, created) = certd.shadow_ca_keys_openpgp_org().unwrap(); assert!(created); let koo = koo.to_cert().expect("valid cert"); let (pd, created) = certd.public_directory_ca().unwrap(); // This should have been created in the last step. assert!(! created); let pd = pd.to_cert().expect("valid cert"); let (tr, created) = certd.trust_root().unwrap(); // This should have been created in the last step. assert!(! created); let tr = tr.to_cert().expect("valid cert"); let (kmc, created) = certd.shadow_ca_keys_mailvelope_com().unwrap(); assert!(created); let kmc = kmc.to_cert().expect("valid cert"); // Avoid the in-memory cache. let certd = CertD::open(&path)?; t!("Checking that the PD CA certified the KOO shadow CA."); check_certifiation(&certd, &pd, koo.fingerprint(), 1); t!("Checking that the PD CA certified the KMC shadow CA."); check_certifiation(&certd, &pd, kmc.fingerprint(), 1); t!("Checking that the trust root certified the PD CA."); check_certifiation(&certd, &tr, pd.fingerprint(), 1); t!("Checking that the trust root didn't the KOO shadow CA."); check_certifiation(&certd, &tr, koo.fingerprint(), 0); t!("Checking that the trust root didn't the KMC shadow CA."); check_certifiation(&certd, &tr, kmc.fingerprint(), 0); Ok(()) } #[test] fn shadow_ca_for_url() -> Result<()> { tracer!(true, "shadow_ca_for_url"); let path = tempfile::tempdir()?; let test = |certd: &CertD, may_create: bool| { let (alice, created) = certd .shadow_ca_for_url("https://example.org/alice.pgp") .unwrap().unwrap(); if ! may_create { assert!(! created); } let alice = alice.fingerprint(); #[derive(Debug)] enum Expectation { Alice, Other, NoCA } use Expectation::*; let lookup = |uri, expectation| { t!("looking up {} expecting {:?}", uri, expectation); let result = certd.shadow_ca_for_url(uri).unwrap(); match expectation { Alice => { let (ca, created) = result.expect("valid URI"); if ! may_create { assert!(! created); } assert_eq!(alice, ca.fingerprint()); }, Other => { let (_ca, created) = result.expect("valid URI"); if ! may_create { assert!(! created); } }, NoCA => { assert!(result.is_none()); }, } }; lookup("https://example.org/alice.pgp", Alice); lookup("HTTPS://EXAMPLE.ORG/alice.pgp", Alice); lookup("https://foo@example.org/alice.pgp", Alice); lookup("https://foo:bar@example.org/alice.pgp", Alice); lookup("https://foo:bar@example.org/alice.pgp#fragment", Alice); // http is not considered secure. lookup("http://example.org/alice.pgp", NoCA); // hkps is not the right protocol. lookup("hkps://example.org/alice.pgp", NoCA); // Different URLs. lookup("https://example.org/bob.pgp", Other); lookup("https://example.net/alice.pgp", Other); lookup("https://example.org/alice.pgp?some=query", Other); lookup("https://example.org/alice.pgp/other/path", Other); }; // The first time through we create the trust root and CAs. t!("Creating CAs"); let certd = CertD::open(&path)?; test(&certd, true); // The second time through we load them from disk. t!("Loading CAs"); let certd2 = CertD::open(&path)?; test(&certd2, false); Ok(()) } #[test] fn shadow_ca_for_web_cerified() -> Result<()> { // Check that the shadow CA and intermediate CA are actually // certified. tracer!(true, "shadow_ca_for_web_cerified"); let path = tempfile::tempdir()?; // The first time through we create the trust root and CAs. t!("Creating CAs"); let certd = CertD::open(&path)?; let (alice, created) = certd .shadow_ca_for_url("https://example.org/alice.pgp") .unwrap().unwrap(); assert!(created); let alice = alice.to_cert().expect("valid cert"); let (tr, created) = certd.trust_root().unwrap(); // This should have been created in the first step. assert!(! created); let tr = tr.to_cert().expect("valid cert"); let (pd, created) = certd.public_directory_ca().unwrap(); // This should have been created in the first step. assert!(! created); let pd = pd.to_cert().expect("valid cert"); let (web, created) = certd.shadow_ca_web().unwrap(); // This should have been created in the first step. assert!(! created); let web = web.to_cert().expect("valid cert"); // Avoid the in-memory cache. let certd = CertD::open(&path)?; t!("Checking that the trust root certified the PD CA."); check_certifiation(&certd, &tr, pd.fingerprint(), 1); t!("Checking that the PD CA certified the web CA."); check_certifiation(&certd, &pd, web.fingerprint(), 1); t!("Checking that the web CA certified the ALICE shadow CA."); check_certifiation(&certd, &web, alice.fingerprint(), 1); t!("Checking that the trust root didn't the web CA."); check_certifiation(&certd, &tr, web.fingerprint(), 0); t!("Checking that the trust root didn't the ALICE shadow CA."); check_certifiation(&certd, &tr, alice.fingerprint(), 0); t!("Checking that the PD CA didn't the ALICE shadow CA."); check_certifiation(&certd, &pd, alice.fingerprint(), 0); Ok(()) } /// Test that we can create and sanitize, or reject CAs for funny /// URLs. #[test] fn shadow_ca_for_url_escapes() -> Result<()> { tracer!(true, "shadow_ca_for_url_escapes"); let path = tempfile::tempdir()?; let test = |certd: &CertD, url, stripped| { t!("trying {:?}", url); let (alice, _created) = certd.shadow_ca_for_url(url).unwrap().unwrap(); if let Some(c) = stripped { // Make sure the character is stripped. assert!(alice.userids().all( |uid| ! str::from_utf8(uid.value()).unwrap().contains(c))); } else { // Make sure the odd character is percent-encoded. assert!(alice.userids().all( |uid| str::from_utf8(uid.value()).unwrap().contains("%"))); } }; let test_fail = |certd: &CertD, url| { t!("trying {:?}", url); certd.shadow_ca_for_url(url).unwrap_err(); }; let certd = CertD::open(&path)?; // In the path: test(&certd, "https://example.org/alice\u{0a}bob.pgp", Some("\u{0a}")); test(&certd, "https://example.org/alice\u{0d}bob.pgp", Some("\u{0d}")); test(&certd, "https://example.org/alice\u{09}bob.pgp", Some("\u{09}")); test(&certd, "https://example.org/alice\u{0c}bob.pgp", Some("\u{0c}")); test(&certd, "https://example.org/alice\u{08}\u{08}\u{08}\u{08}\u{08}bob.pgp", None); test(&certd, "https://example.org/alice\u{200f}bob.pgp", None); // RTL mark // In the query: test(&certd, "https://example.org/alice.pgp?q=\u{0a}bob.pgp", Some("\u{0a}")); test(&certd, "https://example.org/alice.pgp?q=\u{0d}bob.pgp", Some("\u{0d}")); test(&certd, "https://example.org/alice.pgp?q=\u{09}bob.pgp", Some("\u{09}")); test(&certd, "https://example.org/alice.pgp?q=\u{0c}bob.pgp", Some("\u{0c}")); test(&certd, "https://example.org/alice.pgp?q=\u{08}\u{08}\u{08}\u{08}\u{08}bob.pgp", None); test(&certd, "https://example.org/alice.pgp?q=\u{200f}bob.pgp", None); // RTL mark // In the host: test(&certd, "https://example.\u{0a}bob.pgp.org/alice.pgp", Some("\u{0a}")); test(&certd, "https://example.\u{0d}bob.pgp.org/alice.pgp", Some("\u{0d}")); test(&certd, "https://example.\u{09}bob.pgp.org/alice.pgp", Some("\u{09}")); test_fail(&certd, "https://example.\u{0c}bob.pgp.org/alice.pgp"); test_fail(&certd, "https://example.\u{08}\u{08}\u{08}\u{08}\u{08}bob.pgp.org/alice.pgp"); test_fail(&certd, "https://example.\u{200f}bob.pgp.org/alice.pgp"); // RTL mark Ok(()) } } sequoia-cert-store-0.7.1/src/store/certd.rs000064400000000000000000002337131046102023000167670ustar 00000000000000use std::borrow::Cow; use std::collections::BTreeSet; use std::collections::btree_map::{BTreeMap, Entry}; use std::ffi::OsString; use std::path::Path; use std::path::PathBuf; use std::str; use std::sync::Arc; use std::sync::Mutex; use std::sync::RwLock; use std::time::{Duration, Instant}; use std::str::FromStr; use anyhow::Context; use rusqlite::{ Connection, Error as RError, ErrorCode, OpenFlags, Result as RResult, Transaction, TransactionBehavior, params, }; use smallvec::{SmallVec, smallvec}; use sequoia_openpgp as openpgp; use openpgp::Fingerprint; use openpgp::KeyHandle; use openpgp::KeyID; use openpgp::Result; use openpgp::cert::prelude::*; use openpgp::cert::raw::{RawCert, RawCertParser}; use openpgp::packet::UserID; use openpgp::parse::Parse; use openpgp::parse::buffered_reader as br; use openpgp_cert_d as cert_d; use crate::LazyCert; use crate::store::MergeCerts; use crate::store::{Store, StoreError}; use crate::store::StoreUpdate; use crate::store::UserIDIndex; use crate::store::UserIDQueryParams; const TRACE: bool = cfg!(test) || crate::TRACE; mod cache; use cache::CertdSignatureVerificationCache; mod shadow_ca; use shadow_ca::CA; /// Do not scan more often than every SCAN_LIMIT. #[cfg(not(test))] const SCAN_LIMIT: Duration = Duration::from_millis(50); /// No SCAN_LIMIT for tests. We want to make race conditions and lock /// contention more likely. #[cfg(test)] const SCAN_LIMIT: Duration = Duration::from_millis(0); /// The tag is the tag of the in-memory index. /// /// We need to keep both the persistent index (the DB) in sync /// with the disk, and the in-memory index in sync with the disk. /// To detect if the persistent index is out of sync, we compare /// the tag stored in the persistent index with the cert's Tag. /// /// This is not enough to keep the in-memory index in sync. /// Consider: an external process changes the cert-d, and updates /// the persistent index. The cert-d and persistent index are /// consistent, but our in-memory index is out of sync! /// /// Thus, we track the tag that the in-memory index corresponds /// to. This tag is associated with the connection to avoid /// taking additional locks. struct DB { in_memory_tag: Option, conn: Connection, } /// Statistics about the number of times the in-memory index, and the /// persistent index were loaded. pub(crate) struct LoadStats { /// The number of times the in-memory index was loaded from the /// database. #[cfg(test)] in_memory_loads: std::sync::atomic::AtomicUsize, /// The number of times the persistent index was loaded from the /// cert-d. #[cfg(test)] persistent_index_scans: std::sync::atomic::AtomicUsize, } impl LoadStats { /// Gets the in-memory-load counter. /// /// The number of times the in-memory index was loaded from the /// database. /// /// This counter is incremented when we load the persistent index /// into memory. Note: when the memory index is initialized as a /// side effect of creating the persistent index, this is not /// incremented. #[allow(dead_code)] pub(crate) fn in_memory_loads(&self) -> usize { #[allow(unused_mut, unused_assignments)] let mut counter = 0; #[cfg(test)] { counter = self.in_memory_loads.load( std::sync::atomic::Ordering::Relaxed); } counter } /// Increments the in-memory-load counter. fn in_memory_loads_inc(&self) { #[cfg(test)] self.in_memory_loads.fetch_add( 1, std::sync::atomic::Ordering::Relaxed); } /// Gets the persistent-index-scans counter. /// /// The number of times the persistent index was loaded from the /// cert-d. /// /// This counter is incremented when we scan the cert-d in order /// to build or update the persistent index. #[allow(dead_code)] pub(crate) fn persistent_index_scans(&self) -> usize { #[allow(unused_mut, unused_assignments)] let mut counter = 0; #[cfg(test)] { counter = self.persistent_index_scans.load( std::sync::atomic::Ordering::Relaxed); } counter } /// Increments the persistent-index-scans counter. fn persistent_index_scans_inc(&self) { #[cfg(test)] self.persistent_index_scans.fetch_add( 1, std::sync::atomic::Ordering::Relaxed); } } impl Default for LoadStats { fn default() -> Self { Self { #[cfg(test)] in_memory_loads: 0.into(), #[cfg(test)] persistent_index_scans: 0.into(), } } } /// A cert-d backed store with persistent indices. /// /// We use the cert-d as the source of truth, and maintain in-core /// indices mapping from key handles, subkey handles, and user IDs to /// cert fingerprints. We serve all requests from the in-core caches. /// /// We persist the indices in SQLite to cache them, but this is a /// best-effort mechanism: concurrent database mutators may have /// exclusive write rights to the database and our update may fail. /// If that happens, we stop trying to update the database, but still /// update the in-core indices. We abort the transaction. And, since /// we didn't update the cert-d tag in the database, we (or another /// process) will recognize the db as stale next time it is accessed, /// and update it. /// /// # Lock order /// /// This struct uses a series of locks, which must be taken in the /// following order to avoid deadlocks: /// /// - CertD::cas /// - CertD::conn /// - CertD::last_scan /// - CertD::certs_cache /// - CertD::index_cache pub struct CertD<'a> { certd: cert_d::CertD, /// A cache of the trust root, and shadow CAs keyed by the cert-d /// special name. cas: Mutex>>, path: PathBuf, conn: Mutex, /// Cache indexed by primary key fingerprint. certs_cache: RwLock>, cert_d::Tag)>>, /// We rate limit scans for changes. This is the last time a scan /// was done (if ever). last_scan: Mutex>, /// Indices mapping from (sub)key fingerprint, (sub)key key ID, /// and user ID to certificate fingerprint. index_cache: RwLock, pub(crate) load_stats: LoadStats, // We keep this for its drop handler. #[allow(dead_code)] signature_cache: Option, } assert_send_and_sync!(CertD<'_>); /// Indices mapping from (sub)key fingerprint, (sub)key key ID, and /// user ID to certificate fingerprint. #[derive(Default)] struct Index { /// Maps cert and subkey fingerprints to cert fingerprints. /// /// The values are a sorted, de-duplicated list of certificate /// fingerprints. Use `insert_unique` to maintain order and /// uniqueness. by_key_fingerprint: BTreeMap>, /// Maps cert and subkey key IDs to cert fingerprints. /// /// The values are a sorted, de-duplicated list of certificate /// fingerprints. Use `insert_unique` to maintain order and /// uniqueness. by_key_id: BTreeMap>, /// Maps user IDs to cert fingerprints. /// /// XXX: UserIDIndex has an internal RwLock. We could avoid that /// by using UserIDIndexInner, but the query interface is on /// UserIDIndex. by_userid: UserIDIndex, } /// Inserts `fp` into `bucket` if it is not present. fn insert_unique(bucket: &mut SmallVec<[Fingerprint; 1]>, fp: Cow) { if let Err(i) = bucket.binary_search(&fp) { bucket.insert(i, fp.into_owned()); } } impl<'a> CertD<'a> { /// Returns the canonicalized path. /// /// If path is `None`, then returns the default location. fn path(path: Option<&Path>) -> Result { if let Some(path) = path { Ok(path.to_owned()) } else { Ok(cert_d::CertD::user_configured_store_path()?) } } /// Opens the default cert-d for reading and writing. pub fn open_default() -> Result { let path = Self::path(None)?; Self::open(path) } /// Opens a cert-d for reading and writing. pub fn open

(path: P) -> Result where P: AsRef, { tracer!(TRACE, "CertD::open"); let path = path.as_ref(); t!("loading cert-d {:?}", path); let certd = cert_d::CertD::with_base_dir(&path) .map_err(|err| { t!("While opening the certd {:?}: {}", path, err); let err = anyhow::Error::from(err) .context(format!("While opening the certd {:?}", path)); err })?; // We include the hostname in the file name to prevent having // to share databases and their locks across network file // systems. let mut database_name = OsString::from("_sequoia_cert_store_index_v1_on_"); database_name.push(&gethostname::gethostname()); database_name.push(".sqlite"); let index_path = path.join(database_name); let mut conn = Connection::open_with_flags( &index_path, OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_PRIVATE_CACHE)?; if false { // TRACE { fn trace(m: &str) { eprintln!("SQLite: {}", m); } conn.trace(Some(trace)); } // for tests, deactivate the 'double-quoted string literal' feature #[cfg(test)] { conn.set_db_config(rusqlite::config::DbConfig::SQLITE_DBCONFIG_DQS_DDL, false)?; conn.set_db_config(rusqlite::config::DbConfig::SQLITE_DBCONFIG_DQS_DML, false)?; } conn.pragma_update_and_check(None, "secure_delete", true, |_| Ok(()))?; conn.pragma_update(None, "foreign_keys", true)?; conn.pragma_update_and_check(None, "journal_mode", "WAL", |_| Ok(()))?; let certd = Self { certd, cas: Default::default(), path: path.into(), conn: Mutex::new(DB { in_memory_tag: None, conn, }), certs_cache: Default::default(), last_scan: Mutex::new(None), index_cache: RwLock::new(Index { by_key_fingerprint: Default::default(), by_key_id: Default::default(), by_userid: Default::default(), }), load_stats: Default::default(), signature_cache: CertdSignatureVerificationCache::cache_file(path) .map(|filename| { CertdSignatureVerificationCache::load(filename) }) .ok(), }; certd.initialize()?; Ok(certd) } /// Returns a reference to the low-level `CertD`. pub fn certd(&self) -> &cert_d::CertD { &self.certd } /// Returns a mutable reference to the low-level `CertD`. pub fn certd_mut(&mut self) -> &mut cert_d::CertD { &mut self.certd } /// Initializes a certd by creating the persistent index, reading /// the entries and populating the index. fn initialize(&self) -> Result<()> { tracer!(TRACE, "CertD::initialize"); let mut db = self.conn.lock().unwrap(); let tx = Transaction::new_unchecked( &db.conn, TransactionBehavior::Deferred)?; // First, see which database version we're dealing with. match self.db_version(&tx) { Ok(1) => { drop(tx); self.scan(&mut db)?; Ok(()) }, Ok(n) => { t!("Expected version 1, got version {}. Re-initializing.", n); Self::initialize_v1(&tx)?; tx.commit()?; self.initial_scan(&mut db)?; Ok(()) }, Err(RError::SqliteFailure(e, _)) if e.code == ErrorCode::Unknown => { t!("Initial indexing."); Self::initialize_v1(&tx)?; tx.commit()?; self.initial_scan(&mut db)?; Ok(()) }, Err(e) => Err(e.into()), } } /// Creates the disk index, doing the initial scan in parallel. fn initial_scan(&self, db: &mut DB) -> Result<()> { tracer!(false, "CertD::initial_scan"); let tx = Transaction::new_unchecked( &db.conn, TransactionBehavior::Deferred)?; let mut commit = true; let mut last_scan = self.last_scan.lock().unwrap(); let certd_tag = self.certd.tag(); t!("Initial certd tag is {:?}", certd_tag); // Load all certs without canonicalizing them. self.prefetch_internal(true, &[], false); for (fp, (cert, tag)) in self.certs_cache.read().unwrap().iter() { self.db_insert( &tx, &mut commit, tag.clone(), fp, Box::new(cert.keys().map(|sk| sk.fingerprint())), Box::new(cert.userids()))?; } // Save some cycles next time. self.db_set_certd_tag(&tx, certd_tag)?; t!("Setting DB tag to {:?}", certd_tag); if commit { tx.commit()?; } else { tx.rollback()?; } *last_scan = Some(Instant::now()); self.load_stats.persistent_index_scans_inc(); // As a side effect of updating the persistent index, we build // the in-memory index. db.in_memory_tag = Some(certd_tag); Ok(()) } /// Initializes the in-memory index from the database. /// /// This does a hard reinitialization. It's up to the caller to /// decide whether this is necessary. fn initialize_in_memory_index(&self, tx: &Transaction) -> Result<()> { tracer!(TRACE, "CertD::initialize_in_memory_index"); let mut index_cache = self.index_cache.write().unwrap(); let mut by_key_fingerprint = || -> Result<_> { let mut stmt = tx.prepare("SELECT fingerprint, cert_fingerprint \ FROM keys")?; let r = stmt.query_map( [], |row| { Ok((Fingerprint::from_str(&row.get::<_, String>(0)?), Fingerprint::from_str(&row.get::<_, String>(1)?))) })? .filter_map(|row| { if let Ok((subkey, cert)) = row { if let (Ok(subkey), Ok(cert)) = (subkey, cert) { Some((subkey, smallvec![cert])) } else { None } } else { None } }) .collect::)>>(); Ok(r) }().unwrap_or_else(|_| Default::default()); // Sort by subkey fingerprint, then collate adjacent lists // into the first. by_key_fingerprint.sort_unstable_by(|a, b| a.0.cmp(&b.0)); // We maintain a pointer to the first item within a run of the // same fingerprint. We'll collate all other fingerprints // into the bucket of the first item. let mut current: Option<&mut (Fingerprint, SmallVec<[Fingerprint; 1]>)> = None; for item in by_key_fingerprint.iter_mut() { if let Some(c) = current.as_mut() { if c.0 == item.0 { insert_unique(&mut c.1, Cow::Owned(item.1.pop().unwrap())); } else { // The current run of fingerprints has ended and // it is time to update the pointer. current = Some(item); } } else { // Current was None. We found our first current. current = Some(item); } } // Only retain the entries with non-empty buckets. by_key_fingerprint.retain(|(_, cert_bucket)| ! cert_bucket.is_empty()); let by_key_id = by_key_fingerprint.iter() .map(|(subkey, cert_bucket)| { (KeyID::from(subkey), cert_bucket.clone()) }) .collect(); let by_userid = || -> Result<_> { let mut stmt = tx.prepare( "SELECT cert_fingerprint, userid, email \ FROM userids")?; let r = stmt.query_map( [], |row| { Ok((Fingerprint::from_str(&row.get::<_, String>(0)?), UserID::from(row.get::<_, String>(1)?), row.get::<_, Option>(2)?)) })? .filter_map(|row| { if let Ok((Ok(fp), userid, email)) = row { Some((fp, userid, email.filter(|e| ! e.is_empty()))) } else { None } }) .collect::(); Ok(r) }().unwrap_or_else(|_| Default::default()); *index_cache = Index { by_key_fingerprint: by_key_fingerprint.into_iter().collect(), by_key_id, by_userid, }; self.load_stats.in_memory_loads_inc(); Ok(()) } /// Makes sure that the on-disk index and the in-memory index are /// up-to-date. fn scan(&self, db: &mut DB) -> Result<()> { tracer!(false, "CertD::scan"); // Rate-limit scanning. Don't rescan if we did a scan (very) // recently. let mut last_scan = self.last_scan.lock().unwrap(); let now = Instant::now(); if let Some(last_scan) = last_scan.as_ref() { if now < *last_scan { // Time travel? t!("Maybe rescanning (time travel)"); } else if now.duration_since(*last_scan) > SCAN_LIMIT { // It's been too long since the last scan. t!("Maybe rescanning"); } else { return Ok(()) }; } else { t!("Never scanned"); } // We hold last_scan. This blocks other threads from doing a // scan, which is exactly what we want as when they wake up, // they'll see that a scan was just done. *last_scan = Some(now); // See if the database is up-to-date. let disk_certd_tag = self.certd.tag(); // Start a transaction in deferred mode. We may still // discover that then database is current, so we don't need an // immediate write lock on the database. let tx = Transaction::new_unchecked( &db.conn, TransactionBehavior::Deferred)?; let mut commit = true; let db_tag = self.db_certd_tag(&tx)?; if db_tag == Some(disk_certd_tag) { // It is. We're done. t!("Persistent index is up to date, exiting."); if db.in_memory_tag != Some(disk_certd_tag) { t!("In-memory index's tag does not match disk tag, reloading"); self.initialize_in_memory_index(&tx)?; db.in_memory_tag = db_tag; } else { t!("In-memory index's tag matches disk tag, exiting"); } return Ok(()); } t!("Persistent index's tag does not match disk tag, rescanning"); // Read all tags indexed by fingerprint into the core. This // has two benefits. First, we have a quick way to compare // tags later. Second, we can detect corrupted fingerprints // and key IDs and remove them from the index. let mut stmt = tx.prepare_cached( "SELECT rowid, fingerprint, keyid, tag FROM certs")?; let mut bad = Vec::new(); let tags: BTreeMap<_, _> = stmt.query([])? .mapped(|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?))) .filter_map(|row| { let (rowid, fp_str, keyid_str, tag): (i64, String, String, i64) = row.ok()?; match (fp_str.parse::(), keyid_str.parse::()) { (Ok(fp), Ok(_)) => Some((fp, db2tag(tag))), _ => { bad.push(rowid); None }, } }) .collect(); drop(stmt); for bad_rowid in bad { commit = commit && { let mut stmt = tx.prepare_cached( "DELETE FROM certs WHERE rowid = ?0")?; ignore_sqlite_busy(stmt.execute(params![bad_rowid]))? }; } // Scan the subkey index for corrupted fingerprints and key // IDs and remove them from the index. let mut stmt = tx.prepare_cached( "SELECT cert_fingerprint, fingerprint, keyid FROM keys")?; let mut bad = BTreeSet::new(); for row in stmt.query([])? .mapped(|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?))) { let (cert_fp, fp_str, keyid_str): (String, String, String) = row?; match (fp_str.parse::(), keyid_str.parse::()) { (Ok(_), Ok(_)) => (), _ => { bad.insert(cert_fp); }, } } drop(stmt); for bad_cert_fp in bad { commit = commit && { let mut stmt = tx.prepare_cached( "DELETE FROM keys WHERE cert_fingerprint = ?0")?; ignore_sqlite_busy(stmt.execute(params![bad_cert_fp]))? }; } // Scan the subkey index for corrupted fingerprints and key // IDs and remove them from the index. let mut stmt = tx.prepare_cached( "SELECT cert_fingerprint FROM userids")?; let mut bad = BTreeSet::new(); for row in stmt.query([])? .mapped(|r| Ok(r.get::<_, String>(0)?)) { if let Ok(fp) = row { if fp.parse::().is_err() { bad.insert(fp); } } } drop(stmt); for bad_cert_fp in bad { commit = commit && { let mut stmt = tx.prepare_cached( "DELETE FROM userids WHERE cert_fingerprint = ?0")?; ignore_sqlite_busy(stmt.execute(params![bad_cert_fp]))? }; } let transaction = |commit: &mut bool, fp_str: &str, file| -> Result<()> { t!("Considering {}", fp_str); let fp: Fingerprint = fp_str.parse()?; let tag = cert_d::Tag::try_from(&file)?; if tags.get(&fp) == Some(&tag) { t!("{} is up to date!", fp_str); return Ok(()); } let mut parser = br::File::new_with_cookie( file, self.certd.get_path(fp_str)?, Default::default()) .map_err(Into::into) .and_then(RawCertParser::from_buffered_reader) .map_err(|err| { t!("While reading {:?} from the certd {:?}: {}", fp, self.path, err); err })?; match parser.next() { Some(Ok(cert)) => { t!("inserting"); self.insert_rawcert(&tx, commit, cert, tag)?; Ok(()) }, Some(Err(err)) => { t!("While parsing {:?} from the certd {:?}: {}", fp, self.path, err); Err(err) }, None => { t!("While parsing {:?} from the certd {:?}: empty file", fp, self.path); Err(anyhow::anyhow!("empty file")) }, } }; for (fp, file) in self.certd.iter_files().filter_map(cert_d::Result::ok) { if let Err(e) = transaction(&mut commit, &fp, file) { t!("{}: {}", fp, e); let mut stmt = tx.prepare_cached( "DELETE FROM certs WHERE fingerprint = ?1")?; commit = commit && ignore_sqlite_busy(stmt.execute(params![fp]))?; } } // Save some cycles next time. self.db_set_certd_tag(&tx, disk_certd_tag)?; // Make sure the in-memory index is up to date. if db_tag == db.in_memory_tag { // The in-memory index was consistent with the persistent // index. As we updated the persistent index, we also // updated the in-memory index so the in-memory index is // not also consistent with the cert-d. db.in_memory_tag = Some(disk_certd_tag); } else if db.in_memory_tag != Some(disk_certd_tag) { // The in-memory index was not consistent with the cert-d. // Rebuild it. t!("In-memory index out of date relative to db, reloading"); self.initialize_in_memory_index(&tx)?; db.in_memory_tag = db_tag; } *last_scan = Some(Instant::now()); if commit { tx.commit()?; } else { tx.rollback()?; } self.load_stats.persistent_index_scans_inc(); Ok(()) } /// Returns the database version. fn db_version(&self, tx: &Transaction) -> RResult { let mut stmt = tx.prepare_cached( "SELECT version FROM version WHERE id == 0")?; let r = stmt.query([])?.mapped(|r| r.get(0)).next().unwrap_or(Ok(0))?; Ok(r) } /// Returns the certd tag recorded in the database. fn db_certd_tag(&self, tx: &Transaction) -> Result> { let mut stmt = tx.prepare_cached( "SELECT tag FROM certd_tag WHERE id == 0")?; let r = stmt.query([])?.mapped(|r| Ok(db2tag(r.get(0)?))) .next().transpose()?; Ok(r) } /// Sets the certd tag recorded in the database. fn db_set_certd_tag(&self, tx: &Transaction, t: cert_d::Tag) -> Result<()> { if let Ok(mut stmt) = tx.prepare_cached( "INSERT OR REPLACE INTO certd_tag (id, tag) \ VALUES (0, ?1)") { ignore_sqlite_busy(stmt.execute(params![tag2db(t)]))?; } Ok(()) } /// Inserts the raw cert into the database and in-core cache. /// /// Only updates the database as long as `*commit` is true, and /// will clear the flag if writing to the database fails because /// the database is locked for writing. fn insert_rawcert(&self, tx: &Transaction, commit: &mut bool, cert: RawCert<'a>, tag: cert_d::Tag) -> Result<()> { let fp = cert.fingerprint(); self.db_insert( tx, commit, tag, &fp, Box::new(cert.keys().map(|sk| sk.fingerprint())), Box::new(cert.userids()))?; // Now insert it into the cache. self.certs_cache.write().unwrap() .insert(fp, (Arc::new(LazyCert::from(cert)), tag)); Ok(()) } /// Inserts the cert into the database and in-core cache. /// /// `cert_tag` is the certificate's tag. /// /// `certd_tag` is a tuple of the certd's tag prior to the /// insertion, and after the insertion, if known. fn insert_cert(&self, cert: Arc>, cert_tag: cert_d::Tag, certd_tags: Option<(cert_d::Tag, cert_d::Tag)>) -> Result<()> { tracer!(TRACE, "CertD::insert_cert"); let fp = cert.fingerprint(); let mut db = self.conn.lock().unwrap(); let mut set_in_memory_tag = None; if let Ok(tx) = Transaction::new_unchecked( &db.conn, TransactionBehavior::Immediate) { let mut commit = true; self.db_insert( &tx, &mut commit, cert_tag, &fp, Box::new(cert.keys().map(|skb| skb.fingerprint())), Box::new(cert.userids().map(|uidb| uidb.clone())))?; if let Some((pre, post)) = certd_tags { if let Ok(Some(db_certd_tag)) = self.db_certd_tag(&tx) { if pre == db_certd_tag { // If we fail to update the certd tag, nothing // bad will happen: the next time we check the // tag, we'll notice that it's out of date. let _ = self.db_set_certd_tag(&tx, post); t!("DB tag matched cert-d tag, rolling forward"); if Some(pre) == db.in_memory_tag { set_in_memory_tag = Some(post); t!("In-memory tag matched db's cert-d tag, \ rolling forward"); } else { t!("In-memory tag ({:?}) did NOT match db's \ cert-d tag ({:?}), NOT rolling forward \ (to {:?})", db.in_memory_tag, pre, post); } } else { t!("DB tag ({:?}) did NOT match cert-d tag ({:?}), \ NOT rolling forward (to {:?})", db_certd_tag, pre, post); } } } // Commit so that we release the exclusive lock on the // database before acquiring the exclusive lock on the cache. if commit { tx.commit()?; } else { tx.rollback()?; } } if let Some(set_in_memory_tag) = set_in_memory_tag { db.in_memory_tag = Some(set_in_memory_tag); } drop(db); // Now insert it into the cache. self.certs_cache.write().unwrap().insert(fp, (cert, cert_tag)); Ok(()) } /// Inserts information about a cert (subkey fingerprints and user /// IDs) into the database. /// /// Only updates the database as long as `*commit` is true, and /// will clear the flag if writing to the database fails because /// the database is locked for writing. fn db_insert<'i>(&self, tx: &Transaction, commit: &mut bool, tag: cert_d::Tag, cert_fp: &Fingerprint, keys: Box + 'i>, userids: Box + 'i>) -> Result<()> { let keyid = openpgp::KeyID::from(cert_fp); let cert_fp_db = fingerprint2db(cert_fp); // First, insert the cert and get its row id. *commit = *commit && { let mut stmt = tx.prepare_cached( "INSERT OR REPLACE INTO certs (tag, keyid, fingerprint) \ VALUES (?1, ?2, ?3)")?; ignore_sqlite_busy(stmt.execute(params![ tag2db(tag), keyid2db(&keyid), &cert_fp_db]))? }; // Now insert the keys. Note that this doesn't remove // entries for subkeys that have been stripped from the // certificate. let mut stmt = tx.prepare_cached( "INSERT OR IGNORE INTO keys \ (cert_fingerprint, keyid, fingerprint) \ VALUES (?1, ?2, ?3)")?; // And, also update the in-memory index. let mut index = self.index_cache.write().unwrap(); for subkey_fp in keys { let keyid = openpgp::KeyID::from(&subkey_fp); *commit = *commit && ignore_sqlite_busy(stmt.execute(params![ &cert_fp_db, keyid2db(&keyid), fingerprint2db(&subkey_fp)]))?; insert_unique(index.by_key_fingerprint.entry(subkey_fp).or_default(), Cow::Borrowed(cert_fp)); insert_unique(index.by_key_id.entry(keyid).or_default(), Cow::Borrowed(cert_fp)); } // Now insert the user IDs. Note that this doesn't remove // entries for user IDs that have been stripped from the // certificate. let mut by_userid = index.by_userid.inner_mut(); for uid in userids { let uidstr = String::from_utf8_lossy(uid.value()).to_string(); let email = uid.email_normalized().ok().flatten(); by_userid.insert(cert_fp.clone(), uid, email.clone()); *commit = *commit && { let domain = email.as_ref().and_then(|e| e.rfind("@").map(|i| &e[i + 1..])); let mut stmt = tx.prepare_cached( "INSERT OR IGNORE INTO userids \ (cert_fingerprint, userid, email, domain) \ VALUES (?1, ?2, ?3, ?4)")?; ignore_sqlite_busy(stmt.execute(params![ &cert_fp_db, uidstr, email, domain]))? }; } drop(by_userid); drop(index); Ok(()) } /// Initializes the version 1 persistent indices. /// /// Any existing content is lost. fn initialize_v1(tx: &Transaction) -> Result<()> { tx.execute_batch(" -- A table identifying the version and a human-readable magic. DROP TABLE IF EXISTS version; CREATE TABLE version ( id INTEGER PRIMARY KEY, version INTEGER NOT NULL, comment TEXT NOT NULL ); -- Record the schema version. INSERT OR IGNORE INTO version VALUES (0, 1, 'sequoia cert cache v1'); -- Tag of the certd directory to quickly detect whether the db is stale. DROP TABLE IF EXISTS certd_tag; CREATE TABLE certd_tag ( id INTEGER PRIMARY KEY, tag INTEGER NOT NULL ); -- A table for all the certificates. DROP TABLE IF EXISTS certs; CREATE TABLE certs ( tag INTEGER NOT NULL, keyid TEXT NOT NULL, fingerprint TEXT PRIMARY KEY NOT NULL ); CREATE INDEX IF NOT EXISTS certs_keyid ON certs (keyid); -- A mapping from subkey key IDs and fingerprints to certs. DROP TABLE IF EXISTS keys; CREATE TABLE keys ( cert_fingerprint TEXT NOT NULL, keyid TEXT NOT NULL, fingerprint TEXT NOT NULL, FOREIGN KEY(cert_fingerprint) REFERENCES certs(fingerprint) ON DELETE CASCADE, UNIQUE(cert_fingerprint, fingerprint) ); CREATE INDEX IF NOT EXISTS keys_fingerprint ON keys (fingerprint); CREATE INDEX IF NOT EXISTS keys_keyid ON keys (keyid); -- A mapping from user IDs to certs. DROP TABLE IF EXISTS userids; CREATE TABLE userids ( cert_fingerprint TEXT NOT NULL, userid TEXT NOT NULL, email TEXT, domain TEXT, FOREIGN KEY(cert_fingerprint) REFERENCES certs(fingerprint) ON DELETE CASCADE, UNIQUE(cert_fingerprint, userid) ); CREATE INDEX IF NOT EXISTS userids_userid ON userids (userid); CREATE INDEX IF NOT EXISTS userids_email ON userids (email); CREATE INDEX IF NOT EXISTS userids_domain ON userids (domain); ")?; Ok(()) } /// Loads the cert identified by `fp` either from the cache or the /// disk. /// /// This takes the `CertD::certs_cache` and `CertD::index_cache` /// locks. The caller must be careful to not hold them. fn load(&self, fp: &Fingerprint) -> Result>> { tracer!(TRACE, "CertD::load"); let fp_name = format!("{:x}", fp); // We want to retry once. See below for the explanation. let mut tries = 1; 'retry: loop { let transaction = || { let fh = self.certd.get_file(&fp_name)? .ok_or_else(|| StoreError::NotFound(fp.into()))?; let disk_tag = cert_d::Tag::try_from(&fh)?; Ok((fh, disk_tag)) }; let (fh, disk_tag) = transaction().map_err(|e: anyhow::Error| { // Evict the entry from the cache. self.certs_cache.write().unwrap().remove(fp); e })?; // Happy path: look into the cache without acquiring a write // lock and see if the tag is current. let mut initial_cached_tag = None; if let Some((cached_cert, cached_tag)) = self.certs_cache.read().unwrap().get(fp) { if cached_tag == &disk_tag { t!("{} loaded from cache on happy path", fp); return Ok(cached_cert.clone()); } initial_cached_tag = Some(cached_tag.clone()); } // Slow path: either the cert was not present or outdated. // Load it from disk, and update the cache after the I/O is // done. t!("{} outdated in cache, loaded from disk", fp); let raw = br::File::new_with_cookie( fh, self.certd.get_path(&fp_name)?, Default::default()) .map_err(Into::into) .and_then(RawCert::from_buffered_reader) .map_err(|e| { t!("Failed to load cert: {}", e); e })?; let update_index = |raw: &RawCert| { let mut index = self.index_cache.write().unwrap(); for subkey in raw.keys() { let subkey_fp = subkey.fingerprint(); let keyid = openpgp::KeyID::from(&subkey_fp); insert_unique( index.by_key_fingerprint.entry(subkey_fp).or_default(), Cow::Borrowed(fp)); insert_unique( index.by_key_id.entry(keyid).or_default(), Cow::Borrowed(fp)); } index.by_userid.insert(fp, raw.userids()); drop(index); }; // After we acquire the `CertD::certs_cache` lock, we need // to resolve a potential race. // // Let's say we have two threads, A and B. Both threads // want to load the same certificate. At some point, an // external task, E, may modify the on-disk certificate // changing it from C to C'. At the end, C' should be // stored in the cache; C must not overwrite C'. // // - Scenario #0: No mutation, no conflict. This is fine. // // 1. A: Read C // 2. B: Read C // 3. A: Insert C // 4. B: Insert C // // (Steps 1 & 2, and/or steps 3 & 4 can be reversed // without changing the analysis.) // // - Scenario #1: Mutation after A and B read C. This is // fine: the next time some thread reads C, they will // update the cache. // // 1. A: Read C // 2. B: Read C // 3. E: Modifies C -> C' // 4. A: Insert C <- Safe. // 5. B: Insert C <- Safe. // // (Steps 1 & 2, and/or steps 4 & 5 can be reversed // without changing the analysis.) // // - Scenario #2.A: A reads C. C is mutated. B reads C'. // A inserts C into the cache. Then B inserts C' into // the cache. This is fine: the latest version is // stored in the cache. // // 1. A: Read C // 2. E: Modifies C -> C' // 3. B: Read C' // 4. A: Insert C <- Safe. // 5. B: Insert C' <- Safe. // // - Scenario #2.B: B reads C. C is mutated. A reads C'. // B inserts C into the cache. Then A inserts C' into the // cache. This is fine: the latest version is stored in // the cache. // // 1. B: Read C // 2. E: Modifies C -> C' // 3. A: Read C' // 4. B: Insert C <- Safe. // 5. A: Insert C' <- Safe. // // - Scenario #3.A: A reads C. C is mutated. B reads C', // and inserts it into the cache. A inserts C into the // cache overwriting C'. STEP 5 IS A DISASTER. // // 1. A: Read C // 2. E: Modifies C -> C' // 3. B: Read C' // 4. B: Insert C' <- Safe. // 5. A: Insert C <- DOOM. // // - Scenario #3.B: B reads C. C is mutated. A reads C', // and inserts it into the cache. B inserts C into the // cache overwriting C'. STEP 5 IS A DISASTER. // // 1. B: Read C // 2. E: Modifies C -> C' // 3. A: Read C' // 4. A: Insert C' <- Safe. // 5. B: Insert C <- DOOM. // // To prevent scenario #3, we can do the following: after // reacquiring the lock, but before inserting the // certificate into the cache, the thread checks that: // // - The tag in the cache (`new_cached_tag`) matches // what it originally saw in the cache // (`initial_cached_tag`) // // If this is the case, then no one else updated the // in-memory cache, so it is safe to update it. // // - OR, the tag in the cache (`new_cached_tag`) matches // what it originally saw on disk (`disk_tag`) // // If this is the case, then someone else updated the // in-memory cache with the same update the thread // wants to make, so it is safe to return what's in // the in-memory cache. // // If neither hold, then the in-memory cache was updated // to a different value, and the current thread doesn't // know if it has C or C'. // // Unfortunately, this also means that these conditions // hold for step 2.A.5 and step 2.B.5. // // As such we can use the following mitigation to check // that the current thread is NOT at step 3.A.5 or step // 3.B.5: // // ```text // let safe = new_cached_tag == initial_cached_tag // || new_cached_tag == disk_tag; // ``` // // If this condition holds, we simply retry. If we race // again, then we we prefer to avoid live lock. match self.certs_cache.write().unwrap().entry(fp.clone()) { Entry::Occupied(mut e) => { let (cached_cert, new_cached_tag) = e.get(); if &disk_tag == new_cached_tag { // Another thread updated the in-memory cache // with what we expect. Use the in-memory // cache. t!("{} lost race: loading from cache", fp); return Ok(cached_cert.clone()); } else if initial_cached_tag.as_ref() == Some(new_cached_tag) { // The in-memory cache was not updated. // Insert what we read. t!("{}: won race: updating index", fp); update_index(&raw); let cert = Arc::new(LazyCert::from(raw)); e.insert((cert.clone(), disk_tag)); return Ok(cert); } else if tries > 0 { // The tag changed. We don't know which tag // is newer, so we redo the lookup (there // could have been two quick updates, we got // the old tag and cert, the other thread that // won the race got the new tag and cert). In // the likely case that the cached_tag is the // current version, the next iteration will // take the happy path. t!("{} tag mismatch, retrying lookup once", fp); tries -= 1; continue 'retry; } else { // There's high contention. // // To break possible live lock, take one. t!("{} tag mismatch, avoiding live lock", fp); update_index(&raw); let cert = Arc::new(LazyCert::from(raw)); e.insert((cert.clone(), disk_tag)); return Ok(cert); } }, Entry::Vacant(e) => { t!("{} not in cache, loaded from disk", fp); update_index(&raw); let cert = Arc::new(LazyCert::from(raw)); e.insert((cert.clone(), disk_tag)); return Ok(cert); }, } } } } /// Serializes a Fingerprint for storage in the database. fn fingerprint2db(fp: &Fingerprint) -> String { format!("{:x}", fp) } /// Serializes a Key ID for storage in the database. fn keyid2db(id: &KeyID) -> String { format!("{:x}", id) } /// Converts a `Tag` into something representable by SQLite. fn tag2db(t: cert_d::Tag) -> i64 { u64::from(t) as i64 } /// Converts a `Tag` from something representable by SQLite. fn db2tag(t: i64) -> cert_d::Tag { (t as u64).into() } /// Ignores SQLITE_BUSY. /// /// Returns `Ok(true)` if the update was successful, `Ok(false)` if /// the database was locked for writing, and any other error as-is. /// /// This can be used to implement stricter transaction semantics than /// SQLite offers. If you have a SQLite transaction in deferred /// (i.e. read) mode, and try to update a table, the update fails with /// SQLITE_BUSY, but that doesn't invalidate the transaction. Hence, /// a second update in the same transaction may succeed. But that is /// not what we want, we want either all updates to succeed, or none. /// /// # Examples /// /// ```rust /// # use rusqlite::*; /// # fn ignore_sqlite_busy(r: Result) -> Result { todo!() } /// # fn f() -> anyhow::Result<()> { /// let conn = Connection::open_in_memory()?; /// let tx = Transaction::new_unchecked( /// &conn, TransactionBehavior::Deferred)?; /// let mut commit = true; /// /// commit = commit && { /// let mut stmt = tx.prepare_cached( /// "CREATE TABLE foo")?; /// ignore_sqlite_busy(stmt.execute(params![]))? /// }; /// /// if commit { /// tx.commit()?; /// } else { /// tx.rollback()?; /// } /// # Ok(()) } /// ``` fn ignore_sqlite_busy(r: rusqlite::Result) -> Result { match r { Ok(_) => Ok(true), Err(e) => if is_sqlite_busy(&e) { Ok(false) } else { Err(e.into()) }, } } /// Tests if the error is SQLITE_BUSY. fn is_sqlite_busy(e: &rusqlite::Error) -> bool { Some(rusqlite::ErrorCode::DatabaseBusy) == e.sqlite_error_code() } /// Asserts that the given vector contains at least one cert, or /// returns the appropriate error. fn ok_or_not_found<'a, K>(handle: K, c: Vec>>) -> Result>>> where K: Into, { if c.is_empty() { Err(crate::store::StoreError::NotFound(handle.into()).into()) } else { Ok(c) } } impl<'a> Store<'a> for CertD<'a> { fn lookup_by_cert(&self, kh: &KeyHandle) -> Result>>> { match kh { KeyHandle::Fingerprint(fp) => Ok(vec![self.load(fp)?]), KeyHandle::KeyID(id) => { let mut db = self.conn.lock().unwrap(); self.scan(&mut db)?; drop(db); let primaries = self.index_cache.read().unwrap().by_key_id.get(id).cloned(); // Note: it is of the utmost importance to compute // `primaries`, then drop the `CertD::index_cache` // lock, then map over self.load, because we must not // hold the lock over calls to self.load. If this is // done on the right hand side of the `if let` // statement, the lock will be held across the call to // self.load. let mut matches = Vec::new(); if let Some(primaries) = primaries { matches = primaries.iter().filter_map(|fp| { if &KeyID::from(fp) == id { Some(self.load(fp)) } else { None } }).collect::>>()?; } // We didn't find a certificate. ok_or_not_found(id, matches) }, } } fn lookup_by_cert_or_subkey(&self, kh: &KeyHandle) -> Result>>> { tracer!(false, "lookup_by_cert_or_subkey"); t!("{}", kh); let mut db = self.conn.lock().unwrap(); self.scan(&mut db)?; drop(db); match kh { KeyHandle::Fingerprint(fp) => { let primaries = self.index_cache.read().unwrap() .by_key_fingerprint.get(fp).cloned(); // Note: it is of the utmost importance to compute // `primaries`, then drop the `CertD::index_cache` // lock, then map over self.load, because we must not // hold the lock over calls to self.load. If this is // done on the right hand side of the `if let` // statement, the lock will be held across the call to // self.load. if let Some(primaries) = primaries { t!("In-memory cache hit!"); return primaries.iter().map(|fp| self.load(fp)).collect(); } } KeyHandle::KeyID(keyid) => { let primaries = self.index_cache.read().unwrap() .by_key_id.get(keyid).cloned(); // Note: it is of the utmost importance to compute // `primaries`, then drop the `CertD::index_cache` // lock, then map over self.load, because we must not // hold the lock over calls to self.load. If this is // done on the right hand side of the `if let` // statement, the lock will be held across the call to // self.load. if let Some(primaries) = primaries { t!("In-memory cache hit!"); return primaries.iter().map(|fp| self.load(fp)).collect(); } } } t!("In-memory cache miss!"); // We didn't find a certificate. ok_or_not_found(kh.clone(), Vec::new()) } fn select_userid(&self, params: &UserIDQueryParams, pattern: &str) -> Result>>> { let mut db = self.conn.lock().unwrap(); self.scan(&mut db)?; drop(db); tracer!(false, "CertD::select_userid"); t!("params: {:?}, pattern: {:?}", params, pattern); let matches = self.index_cache.read().unwrap() .by_userid.select_userid(params, pattern)?; // Note: it is of the utmost importance to compute `matches`, // then drop the `CertD::index_cache` lock, then map over // self.load, because we must not hold the lock over calls to // self.load. let matches = matches .into_iter() .map(|fp| self.load(&fp)) .collect::>>()?; Ok(matches) } fn fingerprints<'b>(&'b self) -> Box + 'b> { let transaction = || -> Result + 'b>> { let mut db = self.conn.lock().unwrap(); self.scan(&mut db)?; drop(db); let mut fps = self.index_cache.read().unwrap() .by_key_fingerprint.iter() .filter(|(sk_fp, cert_fps)| cert_fps.binary_search(sk_fp).is_ok()) .map(|(fp, _)| fp.clone()) .collect::>(); fps.sort_unstable(); fps.dedup(); Ok(Box::new(fps.into_iter())) }; match transaction() { Ok(v) => v, Err(_) => { // We cannot return errors here, fall back to // accessing the certd. Box::new(self.certd.fingerprints() .filter_map(|fp| fp.ok().and_then(|s| s.parse().ok()))) }, } } fn certs<'b>(&'b self) -> Box>> + 'b> where 'a: 'b { self.prefetch_internal(true, &[], false); Box::new( self.certs_cache.read().unwrap().values() .map(|(cert, _)| cert.clone()) .collect::>() .into_iter()) } fn prefetch_all(&self) { self.prefetch_internal(true, &[], true); } fn prefetch_some(&self, certs: &[KeyHandle]) { self.prefetch_internal(false, certs, true); } } impl<'a> CertD<'a> { /// Canonicalize the certs identified by `khs`, or `all`. fn prefetch_internal(&self, all: bool, khs: &[KeyHandle], canonicalize: bool) { // LazyCert is Sync and Send, but keeping a reference to the // RawCerts in certs prevents us from later updating // certs. This requires a bit of acrobatics to get // right. tracer!(TRACE, "CertD::prefetch_internal"); if all { t!("Prefetch all certificates"); } else { t!("Prefetch {} certificates", khs.len()); } /// Work items. enum Work<'a> { /// Load the certificate from disk, canonicalize if /// `canonicalize` is given. Load(Fingerprint), /// Canonicalize a raw cert that is already in our cache. Canonicalize(RawCert<'a>, cert_d::Tag), } use crossbeam::thread; use crossbeam::channel::unbounded as channel; // Avoid an extra level of indentation. let result = thread::scope(|thread_scope| { let mut work: Vec = Default::default(); let certs_cache = self.certs_cache.read().unwrap(); // First, schedule all raw certs in our cache that we // should canonicalize. if canonicalize { for w in certs_cache.iter() .filter_map(|(fpr, (cert, tag))| { if ! cert.is_parsed() { if all || khs.iter() .any(|kh| { kh.aliases(&KeyHandle::from(fpr.clone())) }) { // Unfortunately we have to clone the bytes, // because we cannot keep certs borrowed. t!("Queuing {} to be prefetched", fpr); use std::ops::Deref; cert.deref().clone().into_raw_cert().ok() .map(|raw_cert| Work::Canonicalize(raw_cert, tag.clone())) } else { None } } else { None } }) { work.push(w); } } // Second, we consider all certs not in the cache. if all { // Either all of them. for fp in self.certd.fingerprints() .into_iter().flatten() .filter_map(|f| f.parse::().ok()) .filter(|f| ! certs_cache.contains_key(f)) { work.push(Work::Load(fp)); } } else { // Or just those in `khs`. Here, for simplicity, we // only consider the fingerprints. for fp in khs.iter() .filter_map(|kh| Fingerprint::try_from(kh).ok()) .filter(|f| ! certs_cache.contains_key(f)) { work.push(Work::Load(fp)); } } // Release our read lock. drop(certs_cache); let cert_count = work.len(); // The threads. We start them on demand. let threads = if cert_count < 16 { // The keyring is small, limit the number of threads. 2 } else { // Sort the certificates so they are ordered from most // packets to least. More packets implies more work, and // this will hopefully result in a more equal distribution // of load. work.sort_unstable_by_key(|w| match w { Work::Load(_) => 0, Work::Canonicalize(raw_cert, _tag) => usize::MAX - raw_cert.count(), }); // Use at least one and not more than we have cores. num_cpus::get().max(1) }; t!("Using {} threads", threads); // A communication channel for sending work to the workers. let (work_tx, work_rx) = channel(); // A communication channel for returning returns to the main // thread. let (results_tx, results_rx) = channel(); let mut threads_extant = Vec::new(); for cert in work.into_iter() { if threads_extant.len() < threads { let tid = threads_extant.len(); t!("Starting thread {} of {}", tid, threads); let mut work = Some(Ok(cert)); // The thread's state. let work_rx = work_rx.clone(); let results_tx = results_tx.clone(); threads_extant.push(thread_scope.spawn(move |_| { loop { match work.take().unwrap_or_else(|| work_rx.recv()) { Err(_) => break, Ok(Work::Load(fp)) => { t!("Thread {}: load{} {}", tid, if canonicalize { "+ canonicalize" } else { "" }, fp); // Silently ignore errors. This will // be caught later when the caller // looks this one up. self.certd.get_file(&fp.to_string()) .ok().flatten() .and_then(|file| { let tag = cert_d::Tag::try_from(&file) .ok()?; match br::File::new_with_cookie( file, self.certd.get_path(&fp.to_string()).ok()?, Default::default()) .map_err(Into::::into) .and_then(|c| { if canonicalize { Ok(LazyCert::from( Cert::from_buffered_reader(c)?)) } else { Ok(LazyCert::from( RawCert::from_buffered_reader(c)?)) } }) { Ok(cert) => { let _ = results_tx.send( (cert, tag)); }, Err(err) => { t!("Error loading {}: {}", fp, err); }, } Some(()) }); }, Ok(Work::Canonicalize(raw, tag)) => { t!("Thread {} dequeuing {}!", tid, raw.keyid()); // Silently ignore errors. This will // be caught later when the caller // looks this one up. match Cert::try_from(&raw) { Ok(cert) => { let _ = results_tx.send( (LazyCert::from(cert), tag)); } Err(err) => { t!("Parsing raw cert {}: {}", raw.keyid(), err); } } }, } } t!("Thread {} exiting", tid); })); } else { work_tx.send(cert).unwrap(); } } // When the threads see this drop, they will exit. drop(work_tx); // Drop our reference to results_tx. When the last thread // exits, the last reference will be dropped and the loop // below will exit. drop(results_tx); let mut count = 0; let mut certs = self.certs_cache.write().unwrap(); while let Ok((cert, tag)) = results_rx.recv() { let fpr = cert.fingerprint(); t!("Caching {}", fpr); certs.insert(fpr, (Arc::new(cert), tag)); count += 1; } t!("Prefetched {} certificates, ({} had errors)", count, cert_count - count); }); // thread scope. // We're just caching results so we can ignore errors. if let Err(err) = result { t!("{:?}", err); } } } impl<'a> StoreUpdate<'a> for CertD<'a> { fn update_by(&self, cert: Arc>, merge_strategy: &dyn MergeCerts<'a>) -> Result>> { tracer!(TRACE, "CertD::update_by"); t!("Inserting {}", cert.fingerprint()); // This is slightly annoying: cert-d expects bytes. But // serializing cert is a complete waste if we have to merge // the certificate with another one. cert-d actually only // needs the primary key, which it uses to derive the // fingerprint, so, we only serialize that. let fpr = cert.fingerprint(); let fpr_str = format!("{:x}", fpr); let mut merged = None; let mut pre_certd_tag = None; let mut post_certd_tag = None; let (cert_tag, _) = self.certd.insert_extended( &fpr_str, (), false, |_| { // Compute the tag with the cert-d lock held. pre_certd_tag = Some(self.certd.tag()); Ok(()) }, |(), disk_bytes| { let disk: Option> = if let Some(disk_bytes) = disk_bytes { let mut parser = RawCertParser::from_bytes(disk_bytes) .with_context(|| { format!("Parsing {} as returned from the cert directory", fpr) }) .map_err(|err| { t!("Reading disk version: {}", err); err })?; let disk = parser.next().transpose() .with_context(|| { format!("Parsing {} as returned from the cert directory", fpr) }) .map_err(|err| { t!("Parsing disk version: {}", err); err })?; disk.map(|disk| Arc::new(LazyCert::from(disk))) } else { t!("No disk version"); None }; let merged_ = merge_strategy.merge_public(cert, disk) .with_context(|| { format!("Merging versions of {}", fpr) }) .map_err(|err| { t!("Merging: {}", err); err })?; let bytes = merged_.to_vec()?; merged = Some(merged_); Ok(cert_d::MergeResult::Data(bytes)) }, |r| { // Compute the tag with the cert-d lock held. post_certd_tag = Some(self.certd.tag()); Ok(r) }, )?; let certd_tags = if let (Some(pre), Some(post)) = (pre_certd_tag, post_certd_tag) { Some((pre, post)) } else { None }; let merged = merged.expect("set"); self.insert_cert(merged.clone(), cert_tag, certd_tags)?; Ok(merged) } } #[cfg(test)] mod tests { use super::*; use std::sync::OnceLock; use anyhow::Context; use openpgp::packet::UserID; use openpgp::serialize::Serialize; use openpgp::serialize::SerializeInto; use crate::store::StoreError; use crate::tests::print_error_chain; const N: usize = 1050; fn roughly_a_thousand_certs() -> &'static [Cert] { static CERTS: OnceLock> = OnceLock::new(); CERTS.get_or_init( || (0..N).into_iter().map(|i| { let userid = format!("<{}@example.org>", i); let (cert, _rev) = CertBuilder::new() .set_cipher_suite(CipherSuite::Cv25519) .add_userid(UserID::from(&userid[..])) .add_storage_encryption_subkey() .generate() .expect("ok"); cert }).collect()).as_slice() } /// Checks that the CertD at `path` fn check_certd(certd: &CertD, certs: &[&Cert], certs_fpr: Option<&[Fingerprint]>, subkeys_fpr: &[Fingerprint], userids: &[&UserID]) -> Result<()> { // Test Store::iter. In particular, make sure we get // everything back. if let Some(certs_fpr) = certs_fpr { let mut certs_read = certd.certs().collect::>(); assert_eq!( certs_read.len(), certs.len(), "Looks like you're exhausting the available file descriptors"); certs_read.sort_by_key(|c| c.fingerprint()); let certs_read_fpr = certs_read.iter().map(|c| c.fingerprint()).collect::>(); assert_eq!(certs_fpr, certs_read_fpr); } // Test Store::by_cert. for cert in certs.iter() { let certs_read = certd.lookup_by_cert(&cert.key_handle()).expect("present"); // We expect exactly one cert. assert_eq!(certs_read.len(), 1); let cert_read = certs_read.iter().next().expect("have one") .to_cert().expect("valid"); assert_eq!(&cert_read, cert); } for subkey in subkeys_fpr.iter() { let kh = KeyHandle::from(subkey.clone()); match certd.lookup_by_cert(&kh) { Ok(certs) => panic!("Expected nothing, got {} certs", certs.len()), Err(err) => { if let Some(&StoreError::NotFound(ref got)) = err.downcast_ref::() { assert_eq!(&kh, got); } else { panic!("Expected NotFound, got: {}", err); } } } } // Test Store::lookup_by_cert_or_subkey. for fpr in certs.iter().map(|cert| cert.fingerprint()) .chain(subkeys_fpr.iter().cloned()) { let certs_read = certd.lookup_by_cert_or_subkey(&KeyHandle::from(fpr.clone())).expect("present"); // We expect exactly one cert. assert_eq!(certs_read.len(), 1); let cert_read = certs_read.iter().next().expect("have one") .to_cert().expect("valid"); assert!(cert_read.keys().any(|k| k.key().fingerprint() == fpr)); } // Test Store::lookup_by_userid. for userid in userids.iter() { let certs_read = certd.lookup_by_userid(&userid).expect("present"); // We expect exactly one cert. assert_eq!(certs_read.len(), 1); let cert_read = certs_read.iter().next().expect("have one") .to_cert().expect("valid"); assert!(cert_read.userids().any(|u| &u.userid() == userid)); } Ok(()) } // Make sure that we can read a huge cert-d. Specifically, the // typical file descriptor limit is 1024. Make sure we can // initialize and iterate over a cert-d with a few more entries // than that. #[test] fn huge_cert_d() -> Result<()> { let path = tempfile::tempdir()?; let certd = cert_d::CertD::with_base_dir(&path) .map_err(|err| { let err = anyhow::Error::from(err) .context(format!("While opening the certd {:?}", path)); print_error_chain(&err); err })?; // Generate some certificates and write them to a cert-d using // the low-level interface. let mut certs = Vec::new(); let mut certs_fpr = Vec::new(); let mut subkeys_fpr = Vec::new(); let mut userids = Vec::new(); for cert in roughly_a_thousand_certs() { certs_fpr.push(cert.fingerprint()); subkeys_fpr.extend(cert.keys().subkeys().map(|ka| ka.key().fingerprint())); userids.push(cert.userids().next().unwrap().userid()); let mut bytes = Vec::new(); cert.serialize(&mut bytes).expect("can serialize to a vec"); certd .insert_data(&bytes, false, |new, disk| { assert!(disk.is_none()); Ok(cert_d::MergeResult::DataRef(new)) }) .with_context(|| { format!("{:?} ({})", path, cert.fingerprint()) }) .expect("can insert"); certs.push(cert); } // One subkey per certificate. assert_eq!(certs_fpr.len(), subkeys_fpr.len()); certs_fpr.sort(); // Open the cert-d and make sure we can read what we wrote via // the low-level interface. let certd = CertD::open(&path).expect("exists"); check_certd( &certd, &certs[..], Some(&certs_fpr), &subkeys_fpr, &userids) } /// Tests four concurrent mutators on the same CertD. #[test] fn concurrent_mutators() -> Result<()> { use rand::prelude::*; let tmpdir = tempfile::tempdir()?; let path = tmpdir.path(); let certd_shared = CertD::open(path)?; let roughly_a_hundred_certs = &roughly_a_thousand_certs()[..101]; std::thread::scope(|s| { for tid in 0..3 { let certd_shared = &certd_shared; s.spawn(move || { // Thread 0 has its own CertD, thread 1 and 2 get // to share one. let certd_owned; let certd = if tid % 3 == 0 { certd_owned = CertD::open(path).unwrap(); &certd_owned } else { certd_shared }; let mut rng = rand::thread_rng(); let mut source_certs = roughly_a_hundred_certs.to_vec(); source_certs.shuffle(&mut rng); let mut certs = Vec::new(); let mut certs_fpr = Vec::new(); let mut subkeys_fpr = Vec::new(); let mut userids = Vec::new(); for cert in source_certs.iter() { eprint!("{}", tid); certs_fpr.push(cert.fingerprint()); subkeys_fpr.extend(cert.keys().subkeys().map(|ka| ka.key().fingerprint())); userids.push(cert.userids().next().unwrap().userid()); certd.update(Arc::new(cert.clone().into())) .expect("can insert"); certs.push(cert); check_certd( &certd, &certs[..], None, &subkeys_fpr, &userids) .expect("consistency and robustness"); } }); } // Finally, add a fourth mutator that doesn't update the // database. s.spawn(move || { let certd = cert_d::CertD::with_base_dir(path).unwrap(); for cert in roughly_a_hundred_certs { std::thread::sleep(Duration::from_millis(10)); eprint!("."); certd.insert_data( &cert.to_vec().unwrap(), false, |new, _disk| Ok(cert_d::MergeResult::DataRef(new))) .with_context(|| { format!("{:?} ({})", path, cert.fingerprint()) }) .expect("can insert"); } }); }); Ok(()) } /// Tests that the database index preserves all the information. #[test] fn database_index() -> Result<()> { let tmpdir = tempfile::tempdir()?; let p = tmpdir.path(); let c = CertBuilder::new() .add_userid("Alice Lovelace") .add_userid("") .add_userid("Alice Lovelace ") .add_signing_subkey() .generate()?.0; // Insert the cert using the low-level cert-d. { let certd = cert_d::CertD::with_base_dir(p)?; certd.insert_data( &c.to_vec()?, false, |new, _disk| Ok(cert_d::MergeResult::DataRef(new)))?; } // Construct the database index. let _ = CertD::open(p)?; // Do the queries. Construct a new CertD for each query so // that we don't get the benefit of updating the in-core index // when loading a cert from disk. for (i, k) in c.keys().enumerate() { let primary = i == 0; if primary { let d = CertD::open(p)?.lookup_by_cert(&k.key().fingerprint().into())?; assert_eq!(&c, d[0].to_cert()?); let d = CertD::open(p)?.lookup_by_cert(&k.key().keyid().into())?; assert_eq!(&c, d[0].to_cert()?); } let d = CertD::open(p)?.lookup_by_cert_or_subkey(&k.key().fingerprint().into())?; assert_eq!(&c, d[0].to_cert()?); let d = CertD::open(p)?.lookup_by_cert_or_subkey(&k.key().keyid().into())?; assert_eq!(&c, d[0].to_cert()?); } for u in c.userids() { let d = CertD::open(p)?.select_userid( UserIDQueryParams::new() .set_anchor_start(true) .set_anchor_end(true) .set_email(false) .set_ignore_case(false), &String::from_utf8(u.userid().value().to_vec())?)?; assert_eq!(&c, d[0].to_cert()?); if let Ok(Some(e)) = u.userid().email() { let d = CertD::open(p)?.select_userid( UserIDQueryParams::new() .set_anchor_start(true) .set_anchor_end(true) .set_email(true) .set_ignore_case(false), e)?; assert_eq!(&c, d[0].to_cert()?); } if let Ok(Some(n)) = u.userid().name() { let d = CertD::open(p)?.select_userid( UserIDQueryParams::new() .set_anchor_start(true) .set_anchor_end(false) .set_email(false) .set_ignore_case(false), n)?; assert_eq!(&c, d[0].to_cert()?); } } Ok(()) } } sequoia-cert-store-0.7.1/src/store/certs.rs000064400000000000000000000363131046102023000170030ustar 00000000000000use std::collections::BTreeMap; use std::collections::btree_map::Entry; use std::sync::Arc; use std::sync::RwLock; use smallvec::SmallVec; use smallvec::smallvec; use anyhow::Context; use sequoia_openpgp as openpgp; use openpgp::cert::Cert; use openpgp::cert::raw::RawCert; use openpgp::cert::raw::RawCertParser; use openpgp::Fingerprint; use openpgp::KeyID; use openpgp::KeyHandle; use openpgp::packet::UserID; use openpgp::parse::Parse; use openpgp::Result; use crate::LazyCert; use crate::store::MergeCerts; use crate::store::Store; use crate::store::StoreError; use crate::store::StoreUpdate; use crate::store::UserIDIndex; use crate::store::UserIDQueryParams; const TRACE: bool = false; /// Manages a slice of bytes, `RawCert`s, `Cert`s, or `LazyCert`s. /// /// `Cert`s implements `StoreUpdate`, but it does not write the /// updates to disk; they are only updated in memory. pub struct Certs<'a> { inner: RwLock>, } assert_send_and_sync!(Certs<'_>); struct CertsInner<'a> { // Indexed by primary key fingerprint. certs: BTreeMap>>, // Indexed by a key's KeyID (primary key or subkey) and maps to // the primary key. keys: BTreeMap>, userid_index: UserIDIndex, } impl<'a> Certs<'a> { /// Returns an empty `Certs` store. /// /// This is useful as a placeholder. But, certificates can also /// be added to it using the [`StoreUpdate`] interface. pub fn empty() -> Self { Certs { inner: RwLock::new(CertsInner { certs: Default::default(), keys: Default::default(), userid_index: UserIDIndex::new(), }), } } /// Returns a new `Certs`. pub fn from_bytes(bytes: &'a [u8]) -> Result { tracer!(TRACE, "Certs::from_bytes"); let raw_certs = RawCertParser::from_bytes(bytes)? .filter_map(|c| { match c { Ok(c) => Some(c), Err(err) => { t!("Parsing raw certificate: {}", err); None } } }); Self::from_certs(raw_certs) } /// Returns a new `Certs`. pub fn from_certs(certs: impl IntoIterator) -> Result where I: Into> { let r = Self::empty(); for cert in certs { r.update(Arc::new(cert.into())).expect("implementation doesn't fail") } Ok(r) } } impl<'a> Store<'a> for CertsInner<'a> { fn lookup_by_cert(&self, kh: &KeyHandle) -> Result>>> { tracer!(TRACE, "Certs::lookup_by_cert"); match kh { KeyHandle::Fingerprint(fpr) => { self.lookup_by_cert_fpr(fpr).map(|c| vec![ c ]) } KeyHandle::KeyID(keyid) => { let certs: Vec> = self.keys.get(keyid) .ok_or_else(|| { anyhow::Error::from( StoreError::NotFound(kh.clone())) })? .iter() .filter_map(|fpr| self.certs.get(fpr).cloned()) // Check the constraints before we convert the // rawcert to a cert. .filter(|cert| cert.key_handle().aliases(kh)) .collect(); if certs.is_empty() { Err(StoreError::NotFound(kh.clone()).into()) } else { Ok(certs) } } } } fn lookup_by_cert_fpr(&self, fingerprint: &Fingerprint) -> Result>> { tracer!(TRACE, "Certs::lookup_by_cert_fpr"); if let Some(cert) = self.certs.get(fingerprint).cloned() { Ok(cert) } else { Err(StoreError::NotFound( KeyHandle::from(fingerprint.clone())).into()) } } fn lookup_by_cert_or_subkey(&self, kh: &KeyHandle) -> Result>>> { tracer!(TRACE, "Certs::lookup_by_cert_or_subkey"); let keyid = KeyID::from(kh); let certs: Vec>> = self.keys.get(&keyid) .ok_or_else(|| { anyhow::Error::from( StoreError::NotFound(kh.clone())) })? .iter() .filter_map(|fpr| self.certs.get(fpr)) // Check the constraints before we convert the rawcert to a // cert. .filter(|cert| { cert.keys().any(|k| k.key_handle().aliases(kh)) }) .cloned() .collect(); if certs.is_empty() { Err(StoreError::NotFound(kh.clone()).into()) } else { Ok(certs) } } fn select_userid(&self, params: &UserIDQueryParams, pattern: &str) -> Result>>> { tracer!(TRACE, "Certs::select_userid"); t!("params: {:?}, pattern: {:?}", params, pattern); let matches = self.userid_index.select_userid(params, pattern)?; let matches = matches .into_iter() .map(|fpr| { self.lookup_by_cert_fpr(&fpr).expect("indexed") }) .collect(); Ok(matches) } fn fingerprints<'b>(&'b self) -> Box + 'b> { Box::new(self.certs.keys().cloned()) } fn certs<'b>(&'b self) -> Box>> + 'b> where 'a: 'b { Box::new(self.certs.values().cloned()) } fn prefetch_all(&self) { // Because the interior mutability is implemented via Certs, // not CertInner, and we need a mutable reference here, but we // only have a normal, non-mutable reference, the prefetch // implementation is dispatched via `Certs`. } fn prefetch_some(&self, _certs: &[KeyHandle]) { // Because the interior mutability is implemented via Certs, // not CertInner, and we need a mutable reference here, but we // only have a normal, non-mutable reference, the prefetch // implementation is dispatched via `Certs`. } } impl<'a> Store<'a> for Certs<'a> { fn lookup_by_cert(&self, kh: &KeyHandle) -> Result>>> { let inner = self.inner.read().unwrap(); inner.lookup_by_cert(kh) } fn lookup_by_cert_fpr(&self, fingerprint: &Fingerprint) -> Result>> { let inner = self.inner.read().unwrap(); inner.lookup_by_cert_fpr(fingerprint) } fn lookup_by_cert_or_subkey(&self, kh: &KeyHandle) -> Result>>> { let inner = self.inner.read().unwrap(); inner.lookup_by_cert_or_subkey(kh) } fn select_userid(&self, query: &UserIDQueryParams, pattern: &str) -> Result>>> { let inner = self.inner.read().unwrap(); inner.select_userid(query, pattern) } fn lookup_by_userid(&self, userid: &UserID) -> Result>>> { let inner = self.inner.read().unwrap(); inner.lookup_by_userid(userid) } fn grep_userid(&self, pattern: &str) -> Result>>> { let inner = self.inner.read().unwrap(); inner.grep_userid(pattern) } fn lookup_by_email(&self, email: &str) -> Result>>> { let inner = self.inner.read().unwrap(); inner.lookup_by_email(email) } fn grep_email(&self, pattern: &str) -> Result>>> { let inner = self.inner.read().unwrap(); inner.grep_email(pattern) } fn lookup_by_email_domain(&self, domain: &str) -> Result>>> { let inner = self.inner.read().unwrap(); inner.lookup_by_email_domain(domain) } fn fingerprints<'b>(&'b self) -> Box + 'b> { // We aren't lazy to avoid holding the lock. let inner = self.inner.read().unwrap(); let fprs: Vec<_> = inner.fingerprints().collect(); Box::new(fprs.into_iter()) } fn certs<'b>(&'b self) -> Box>> + 'b> where 'a: 'b { // We aren't lazy to avoid holding the lock. let inner = self.inner.read().unwrap(); let certs: Vec<_> = inner.certs().collect(); Box::new(certs.into_iter()) } fn prefetch_all(&self) { let mut inner = self.inner.write().unwrap(); inner.prefetch(true, &[]) } fn prefetch_some(&self, certs: &[KeyHandle]) { let mut inner = self.inner.write().unwrap(); inner.prefetch(false, certs) } } impl CertsInner<'_> { /// Prefetches the certs identified by `khs`, or `all`. fn prefetch(&mut self, all: bool, khs: &[KeyHandle]) { // LazyCert is Sync and Send, but keeping a reference to the // RawCerts in self.certs prevents us from later updating // self.certs. This requires a bit of acrobatics to get // right. tracer!(TRACE, "Certs::prefetch_some"); t!("Prefetch: {} certificates", khs.len()); use crossbeam::thread; use crossbeam::channel::unbounded as channel; // Avoid an extra level of indentation. let result = thread::scope(|thread_scope| { let mut certs: Vec = self.certs.iter().filter_map(|(fpr, cert)| { if ! cert.is_parsed() { if all || khs.iter() .any(|kh| { kh.aliases(&KeyHandle::from(fpr.clone())) }) { // Unfortunately we have to clone the bytes, // because we cannot keep self.certs borrowed. t!("Queuing {} to be prefetched", fpr); use std::ops::Deref; cert.deref().clone().into_raw_cert().ok() } else { None } } else { None } }).collect(); let cert_count = certs.len(); // The threads. We start them on demand. let threads = if cert_count < 16 { // The keyring is small, limit the number of threads. 2 } else { // Sort the certificates so they are ordered from most // packets to least. More packets implies more work, and // this will hopefully result in a more equal distribution // of load. certs.sort_unstable_by_key(|c| { usize::MAX - c.count() }); // Use at least one and not more than we have cores. num_cpus::get().max(1) }; t!("Using {} threads", threads); // A communication channel for sending work to the workers. let (work_tx, work_rx) = channel(); // A communication channel for returning returns to the main // thread. let (results_tx, results_rx) = channel(); let mut threads_extant = Vec::new(); for cert in certs.into_iter() { if threads_extant.len() < threads { let tid = threads_extant.len(); t!("Starting thread {} of {}", tid, threads); let mut work = Some(Ok(cert)); // The thread's state. let work_rx = work_rx.clone(); let results_tx = results_tx.clone(); threads_extant.push(thread_scope.spawn(move |_| { loop { match work.take().unwrap_or_else(|| work_rx.recv()) { Err(_) => break, Ok(raw) => { t!("Thread {} dequeuing {}!", tid, raw.keyid()); // Silently ignore errors. This will // be caught later when the caller // looks this one up. match Cert::try_from(&raw) { Ok(cert) => { let _ = results_tx.send(cert); } Err(err) => { t!("Parsing raw cert {}: {}", raw.keyid(), err); } } } } } t!("Thread {} exiting", tid); })); } else { work_tx.send(cert).unwrap(); } } // When the threads see this drop, they will exit. drop(work_tx); // Drop our reference to results_tx. When the last thread // exits, the last reference will be dropped and the loop // below will exit. drop(results_tx); let mut count = 0; while let Ok(cert) = results_rx.recv() { let fpr = cert.fingerprint(); t!("Caching {}", fpr); self.certs.insert(fpr, Arc::new(cert.into())); count += 1; } t!("Prefetched {} certificates, ({} RawCerts had errors)", count, cert_count - count); }); // thread scope. // We're just caching results so we can ignore errors. if let Err(err) = result { t!("{:?}", err); } } } impl<'a> StoreUpdate<'a> for Certs<'a> { fn update_by(&self, cert: Arc>, merge_strategy: &dyn MergeCerts<'a>) -> Result>> { tracer!(TRACE, "Certs::update_by"); let mut inner = self.inner.write().unwrap(); let fpr = cert.fingerprint(); // Add the cert fingerprint -> cert entry. let merged: Arc; match inner.certs.entry(fpr.clone()) { Entry::Occupied(mut oe) => { t!("Updating {}", fpr); let old = oe.get().clone(); merged = merge_strategy.merge_public(cert, Some(old)) .with_context(|| { format!("Merging two version of {}", fpr) })?; *oe.get_mut() = merged.clone(); } Entry::Vacant(ve) => { t!("Inserting {}", fpr); merged = merge_strategy.merge_public(cert, None)?; ve.insert(merged.clone()); } } // Populate the key map. This is a merge so we are not // removing anything. for k in merged.keys() { match inner.keys.entry(k.keyid()) { Entry::Occupied(mut oe) => { let fprs = oe.get_mut(); if ! fprs.contains(&fpr) { fprs.push(fpr.clone()); } } Entry::Vacant(ve) => { ve.insert(smallvec![ fpr.clone() ]); } } } inner.userid_index.insert(&fpr, merged.userids()); Ok(merged) } } sequoia-cert-store-0.7.1/src/store/keyserver.rs000064400000000000000000000363101046102023000176770ustar 00000000000000use std::collections::BTreeMap; use std::collections::BTreeSet; use std::sync::Arc; use std::sync::Mutex; use sequoia_openpgp as openpgp; use openpgp::Cert; use openpgp::Fingerprint; use openpgp::KeyHandle; use openpgp::KeyID; use openpgp::Result; use openpgp::policy::NullPolicy; use sequoia_net as net; use net::reqwest; use crate::email_to_userid; use crate::LazyCert; use crate::Store; use crate::store::StatusListener; use crate::store::StatusUpdate; use crate::store::StoreError; use crate::store::UserIDQueryParams; use super::TRACE; const NP: &NullPolicy = unsafe { &NullPolicy::new() }; // Reliable keyservers. /// keys.openpgp.org. pub const KEYS_OPENPGP_ORG_URL: &str = "hkps://keys.openpgp.org"; /// A reliable SKS keyserver. pub const SKS_URL: &str = "hkps://keyserver.ubuntu.com"; /// mailvelope's keyserver. pub const MAILVELOPE_URL: &str = "hkps://keys.mailvelope.com"; /// proton's keyserver. pub const PROTON_URL: &str = "hkps://api.protonmail.ch"; /// A keyserver backend. pub struct KeyServer<'a> { inner: Mutex>, } assert_send_and_sync!(KeyServer<'_>); struct KeyServerInner<'a> { keyserver: net::KeyServer, id: String, tx: usize, listeners: Vec>, // A cache. We only cache certificates; we don't cache User ID // searches. // Primary keys and subkeys. hits_fpr: BTreeMap>>, hits_keyid: BTreeMap, // What we failed to look up. misses_fpr: BTreeSet, misses_keyid: BTreeSet, } impl KeyServer<'_> { /// Returns a new key server instance. pub fn new(url: &str) -> Result { Ok(Self { inner: Mutex::new(KeyServerInner { keyserver: net::KeyServer::new(url)?, id: if url.len() <= 10 { // Only prefix "key server" if the URL is short. format!("key server {}", url) } else { url.to_string() }, tx: 0, listeners: Vec::new(), hits_fpr: Default::default(), hits_keyid: Default::default(), misses_fpr: Default::default(), misses_keyid: Default::default(), }), }) } /// Returns a key server instance that uses `keys.openpgp.org`. pub fn keys_openpgp_org() -> Result { Self::new(KEYS_OPENPGP_ORG_URL) } /// Returns a key server instance that uses a reliable SKS /// keyserver. pub fn sks() -> Result { Self::new(SKS_URL) } /// Returns a key server instance that uses mailvelope's /// keyserver. pub fn mailvelope() -> Result { Self::new(MAILVELOPE_URL) } /// Returns a key server instance that uses proton's keyserver. pub fn proton() -> Result { Self::new(PROTON_URL) } /// Sends status updates to the listener. pub fn add_listener(&mut self, listener: Box) { self.inner.lock().unwrap().listeners.push(listener); } } impl<'a> KeyServerInner<'a> { // Looks for a certificate in the cache. fn check_cache(&self, kh: &KeyHandle) -> Option>>>> { let kh_; let kh = if let KeyHandle::KeyID(keyid) = kh { if let Some(fpr) = self.hits_keyid.get(keyid) { kh_ = KeyHandle::Fingerprint(fpr.clone()); &kh_ } else if self.misses_keyid.get(keyid).is_some() { return Some(Err(StoreError::NotFound( KeyHandle::from(kh.clone())).into())); } else { kh } } else { kh }; if let KeyHandle::Fingerprint(fpr) = kh { if let Some(cert) = self.hits_fpr.get(fpr) { return Some(Ok(vec![ cert.clone() ])); } if self.misses_fpr.get(fpr).is_some() { return Some(Err(StoreError::NotFound( KeyHandle::from(kh.clone())).into())); } } None } // Adds the cert to the in-memory cache. fn cache(&mut self, cert: &Arc>) { for k in cert.keys() { self.hits_fpr.insert(k.fingerprint(), cert.clone()); self.hits_keyid.insert(k.keyid(), k.fingerprint()); } } /// Deletes the key with the given fingerprint from the cache. fn delete_from_cache(&mut self, cert: &LazyCert<'_>) { for key in cert.keys() { let fp = key.fingerprint(); self.hits_fpr.remove(&fp); let keyid = KeyID::from(fp); // XXX: Technically, a key ID could map to multiple // fingerprints, so we may be removing the wrong entry here. // But, by using a BTreeMap we do accept // this kind of loss in the first place, as later cache // insertions will overwrite any existing mappings. self.hits_keyid.remove(&keyid); } } } impl<'a> KeyServer<'a> { /// Deletes the key with the given fingerprint from the cache. pub(crate) fn delete_from_cache(&self, certs: I) where I: Iterator>>, { let mut inner = self.inner.lock().unwrap(); for cert in certs { inner.delete_from_cache(&cert); } } } /// Sends a status update. macro_rules! update { ( $self:expr, $update:ident, $($args:expr),* ) => {{ if ! $self.listeners.is_empty() { let update = StatusUpdate::$update( $self.tx, &$self.id, $($args),*); for sub in $self.listeners.iter() { sub.update(&update); } } }} } impl<'a> Store<'a> for KeyServer<'a> { fn lookup_by_cert(&self, kh: &KeyHandle) -> Result>>> { let mut certs = self.lookup_by_cert_or_subkey(kh)?; // The match may be on a subkey. Only return the certificates // whose primary key aliases kh. certs.retain(|cert| { kh.aliases(KeyHandle::from(cert.fingerprint())) }); if certs.is_empty() { Err(StoreError::NotFound(KeyHandle::from(kh.clone())).into()) } else { Ok(certs) } } fn lookup_by_cert_or_subkey(&self, kh: &KeyHandle) -> Result>>> { tracer!(TRACE, "KeyServer::lookup_by_cert_or_subkey"); // Check the cache. t!("Looking up {} on keyserver...", kh); let mut inner = self.inner.lock().unwrap(); if ! inner.listeners.is_empty() { inner.tx += 1; update!(inner, LookupStarted, kh, None); }; if let Some(r) = inner.check_cache(kh) { t!("Found in in-memory cache"); match r.as_ref() { Ok(certs) => { update!(inner, LookupFinished, kh, &certs[..], Some("Found in in-memory cache")); } Err(err) => { update!(inner, LookupFailed, kh, Some(&err)); } } return r; } // It's not in the cache, look it up on the key server. let rt = tokio::runtime::Runtime::new().unwrap(); // The keyserver interface currently only returns a single // result. let r = rt.block_on(async { inner.keyserver.get(kh.clone()).await }); match r { Ok(certs) => { let certs: Vec<_> = certs.into_iter() .filter_map(Result::ok) .map(|c| Arc::new(LazyCert::from(c))).collect(); for cert in &certs { t!("keyserver returned {}", cert.fingerprint()); // Add the result to the cache. inner.cache(cert); } // Make sure the key server gave us the right // certificate. let requested: Vec<_> = certs.iter().filter( |cert| cert.keys().any(|k| k.key_handle().aliases(kh))) .cloned() .collect(); if ! requested.is_empty() { update!(inner, LookupFinished, kh, &requested[..], None); Ok(requested) } else { let err = KeyServerError::UnexpectedResults( kh.clone(), certs.iter().map(|c| c.fingerprint()).collect()) .into(); t!("{}", err); update!(inner, LookupFailed, kh, Some(&err)); Err(StoreError::NotFound( KeyHandle::from(kh.clone())).into()) } } Err(err) => { t!("keyserver returned an error: {}", err); if let Some(net::Error::NotFound) = err.downcast_ref::() { update!(inner, LookupFailed, kh, None); Err(StoreError::NotFound( KeyHandle::from(kh.clone())).into()) } else { update!(inner, LookupFailed, kh, Some(&err)); Err(err) } } } } fn select_userid(&self, query: &UserIDQueryParams, pattern: &str) -> Result>>> { tracer!(TRACE, "KeyServer::select_userid"); t!("{}", pattern); t!("Looking {:?} up on the keyserver... ", pattern); let mut inner = self.inner.lock().unwrap(); if ! inner.listeners.is_empty() { inner.tx += 1; update!(inner, SearchStarted, pattern, None); } let email_; let email = if query.email && query.anchor_start && query.anchor_end { match email_to_userid(pattern) { Ok(email) => { email_ = email.value().to_vec(); Some(String::from_utf8_lossy(&email_)) }, Err(err) => { t!("{:?}: invalid email address: {}", pattern, err); None } } } else { None }; let rt = tokio::runtime::Runtime::new().unwrap(); let (ks, wkd) = rt.block_on(async { // Query the keyserver. let ks = inner.keyserver.search(pattern); // And the WKD (if it appears to be an email address). let wkd = async { if let Some(email) = email.as_ref() { net::wkd::get(&reqwest::Client::new(), email).await } else { // If it is not an email, it's not an error. Ok(Vec::new()) } }; tokio::join!(ks, wkd) }); let mut certs: Vec> = Vec::new(); match ks { Ok(c) => { t!("Key server returned {} results", c.len()); if ! inner.listeners.is_empty() { let msg = format!("Key server returned {} results", c.len()); update!(inner, SearchStatus, pattern, &msg); } certs.extend(c); }, Err(err) => t!("Key server response: {}", err), } match wkd { Ok(c) => { t!("WKD server returned {} results", c.len()); if ! inner.listeners.is_empty() { let msg = format!("WKD server returned {} results", c.len()); update!(inner, SearchStatus, pattern, &msg); } certs.extend(c); }, Err(err) => t!("WKD server response: {}", err), } // Sort, merge, and cache. let mut certs = certs.into_iter().flatten().collect::>(); certs.sort_by_key(|c| c.fingerprint()); certs.dedup_by(|a, b| { if a.fingerprint() != b.fingerprint() { return false; } // b is kept. So merge into b. match b.clone().merge_public(a.clone()) { Ok(combined) => *b = combined, Err(err) => { t!("Merging copies of {}: {}", a.keyid(), err); } } true }); let mut certs: Vec<_> = certs.into_iter().map(|c| Arc::new(LazyCert::from(c))).collect(); // Add the results to the cache. certs.iter().for_each(|cert| inner.cache(cert)); // Only keep the certificates that actually satisfy the // constraints. certs.retain(|cert| { let cert = cert.to_cert() .expect("LazyCert should already be canonicalized"); query.check_cert(cert, pattern) }); if certs.is_empty() { update!(inner, SearchFailed, pattern, None); Err(StoreError::NoMatches(pattern.to_string()).into()) } else { if TRACE || ! inner.listeners.is_empty() { let msg = format!( "Got {} results:\n {}", certs.len(), certs.iter() .map(|cert: &Arc| { format!( "{} ({})", cert.keyid().to_hex(), cert.with_policy(NP, None) .and_then(|vc| vc.primary_userid()) .map(|ua| { String::from_utf8_lossy(ua.userid().value()) .into_owned() }) .unwrap_or_else(|_| { cert.userids().next() .map(|userid| { String::from_utf8_lossy(userid.value()) .into_owned() }) .unwrap_or("".into()) })) }) .collect::>() .join("\n ")); t!("{}", msg); update!(inner, SearchFinished, pattern, &certs[..], Some(&msg)); } Ok(certs) } } fn fingerprints<'b>(&'b self) -> Box + 'b> { // Return the entries in our cache. Box::new(self .inner.lock().unwrap() .hits_fpr .keys() .cloned() .collect::>() .into_iter()) } } /// [`KeyServer`] specific error codes. #[non_exhaustive] #[derive(thiserror::Error, Debug)] pub enum KeyServerError { /// Keyserver returned the wrong certs. #[error("Keyserver returned the wrong certs: {1:?} (wanted: {0})")] UnexpectedResults(KeyHandle, Vec), } sequoia-cert-store-0.7.1/src/store/no_keyserver.rs000064400000000000000000000025121046102023000203700ustar 00000000000000//! Dummy replacement for the keyserver store. use std::sync::Arc; use sequoia_openpgp::{Fingerprint, KeyHandle}; use crate::{ LazyCert, Result, Store, store::UserIDQueryParams, }; /// Replacement for the keyserver store if feature `keyserver` is /// disabled. /// /// This type cannot be constructed. Hence, its methods cannot be /// invoked. pub struct KeyServer<'a> { spooky: std::marker::PhantomData<&'a ()>, } impl<'a> KeyServer<'a> { /// Deletes the key with the given fingerprint from the cache. #[allow(dead_code)] pub(crate) fn delete_from_cache(&self, _: I) where I: Iterator>>, { unreachable!("no instance can be created") } } impl<'a> Store<'a> for KeyServer<'a> { fn lookup_by_cert(&self, _: &KeyHandle) -> Result>>> { unreachable!("no instance can be created") } fn lookup_by_cert_or_subkey(&self, _: &KeyHandle) -> Result>>> { unreachable!("no instance can be created") } fn select_userid(&self, _: &UserIDQueryParams, _: &str) -> Result>>> { unreachable!("no instance can be created") } fn fingerprints<'b>(&'b self) -> Box + 'b> { unreachable!("no instance can be created") } } sequoia-cert-store-0.7.1/src/store/pep.rs000064400000000000000000001504531046102023000164510ustar 00000000000000use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use std::sync::Mutex; use anyhow::Context; use rusqlite::{ CachedStatement, Connection, OpenFlags, OptionalExtension, params, Row, }; use sequoia_openpgp as openpgp; use openpgp::{ Cert, cert::raw::RawCertParser, Fingerprint, KeyHandle, KeyID, packet::UserID, parse::Parse, serialize::Serialize, }; use crate::LazyCert; use crate::Result; use crate::Store; use crate::StoreUpdate; use crate::store::MergeCerts; use crate::store::StoreError; use crate::store::UserIDQueryParams; // Maximum busy wait time. pub const BUSY_WAIT_TIME: std::time::Duration = std::time::Duration::from_secs(5); // The location of the keys DB relative to the user's home directory. pub const KEYS_DB: &[ &str ] = &[ "keys.db" ]; use crate::TRACE; #[non_exhaustive] #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Opening the database failed: {1}")] InitCannotOpenDB(#[source] anyhow::Error, String), #[error("Unknown error: {1}")] UnknownError(#[source] anyhow::Error, String), #[error("Database error: {1}")] UnknownDbError(#[source] anyhow::Error, String), #[error("Cannot delete key: {0}")] CannotDeleteKey(#[source] anyhow::Error, String), } // Transforms an error from some error type to the pep::Error. macro_rules! wrap_err { ($e:expr, $err:ident, $msg:expr) => { $e.map_err(|err| { eprintln!("Error: {}: {}", err, $msg); anyhow::Error::from(Error::$err( anyhow::Error::from(err).into(), String::from($msg))) }) } } /// A pEp certificate store backend. /// /// A backend, which provides access to a [pEp] certificate store. /// /// [pEp]: https://gitea.pep.foundation/pEp.foundation/pEpEngine pub struct Pep { conn: Mutex, } assert_send_and_sync!(Pep); // Generates a convenience method that returns a prepared statement // for the specified sql. If preparing the statement results in an // error, the error is converted to out native error type. macro_rules! sql_stmt { ($name:ident, $sql:expr) => { fn $name(conn: &Connection) -> Result> { let mut name: &str = stringify!($name); if name.ends_with("_stmt") { name = &name[..name.len() - "_stmt".len()]; } wrap_err!( conn.prepare_cached( $sql), UnknownDbError, format!("preparing {} query", name)) } } } // Execute a query to load certificates, and actually load the // certificates. macro_rules! cert_query { ($stmt:expr, $args:expr, $err:expr) => {{ let rows = wrap_err!( $stmt.query_map($args, Self::key_load), UnknownDbError, "executing query")?; let mut results: Vec<_> = Vec::new(); for row in rows { let (keydata, _private) = wrap_err!(row, UnknownError, "parsing result")?; match Cert::from_bytes(&keydata) { Ok(cert) => results.push(Arc::new(LazyCert::from(cert))), Err(err) => { t!("Warning: unable to parse a certificate: {}\n{:?}", err, String::from_utf8(keydata)); } } } if results.is_empty() { Err(anyhow::Error::from($err)) } else { Ok(results) } }} } impl Pep { /// Opens a `Pep` certificate store. /// /// If `path` is `None`, then this uses the default location, which /// is `$HOME/.pEp/keys.db`. /// /// This initializes the database, if necessary. pub fn open

(path: Option

) -> Result where P: AsRef { match path { Some(p) => Self::init_(Some(p.as_ref())), None => { let mut set = false; let mut keys_db = PathBuf::new(); #[cfg(not(windows))] if cfg!(debug_assertions) { if let Ok(pep_home) = std::env::var("PEP_HOME") { set = true; keys_db = PathBuf::from(pep_home); } } if ! set { if let Some(home) = dirs::home_dir() { keys_db = home } else { return Err(anyhow::anyhow!( "Failed to find home directory")); } } for n in KEYS_DB { keys_db.push(n); } Self::init_(Some(&keys_db)) } } } /// Returns a new `Pep`. /// /// This uses an in-memory sqlite database. pub fn empty() -> Result { Self::init_in_memory() } /// Returns a new `Pep`. /// /// This uses an in-memory sqlite database, and loads it with the /// keyring. pub fn from_bytes<'a>(bytes: &'a [u8]) -> Result { tracer!(TRACE, "Pep::from_bytes"); let raw_certs = RawCertParser::from_bytes(bytes)? .filter_map(|c| { match c { Ok(c) => Some(c), Err(err) => { t!("Parsing raw certificate: {}", err); None } } }); Self::from_certs(raw_certs) } /// Returns a new `Pep`. /// /// This uses an in-memory sqlite database, and loads it with the /// specified certificates. pub fn from_certs<'a, I>(certs: impl IntoIterator) -> Result where I: Into> { let r = Self::init_in_memory()?; for cert in certs { r.update(Arc::new(cert.into())).expect("implementation doesn't fail") } Ok(r) } /// Initializes an in-memory key store. /// /// This is used for the unit tests. //#[cfg(test)] pub(crate) fn init_in_memory() -> Result { Self::init_(None) } fn init_(home: Option<&Path>) -> Result { let mut keys_db = PathBuf::new(); let conn = if let Some(home) = home { keys_db.push(home); if home.is_dir() { for n in KEYS_DB { keys_db.push(n); } } wrap_err!( Connection::open_with_flags( &keys_db, OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_PRIVATE_CACHE), InitCannotOpenDB, format!("Opening keys DB ('{}')", keys_db.display()))? } else { // Create an in-memory DB. wrap_err!( Connection::open_in_memory(), InitCannotOpenDB, "Creating in-memory keys DB")? }; wrap_err!( conn.execute_batch("PRAGMA secure_delete=true; PRAGMA foreign_keys=true; PRAGMA locking_mode=NORMAL; PRAGMA journal_mode=WAL;"), InitCannotOpenDB, format!("Setting pragmas on keys DB ('{}')", keys_db.display()))?; wrap_err!( conn.busy_timeout(BUSY_WAIT_TIME), InitCannotOpenDB, format!("Setting busy time ('{}')", keys_db.display()))?; wrap_err!( conn.create_collation("EMAIL", Self::email_cmp), InitCannotOpenDB, format!("Registering EMAIL collation function"))?; wrap_err!( conn.execute_batch( "CREATE TABLE IF NOT EXISTS keys ( primary_key TEXT UNIQUE PRIMARY KEY, secret BOOLEAN, tpk BLOB ); CREATE INDEX IF NOT EXISTS keys_index ON keys (primary_key, secret)"), InitCannotOpenDB, format!("Creating keys table ('{}')", keys_db.display()))?; wrap_err!( conn.execute_batch( "CREATE TABLE IF NOT EXISTS subkeys ( subkey TEXT NOT NULL /* KeyID */, primary_key TEXT NOT NULL /* Fingerprint */, UNIQUE(subkey, primary_key), FOREIGN KEY (primary_key) REFERENCES keys(primary_key) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS subkeys_index ON subkeys (subkey, primary_key)"), InitCannotOpenDB, format!("Creating subkeys table ('{}')", keys_db.display()))?; wrap_err!( conn.execute_batch( "CREATE TABLE IF NOT EXISTS userids ( userid TEXT NOT NULL COLLATE EMAIL, primary_key TEXT NOT NULL, UNIQUE(userid, primary_key), FOREIGN KEY (primary_key) REFERENCES keys(primary_key) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS userids_index ON userids (userid COLLATE EMAIL, primary_key)"), InitCannotOpenDB, format!("Creating userids table ('{}')", keys_db.display()))?; Ok(Pep { conn: Mutex::new(conn), }) } // Returns a prepared statement for finding a certificate by // primary key fingerprint. sql_stmt!(cert_find_stmt, "SELECT tpk, secret FROM keys WHERE primary_key == ?"); // This only works for v4 certificates! For v6 certificates the // keyid is the start of the fingerprint, not the end. sql_stmt!(cert_find_by_keyid_stmt, "SELECT tpk, secret FROM keys WHERE primary_key like '%' || ?"); // Returns a prepared statement for finding a key by primary key // fingerprint. sql_stmt!(tsk_find_stmt, "SELECT tpk, secret FROM keys WHERE primary_key == ? and secret == 1"); // Returns a prepared statement for finding a certificate that // contains a key with the specified key id. That is, this // matches on the primary key's key ID as well as any subkeys' key // ID. sql_stmt!(cert_find_with_key_stmt, "SELECT tpk, secret FROM subkeys LEFT JOIN keys ON subkeys.primary_key == keys.primary_key WHERE subkey == ?"); // Returns a prepared statement for finding a certificate with // secret key material that contains a key (with or without secret // key material) with the specified key id. That is, this matches // on the primary key's key ID as well as any subkeys' key ID. sql_stmt!(tsk_find_with_key_stmt, "SELECT tpk, secret FROM subkeys LEFT JOIN keys ON subkeys.primary_key == keys.primary_key WHERE subkey == ? and keys.secret == 1"); // Returns a prepared statement for finding a certificate with the // specified email address. sql_stmt!(cert_find_by_email_stmt, "SELECT tpk, secret FROM userids LEFT JOIN keys ON userids.primary_key == keys.primary_key WHERE userid == ?"); // Returns a prepared statement for returning all the fingerprints // of all certificates in the database. sql_stmt!(cert_list_stmt, "select primary_key from keys"); // Returns a prepared statement for returning all certificates in // the database. sql_stmt!(cert_all_stmt, "select tpk, secret from keys"); // Returns a prepared statement for returning all certificates in // the database, which contain secret key material. sql_stmt!(tsk_all_stmt, "select tpk, secret from keys where secret = 1"); // Returns a prepared statement for updating the keys table. sql_stmt!(cert_save_insert_primary_stmt, "INSERT OR REPLACE INTO keys (primary_key, secret, tpk) VALUES (?, ?, ?)"); // Returns a prepared statement for updating the subkeys table. sql_stmt!(cert_save_insert_subkeys_stmt, "INSERT OR REPLACE INTO subkeys (subkey, primary_key) VALUES (?, ?)"); // Returns a prepared statement for updating the userids table. sql_stmt!(cert_save_insert_userids_stmt, "INSERT OR REPLACE INTO userids (userid, primary_key) VALUES (?, ?)"); // Returns a prepared statement for deleting a certificate. // // Note: due to the use of foreign keys, when a key is removed // from the keys table, the subkeys and userids tables are also // automatically update. sql_stmt!(cert_delete_stmt, "DELETE FROM keys WHERE primary_key = ?"); // Compares two User IDs. // // Extracts the email address or URI stored in each User ID and // compares them. A User ID that does not contain an email // address or URI is sorted earlier than one that does. // // This is used as the collation function. pub fn email_cmp(a: &str, b: &str) -> std::cmp::Ordering { let a_userid = UserID::from(a); let b_userid = UserID::from(b); let a_email = a_userid .email_normalized() .or_else(|_| a_userid.uri().map(|o| o.map(Into::into))) .ok(); let b_email = b_userid .email_normalized() .or_else(|_| b_userid.uri().map(|o| o.map(Into::into))) .ok(); match (a_email, b_email) { (None, None) => std::cmp::Ordering::Equal, (None, Some(_)) => std::cmp::Ordering::Less, (Some(_), None) => std::cmp::Ordering::Greater, (Some(a), Some(b)) => a.cmp(&b) } } // The callback used by functions returning a certificate and // whether the certificate contains any secret key material. fn key_load(row: &Row) -> rusqlite::Result<(Vec, bool)> { let cert = row.get(0)?; let secret_key_material = row.get(1)?; Ok((cert, secret_key_material)) } /// Returns the matching TSK. /// /// Like [`Store::lookup_by_cert_fpr`], but only returns /// certificates with private key material. pub fn tsk_lookup_by_cert_fpr(&self, fpr: &Fingerprint) -> Result> { tracer!(TRACE, "Pep::tsk_lookup_by_cert_fpr"); let conn = self.conn.lock().unwrap(); let mut stmt = Self::tsk_find_stmt(&conn)?; let r = cert_query!(stmt, [ fpr.to_hex() ], StoreError::NotFound(KeyHandle::from(fpr)))?; let r = r.into_iter() .next() .ok_or_else(|| StoreError::NotFound(KeyHandle::from(fpr)))?; Ok(r) } /// Returns the matching TSK. /// /// Like [`Store::lookup_by_cert_or_subkey`], but only returns certificates /// with private key material. pub fn tsk_lookup_by_cert_or_subkey(&self, kh: &KeyHandle) -> Result>> { tracer!(TRACE, "Pep::tsk_lookup_by_cert_or_subkey"); let conn = self.conn.lock().unwrap(); let mut stmt = Self::tsk_find_with_key_stmt(&conn)?; let keyid = KeyID::from(kh).to_hex(); t!("({})", keyid); cert_query!(stmt, [ keyid ], StoreError::NotFound(kh.clone())) } /// Returns all of the TSKs. /// /// Like [`Store::certs`], but only returns certificates with /// private key material. pub fn tsks<'b>(&'b self) -> Box>> + 'b> { tracer!(TRACE, "Pep::tsks"); let inner = || -> Result> { let conn = self.conn.lock().unwrap(); let mut stmt = Self::tsk_all_stmt(&conn)?; cert_query!(stmt, [ ], StoreError::NoMatches("EOF".into())) }; match inner() { Ok(results) => Box::new(results.into_iter()), Err(err) => { t!("Listing TSKs: {}", err); Box::new(std::iter::empty()) } } } /// Deletes the specified certificate from the database. /// /// If the certificate contains any private key material, this is /// also deleted. /// /// Returns an error if the specified certificate is not found. pub fn cert_delete(&mut self, fpr: Fingerprint) -> Result<()> { let conn = self.conn.lock().unwrap(); let changes = wrap_err!( Self::cert_delete_stmt(&conn)? .execute(params![ fpr.to_hex() ]), CannotDeleteKey, format!("Deleting {}", fpr))?; if changes == 0 { Err(StoreError::NotFound(KeyHandle::from(fpr.clone())).into()) } else { Ok(()) } } } impl<'a> Store<'a> for Pep { /// Returns the certificates whose fingerprint matches the handle. /// /// Returns [`StoreError::NotFound`] if no certificate is found. /// /// The caller may assume that looking up a fingerprint returns at /// most one certificate. fn lookup_by_cert(&self, kh: &KeyHandle) -> Result>>> { tracer!(TRACE, "Pep::lookup_by_cert"); let conn = self.conn.lock().unwrap(); let mut stmt = match kh { KeyHandle::Fingerprint(_) => { Self::cert_find_stmt(&conn)? } KeyHandle::KeyID(_) => { Self::cert_find_by_keyid_stmt(&conn)? } }; cert_query!(stmt, [ kh.to_hex() ], StoreError::NotFound(kh.clone())) } /// Returns certificates that have a key with the specified /// handle, if any. /// /// Returns [`StoreError::NotFound`] if no certificate is not found. /// /// Note: even if you pass a fingerprint, this may return multiple /// certificates as the same subkey may be attached to multiple /// certificates. fn lookup_by_cert_or_subkey(&self, kh: &KeyHandle) -> Result>>> { tracer!(TRACE, "Pep::lookup_by_cert_or_subkey"); let conn = self.conn.lock().unwrap(); let mut stmt = Self::cert_find_with_key_stmt(&conn)?; let keyid = KeyID::from(kh).to_hex(); t!("({})", keyid); let mut certs: Vec>> = cert_query!(stmt, [ keyid ], StoreError::NotFound(kh.clone()))?; if let KeyHandle::Fingerprint(fpr) = kh { // Self::cert_find_with_key_stmt works with key ids. Make // sure the fingerprint appears. certs = certs .into_iter() .filter(|cert: &Arc>| -> bool { cert.keys().any(|ka| &ka.fingerprint() == fpr) }) .collect::>>>(); } Ok(certs) } /// Returns certificates that have a User ID matching the /// specified pattern according to the query parameters. fn select_userid(&self, query: &UserIDQueryParams, pattern: &str) -> Result>>> { tracer!(TRACE, "Pep::select_userid"); let results: Vec>; match (query.email(), query.ignore_case(), query.anchor_start(), query.anchor_end()) { // Email. (true, _, true, true) => { match UserIDQueryParams::is_email(pattern) { Ok(email) => return self.lookup_by_email(&email), Err(err) => { t!("{:?} is not a valid email address: {}", pattern, err); return Ok(vec![ ]); } } } _ => { // Iterate over all the certificates, and return those // that match. // // This is potentially very expensive. Where possible // we should use the the indices to reduce false // positives. results = self.certs() .filter(|cert| { query.check_lazy_cert(&cert, pattern) }) .collect(); } } if results.is_empty() { Err(StoreError::NoMatches(pattern.into()).into()) } else { Ok(results) } } /// Returns certificates that have a User ID with the specified /// email address. /// /// The pattern is interpreted as an email address. It is first /// normalized, and then matched against the normalized email /// address, it is anchored, and the match is case sensitive. fn lookup_by_email(&self, email: &str) -> Result>>> { tracer!(TRACE, "Pep::lookup_by_email"); let userid = crate::email_to_userid(&email)?; let email = userid.email_normalized()?.expect("have one"); let conn = self.conn.lock().unwrap(); let mut stmt = Self::cert_find_by_email_stmt(&conn)?; cert_query!(stmt, [ &email ], StoreError::NoMatches(email.into())) } /// Lists all of the certificates. /// /// If a backend is not able to enumerate all the certificates, /// then it should return those that it knows about. For /// instance, some keyservers allow certificates to be looked up /// by fingerprint, but not to enumerate all of the certificates. /// Thus, a user must not assume that if a certificate is not /// returned by this function, it cannot be found by name. fn fingerprints<'b>(&'b self) -> Box + 'b> { tracer!(TRACE, "Pep::fingerprints"); let inner = || -> Result> { let conn = self.conn.lock().unwrap(); let mut stmt = Self::cert_list_stmt(&conn)?; let rows = wrap_err!( stmt.query_map([ ], |row: &Row| { let fpr: String = row.get(0)?; Ok(fpr) }), UnknownDbError, "executing query")?; let mut results: Vec<_> = Vec::new(); for row in rows { let fpr = wrap_err!(row, UnknownError, "parsing result")?; match fpr.parse::() { Ok(fpr) => results.push(fpr), Err(err) => { t!("Warning: unable to parse {:?} as a fingerprint: {}", fpr, err); } } }; Ok(results) }; match inner() { Ok(results) => Box::new(results.into_iter()), Err(err) => { t!("Listing fingerprints: {}", err); Box::new(std::iter::empty()) } } } /// Returns all of the certificates. /// /// The default implementation is implemented in terms of /// [`Store::fingerprints`] and [`Store::lookup_by_cert_fpr`]. Many backends /// will be able to do this more efficiently. fn certs<'b>(&'b self) -> Box>> + 'b> where 'a: 'b { tracer!(TRACE, "Pep::certs"); let inner = || -> Result> { let conn = self.conn.lock().unwrap(); let mut stmt = Self::cert_all_stmt(&conn)?; cert_query!(stmt, [ ], StoreError::NoMatches("EOF".into())) }; match inner() { Ok(results) => Box::new(results.into_iter()), Err(err) => { t!("Error: {}", err); Box::new(std::iter::empty()) } } } } impl<'a> StoreUpdate<'a> for Pep { fn update_by(&self, cert: Arc>, merge_strategy: &dyn MergeCerts<'a>) -> Result>> { tracer!(TRACE, "Pep::update_by"); let fpr = cert.fingerprint(); t!("Updating {}", fpr); let mut conn = self.conn.lock().unwrap(); let tx = wrap_err!( conn.transaction(), UnknownDbError, "starting transaction" )?; // If the certificate already exists, we merge the existing // variant with the new variant. let r = wrap_err!( Self::cert_find_stmt(&tx)? .query_row(&[ &fpr.to_hex() ], Self::key_load).optional(), UnknownDbError, "executing query")?; let existing = if let Some((existing_keydata, _)) = r { t!("Got {} bytes of existing certificate data", existing_keydata.len()); match Cert::from_bytes(&existing_keydata) { Ok(existing) => Some((existing_keydata, LazyCert::from(existing))), Err(err) => { t!("Failed to parse existing data for {} (overwriting): {}", fpr, err); None } } } else { t!("New certificate"); None }; let merged = if let Some((_, existing_cert)) = &existing { t!("Updating {}", fpr); merge_strategy.merge_public(cert, Some(Arc::new(existing_cert.clone()))) .with_context(|| { format!("Merging two versions of {}", fpr) })? } else { t!("Inserting {}", fpr); merge_strategy.merge_public(cert, None)? }; let merged = merged.to_cert() .context("Resolving merged certificate")? .clone(); let mut merged_keydata = Vec::new(); wrap_err!( merged.as_tsk().serialize(&mut merged_keydata), UnknownDbError, "Serializing certificate")?; let new_or_changed = if let Some((existing_keydata, _)) = &existing { &merged_keydata != existing_keydata } else { true }; if ! new_or_changed { t!("Data unchanged."); return Ok(Arc::new(LazyCert::from(merged))); } t!("Serializing {} bytes ({:X})", merged_keydata.len(), { use std::collections::hash_map::DefaultHasher; use std::hash::Hasher; let mut hasher = DefaultHasher::new(); hasher.write(&merged_keydata); hasher.finish() }); // Save the certificate. { let mut stmt = Self::cert_save_insert_primary_stmt(&tx)?; wrap_err!( stmt.execute( params![fpr.to_hex(), merged.is_tsk(), &merged_keydata]), UnknownDbError, "Executing cert_save_insert_primary")?; } // Update the subkey table. { let mut stmt = Self::cert_save_insert_subkeys_stmt(&tx)?; for (i, ka) in merged.keys().enumerate() { t!(" {}key: {} ({} secret key material)", if i == 0 { "primary " } else { "sub" }, ka.key().keyid(), if ka.key().has_secret() { "has" } else { "no" }); wrap_err!( stmt.execute( params![ka.key().keyid().to_hex(), fpr.to_hex()]), UnknownDbError, "Executing cert save insert subkeys")?; } } // Update the userid table. { let mut stmt = Self::cert_save_insert_userids_stmt(&tx)?; for ua in merged.userids() { let uid = if let Ok(Some(email)) = ua.userid().email_normalized() { email } else if let Ok(Some(uri)) = ua.userid().uri() { uri.to_string() } else { continue; }; t!(" User ID: {}", uid); wrap_err!( stmt.execute(params![uid, fpr.to_hex()]), UnknownDbError, "Executing cert save insert userids")?; } } wrap_err!( tx.commit(), UnknownDbError, "committing transaction" )?; t!("saved"); Ok(Arc::new(LazyCert::from(merged))) } } #[cfg(test)] mod tests { use super::*; #[test] fn keys_db() -> Result<()> { tracer!(TRACE, "keys_db"); struct Record { fingerprint: &'static str, subkeys: &'static [&'static str], userids: &'static [&'static str], } impl Record { fn fingerprint(&self) -> Fingerprint { self.fingerprint.parse::().expect("valid") } fn keys(&self) -> impl Iterator { std::iter::once(self.fingerprint()) .chain(self.subkeys.iter().map(|sk| { sk.parse::().expect("valid") })) } } let records = &[ Record { fingerprint: "04880CB55875B6548C25C729A00E4CD660454746", subkeys: &[ "F9CBBC92F2C34E518722CA77CADEEF67FAADD951", "05793483F1826E44A2B866E0A7CB62B3422503EE", "2C0E08F26EE06409C4712149DAC435B561D44E7B", ], userids: &[ "francis@fake.pep.foundation", ] }, Record { fingerprint: "08C6A9408241E6ED99A0A2767A6B35253722954D", subkeys: &[ "2F7B12FD253CF4ECC9255D7053D8C1B7F801D54D", "DB806800E939D5F0611BCD0DD9D4C006F09088D1", "9F62CE8B0786820B933427A50AC73BFE55E17D22", ], userids: &[ "Luca Saiu (free software hacker at pEp foundation) ", "Luca Saiu (free software hacker, GNU maintainer, computer scientist) ", "Luca Saiu (free software hacker, GNU maintainer, computer scientist) ", "Luca Saiu (free software hacker, GNU maintainer, computer scientist) ", "Luca Saiu (free software hacker, GNU maintainer, computer scientist) ", "Luca Saiu (free software hacker, GNU maintainer, computer scientist) ", "Luca Saiu ", "Luca Saiu on mobile (do not use: only for myself) ", ] }, Record { fingerprint: "10E37AA3BBFD3348CF9AE3698EF85F1B1E37396F", subkeys: &[ "E111DA754DD6839738F83CDC4A869829AAB70CBE", "E3AB3FFE39F7B12880B1A7035A826CAA7026DB6F", ], userids: &[ "pEpUserThree ", ] }, Record { fingerprint: "1CF1202EC58B5514EADC477AAA9CAC9C7B935A45", subkeys: &[ "423D41EF768D8D1C56CAB1FD994931632B55CF76", "D9947827069496C55E92D514DA2E401BDCC26140", "0706AEDD2CA382D2F3B0EF5C34450D8FA47BE578", ], userids: &[ "john@fake.pep.foundation", ] }, Record { fingerprint: "22A589136F68CC46076FFA6071B2EDA40DC29CC7", subkeys: &[ "C5D71768F5D7CC5C94659384ADF2379966C74972", "528A548BD73B7035AF469F3147D04DC2B74725E1", "65444DDC1BB08DE9AAB7A6BA38E7A3645666C769", ], userids: &[ "david@fake.pep.foundation", ] }, Record { fingerprint: "2349DF0D7DBD60C6C20453350553D1E9E9AE5C54", subkeys: &[ "1B8185A37003F109AE7D56921908736247B42C7C", "1815AFE0BEB4EE7195B8A8402F8A666BE91A400F", "C55EE7CACBBCEB75228CDCFBCDFBE06BBC5F6D29", ], userids: &[ "sabrina@fake.pep.foundation", ] }, Record { fingerprint: "2533E4E13EC84A784DE6F2962C3E64162620C978", subkeys: &[ "5B71667A87F2006D8C874F448134877075548DB7", "1DB026D73D7B6BB8B17646B17AB77AECC3703ED5", "434E3BB9D702FF0E4B4033454D2476E6EDCA3BE2", ], userids: &[ "zachary@fake.pep.foundation", ] }, Record { fingerprint: "2F8EB6F51987B06706ED6E45064ABE0B9108844E", subkeys: &[ "3872E86087B7C38EE469BD962DD7EC6B89966D7D", "AC677AC99D84F60E71BB1A533BDE948165BE1800", "39C6CDBC2D0DF90960BD7FBF3AD07BB3F42BD08E", ], userids: &[ "gilbert@fake.pep.foundation", ] }, Record { fingerprint: "43C5C721DCFF1D7D8FD1372F49B0B5F169F745F3", subkeys: &[ "86801702F48ED113958C70FD28967B47191899F8", "932B684FB0672F374626F8889F5599183124F72D", "DD8B64AEE46E8DE72DBFBC03185B7A6512A48391", ], userids: &[ "ernest@fake.pep.foundation", ] }, Record { fingerprint: "4925E278E468D55BA68B3AC49E1F03BA787E31EA", subkeys: &[ "6C4B1AE1D50683DBF1224F0338CBCE44873FE1A7", "E350B80A80C6C77E4D8D0C62C96ADEF6A2CCFA98", "9578E63C4EBCB861521BE6F71949FBA7A090B71D", ], userids: &[ "yoko@fake.pep.foundation", ] }, Record { fingerprint: "4FDC731D26FAB6E8EAE6993C637DE61020C5DD1B", subkeys: &[ "BEED00902BCD166C0F2B10C51981CA07B08A2AA3", "1EDB7F72E67CA292FE1F3428650BEEAE8BE0BA78", "4658FDA3D8FB8E9154D2EAAFE8F03F5A4682799E", ], userids: &[ "bob@fake.pep.foundation", ] }, Record { fingerprint: "507212E4796EFBF4FF8E4B1BF411A72A5C89092C", subkeys: &[ "282E7BE3396051799239B462A9173FFEBD40DA28", "DFBC57AAC0D7C9D5E5B974ADE6F0D74501D65927", ], userids: &[ "pEpUserTwo ", ] }, Record { fingerprint: "5C55764028BE2DD718F9DAAAD8D14CE515A32801", subkeys: &[ "71BB66F7E592075A11F97E2BB058180EE35C8BE0", "D87E291B879F72682D7FA06804EEFDB17B04F78B", "155A466B86DDC61DDF25E85D50B9CB75C2151872", ], userids: &[ "ophelia@fake.pep.foundation", ] }, Record { fingerprint: "68A4E57878501CF89B9844039B3560C27904221F", subkeys: &[ "C3770108DC6DC449323718F101076D890F515D6F", "B99A6A6BAB7CE86DE08AFC770509657A6BAC730C", "689D45E46A723E234DDC7AFAB4E5A2B2EF97E405", ], userids: &[ "nkls@fake.pep.foundation", ] }, Record { fingerprint: "6C36B5E1ED2B4B1D06CD03FF5D5025F89BBA7A07", subkeys: &[ "FFF6F17D7807A8E40F08253B1B23CEF521884A3F", "0430CE889630E018D824177C1DB2D0B226A87A27", "82C2CD0877B7DA063BD09ECE69AF5A6299EC8A60", ], userids: &[ "ziggy@fake.pep.foundation", ] }, Record { fingerprint: "7A757276DFF48471EA032D2B98611755153523E6", subkeys: &[ "B6F72694DFE3ED77F6E3EE6DEEE9E2E4D0403BA2", "36469B4676CEE3B28710D6441C23477C835E5CEB", "EE98545F41E1AFDB67C4498D42916177BEAA8874", ], userids: &[ "yves@fake.pep.foundation", ] }, Record { fingerprint: "7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76", subkeys: &[ "C8016468CCDF2C33997019647F6B5258C086DC6B", "19529BB44C4AE0FB5F7976194B8313F3BF069E6A", "9DF6E3803A0F57579C5399962C7C1FA3369E4A4D", ], userids: &[ "paul@fake.pep.foundation", ] }, Record { fingerprint: "8049F106768CD5D6374645F3B0C0ABEFB3892D73", subkeys: &[ "69FE5FFBF2EC257B452E880066CC46F715A66DBC", "C21839A49A47E534731137549079AA6B02A61A8B", "D2DC92A6E0DC69F80B355308FD289E27C690EE4E", ], userids: &[ "mary@fake.pep.foundation", ] }, Record { fingerprint: "8A0293871D97E954B8397DFD072889CB0E82D77B", subkeys: &[ "614F9A11152D7D2373A4F505D49FD2CB8EEF9103", "8E7242E2C05D1FB2B49CC19A6C9FF639C2D20ED2", "CC68938EB2949794B5D34061132E928709CB1956", ], userids: &[ "randy@fake.pep.foundation", ] }, Record { fingerprint: "912E63F55388D9886151101CDA4FE8ECD8365D6E", subkeys: &[ "AB22733035AA5F96C357D0D3773082B0012A791A", "673D7C2892CB4D38CDB1DD41AF26C6724E7E40F3", "E5772DCDB9CBD8631A2A03A93F0586841937BDFE", ], userids: &[ "ned@fake.pep.foundation", ] }, Record { fingerprint: "9BC60CF498E584E5620014340C099ECDA62431FE", subkeys: &[ "6675F0E49F87BBB75D0EE762884DFEDE8DF8A5B1", "C4669EDC5091DE34BEDD65331743133E219C1EB1", "6F2087132F126BCFB19B8B0F94D1A2CC4A978727", ], userids: &[ "victor@fake.pep.foundation", ] }, Record { fingerprint: "9C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1", subkeys: &[ "32FF5498B9FF8E14D8575139DE99B5AACE801044", "568042CD692658C0FCD8B983AC9255C490B72219", "634B9C334069B2A083D1C9E66286EF138550E5C0", ], userids: &[ "irene@fake.pep.foundation", ] }, Record { fingerprint: "A20EF4E353FF61FF6B8B401AC48BF850A5E9C611", subkeys: &[ "436A273DC129DD70B4C8233B33DB91F8AAECA2FD", "BA455203A9BDEC6B168EEDE851074EA7D24AA490", "F1657C4DC11A61C47DF1CD628451B62D9873AE16", ], userids: &[ "henry@fake.pep.foundation", ] }, Record { fingerprint: "A2439F4C712EA9FA6B65BC17DECD473509E96847", subkeys: &[ "F691FDB3FE8683AC485CABEDAA761009D6D131A0", "36ECEAE6F6A2ED1264001B98B974664B34752AFC", "A762875E701B76729547C5C55609A4F9A4344957", ], userids: &[ "ulysses@fake.pep.foundation", ] }, Record { fingerprint: "AAB978A882B9A6E793960B071ADFC82AC3586C14", subkeys: &[ "F2A0CBDC287931D3D69988E5EDF969810BA5194C", ], userids: &[ "Volker Birk ", "Volker Birk ", "Volker Birk ", "Volker Birk ", "Volker Birk ", "Volker Birk ", "Volker Birk ", "Volker Birk ", ] }, Record { fingerprint: "B5AAD4575B2988D99E3FB3EE973EE71028459E9A", subkeys: &[ "CA0BD82382BC588F51FDDE5083C69AF0F851437A", "826E96AD20F30B027CD6DDD70D6DF17AFEF1FFBA", "D798ABEE594EE900CDA2B0DECEFC4DCD7DD09BF9", ], userids: &[ "xenia@fake.pep.foundation", ] }, Record { fingerprint: "B828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79", subkeys: &[ "823A732A9518DC79F687FFBC3D4778FC2C38084F", "9B585A80E1C81CC7A2B9B7DDDF5F5C00DA897592", "5FD13A0E30F49F9F50AD131ACDD45401DD0E759F", ], userids: &[ "alice@fake.pep.foundation", ] }, Record { fingerprint: "C11BCCBB3F843593B8975AFB958E85734EF0ADE0", subkeys: &[ "01389D318C1B7146464F7E0DDD26BFF997B128D0", "13830079F9A096D9333683EAE572969A4BF0EAA1", "FFA2C2B2685B4D258BDE8CCFC0D868F1963180D8", ], userids: &[ "ken@fake.pep.foundation", ] }, Record { fingerprint: "CB4287D637F9BC0CB38F978AEB64F54F552F0AB7", subkeys: &[ "CA30E474FBB1021F3CD7DB74C25C74A408C599D8", "A8B3E97A82D39C6DE4890C4854E2200EE3627373", "D533C66AE6DE8DC54C9D695564BF7EACFA7E07D3", ], userids: &[ "valeria@fake.pep.foundation", ] }, Record { fingerprint: "CDAE31CA330249BC5284C1F96033761A09D517DF", subkeys: &[ "8A8B91F58F4B20C3D79F3710054118196B963B7D", "F9B079BE2882793F7C7EF867EA301A7C9A893815", "6F2B8C671245EFF407259CE82C45488C98085D61", ], userids: &[ "wyatt@fake.pep.foundation", ] }, Record { fingerprint: "D27B4F82C717A04DFF3A986489929D075908994E", subkeys: &[ "C8F9A3AC57C402EACACCA0925DFAD7D10C678DBE", "F405EE2559711EAC16F4B0E37903174192105E48", "0878C54CDFC955756A002259A685E0D6F5530952", ], userids: &[ "xavier@fake.pep.foundation", ] }, Record { fingerprint: "E1C12525BFAF4F092D109F2152B7186506DCC483", subkeys: &[ "F03D87EDA63C5BFAB30182CC2B45B386DCB3A0CF", "8AF21DCC9537E2644C844383DB24D05A4CFF4204", "84FE32BC7C903DE726A36E18BDE25B8E80237EE2", ], userids: &[ "tamara@fake.pep.foundation", ] }, Record { fingerprint: "E4C1FB98268170769154C21BBD1637EEB34E933B", subkeys: &[ "60EF08693793CBC2A0452C59E7D3FED35EAF68B9", "ECA3B609C1661937540C50871D7FA83415FD0C9C", "DB2234A696DBAF7CDF04264FB929651D1EDAD644", ], userids: &[ "zenobia@fake.pep.foundation", ] }, Record { fingerprint: "EB4750A0B0A0F558ED5F768F8B893A26133B3F66", subkeys: &[ "6D600191F05865CFE5F4B93214180E23218FFEDA", "C101BCA52FBC77A2F00241B58F07A6D986845B5A", "A35FC3F687EDE1749A677486F300560992B840A8", ], userids: &[ "owen@fake.pep.foundation", ] }, Record { fingerprint: "F29B751A3123A1502E5C0665745FB6564FC5F7DC", subkeys: &[ "A1CEFF2AD2055D84311A34DD19BAAA837AA0EDE3", "B3358A65A142E96D742006A189E01368B3CFEA66", "ADE383408E6A66E64934114FDF466C19D1E09129", ], userids: &[ "carol@fake.pep.foundation", ] }, Record { fingerprint: "F749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3", subkeys: &[ "59901649426B2C70871133CB0DCD1F051766107F", "4560CBD01CD2BE321EC50DF9C1B27AC998ABABB8", "83E8BC30EE962BF2D160A3F59C4396FA8EE7BB36", ], userids: &[ "quasimodo@fake.pep.foundation", ] }, Record { fingerprint: "FBD19974E304C95589F976BD71059020F2CC257C", subkeys: &[ "90FE526A624C3F4503B8E77E94E766EE6359246D", "32294836077FF576868D4958848F73B0CCB468D9", "104807CB7B51CD3CD2FF7366780F3DCE8BA84184", ], userids: &[ "louis@fake.pep.foundation", ] } ]; let tmp = tempfile::tempdir()?; let filename = tmp.path().join("keys.db"); { let mut orig = PathBuf::from(env!("CARGO_MANIFEST_DIR")); orig.push("tests/pep/keys.db"); let data = std::fs::read(orig)?; std::fs::write(&filename, data)?; } let pep = Pep::open(Some(filename)).expect("can open"); // Certs. assert_eq!(records.len(), pep.certs().count()); // Fingerprints. { let mut pep_fprs = pep.fingerprints().collect::>(); pep_fprs.sort(); let mut expected_fprs = records.iter() .map(|r| r.fingerprint()) .collect::>(); expected_fprs.sort(); assert_eq!(pep_fprs.len(), expected_fprs.len()); assert_eq!(pep_fprs, expected_fprs); } for record in records.iter() { // Lookup by certs fingerprint. for (i, key) in record.keys().enumerate() { let r = pep.lookup_by_cert_fpr(&key); if i == 0 { assert!(r.is_ok()); } else { // Looking up by subkey won't work. assert!(r.is_err()); } // By fingerprint. let kh = KeyHandle::from(key); let r = pep.lookup_by_cert(&kh).ok().map(|c| c.len()); if i == 0 { assert_eq!(r, Some(1)); } else { assert_eq!(r, None); } // By Key ID. let kh = KeyHandle::from(KeyID::from(kh)); let r = pep.lookup_by_cert(&kh).ok().map(|c| c.len()); if i == 0 { assert_eq!(r, Some(1)); } else { assert_eq!(r, None); } } // Lookup by keys. for key in record.keys() { // By fingerprint. let kh = KeyHandle::from(key); assert_eq!(pep.lookup_by_cert_or_subkey(&kh).ok().map(|c| c.len()), Some(1)); // By Key ID. let kh = KeyHandle::from(KeyID::from(kh)); assert_eq!(pep.lookup_by_cert_or_subkey(&kh).ok().map(|c| c.len()), Some(1)); } // Look up User IDs. for &userid in record.userids.into_iter() { t!("Checking that {} has User ID {:?}", record.fingerprint(), userid); let matches = pep.lookup_by_userid(&UserID::from(userid)) .unwrap_or(Vec::new()) .into_iter() .map(|c| c.fingerprint()) .collect::>(); assert_eq!(matches, vec![ record.fingerprint() ]); if let Ok(email) = UserIDQueryParams::is_email(userid) { t!("Checking that {} has email {:?}", record.fingerprint(), email); let matches = pep.lookup_by_email(&email) .unwrap_or(Vec::new()) .into_iter() .map(|c| c.fingerprint()) .collect::>(); assert_eq!(matches, vec![ record.fingerprint() ]); } } } // Look up by domain. t!("email domain"); let matches = pep.lookup_by_email_domain("fake.pep.foundation") .unwrap_or(Vec::new()) .into_iter() .map(|c| c.fingerprint()) .collect::>(); assert_eq!(matches.len(), 33); let matches = pep.lookup_by_email_domain("@fake.pep.foundation") .unwrap_or(Vec::new()) .into_iter() .map(|c| c.fingerprint()) .collect::>(); assert_eq!(matches.len(), 0); let matches = pep.lookup_by_email_domain("e.pep.foundation") .unwrap_or(Vec::new()) .into_iter() .map(|c| c.fingerprint()) .collect::>(); assert_eq!(matches.len(), 0); let matches = pep.lookup_by_email_domain("pep.foundation") .unwrap_or(Vec::new()) .into_iter() .map(|c| c.fingerprint()) .collect::>(); assert_eq!(matches.len(), 2); let matches = pep.lookup_by_email_domain("ageinghacker.net") .unwrap_or(Vec::new()) .into_iter() .map(|c| c.fingerprint()) .collect::>(); assert_eq!(matches.len(), 3); // grep t!("Grepping"); let matches = pep.grep_email("pep.foundation") .unwrap_or(Vec::new()) .into_iter() .map(|c| c.fingerprint()) .collect::>(); assert_eq!(matches.len(), 35); t!("TSKs"); let tsk_fpr = "EB4750A0B0A0F558ED5F768F8B893A26133B3F66" .parse::() .expect("valid"); let tsks = records.iter() .filter(|c| c.fingerprint() == tsk_fpr) .collect::>(); assert_eq!(tsks.len(), 1); let tsk_record = tsks.into_iter().next().expect("have one"); assert_eq!(tsk_record.fingerprint(), tsk_fpr); let tsks = pep.tsks() .map(|c| c.to_cert().expect("valid").clone()) .collect::>(); let matches = tsks .iter() .map(|c| c.fingerprint()) .collect::>(); assert_eq!(matches, vec![ tsk_record.fingerprint() ]); let tsk = tsks.into_iter().next().expect("have one"); t!("Updating the tsk"); let tsk_as_cert = tsk.clone().strip_secret_key_material(); pep.update(Arc::new(LazyCert::from(tsk_as_cert))) .expect("can update"); t!("Checking that the TSK is still a TSK"); let tsks = pep.tsks() .map(|c| c.to_cert().expect("valid").clone()) .collect::>(); let matches = tsks .iter() .map(|c| c.fingerprint()) .collect::>(); assert_eq!(matches, vec![ tsk_record.fingerprint() ]); let tsk_updated = tsks.into_iter().next().expect("have one"); assert_eq!(tsk, tsk_updated); Ok(()) } } sequoia-cert-store-0.7.1/src/store/userid_index.rs000064400000000000000000000253511046102023000203450ustar 00000000000000use std::collections::BTreeMap; use std::collections::BTreeSet; use std::collections::VecDeque; use std::str; use std::sync::RwLock; use std::sync::{RwLockReadGuard, RwLockWriteGuard}; use sequoia_openpgp as openpgp; use openpgp::Fingerprint; use openpgp::Result; use openpgp::packet::UserID; use crate::store::StoreError; use crate::store::UserIDQueryParams; use super::TRACE; /// A helper data structure for implementations of [`Store`]. /// /// This data structure maintains an in-memory index of User IDs and /// email addresses, and maps them to fingerprints. It is a /// convenient helper for a [`Store`] implementing /// [`Store::select_userid`]. /// /// [`Store`]: crate::Store /// [`Store::select_userid`]: crate::Store::select_userid /// /// This data structure returns certificates with a given User ID or /// email address in `O(log n)` time. Substring and case insensitive /// matching, however, currently requires `O(n)` time. pub struct UserIDIndex { inner: RwLock, } assert_send_and_sync!(UserIDIndex); pub(crate) struct UserIDIndexInner { by_userid: BTreeMap>, // The *normalized* email. // // XXX: As we also want to search by domain, it would be better to // use a different representation / different data structure to // avoid having to do a full scan. One possibility would be to // use a `BTreeMap::range`. That requires a bit of gymnastics. // Alternatively we could use a trie, which is keyed on (domain, // localpart). by_email: BTreeMap>, // Because extracting the email address is expensive, we wait // until a caller actually needs `by_email`. In particular, when // used in a one-shot context, `by_email` is not access at all. by_email_pending: VecDeque<(UserID, Fingerprint)>, } impl UserIDIndexInner { /// Inserts the given mapping. /// /// `email` is the canonicalized email address. If that hasn't /// been computed, hand in `None`, and it will be computed on /// demand. pub(crate) fn insert(&mut self, fp: Fingerprint, userid: UserID, email: Option) { if let Some(email) = email { self.by_email .entry(email) .or_default() .insert(fp.clone()); } else { self.by_email_pending .push_back((userid.clone(), fp.clone())); } self.by_userid .entry(userid) .or_default() .insert(fp); } } impl Default for UserIDIndex { fn default() -> Self { UserIDIndex { inner: RwLock::new(UserIDIndexInner { by_userid: Default::default(), by_email: Default::default(), by_email_pending: VecDeque::new(), }), } } } impl FromIterator<(Fingerprint, UserID, Option)> for UserIDIndex { fn from_iter)>>(iter: I) -> Self { let index = UserIDIndex::default(); let mut inner = index.inner.write().unwrap(); for (fp, userid, email) in iter { inner.insert(fp, userid, email); } drop(inner); index } } impl UserIDIndex { /// Returns a new, empty UserIDIndex. pub fn new() -> Self { Self::default() } /// Adds an entry to UserIDIndex. /// /// This does *not* support removing mappings from the index. /// That is, if this function is called with the same fingerprint, /// but a User ID is removed, then the User ID is not removed from /// the index. Normally, this doesn't matter as User IDs are not /// removed (certificates are append only data structures). pub fn insert(&mut self, fpr: &Fingerprint, userids: I) where I: Iterator { let mut inner = self.inner.write().unwrap(); for userid in userids { inner.by_userid .entry(userid.clone()) .or_default() .insert(fpr.clone()); inner.by_email_pending .push_back((userid, fpr.clone())); } } /// Executes any pending insertions. /// /// This needs to be called before accessing by_email. fn execute_pending_insertions(&self) -> RwLockReadGuard { loop { // First, check if we have anything to do. let inner = self.inner.read().unwrap(); if inner.by_email_pending.is_empty() { return inner; } drop(inner); // Looks like we do. Get the writer lock. let mut inner = self.inner.write().unwrap(); let mut pending = std::mem::take(&mut inner.by_email_pending); for (userid, fpr) in pending.drain(..) { if let Ok(Some(email)) = userid.email_normalized() { inner.by_email .entry(email) .or_default() .insert(fpr.clone()); } } // std::sync::RwLock doesn't implement downgrading a // writer lock to a reader lock, so we loop. drop(inner); } } /// An implementation of [`Store::select_userid`]. /// /// [`Store::select_userid`]: crate::Store::select_userid pub fn select_userid(&self, params: &UserIDQueryParams, pattern: &str) -> Result> { tracer!(TRACE, "UserIDIndex::select_userid"); t!("params: {:?}, pattern: {:?}", params, pattern); // XXX: If you change this function, // UserIDQueryParams::select_userid contains similar code. // Update that too. let mut matches = match params { UserIDQueryParams { anchor_start: true, anchor_end: true, email: false, ignore_case: false, } => { // Exact User ID match. let userid = UserID::from(pattern); let inner = self.inner.read().unwrap(); inner.by_userid.get(&userid) .ok_or_else(|| { StoreError::NoMatches(pattern.into()) })? .iter() .cloned() .collect() } UserIDQueryParams { anchor_start: true, anchor_end: true, email: true, ignore_case: false, } => { // Exact email match. let inner = self.execute_pending_insertions(); inner.by_email .get(pattern) .ok_or_else(|| { StoreError::NoMatches(pattern.into()) })? .iter() .cloned() .collect() } UserIDQueryParams { anchor_start, anchor_end, email, ignore_case, } => { // Substring search. let mut pattern = pattern; let _pattern; if *ignore_case { // Convert to lowercase without tailoring, // i.e. without taking any locale into account. // See: // // - https://www.w3.org/International/wiki/Case_folding // - https://doc.rust-lang.org/std/primitive.str.html#method.to_lowercase // - http://www.unicode.org/versions/Unicode7.0.0/ch03.pdf#G33992 _pattern = pattern.to_lowercase(); pattern = &_pattern[..]; } // Checks if user id a match. let check = |userid: &str| -> bool { let mut userid = userid; let _userid: String; if *ignore_case { _userid = userid.to_lowercase(); userid = &_userid[..]; } t!("Considering if {:?} matches {:?} \ (anchors: {}, {}, ignore case: {})", pattern, userid, anchor_start, anchor_end, ignore_case); // XXX: Consider using // https://crates.io/crates/memchr instead. if match (*anchor_start, *anchor_end) { (true, true) => userid == pattern, (true, false) => userid.starts_with(pattern), (false, true) => userid.ends_with(pattern), (false, false) => userid.contains(pattern), } { t!("*** {:?} matches {:?} (anchors: {}, {})", pattern, userid, *anchor_start, anchor_end); true } else { false } }; if *email { let inner = self.execute_pending_insertions(); inner.by_email .iter() .filter_map(|(email, matches)| { if check(email) { Some(matches.iter()) } else { None } }) .flatten() .cloned() .collect::>() } else { let inner = self.inner.read().unwrap(); inner.by_userid .iter() .filter_map(|(userid, matches)| { let userid = String::from_utf8_lossy(userid.value()); if check(&userid) { Some(matches.iter()) } else { None } }) .flatten() .cloned() .collect::>() } } }; if matches.is_empty() { return Err(StoreError::NoMatches(pattern.into()).into()); } matches.sort(); matches.dedup(); Ok(matches) } /// Returns a write-locked reference to the inner. /// /// Can be used to insert in bulk. pub(crate) fn inner_mut(&self) -> RwLockWriteGuard { self.inner.write().unwrap() } } sequoia-cert-store-0.7.1/src/store.rs000064400000000000000000001371511046102023000156650ustar 00000000000000use std::borrow::Cow; use std::str; use std::sync::Arc; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use anyhow::Context; use sequoia_openpgp as openpgp; use openpgp::Fingerprint; use openpgp::KeyHandle; use openpgp::Result; use openpgp::cert::Cert; use openpgp::cert::ValidCert; use openpgp::packet::UserID; pub use openpgp_cert_d; mod userid_index; pub use userid_index::UserIDIndex; pub mod certd; pub use certd::CertD; pub mod certs; pub use certs::Certs; // The keyserver backend is optional. #[cfg(feature = "keyserver")] pub mod keyserver; #[cfg(feature = "keyserver")] pub use keyserver::KeyServer; // If it is disabled, we swap in a dummy to reduce changes to the rest // of the code. pub mod no_keyserver; #[cfg(not(feature = "keyserver"))] use no_keyserver as keyserver; #[cfg(not(feature = "keyserver"))] pub(crate) use keyserver::KeyServer; pub mod pep; pub use pep::Pep; use super::TRACE; use crate::LazyCert; #[derive(Debug, Clone)] pub struct UserIDQueryParams { anchor_start: bool, anchor_end: bool, email: bool, ignore_case: bool, } assert_send_and_sync!(UserIDQueryParams); impl UserIDQueryParams { /// Returns a new `UserIDQueryParams`. /// /// By default, the query is configured to perform an exact match /// on the User ID. That is, the pattern must match the start and /// end of the User ID, and case is considered significant. pub fn new() -> Self { Self { anchor_start: true, anchor_end: true, email: false, ignore_case: false, } } /// Sets whether the pattern must match the start of the User ID /// or email address. pub fn set_anchor_start(&mut self, anchor_start: bool) -> &mut Self { self.anchor_start = anchor_start; self } /// Returns whether the pattern must match the start of the User /// ID or email address. pub fn anchor_start(&self) -> bool { self.anchor_start } /// Sets whether the pattern must match the end of the User /// ID or email address. pub fn set_anchor_end(&mut self, anchor_end: bool) -> &mut Self { self.anchor_end = anchor_end; self } /// Returns whether the pattern must match the end of the User /// ID or email address. pub fn anchor_end(&self) -> bool { self.anchor_end } /// Sets whether the pattern must match the User ID or the /// normalized email address. /// /// The email address to check the pattern against is extracted /// from the User ID using [`UserID::email_normalized`]. /// /// Note: the pattern is *not* normalized, even if the anchors are /// set. If you want to search by email address you need to /// normalize it your yourself. /// /// [`UserID::email_normalized`]: https://docs.rs/sequoia-openpgp/latest/sequoia_openpgp/packet/prelude/struct.UserID.html#method.email_normalized pub fn set_email(&mut self, email: bool) -> &mut Self { self.email = email; self } /// Returns whether the pattern must match the User ID or the /// normalized email address. /// /// See [`UserIDQueryParams::set_email`] for more details. pub fn email(&self) -> bool { self.email } /// Sets whether to ignore the case when matching the User ID or /// email address. /// /// Uses the empty local. /// /// When matching an email address, the domain is always matched /// in a case insensitive manner. The localpart, however, is /// matched in a case sensitive manner by default. pub fn set_ignore_case(&mut self, ignore_case: bool) -> &mut Self { self.ignore_case = ignore_case; self } /// Returns whether to ignore the case when matching the User ID /// or email address. /// /// See [`UserIDQueryParams::set_ignore_case`] for more details. pub fn ignore_case(&self) -> bool { self.ignore_case } /// Checks that the User ID satisfies the constraints. pub fn check(&self, userid: &UserID, pattern: &str) -> bool { tracer!(TRACE, "UserIDQueryParams::check"); // XXX: If you change this function, // UserIDIndex::select_userid contains similar code. Update // that too. match self { UserIDQueryParams { anchor_start: true, anchor_end: true, email: false, ignore_case: false, } => { // Exact User ID match. userid.value() == pattern.as_bytes() } UserIDQueryParams { anchor_start: true, anchor_end: true, email: true, ignore_case: false, } => { // Exact email match. if let Ok(Some(email)) = userid.email_normalized() { email == pattern } else { false } } UserIDQueryParams { anchor_start, anchor_end, email, ignore_case, } => { t!("Considering if {:?} matches {:?} \ (anchors: {}, {}, ignore case: {})", pattern, userid, anchor_start, anchor_end, ignore_case); // Substring search. let mut userid = if *email { if let Ok(Some(email)) = userid.email_normalized() { Cow::Owned(email) } else { t!("User ID does not contain a valid email address"); return false; } } else { String::from_utf8_lossy(userid.value()) }; if *ignore_case { userid = Cow::Owned(userid.to_lowercase()); } let mut pattern = pattern; let _pattern; if *ignore_case { // Convert to lowercase without tailoring, // i.e. without taking any locale into account. // See: // // - https://www.w3.org/International/wiki/Case_folding // - https://doc.rust-lang.org/std/primitive.str.html#method.to_lowercase // - http://www.unicode.org/versions/Unicode7.0.0/ch03.pdf#G33992 _pattern = pattern.to_lowercase(); pattern = &_pattern[..]; } if match (*anchor_start, *anchor_end) { (true, true) => userid == pattern, (true, false) => userid.starts_with(pattern), (false, true) => userid.ends_with(pattern), (false, false) => userid.contains(pattern), } { t!("*** {:?} matches {:?} (anchors: {}, {})", pattern, userid, *anchor_start, anchor_end); true } else { false } } } } /// Checks that at least one User ID satisfies the constraints. pub fn check_lazy_cert(&self, cert: &LazyCert, pattern: &str) -> bool { cert.userids().any(|userid| self.check(&userid, pattern)) } /// Checks that at least one User ID satisfies the constraints. pub fn check_cert(&self, cert: &Cert, pattern: &str) -> bool { cert.userids().any(|ua| self.check(ua.userid(), pattern)) } /// Checks that at least one User ID satisfies the constraints. pub fn check_valid_cert(&self, vc: &ValidCert, pattern: &str) -> bool { vc.userids().any(|ua| self.check(ua.userid(), pattern)) } /// Returns whether the supplied email address is actually a valid /// email address. /// /// If it is valid, returns the normalized email address. pub fn is_email(email: &str) -> Result { let email_check = UserID::from(format!("<{}>", email)); match email_check.email() { Ok(Some(email_check)) => { if email != email_check { return Err(StoreError::InvalidEmail( email.to_string(), None).into()); } } Ok(None) => { return Err(StoreError::InvalidEmail( email.to_string(), None).into()); } Err(err) => { return Err(StoreError::InvalidEmail( email.to_string(), Some(err)).into()); } } match UserID::from(&email[..]).email_normalized() { Err(err) => { Err(StoreError::InvalidEmail( email.to_string(), Some(err)).into()) } Ok(None) => { Err(StoreError::InvalidEmail( email.to_string(), None).into()) } Ok(Some(email)) => { Ok(email) } } } /// Returns whether the supplied domain address is actually a /// valid domain for an email address. /// /// Returns the normalized domain. pub fn is_domain(domain: &str) -> Result { let localpart = "user@"; let email = format!("{}{}", localpart, domain); let email = Self::is_email(&email)?; // We get the normalized email address back. Chop off the // username and the @. assert!(email.starts_with(localpart)); Ok(email[localpart.len()..].to_string()) } } /// [`Store`] specific error codes. #[non_exhaustive] #[derive(thiserror::Error, Debug)] pub enum StoreError { /// No certificate was found. #[error("{0} was not found")] NotFound(KeyHandle), /// No certificate matches the search criteria. #[error("No certificates matched {0}")] NoMatches(String), /// The email address does not appear to be a valid email address. #[error("{0:?} does not appear to be a valid email address")] InvalidEmail(String, #[source] Option), } /// Status messages. /// /// Status messages sent by [`StatusListener::update`]. /// /// The transaction id allows messages to be grouped together. For /// instance, `LookupStarted` will return a new transaction id and /// further messages related to that lookup including `LookupFinished` /// and `LookupFailed` will use the same transaction id. #[non_exhaustive] #[derive(Debug)] pub enum StatusUpdate<'a, 'c> { /// Sent when a lookup is starting. /// /// usize is the transaction id. /// /// The first `&str` is a short human-readable description of the /// backend. /// /// `KeyHandle` is what is being looked up. /// /// The last `&str` is a short human-readable message describing /// the look up. LookupStarted(usize, &'a str, &'a KeyHandle, Option<&'a str>), /// Sent while a lookup is on-going. /// /// usize is the transaction id. It will match the transaction id /// sent in the `LookupStart` message. /// /// The first `&str` is a short human-readable description of the /// backend. /// /// `KeyHandle` is what was being looked up. /// /// The last `&str` is a short human-readable message describing /// something that happened, e.g., "WKD returned FPR", etc. LookupStatus(usize, &'a str, &'a KeyHandle, &'a str), /// Sent when a lookup has been successful. /// /// usize is the transaction id. It will match the transaction id /// sent in the `LookupStart` message. /// /// The first `&str` is a short human-readable description of the /// backend. /// /// `KeyHandle` is what was being looked up. /// /// The certificates are the returned results. /// /// The last `&str` is a short human-readable message describing /// what happened, e.g., "found in cache", etc. LookupFinished(usize, &'a str, &'a KeyHandle, &'a [Arc>], Option<&'a str>), /// Sent when a lookup has failed. /// /// usize is the transaction id. It will match the transaction id /// sent in the `LookupStart` message. /// /// The first `&str` is a short human-readable description of the /// backend. /// /// `KeyHandle` is what was being looked up. /// /// The error is the reason that the lookup failed. A backend /// should set this to `None` if no certificate was present. LookupFailed(usize, &'a str, &'a KeyHandle, Option<&'a anyhow::Error>), /// Sent when a search is started. /// /// usize is the transaction id. /// /// The first `&str` is a short human-readable description of the /// backend. /// /// The second `&str` is the pattern being searched for. /// /// The last `&str` is a short human-readable message describing /// what is being looked up. SearchStarted(usize, &'a str, &'a str, Option<&'a str>), /// Sent while a search is on-going. /// /// usize is the transaction id. It will match the transaction id /// sent in the `LookupStart` message. /// /// The first `&str` is a short human-readable description of the /// backend. /// /// The second `&str` is the pattern being searched for. /// /// The last `&str` is a short human-readable message describing /// something that happened, e.g., "WKD returned FPR", etc. SearchStatus(usize, &'a str, &'a str, &'a str), /// Sent when a search is successful. /// /// usize is the transaction id. It will match the transaction id /// sent in the `LookupStart` message. /// /// The first `&str` is a short human-readable description of the /// backend. /// /// The second `&str` is the pattern being searched for. /// /// The certificates are the returned results. /// /// The last `&str` is a short human-readable message describing /// what happened, e.g., "found in cache", "found 5 matching /// certificates", etc. SearchFinished(usize, &'a str, &'a str, &'a [Arc>], Option<&'a str>), /// Sent whenever something has been lookup successfully. /// /// usize is the transaction id. It will match the transaction id /// sent in the `LookupStart` message. /// /// The first `&str` is a short human-readable description of the /// backend. /// /// The second `&str` is the pattern being searched for. /// /// The error is the reason that the search failed. A backend /// should set this to `None` if no matching certificate was /// found. SearchFailed(usize, &'a str, &'a str, Option<&'a anyhow::Error>), } impl<'a, 'c> std::fmt::Display for StatusUpdate<'a, 'c> { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { use StatusUpdate::*; match self { LookupStarted(_tx, backend, kh, msg) => { if let Some(msg) = msg { write!(fmt, "{}: Looking up {}: {}...", backend, kh, msg) } else { write!(fmt, "{}: Looking up {}...", backend, kh) } } LookupStatus(_tx, backend, kh, msg) => { write!(fmt, "{}: Looking up {}: {}", backend, kh, msg) } LookupFinished(_tx, backend, kh, results, msg) => { if let Some(msg) = msg { write!(fmt, "{}: Looking up {}, returned {} results: {}", backend, kh, results.len(), msg) } else { write!(fmt, "{}: Looking up {}, returned {} results", backend, kh, results.len()) } } LookupFailed(_tx, backend, kh, err) => { if let Some(err) = err { write!(fmt, "{}: Looking up {}, failed: {}", backend, kh, err) } else { write!(fmt, "{}: Looking up {}, returned no results", backend, kh) } } SearchStarted(_tx, backend, pattern, msg) => { if let Some(msg) = msg { write!(fmt, "{}: Searching for {:?}: {}...", backend, pattern, msg) } else { write!(fmt, "{}: Searching for {:?}...", backend, pattern) } } SearchStatus(_tx, backend, pattern, msg) => { write!(fmt, "{}: Searching for {:?}: {}", backend, pattern, msg) } SearchFinished(_tx, backend, pattern, results, msg) => { if let Some(msg) = msg { write!(fmt, "{}: Searching for {:?}, returned {} results: {}", backend, pattern, results.len(), msg) } else { write!(fmt, "{}: Searching for {:?}, returned {} results", backend, pattern, results.len()) } } SearchFailed(_tx, backend, pattern, err) => { if let Some(err) = err { write!(fmt, "{}: Searching for {:?} failed: {}", backend, pattern, err) } else { write!(fmt, "{}: Searching for {:?}, returned no results", backend, pattern) } } } } } /// A callback mechanism to indicate what a backend is doing. /// /// We use an enum instead of a separate function for each message so /// that a naive listener can just print each message to stdout. /// /// This is primarily interesting for backends like a keyserver. /// /// Currently, backends are not required to implement this trait. In /// fact [`KeyServer`] is the only backend that implements it. A /// caller can add a listener to a `KeyServer` using /// [`KeyServer::add_listener`]. pub trait StatusListener { /// A status update. fn update(&self, status: &StatusUpdate); } /// Returns certificates from a backing store. pub trait Store<'a> { /// Returns the certificates whose fingerprint matches the handle. /// /// Returns [`StoreError::NotFound`] if no certificate is found. /// /// The caller may assume that looking up a fingerprint returns at /// most one certificate. fn lookup_by_cert(&self, kh: &KeyHandle) -> Result>>>; /// Returns the certificate with the specified fingerprint, if any. /// /// Returns [`StoreError::NotFound`] if the certificate is not found. /// /// The default implementation is implemented in terms of /// [`Store::lookup_by_cert`]. fn lookup_by_cert_fpr(&self, fingerprint: &Fingerprint) -> Result>> { let kh = KeyHandle::from(fingerprint.clone()); self.lookup_by_cert(&kh) .and_then(|v| { assert!(v.len() <= 1, "Looking up {} returned multiple certificates: {}", fingerprint, v.iter() .map(|c| { c.fingerprint().to_string() }) .collect::>() .join(", ")); v.into_iter().next() .ok_or(StoreError::NotFound(kh).into()) }) } /// Returns certificates that have a key with the specified /// handle, if any. /// /// Returns [`StoreError::NotFound`] if no certificate is not found. /// /// Note: even if you pass a fingerprint, this may return multiple /// certificates as the same subkey may be attached to multiple /// certificates. fn lookup_by_cert_or_subkey(&self, kh: &KeyHandle) -> Result>>>; /// Returns certificates that have a User ID matching the /// specified pattern according to the query parameters. /// /// User IDs are interpreted as UTF-8 strings. If a user ID is /// not valid UTF-8, then it will be converted to UTF-8 in a lossy /// manner using the same semantics as /// [`String::from_utf8_lossy`]. /// /// This function returns all matching user IDs, even those that /// are not self signed. It's up to the caller to authenticate /// the returned user IDs. fn select_userid(&self, query: &UserIDQueryParams, pattern: &str) -> Result>>>; /// Performs an exact match on the User ID. /// /// The pattern is anchored, and the match is case sensitive. /// /// The user ID is interpreted as a UTF-8 string. If the user ID /// is not valid UTF-8, then it will be converted to UTF-8 in a /// lossy manner using the same semantics as /// [`String::from_utf8_lossy`]. /// /// This function returns all matching user IDs, even those that /// are not self signed. It's up to the caller to authenticate /// the returned user IDs. fn lookup_by_userid(&self, userid: &UserID) -> Result>>> { self.select_userid( &UserIDQueryParams::new() .set_email(false) .set_anchor_start(true) .set_anchor_end(true) .set_ignore_case(false), &String::from_utf8_lossy(userid.value())) } /// Performs a case insenitive, substring match on the User ID. /// /// The pattern is not anchored, and it is matched case /// insensitively. /// /// User IDs are interpreted as UTF-8 strings. If a user ID is /// not valid UTF-8, then it will be converted to UTF-8 in a lossy /// manner using the same semantics as /// [`String::from_utf8_lossy`]. /// /// This function returns all matching user IDs, even those that /// are not self signed. It's up to the caller to authenticate /// the returned user IDs. fn grep_userid(&self, pattern: &str) -> Result>>> { self.select_userid( &UserIDQueryParams::new() .set_email(false) .set_anchor_start(false) .set_anchor_end(false) .set_ignore_case(true), pattern) } /// Returns certificates that have a User ID with the specified /// email address. /// /// The pattern is interpreted as an email address. It is first /// normalized, and then matched against the normalized email /// address, it is anchored, and the match is case sensitive. /// /// User IDs are interpreted as UTF-8 strings. If a user ID is /// not valid UTF-8, then it will be converted to UTF-8 in a lossy /// manner using the same semantics as /// [`String::from_utf8_lossy`]. /// /// This function returns all matching user IDs, even those that /// are not self signed. It's up to the caller to authenticate /// the returned user IDs. fn lookup_by_email(&self, email: &str) -> Result>>> { let userid = crate::email_to_userid(&email)?; let email = userid.email_normalized()?.expect("have one"); self.select_userid( &UserIDQueryParams::new() .set_email(true) .set_anchor_start(true) .set_anchor_end(true) .set_ignore_case(false), &email) } /// Performs a case insenitive, substring match on the normalized /// email address. /// /// The pattern is matched against the normalized email address, /// it is not anchored, and it is matched case insensitively. The /// pattern itself is *not* normalized. /// /// User IDs are interpreted as UTF-8 strings. If a user ID is /// not valid UTF-8, then it will be converted to UTF-8 in a lossy /// manner using the same semantics as /// [`String::from_utf8_lossy`]. /// /// This function returns all matching user IDs, even those that /// are not self signed. It's up to the caller to authenticate /// the returned user IDs. fn grep_email(&self, pattern: &str) -> Result>>> { self.select_userid( &UserIDQueryParams::new() .set_email(true) .set_anchor_start(false) .set_anchor_end(false) .set_ignore_case(true), pattern) } /// Returns certificates that have User ID with an email address /// from the specified domain. /// /// The pattern is interpreted as a domain address. It is first /// normalized, and then matched against the normalized email /// address, it is anchored, and the match is case sensitive. /// /// `domain` must be a bare domain, like `example.org`; it should /// not start with an `@`. This does not match subdomains. That /// is, it will match `alice@foo.bar.com` when searching for /// `bar.com`. /// /// User IDs are interpreted as UTF-8 strings. If a user ID is /// not valid UTF-8, then it will be converted to UTF-8 in a lossy /// manner using the same semantics as /// [`String::from_utf8_lossy`]. /// /// This function returns all matching user IDs, even those that /// are not self signed. It's up to the caller to authenticate /// the returned user IDs. fn lookup_by_email_domain(&self, domain: &str) -> Result>>> { let localpart = "localpart"; let email = format!("{}@{}", localpart, domain); let userid = crate::email_to_userid(&email)?; let email = userid.email_normalized()?.expect("have one"); let domain = &email[email.rfind('@').expect("have an @")..]; self.select_userid( &UserIDQueryParams::new() .set_email(true) .set_anchor_start(false) .set_anchor_end(true) .set_ignore_case(false), domain) } /// Lists all of the certificates. /// /// If a backend is not able to enumerate all the certificates, /// then it should return those that it knows about. For /// instance, some keyservers allow certificates to be looked up /// by fingerprint, but not to enumerate all of the certificates. /// Thus, a user must not assume that if a certificate is not /// returned by this function, it cannot be found by name. fn fingerprints<'b>(&'b self) -> Box + 'b>; /// Returns all of the certificates. /// /// The default implementation is implemented in terms of /// [`Store::fingerprints`] and [`Store::lookup_by_cert_fpr`]. Many backends /// will be able to do this more efficiently. fn certs<'b>(&'b self) -> Box>> + 'b> where 'a: 'b { Box::new(self.fingerprints() .filter_map(|fpr| { self.lookup_by_cert_fpr(&fpr).ok() })) } /// Prefills the cache. /// /// Prefilling the cache makes sense when you plan to examine most /// certificates. It doesn't make sense if you are just /// authenticating a single or a few bindings. /// /// This function may be multi-threaded. /// /// Errors should be silently ignored and propagated when the /// operation in question is executed directly. fn prefetch_all(&self) { } /// Prefetches some certificates. /// /// Prefilling the cache makes sense when you plan to examine some /// certificates in the near future. /// /// This interface is useful as it allows batching, which may be /// more efficient, especially when the certificates are accessed /// over the network. And, the function may be multi-threaded. /// /// Errors should be silently ignored and propagated when the /// operation in question is executed directly. fn prefetch_some(&self, certs: &[KeyHandle]) { let _ = certs; } } // The references in Store need a different lifetime from the contents // of the Box. Otherwise, a `Backend` that is a `&Box` would // create a self referential data structure. impl<'a: 't, 't, T> Store<'a> for Box where T: Store<'a> + ?Sized + 't { fn lookup_by_cert(&self, kh: &KeyHandle) -> Result>>> { self.as_ref().lookup_by_cert(kh) } fn lookup_by_cert_fpr(&self, fingerprint: &Fingerprint) -> Result>> { self.as_ref().lookup_by_cert_fpr(fingerprint) } fn lookup_by_cert_or_subkey(&self, kh: &KeyHandle) -> Result>>> { self.as_ref().lookup_by_cert_or_subkey(kh) } fn select_userid(&self, query: &UserIDQueryParams, pattern: &str) -> Result>>> { self.as_ref().select_userid(query, pattern) } fn lookup_by_userid(&self, userid: &UserID) -> Result>>> { self.as_ref().lookup_by_userid(userid) } fn grep_userid(&self, pattern: &str) -> Result>>> { self.as_ref().grep_userid(pattern) } fn lookup_by_email(&self, email: &str) -> Result>>> { self.as_ref().lookup_by_email(email) } fn grep_email(&self, pattern: &str) -> Result>>> { self.as_ref().grep_email(pattern) } fn lookup_by_email_domain(&self, domain: &str) -> Result>>> { self.as_ref().lookup_by_email_domain(domain) } fn fingerprints<'b>(&'b self) -> Box + 'b> { self.as_ref().fingerprints() } fn certs<'b>(&'b self) -> Box>> + 'b> where 'a: 'b { self.as_ref().certs() } fn prefetch_all(&self) { self.as_ref().prefetch_all() } fn prefetch_some(&self, certs: &[KeyHandle]) { self.as_ref().prefetch_some(certs) } } impl<'a: 't, 't, T> Store<'a> for &'t T where T: Store<'a> + ?Sized { fn lookup_by_cert(&self, kh: &KeyHandle) -> Result>>> { (*self).lookup_by_cert(kh) } fn lookup_by_cert_fpr(&self, fingerprint: &Fingerprint) -> Result>> { (*self).lookup_by_cert_fpr(fingerprint) } fn lookup_by_cert_or_subkey(&self, kh: &KeyHandle) -> Result>>> { (*self).lookup_by_cert_or_subkey(kh) } fn select_userid(&self, query: &UserIDQueryParams, pattern: &str) -> Result>>> { (*self).select_userid(query, pattern) } fn lookup_by_userid(&self, userid: &UserID) -> Result>>> { (*self).lookup_by_userid(userid) } fn grep_userid(&self, pattern: &str) -> Result>>> { (*self).grep_userid(pattern) } fn lookup_by_email(&self, email: &str) -> Result>>> { (*self).lookup_by_email(email) } fn grep_email(&self, pattern: &str) -> Result>>> { (*self).grep_email(pattern) } fn lookup_by_email_domain(&self, domain: &str) -> Result>>> { (*self).lookup_by_email_domain(domain) } fn fingerprints<'b>(&'b self) -> Box + 'b> { (*self).fingerprints() } fn certs<'b>(&'b self) -> Box>> + 'b> where 'a: 'b { (*self).certs() } } impl<'a: 't, 't, T> Store<'a> for &'t mut T where T: Store<'a> + ?Sized { fn lookup_by_cert(&self, kh: &KeyHandle) -> Result>>> { (**self).lookup_by_cert(kh) } fn lookup_by_cert_fpr(&self, fingerprint: &Fingerprint) -> Result>> { (**self).lookup_by_cert_fpr(fingerprint) } fn lookup_by_cert_or_subkey(&self, kh: &KeyHandle) -> Result>>> { (**self).lookup_by_cert_or_subkey(kh) } fn select_userid(&self, query: &UserIDQueryParams, pattern: &str) -> Result>>> { (**self).select_userid(query, pattern) } fn lookup_by_userid(&self, userid: &UserID) -> Result>>> { (**self).lookup_by_userid(userid) } fn grep_userid(&self, pattern: &str) -> Result>>> { (**self).grep_userid(pattern) } fn lookup_by_email(&self, email: &str) -> Result>>> { (**self).lookup_by_email(email) } fn grep_email(&self, pattern: &str) -> Result>>> { (**self).grep_email(pattern) } fn lookup_by_email_domain(&self, domain: &str) -> Result>>> { (**self).lookup_by_email_domain(domain) } fn fingerprints<'b>(&'b self) -> Box + 'b> { (**self).fingerprints() } fn certs<'b>(&'b self) -> Box>> + 'b> where 'a: 'b { (**self).certs() } fn prefetch_all(&self) { (**self).prefetch_all() } fn prefetch_some(&self, certs: &[KeyHandle]) { (**self).prefetch_some(certs) } } /// Merges two certificates. /// /// This is primarily useful as the `merge_strategy` callback to /// [`StoreUpdate::update_by`]. pub trait MergeCerts<'a> { /// Merges two certificates. /// /// This is primarily useful as the `merge_strategy` callback to /// [`StoreUpdate::update_by`]. /// /// The default implementation merges the two certificates using /// [`Cert::merge_public`]. This means that any secret key /// material in `disk` is preserved, any secret key material in /// `new` is ignored, and unhashed subpacket areas are merged. /// /// Note: `self` is a normal reference, not a mutable reference. /// This means that the underlying implementation has to use /// interior mutability. The advantage is that multiple threads /// can have a reference to the store, and update it in parallel. fn merge_public<'b>(&self, new: Arc>, disk: Option>>) -> Result>> { if let Some(disk) = disk { let merged = disk.to_cert()?.clone() .merge_public(new.to_cert()?.clone())?; Ok(Arc::new(LazyCert::from(merged))) } else { if new.is_tsk() { Ok(Arc::new(LazyCert::from(new.to_cert()?.clone() .strip_secret_key_material()))) } else { Ok(new) } } } } impl<'a> MergeCerts<'a> for () { } /// Provides an interface to update a backing store. pub trait StoreUpdate<'a>: Store<'a> { /// Insert a certificate. /// /// This uses the default implementation of [`MergeCerts`] to /// merge the certificate with any existing certificate. /// /// Note: `self` is a normal reference, not a mutable reference. /// This means that the underlying implementation has to use /// interior mutability. The advantage is that multiple threads /// can have a reference to the store, and update it in parallel. fn update(&self, cert: Arc>) -> Result<()> { self.update_by(cert, &mut ())?; Ok(()) } /// Inserts a certificate into the store. /// /// Inserts a certificate into the store and uses `merge_strategy` /// to merge it with the existing certificate, if any. /// /// Unless there is an error, you must call `merge_strategy`. /// This is the case even if the certificate is not on the /// backend. In that case, you must pass `None` for the on-disk /// version. This allows `merge_strategy` to generate statistics, /// and to modify the certificate before it is saved, e.g., by /// stripping third-party certifications. /// /// To use the default merge strategy, either call /// [`StoreUpdate::update`] directly, or pass `&mut ()`. /// /// Note: `self` and `merge_strategy` are normal references, not a /// mutable reference. This means that the underlying /// implementation has to use interior mutability. The advantage /// is that multiple threads can have a reference to the store, /// and update it in parallel. fn update_by(&self, cert: Arc>, merge_strategy: &dyn MergeCerts<'a>) -> Result>>; } impl<'a: 't, 't, T> StoreUpdate<'a> for Box where T: StoreUpdate<'a> + ?Sized + 't { fn update(&self, cert: Arc>) -> Result<()> { self.as_ref().update(cert) } fn update_by(&self, cert: Arc>, merge_strategy: &dyn MergeCerts<'a>) -> Result>> { self.as_ref().update_by(cert, merge_strategy) } } impl<'a: 't, 't, T> StoreUpdate<'a> for &'t T where T: StoreUpdate<'a> + ?Sized { fn update(&self, cert: Arc>) -> Result<()> { (*self).update(cert) } fn update_by(&self, cert: Arc>, merge_strategy: &dyn MergeCerts<'a>) -> Result>> { (*self).update_by(cert, merge_strategy) } } /// Merges two certificates and collects statistics. /// /// This is primarily useful as the `merge_strategy` callback to /// [`StoreUpdate::update_by`]. #[derive(Debug)] #[non_exhaustive] pub struct MergePublicCollectStats { /// Number of new certificates. new_certs: AtomicUsize, /// Number of unchanged certificates. /// /// Note: there may be false negative. That is some certificates /// may be unchanged, but the heuristic thinks that they have been /// updated. unchanged_certs: AtomicUsize, /// Number of update certificates. updated_certs: AtomicUsize, /// Number of errors. errors: AtomicUsize, } assert_send_and_sync!(MergePublicCollectStats); impl MergePublicCollectStats { /// Returns a new `MergePublicCollectStats` with all stats set to 0. pub fn new() -> Self { Self { new_certs: AtomicUsize::new(0), unchanged_certs: AtomicUsize::new(0), updated_certs: AtomicUsize::new(0), errors: AtomicUsize::new(0), } } /// Returns the number of new certificates. pub fn new_certs(&self) -> usize { self.new_certs.load(Ordering::Relaxed) } /// Returns the number of unchanged certificates. pub fn unchanged_certs(&self) -> usize { self.unchanged_certs.load(Ordering::Relaxed) } /// Returns the number of updated certificates. pub fn updated_certs(&self) -> usize { self.updated_certs.load(Ordering::Relaxed) } /// Returns the number of errors. pub fn errors(&self) -> usize { self.errors.load(Ordering::Relaxed) } /// Increments the number of new certificates by 1. pub fn inc_new_certs(&self) { self.new_certs.fetch_add(1, Ordering::Relaxed); } /// Increments the number of unchanged certificates by 1. pub fn inc_unchanged_certs(&self) { self.unchanged_certs.fetch_add(1, Ordering::Relaxed); } /// Increments the number of updated certificates by 1. pub fn inc_updated_certs(&self) { self.updated_certs.fetch_add(1, Ordering::Relaxed); } /// Increments the number of errors by 1. pub fn inc_errors(&self) { self.errors.fetch_add(1, Ordering::Relaxed); } } impl<'a> MergeCerts<'a> for MergePublicCollectStats { /// Merges two certificates. /// /// This is primarily useful as the `merge_strategy` callback to /// [`StoreUpdate::update_by`]. /// /// This implementation has the same merge semantics as the /// default implementation, but it also updates the statistics in /// `self`. /// /// # Examples /// /// ```rust /// use std::any::Any; /// use std::sync::Arc; /// /// use sequoia_openpgp as openpgp; /// # use openpgp::Result; /// use openpgp::cert::prelude::*; /// use openpgp::parse::Parse; /// /// use sequoia_cert_store as cert_store; /// use cert_store::CertStore; /// use cert_store::LazyCert; /// use cert_store::store::MergePublicCollectStats; /// use cert_store::store::StoreUpdate; /// /// # fn main() -> Result<()> { /// let (cert, _rev) = CertBuilder::new().generate()?; /// /// let mut certs = CertStore::empty(); /// /// let mut stats = MergePublicCollectStats::new(); /// /// certs.update_by(Arc::new(LazyCert::from(cert)), &mut stats) /// .expect("valid"); /// /// assert_eq!(stats.new_certs(), 1); /// # Ok(()) } /// ``` fn merge_public<'b, 'rb>(&self, new: Arc>, disk: Option>>) -> Result>> { let disk = if let Some(disk) = disk { disk } else { self.inc_new_certs(); if new.is_tsk() { return Ok(Arc::new(LazyCert::from( new.to_cert()?.clone().strip_secret_key_material()))) } else { return Ok(new); } }; let fpr = new.fingerprint(); let disk = disk.to_cert() .with_context(|| { format!("Parsing {} as returned from the cert directory", fpr) })? .clone(); let new = new.to_cert() .with_context(|| { format!("Parsing {} as being inserted into \ the cert directory", fpr) })? .clone(); if disk == new { self.inc_unchanged_certs(); Ok(Arc::new(LazyCert::from(new))) } else { // If the on-disk version has secrets, we preserve them. // If new has secrets, we ignore them. match disk.clone().merge_public(new) { Ok(merged) => { if merged == disk { self.inc_unchanged_certs(); } else { self.inc_updated_certs(); } Ok(Arc::new(LazyCert::from(merged))) } Err(err) => { self.inc_errors(); Err(err.into()) } } } } } #[cfg(test)] mod tests { use super::*; use crate::store; // Make sure we can pass a &Box where a generic type // needs to implement Store. #[test] fn store_boxed() -> Result<()> { struct Foo<'a, B> where B: Store<'a> { backend: B, _a: std::marker::PhantomData<&'a ()>, } impl<'a, B> Foo<'a, B> where B: Store<'a> { fn new(backend: B) -> Self { Foo { backend, _a: std::marker::PhantomData, } } fn count(&self) -> usize { self.backend.certs().count() } } let backend = store::Certs::empty(); let backend: Box = Box::new(backend); let foo = Foo::new(&backend); // Do something (anything) with the backend. assert_eq!(foo.count(), 0); Ok(()) } // Make sure we can pass a &Box where a generic type // needs to implement Store. #[test] fn store_update_boxed() -> Result<()> { struct Foo<'a, B> where B: StoreUpdate<'a> { backend: B, _a: std::marker::PhantomData<&'a ()>, } impl<'a, B> Foo<'a, B> where B: StoreUpdate<'a> { fn new(backend: B) -> Self { Foo { backend, _a: std::marker::PhantomData, } } fn count(&self) -> usize { self.backend.certs().count() } } let backend = store::Certs::empty(); let backend: Box = Box::new(backend); let foo = Foo::new(&backend); // Do something (anything) with the backend. assert_eq!(foo.count(), 0); Ok(()) } #[test] fn is_email() { assert!(UserIDQueryParams::is_email("foo@domain.com").is_ok()); // Need a local part. assert!(UserIDQueryParams::is_email("@domain.com").is_err()); // Need a domain. assert!(UserIDQueryParams::is_email("foo@").is_err()); // One @ assert!(UserIDQueryParams::is_email("foo").is_err()); assert!(UserIDQueryParams::is_email("foo@@domain.com").is_err()); assert!(UserIDQueryParams::is_email("foo@a@domain.com").is_err()); // Bare email address, not wrapped in angle brackets. assert!(UserIDQueryParams::is_email("").is_err()); // Whitespace is not allowed. assert!(UserIDQueryParams::is_email(" foo@domain.com").is_err()); assert!(UserIDQueryParams::is_email("foo o@domain.com").is_err()); assert!(UserIDQueryParams::is_email("foo@do main.com").is_err()); assert!(UserIDQueryParams::is_email("foo@domain.com ").is_err()); } #[test] fn is_domain() { assert!(UserIDQueryParams::is_domain("domain.com").is_ok()); // No at. assert!(UserIDQueryParams::is_domain("foo").is_ok()); assert!(UserIDQueryParams::is_domain("@domain.com").is_err()); assert!(UserIDQueryParams::is_domain("foo@").is_err()); assert!(UserIDQueryParams::is_domain("foo@@domain.com").is_err()); assert!(UserIDQueryParams::is_domain("foo@a@domain.com").is_err()); assert!(UserIDQueryParams::is_domain("").is_err()); } include!("../tests/keyring.rs"); // Check that MergePublicCollectStats works as advertised. #[test] fn store_update_merge_public_collect_stats() { use std::collections::HashSet; use openpgp::Cert; use openpgp::parse::Parse; use crate::CertStore; use crate::store::MergePublicCollectStats; let certs = CertStore::empty(); let mut stats = MergePublicCollectStats::new(); let mut seen = HashSet::new(); for (i, cert) in keyring::certs.iter().enumerate() { let cert = Cert::from_bytes(&cert.bytes()).expect("valid"); let fpr = cert.fingerprint(); seen.insert(fpr.clone()); certs.update_by(Arc::new(LazyCert::from(cert)), &mut stats) .expect("valid"); eprintln!("After inserting {} ({}), stats: {:?}", i, fpr, stats); assert_eq!(stats.new_certs(), seen.len()); assert_eq!(stats.new_certs() + stats.updated_certs() + stats.unchanged_certs(), i + 1); } let new = stats.new_certs(); let updated = stats.updated_certs(); let unchanged = stats.unchanged_certs(); // Insert again. This time nothing should change. for (i, cert) in keyring::certs.iter().enumerate() { let cert = Cert::from_bytes(&cert.bytes()).expect("valid"); let fpr = cert.fingerprint(); certs.update_by(Arc::new(LazyCert::from(cert)), &mut stats) .expect("valid"); eprintln!("After reinserting {} ({}), stats: {:?}", i, fpr, stats); // These should not change: assert_eq!(stats.new_certs(), new); // Update should also not change, but there may be false // positives. assert_eq!(stats.unchanged_certs() + stats.updated_certs(), updated + unchanged + i + 1); } } } sequoia-cert-store-0.7.1/tests/Makefile000064400000000000000000000006411046102023000161670ustar 00000000000000certs = data/alice.pgp \ data/alice2.pgp \ data/alice2-adopted-alice.pgp \ data/bob.pgp \ data/carol.pgp \ data/david.pgp \ data/ed.pgp \ data/halfling-signing.pgp \ data/halfling-encryption.pgp \ data/hans-puny-code.pgp \ data/una.pgp \ data/peter.pgp \ data/steve.pgp keyring.rs keyring.pgp: $(certs) cert2rust.sh ./cert2rust.sh $(certs) > keyring.rs sq toolbox keyring merge $(certs) > keyring.pgp sequoia-cert-store-0.7.1/tests/cert2rust.sh000075500000000000000000000046601046102023000170300ustar 00000000000000#! /bin/bash set -e echo "#[allow(non_upper_case_globals, dead_code)] mod keyring { use std::path::Path; use anyhow::Result; use sequoia_openpgp as openpgp; use openpgp::parse::Parse; pub struct Cert { pub filename: &'static str, pub base: &'static str, pub fingerprint: &'static str, pub subkeys: &'static [&'static str], pub userids: &'static [&'static str], } impl Cert { pub fn bytes(&self) -> Vec { let filename = Path::new(env!(\"CARGO_MANIFEST_DIR\")) .join(\"tests\") .join(self.filename); std::fs::read(filename).expect(\"exists\") } pub fn to_cert(&self) -> Result { openpgp::Cert::from_bytes(&self.bytes()) } } " for file in "$@" "EOF!" do base=${file%-priv.pgp} base=${base%.pgp} base=${base#*/} base=$(echo $base | sed 's/-/_/g') echo "FILE:$file:$base" if test "x$file" != "xEOF!" then sq toolbox packet dump "$file" fi done | awk -F '[ \t]*:[ \t]*' ' BEGIN { # Initialize the arrays. delete certs[0]; delete fprs[0]; delete userids[0]; print " pub const certs: &[Cert] = &[" } END { print " ];" for (i = 0; i < length(certs) - 1; i ++) { print " pub const "certs[i]": &Cert = &certs["i"];"; } } $1 ~ /^FILE$/ { # Print the pending record. if (length(fprs) > 0) { print " // "base print " Cert {" print " filename: \""file"\"," print " base: \""base"\"," print " fingerprint: \""fprs[0]"\"," print " subkeys: &["; for(i = 1; i < length(fprs); i ++) { print " \""fprs[i]"\"," } print " ],"; print " userids: &["; for(i = 0; i < length(userids); i ++) { print " \""userids[i]"\"," } print " ]," print " }," } # Reinitialize the state. delete fprs; delete userids; # Make sure the arrays are interrupted as arrays and not scalars # (need for length(fprs). delete fprs[0]; delete userids[0]; file=$2; base=$3; certs[length(certs)] = base; } $1 ~ /^ *Fingerprint$/ { fprs[length(fprs)] = $2; } $1 ~ /^ *Value$/ { userids[length(userids)] = $2; } ' echo "} // mod keyring" sequoia-cert-store-0.7.1/tests/data/README.md000064400000000000000000000126021046102023000167170ustar 00000000000000The certificates: ``` $ { for i in $(ls -1 *.pgp | grep -v -- -priv.pgp | sort); do echo; echo "- $i:"; sq inspect $i; done | grep -E '^- |Fingerprint|Key flags|Subkey|UserID'; } 2>/dev/null - alice2-adopted-alice.pgp: Fingerprint: 23CFE49D4BB7A0AA83619C147E716FFE77DF170A Key flags: certification Subkey: 662F03FC47C05D070B53A93AD5A5048A71CD012A Key flags: signing Subkey: 5989D7BE9908AE24799DF6CFBE678043781349F1 Key flags: transport encryption, data-at-rest encryption UserID: UserID: - alice2.pgp: Fingerprint: 23CFE49D4BB7A0AA83619C147E716FFE77DF170A Key flags: certification UserID: UserID: - alice.pgp: Fingerprint: 30505BCEB7403A1BBFA9DBF0BFBE63567B4BA57A Key flags: certification Subkey: 662F03FC47C05D070B53A93AD5A5048A71CD012A Key flags: signing Subkey: A6D92948A7ADEB809F04202F1CF1943DFE153D1E Key flags: authentication Subkey: 5989D7BE9908AE24799DF6CFBE678043781349F1 Key flags: transport encryption, data-at-rest encryption UserID: UserID: - bob.pgp: Fingerprint: 9994DBF9D34E88E2A21D0CE8E79C9395A1004BB0 Key flags: certification Subkey: 7E01441CBF6FAB5C4AB457E2FBD6F5322354B331 Key flags: authentication UserID: - carol.pgp: Fingerprint: E9C6EFC0E39CE6F9DF5274E7E362D45C7FF7B654 Key flags: certification Subkey: CD22D4BD99FF10FDA11A83D4213DCB92C95346CE Key flags: authentication UserID: UserID: - david.pgp: Fingerprint: A82BC944220BD5EBECC4D42883F74A0EAC207446 Key flags: certification Subkey: DF674FBAC52E00F0E6E48436481D2E18158FB594 Key flags: authentication Subkey: CD22D4BD99FF10FDA11A83D4213DCB92C95346CE UserID: - ed.pgp: Fingerprint: 0C346B2B6241263F64E9C7CF1EA300797258A74E Key flags: certification Subkey: 0C346B2B6241263F64E9C7CF1EA300797258A74E Key flags: certification UserID: - halfling-encryption.pgp: Fingerprint: D58E047C05D115EA4F3D1A98A67A733127BBE804 Key flags: certification Subkey: 69669E91C8D5C546D442FB246FE6D4751AC09E15 Key flags: authentication Subkey: CC4EFA3BFAB8E92A54CDEA3F3DC7543293DD4E53 Key flags: transport encryption, data-at-rest encryption UserID: UserID: Halfling - halfling-signing.pgp: Fingerprint: D58E047C05D115EA4F3D1A98A67A733127BBE804 Key flags: certification Subkey: 69669E91C8D5C546D442FB246FE6D4751AC09E15 Key flags: authentication Subkey: 9DCDA2A95A17B728D6A5115EFF5C6582E4D14B68 Key flags: signing UserID: UserID: Halfling - hans-puny-code.pgp: Fingerprint: F6675D0E4DA40823715C4811B89491F07D08E4F8 Key flags: certification Subkey: 3F60EA0AEBC13E290939A080DB1F5F11C17CB2D4 Key flags: signing UserID: Hans - peter.pgp: Fingerprint: 692E359EFB0BE115373F6CEAD5D8BA16C805A81F Key flags: certification Subkey: 69D325E20DA4D404279A6534A6DE34867DE8927E Key flags: authentication Subkey: 62D2D97F00DF96D2DFED85F98CE4FE6BD01282DE Key flags: signing Subkey: DBEF1760AA2631194301E60F485BDDEF4BD2AD9A Key flags: transport encryption, data-at-rest encryption UserID: Dad UserID: Peter - steve.pgp: Fingerprint: 217E256E176719A5452EDFF935AADEC66B56585B Key flags: certification Subkey: 32C5820540308752B7092EE5B596B656FD8F700B Key flags: signing UserID: Steve - una.pgp: Fingerprint: 119B01460659D8EF3732BEC271424ADE3EC61BBC Key flags: certification Subkey: EE58C32E3D2336F223BD89CED0BE447BF39B439F Key flags: signing UserID: Una ``` alice: A normal certificate with two User IDs. Shares one with alice. alice2: A normal certificate with two User IDs. Shares one with alice. alice2-adopted-alice: alice2, but adopts the signing and encryption subkeys from alice. (Note: does *not* adopt the authentication subkey.) bob: A normal certificate with two User IDs. carol: A normal certificate with two User IDs. david: A normal certificate, but with a copy of one of carol's subkeys (CD22D4BD99FF10FDA11A83D4213DCB92C95346CE). That is, the subkey is appended to david's certificate, but there is no binding signature. ed: An unusual certificate: his primary key is also a subkey on his certificate! halfling: Two versions of the same certificate. Both have the same authentication subkey, one has a signing subkey, the other an encryption subkey. Likewise, both have one User ID in common (regis@pup.com) and a second User ID ('Halfling ' and 'Halfling ', respectively). hans: A certificate with an email address that uses puny code. peter: A certificate with a user ID that has a third-party certification from Una, but is not self signed. steve: A certificate with an email address for a subdomain of `sub.company.com`. una: A certificate with an email address for the domain `company.com`. sequoia-cert-store-0.7.1/tests/data/alice-priv.pgp000064400000000000000000000051251046102023000202050ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 3050 5BCE B740 3A1B BFA9 DBF0 BFBE 6356 7B4B A57A Comment: Comment: xVgEY+UZ6hYJKwYBBAHaRw8BAQdApx8xAVNjcUkGW31wZ6Q9vu3nyxJWtXmHZv3s fNltYBAAAQDsK+BQsRsLPvAIHJu3C+BKXcHM8mT1IjaM8DWG+nzGUxBLwsALBB8W CgB9BYJj5RnqAwsJBwkQv75jVntLpXpHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu c2VxdW9pYS1wZ3Aub3Jnva4FJbJxx7sK7X3dhFq2wNvcZsFejb7GQ3lC8VLAYgoD FQoIApsBAh4BFiEEMFBbzrdAOhu/qdvwv75jVntLpXoAAP5cAQD061B+gQ3jEOpw 6Q/wjnDozuYsGzLHBFWEFVi/IbItLwD/TWIPqi8u+rg6izg5FyWKUKmOM4aQJ1Vf 197wsnS4owbNEzxhbGljZUBiZWlzcGllbC5kZT7CwAsEExYKAH0FgmPlGeoDCwkH CRC/vmNWe0ulekcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5v cmdH+TPK9LgWpgPdmdKdauHGsYnmmqULgjFaoKg1gCusNwMVCggCmwECHgEWIQQw UFvOt0A6G7+p2/C/vmNWe0ulegAAeYEBALQonPFqr7aboFpg0XJYDP87Yj/p2pdQ r/Esmfq6B+3rAP0VuZjSj3jGQdhANQEajzgt+jPiBdHiXYyg1EL3bYJ8Cc0TPGFs aWNlQGV4YW1wbGUub3JnPsLADgQTFgoAgAWCY+UZ6gMLCQcJEL++Y1Z7S6V6RxQA AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ6VXkc72cU7i9Ybj ZAE/9tKUTWlrkjezQ4RNMjCAFjW5AxUKCAKZAQKbAQIeARYhBDBQW863QDobv6nb 8L++Y1Z7S6V6AAC0swEAjcl5uncoHJJq3TDFd9djxobft/sG1fRfDZkCbnSDSaEA /Rjc4Q1eMgz1cM8W2wqvbdgdlFVr7kzTkdrsKUaN9ZoIx1gEY+UZ6hYJKwYBBAHa Rw8BAQdA7Xh768P0ELWKVha7jvlrf96N6bK8+ISBFSWBwQeTLakAAQCtqQCQsy1s v2FYdJMNdYhSWii805Yl5cC3A0IYgnrnpw8dwsC/BBgWCgExBYJj5RnqCRC/vmNW e0ulekcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdtYYQ1 1XlNWLv9gxhoTYet69Lh8X77HOWBpzAdtJBmXwKbAr6gBBkWCgBvBYJj5RnqCRDV pQSKcc0BKkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdQ lNFzuInCVzUbKl51HCYaZFdTs2LNrQh8L9q/1owAWhYhBGYvA/xHwF0HC1OpOtWl BIpxzQEqAADLjAD/QUyfRGsEP2hWDBpFpCtUdGfP6A1jeKQn+9Xw9DtxbKQA/3R4 DWT92tQVKhxACYZ0fwonKES3O/LoXUNlAiopk3oHFiEEMFBbzrdAOhu/qdvwv75j VntLpXoAAApDAP0aJdpJydTAcca0k3PXSbkesKGt6FzRX2FfaVYsF344PgEA53Wg I0BP1JaXYVluRw/e810Cs6WU1mfx8+es89IjMg/HWARj5RnqFgkrBgEEAdpHDwEB B0D5ukYoevx2XvwlWHTFXAwCOan5xlwnfSGOMQ1zfxvZNwAA/iWeus2V/ORnJWtL uI/494HqbfeN0iqt8No362LvB4bvFPPCwAAEGBYKAHIFgmPlGeoJEL++Y1Z7S6V6 RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ+lcv7W/p/Na pcQEogXqYyBZMz8ravDhJf05LAAF5taFApsgFiEEMFBbzrdAOhu/qdvwv75jVntL pXoAABJ9AQCWbnMz/xaOEjnPVOGDL+zL24fn0p0hsaeoqg3vi5fWxwD8CLVImAhu tMIo5RVprJMPtvB/HYohbEzKhGfPnjDGjALHXQRj5RnqEgorBgEEAZdVAQUBAQdA Znn93nJMi4jDuZCRv4apwCIdKrm/NZbCy0wnDX7MEwgDAQgHAAD/WLQhnzGd2f++ ac/vRlm+/e4+FNeEjUhR0szxsnYg3CgTTMLAAAQYFgoAcgWCY+UZ6gkQv75jVntL pXpHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jny+Hpckkt MWCWN9rhA2ojDCEHFohdcAL/OqZVDI4A7dQCmwwWIQQwUFvOt0A6G7+p2/C/vmNW e0ulegAAAfwBAI57hRdq6yb6HQLiH1zd4VuKvqCwpHPvNgtniyN8iWKwAQCSHyAV X0P3g8IpYr9kGrcaOJlZBL7lGacyWnqO5X8rAA== =fTyc -----END PGP PRIVATE KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/alice.pgp000064400000000000000000000046101046102023000172250ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- Comment: 3050 5BCE B740 3A1B BFA9 DBF0 BFBE 6356 7B4B A57A Comment: Comment: xjMEY+UZ6hYJKwYBBAHaRw8BAQdApx8xAVNjcUkGW31wZ6Q9vu3nyxJWtXmHZv3s fNltYBDCwAsEHxYKAH0FgmPlGeoDCwkHCRC/vmNWe0ulekcUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcme9rgUlsnHHuwrtfd2EWrbA29xmwV6N vsZDeULxUsBiCgMVCggCmwECHgEWIQQwUFvOt0A6G7+p2/C/vmNWe0ulegAA/lwB APTrUH6BDeMQ6nDpD/COcOjO5iwbMscEVYQVWL8hsi0vAP9NYg+qLy76uDqLODkX JYpQqY4zhpAnVV/X3vCydLijBs0TPGFsaWNlQGJlaXNwaWVsLmRlPsLACwQTFgoA fQWCY+UZ6gMLCQcJEL++Y1Z7S6V6RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl cXVvaWEtcGdwLm9yZ0f5M8r0uBamA92Z0p1q4caxieaapQuCMVqgqDWAK6w3AxUK CAKbAQIeARYhBDBQW863QDobv6nb8L++Y1Z7S6V6AAB5gQEAtCic8WqvtpugWmDR clgM/ztiP+nal1Cv8SyZ+roH7esA/RW5mNKPeMZB2EA1ARqPOC36M+IF0eJdjKDU QvdtgnwJzRM8YWxpY2VAZXhhbXBsZS5vcmc+wsAOBBMWCgCABYJj5RnqAwsJBwkQ v75jVntLpXpHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn pVeRzvZxTuL1huNkAT/20pRNaWuSN7NDhE0yMIAWNbkDFQoIApkBApsBAh4BFiEE MFBbzrdAOhu/qdvwv75jVntLpXoAALSzAQCNyXm6dygckmrdMMV312PGht+3+wbV 9F8NmQJudINJoQD9GNzhDV4yDPVwzxbbCq9t2B2UVWvuTNOR2uwpRo31mgjOMwRj 5RnqFgkrBgEEAdpHDwEBB0DteHvrw/QQtYpWFruO+Wt/3o3psrz4hIEVJYHBB5Mt qcLAvwQYFgoBMQWCY+UZ6gkQv75jVntLpXpHFAAAAAAAHgAgc2FsdEBub3RhdGlv bnMuc2VxdW9pYS1wZ3Aub3JnbWGENdV5TVi7/YMYaE2HrevS4fF++xzlgacwHbSQ Zl8CmwK+oAQZFgoAbwWCY+UZ6gkQ1aUEinHNASpHFAAAAAAAHgAgc2FsdEBub3Rh dGlvbnMuc2VxdW9pYS1wZ3Aub3JnUJTRc7iJwlc1GypedRwmGmRXU7Niza0IfC/a v9aMAFoWIQRmLwP8R8BdBwtTqTrVpQSKcc0BKgAAy4wA/0FMn0RrBD9oVgwaRaQr VHRnz+gNY3ikJ/vV8PQ7cWykAP90eA1k/drUFSocQAmGdH8KJyhEtzvy6F1DZQIq KZN6BxYhBDBQW863QDobv6nb8L++Y1Z7S6V6AAAKQwD9GiXaScnUwHHGtJNz10m5 HrChrehc0V9hX2lWLBd+OD4BAOd1oCNAT9SWl2FZbkcP3vNdArOllNZn8fPnrPPS IzIPzjMEY+UZ6hYJKwYBBAHaRw8BAQdA+bpGKHr8dl78JVh0xVwMAjmp+cZcJ30h jjENc38b2TfCwAAEGBYKAHIFgmPlGeoJEL++Y1Z7S6V6RxQAAAAAAB4AIHNhbHRA bm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ+lcv7W/p/NapcQEogXqYyBZMz8ravDh Jf05LAAF5taFApsgFiEEMFBbzrdAOhu/qdvwv75jVntLpXoAABJ9AQCWbnMz/xaO EjnPVOGDL+zL24fn0p0hsaeoqg3vi5fWxwD8CLVImAhutMIo5RVprJMPtvB/HYoh bEzKhGfPnjDGjALOOARj5RnqEgorBgEEAZdVAQUBAQdAZnn93nJMi4jDuZCRv4ap wCIdKrm/NZbCy0wnDX7MEwgDAQgHwsAABBgWCgByBYJj5RnqCRC/vmNWe0ulekcU AAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfL4elySS0xYJY3 2uEDaiMMIQcWiF1wAv86plUMjgDt1AKbDBYhBDBQW863QDobv6nb8L++Y1Z7S6V6 AAAB/AEAjnuFF2rrJvodAuIfXN3hW4q+oLCkc+82C2eLI3yJYrABAJIfIBVfQ/eD wiliv2Qatxo4mVkEvuUZpzJaeo7lfysA =BY+x -----END PGP PUBLIC KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/alice2-adopted-alice-priv.pgp000064400000000000000000000043151046102023000227600ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 23CF E49D 4BB7 A0AA 8361 9C14 7E71 6FFE 77DF 170A Comment: Comment: xVgEY+UaGxYJKwYBBAHaRw8BAQdAEHlpai+gE3eN9Sewy6RIwvaHi2OEIDB7fQr6 pram3vMAAP9zhzAUmgRsXfq0k+HAauIoMJZolK3EM2kI5CsiIsAniw+WwsALBB8W CgB9BYJj5RobAwsJBwkQfnFv/nffFwpHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu c2VxdW9pYS1wZ3Aub3JncVBBE88Gk0KMiijn6+D2YEwnUHeM/zg0vALIwDl2RrQD FQoIApsBAh4BFiEEI8/knUu3oKqDYZwUfnFv/nffFwoAAOMlAP9D/ZD6KFO8d1TB Ki2nvKSmm/RwX6aVPDEVS0uPu9iMtwD/QhVzSsD6wzcd80Ula9IDT+C7yEZ2CMpc IeiIA2WGQwjNEzxhbGljZUBleGFtcGxlLm9yZz7CwA4EExYKAIAFgmPlGhsDCwkH CRB+cW/+d98XCkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5v cmdoocXr9PuRzwDItT2MITA36GIBcBfqXedZAbR7P5nqLgMVCggCmQECmwECHgEW IQQjz+SdS7egqoNhnBR+cW/+d98XCgAAqY0BALITDqYhnN3dLZlK3ybrImivQ83D b17zs/FaDBU8rM7MAPsEPi8BtIDh3sgcsjn600t0hlEszveHHFQgoMnuYc3jBM0R PGFsaWNlQHZlcmVpbi5kZT7CwAsEExYKAH0FgmPlGhsDCwkHCRB+cW/+d98XCkcU AAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmckJt7Ocdb1SmQu A6weRYznf4bioouOY5+r/FSgtpPc+wMVCggCmwECHgEWIQQjz+SdS7egqoNhnBR+ cW/+d98XCgAA9hUBAPQK/vysGLQwjN0dZD2WILPlzbt7qn6VGc9hDCHNPsUDAP4k CdMGyA0acViE1AIc5Twwn3sEPIXJihzjfsUyuk2FDcdYBGPlGeoWCSsGAQQB2kcP AQEHQO14e+vD9BC1ilYWu475a3/ejemyvPiEgRUlgcEHky2pAAEArakAkLMtbL9h WHSTDXWIUloovNOWJeXAtwNCGIJ656cPHcLAvwQYFgoBMQWCY+UagAkQfnFv/nff FwpHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnqMoVP0Pd Oi25d/qc8ueT5jt9aF33lxjSugNbcEQ4a4wCmwK+oAQZFgoAbwWCY+UavAkQ1aUE inHNASpHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnRoBN k8e87zbsqsgeb64KBZIzl9XnTskLml6gFmtRn6wWIQRmLwP8R8BdBwtTqTrVpQSK cc0BKgAAu0oA/0JDvSmuSOijhvPXMy8g0hX0MjiamfiVNTrk5P1PcnLfAPwNzR12 KvEokeqJ+kbZsl9T1qVqMMfR2/dFEjMPc6atChYhBCPP5J1Lt6Cqg2GcFH5xb/53 3xcKAAAdJQD+OZbuEBjzGBetUP8CPeqyckDWZQCe0dCNw6BBp9JUcAsBAKyVZINQ Q4xRXEaSr/j3yEXXeLAktweByQ9rq75bMTECx10EY+UZ6hIKKwYBBAGXVQEFAQEH QGZ5/d5yTIuIw7mQkb+GqcAiHSq5vzWWwstMJw1+zBMIAwEIBwAA/1i0IZ8xndn/ vmnP70ZZvv3uPhTXhI1IUdLM8bJ2INwoE0zCwAAEGBYKAHIFgmPlGoAJEH5xb/53 3xcKRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZzKM37Ew uBHlB0dJ4Ljo9MIUDIEaVEFyJ+l+659sYwQsApsMFiEEI8/knUu3oKqDYZwUfnFv /nffFwoAANJrAP0aUjgxFCD789m2w0nAry0SA5JlLjuxWOJ42ziWEYAYUgEAzJwW RDgabsLC7VZ+v+TZ7wEh0JhGk6dX2lbH0v1apwE= =Qflk -----END PGP PRIVATE KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/alice2-adopted-alice.pgp000064400000000000000000000040651046102023000220040ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- Comment: 23CF E49D 4BB7 A0AA 8361 9C14 7E71 6FFE 77DF 170A Comment: Comment: xjMEY+UaGxYJKwYBBAHaRw8BAQdAEHlpai+gE3eN9Sewy6RIwvaHi2OEIDB7fQr6 pram3vPCwAsEHxYKAH0FgmPlGhsDCwkHCRB+cW/+d98XCkcUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdxUEETzwaTQoyKKOfr4PZgTCdQd4z/ ODS8AsjAOXZGtAMVCggCmwECHgEWIQQjz+SdS7egqoNhnBR+cW/+d98XCgAA4yUA /0P9kPooU7x3VMEqLae8pKab9HBfppU8MRVLS4+72Iy3AP9CFXNKwPrDNx3zRSVr 0gNP4LvIRnYIylwh6IgDZYZDCM0TPGFsaWNlQGV4YW1wbGUub3JnPsLADgQTFgoA gAWCY+UaGwMLCQcJEH5xb/533xcKRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl cXVvaWEtcGdwLm9yZ2ihxev0+5HPAMi1PYwhMDfoYgFwF+pd51kBtHs/meouAxUK CAKZAQKbAQIeARYhBCPP5J1Lt6Cqg2GcFH5xb/533xcKAACpjQEAshMOpiGc3d0t mUrfJusiaK9DzcNvXvOz8VoMFTyszswA+wQ+LwG0gOHeyByyOfrTS3SGUSzO94cc VCCgye5hzeMEzRE8YWxpY2VAdmVyZWluLmRlPsLACwQTFgoAfQWCY+UaGwMLCQcJ EH5xb/533xcKRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9y ZyQm3s5x1vVKZC4DrB5FjOd/huKii45jn6v8VKC2k9z7AxUKCAKbAQIeARYhBCPP 5J1Lt6Cqg2GcFH5xb/533xcKAAD2FQEA9Ar+/KwYtDCM3R1kPZYgs+XNu3uqfpUZ z2EMIc0+xQMA/iQJ0wbIDRpxWITUAhzlPDCfewQ8hcmKHON+xTK6TYUNzjMEY+UZ 6hYJKwYBBAHaRw8BAQdA7Xh768P0ELWKVha7jvlrf96N6bK8+ISBFSWBwQeTLanC wL8EGBYKATEFgmPlGoAJEH5xb/533xcKRxQAAAAAAB4AIHNhbHRAbm90YXRpb25z LnNlcXVvaWEtcGdwLm9yZ6jKFT9D3TotuXf6nPLnk+Y7fWhd95cY0roDW3BEOGuM ApsCvqAEGRYKAG8FgmPlGrwJENWlBIpxzQEqRxQAAAAAAB4AIHNhbHRAbm90YXRp b25zLnNlcXVvaWEtcGdwLm9yZ0aATZPHvO827KrIHm+uCgWSM5fV507JC5peoBZr UZ+sFiEEZi8D/EfAXQcLU6k61aUEinHNASoAALtKAP9CQ70prkjoo4bz1zMvINIV 9DI4mpn4lTU65OT9T3Jy3wD8Dc0ddirxKJHqifpG2bJfU9alajDH0dv3RRIzD3Om rQoWIQQjz+SdS7egqoNhnBR+cW/+d98XCgAAHSUA/jmW7hAY8xgXrVD/Aj3qsnJA 1mUAntHQjcOgQafSVHALAQCslWSDUEOMUVxGkq/498hF13iwJLcHgckPa6u+WzEx As44BGPlGeoSCisGAQQBl1UBBQEBB0Bmef3eckyLiMO5kJG/hqnAIh0qub81lsLL TCcNfswTCAMBCAfCwAAEGBYKAHIFgmPlGoAJEH5xb/533xcKRxQAAAAAAB4AIHNh bHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZzKM37EwuBHlB0dJ4Ljo9MIUDIEa VEFyJ+l+659sYwQsApsMFiEEI8/knUu3oKqDYZwUfnFv/nffFwoAANJrAP0aUjgx FCD789m2w0nAry0SA5JlLjuxWOJ42ziWEYAYUgEAzJwWRDgabsLC7VZ+v+TZ7wEh 0JhGk6dX2lbH0v1apwE= =teyh -----END PGP PUBLIC KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/alice2-priv.pgp000064400000000000000000000023011046102023000202600ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 23CF E49D 4BB7 A0AA 8361 9C14 7E71 6FFE 77DF 170A Comment: Comment: xVgEY+UaGxYJKwYBBAHaRw8BAQdAEHlpai+gE3eN9Sewy6RIwvaHi2OEIDB7fQr6 pram3vMAAP9zhzAUmgRsXfq0k+HAauIoMJZolK3EM2kI5CsiIsAniw+WwsALBB8W CgB9BYJj5RobAwsJBwkQfnFv/nffFwpHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu c2VxdW9pYS1wZ3Aub3JncVBBE88Gk0KMiijn6+D2YEwnUHeM/zg0vALIwDl2RrQD FQoIApsBAh4BFiEEI8/knUu3oKqDYZwUfnFv/nffFwoAAOMlAP9D/ZD6KFO8d1TB Ki2nvKSmm/RwX6aVPDEVS0uPu9iMtwD/QhVzSsD6wzcd80Ula9IDT+C7yEZ2CMpc IeiIA2WGQwjNEzxhbGljZUBleGFtcGxlLm9yZz7CwA4EExYKAIAFgmPlGhsDCwkH CRB+cW/+d98XCkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5v cmdoocXr9PuRzwDItT2MITA36GIBcBfqXedZAbR7P5nqLgMVCggCmQECmwECHgEW IQQjz+SdS7egqoNhnBR+cW/+d98XCgAAqY0BALITDqYhnN3dLZlK3ybrImivQ83D b17zs/FaDBU8rM7MAPsEPi8BtIDh3sgcsjn600t0hlEszveHHFQgoMnuYc3jBM0R PGFsaWNlQHZlcmVpbi5kZT7CwAsEExYKAH0FgmPlGhsDCwkHCRB+cW/+d98XCkcU AAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmckJt7Ocdb1SmQu A6weRYznf4bioouOY5+r/FSgtpPc+wMVCggCmwECHgEWIQQjz+SdS7egqoNhnBR+ cW/+d98XCgAA9hUBAPQK/vysGLQwjN0dZD2WILPlzbt7qn6VGc9hDCHNPsUDAP4k CdMGyA0acViE1AIc5Twwn3sEPIXJihzjfsUyuk2FDQ== =gFrK -----END PGP PRIVATE KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/alice2.pgp000064400000000000000000000022121046102023000173030ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- Comment: 23CF E49D 4BB7 A0AA 8361 9C14 7E71 6FFE 77DF 170A Comment: Comment: xjMEY+UaGxYJKwYBBAHaRw8BAQdAEHlpai+gE3eN9Sewy6RIwvaHi2OEIDB7fQr6 pram3vPCwAsEHxYKAH0FgmPlGhsDCwkHCRB+cW/+d98XCkcUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdxUEETzwaTQoyKKOfr4PZgTCdQd4z/ ODS8AsjAOXZGtAMVCggCmwECHgEWIQQjz+SdS7egqoNhnBR+cW/+d98XCgAA4yUA /0P9kPooU7x3VMEqLae8pKab9HBfppU8MRVLS4+72Iy3AP9CFXNKwPrDNx3zRSVr 0gNP4LvIRnYIylwh6IgDZYZDCM0TPGFsaWNlQGV4YW1wbGUub3JnPsLADgQTFgoA gAWCY+UaGwMLCQcJEH5xb/533xcKRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl cXVvaWEtcGdwLm9yZ2ihxev0+5HPAMi1PYwhMDfoYgFwF+pd51kBtHs/meouAxUK CAKZAQKbAQIeARYhBCPP5J1Lt6Cqg2GcFH5xb/533xcKAACpjQEAshMOpiGc3d0t mUrfJusiaK9DzcNvXvOz8VoMFTyszswA+wQ+LwG0gOHeyByyOfrTS3SGUSzO94cc VCCgye5hzeMEzRE8YWxpY2VAdmVyZWluLmRlPsLACwQTFgoAfQWCY+UaGwMLCQcJ EH5xb/533xcKRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9y ZyQm3s5x1vVKZC4DrB5FjOd/huKii45jn6v8VKC2k9z7AxUKCAKbAQIeARYhBCPP 5J1Lt6Cqg2GcFH5xb/533xcKAAD2FQEA9Ar+/KwYtDCM3R1kPZYgs+XNu3uqfpUZ z2EMIc0+xQMA/iQJ0wbIDRpxWITUAhzlPDCfewQ8hcmKHON+xTK6TYUN =YlOd -----END PGP PUBLIC KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/bob-priv.pgp000064400000000000000000000023611046102023000176710ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 9994 DBF9 D34E 88E2 A21D 0CE8 E79C 9395 A100 4BB0 Comment: xVgEY+UatRYJKwYBBAHaRw8BAQdAQIdC7Mtji2y/IXhu8JCxu1G0qqYTgr49Bdj8 snAIEM8AAQCfCfQG/hHyGgmqdj4+UgZp+vloXbyCYj8D42Gex98QCA5ewsALBB8W CgB9BYJj5Rq1AwsJBwkQ55yTlaEAS7BHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu c2VxdW9pYS1wZ3Aub3Jn7D3pLAHwD1YR/1SDpvqqikIW/jpemK0r8zWs/l/3DdUD FQoIApsBAh4BFiEEmZTb+dNOiOKiHQzo55yTlaEAS7AAAPV6AQDhSfx5nt+3MJyo 7B52cukyqIOnoqCYGWo+4t828i9XaAEAx2jWVjseyux9vdanQYlrLdgHsAx0B4tJ ogMFSR6XRwjNETxib2JAZXhhbXBsZS5vcmc+wsAOBBMWCgCABYJj5Rq1AwsJBwkQ 55yTlaEAS7BHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn bfF/2XNAdjk1Z4tNpf8fjcymgy+YqdjnN0x32eHY/k0DFQoIApkBApsBAh4BFiEE mZTb+dNOiOKiHQzo55yTlaEAS7AAAI8xAQCUN00BU9dDjLwlBK4IyF/UBjOXxAR+ +/vMa/HG7n3HHAD5Aa3gipNh3e/ebxKxU6nUel0/UWWc9c4/gTm+uQEQFAvHWARj 5Rq1FgkrBgEEAdpHDwEBB0CCpHHKZeveaGq/F/p022nymMdI3AuvaxqA0AR8avec TwAA/16FbccP13iBXOXTbvnujSBR2XdVR/24wXn/jX85yzEMEoPCwAAEGBYKAHIF gmPlGrUJEOeck5WhAEuwRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEt cGdwLm9yZ5os3w7f5+o4HuUxBV2h63k5B43y/CMMuZ273JSDTn7ZApsgFiEEmZTb +dNOiOKiHQzo55yTlaEAS7AAACSnAQDdhmWSrUA+k1fugEjo5Z6qZ2CRHAdMs0ou 70ih530UIgD+IphnO+r+t4vmL38Q5axtrveDaT5/CaObmyWdGpPnJgA= =t/bH -----END PGP PRIVATE KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/bob.pgp000064400000000000000000000022121046102023000167060ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- Comment: 9994 DBF9 D34E 88E2 A21D 0CE8 E79C 9395 A100 4BB0 Comment: xjMEY+UatRYJKwYBBAHaRw8BAQdAQIdC7Mtji2y/IXhu8JCxu1G0qqYTgr49Bdj8 snAIEM/CwAsEHxYKAH0FgmPlGrUDCwkHCRDnnJOVoQBLsEcUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfsPeksAfAPVhH/VIOm+qqKQhb+Ol6Y rSvzNaz+X/cN1QMVCggCmwECHgEWIQSZlNv5006I4qIdDOjnnJOVoQBLsAAA9XoB AOFJ/Hme37cwnKjsHnZy6TKog6eioJgZaj7i3zbyL1doAQDHaNZWOx7K7H291qdB iWst2AewDHQHi0miAwVJHpdHCM0RPGJvYkBleGFtcGxlLm9yZz7CwA4EExYKAIAF gmPlGrUDCwkHCRDnnJOVoQBLsEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1 b2lhLXBncC5vcmdt8X/Zc0B2OTVni02l/x+NzKaDL5ip2Oc3THfZ4dj+TQMVCggC mQECmwECHgEWIQSZlNv5006I4qIdDOjnnJOVoQBLsAAAjzEBAJQ3TQFT10OMvCUE rgjIX9QGM5fEBH77+8xr8cbufcccAPkBreCKk2Hd795vErFTqdR6XT9RZZz1zj+B Ob65ARAUC84zBGPlGrUWCSsGAQQB2kcPAQEHQIKkccpl695oar8X+nTbafKYx0jc C69rGoDQBHxq95xPwsAABBgWCgByBYJj5Rq1CRDnnJOVoQBLsEcUAAAAAAAeACBz YWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeaLN8O3+fqOB7lMQVdoet5OQeN 8vwjDLmdu9yUg05+2QKbIBYhBJmU2/nTTojioh0M6Oeck5WhAEuwAAAkpwEA3YZl kq1APpNX7oBI6OWeqmdgkRwHTLNKLu9Ioed9FCIA/iKYZzvq/reL5i9/EOWsba73 g2k+fwmjm5slnRqT5yYA =Td4n -----END PGP PUBLIC KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/carol-priv.pgp000064400000000000000000000030741046102023000202310ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: E9C6 EFC0 E39C E6F9 DF52 74E7 E362 D45C 7FF7 B654 Comment: Comment: xVgEY+Ua1hYJKwYBBAHaRw8BAQdAY65xupenq6zarpL8+IP4ET8SMHNc2uB5lyRk SkP9o7MAAP96LHgnISslAvTOZmzOKS0ht2NaL6mTXhcJrbZPk/QM+A4qwsALBB8W CgB9BYJj5RrWAwsJBwkQ42LUXH/3tlRHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu c2VxdW9pYS1wZ3Aub3JntxsJyG6tZauXoqu4wDrfT47CuTSEhcZGU50batT3ybUD FQoIApsBAh4BFiEE6cbvwOOc5vnfUnTn42LUXH/3tlQAAPElAP41eTwyyOfcsuHT rFbj3HiWH0KlVONvhUHQMJwKYwzGzgD7BHMzoL1/nVJyaLvUe/REk7bBEloWyWMb HYB+CqT+Zw7NEDxjYXJvbEBjbHViLm9yZz7CwA4EExYKAIAFgmPlGtYDCwkHCRDj YtRcf/e2VEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcB Mlo76iLhVDgLJ277jEun5W2LaQAUV8oTStIxVQ22QAMVCggCmQECmwECHgEWIQTp xu/A45zm+d9SdOfjYtRcf/e2VAAAODUBAOVX3lR6yBsA17Io99wqBExrThnBZP+u hYplpK64E+JwAQDTid1PU54Go8KtN23T39ucl996t3VsIMRVFFqdqhC/Cc0RPGNh cm9sQHZlcmVpbi5kZT7CwAsEExYKAH0FgmPlGtYDCwkHCRDjYtRcf/e2VEcUAAAA AAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfPf6EmtFTrqcqACSnG Zh0/hfvNau9XtWDwMALA524ExAMVCggCmwECHgEWIQTpxu/A45zm+d9SdOfjYtRc f/e2VAAAUXwBAK/EgWyclBzKPzfBtfwphRtLzAEiHDsKXzNyLsreeotbAQDxc4Oq PIG0N71AIBdaQC/l8PRD6WpkyUWPHOhcU9ONAMdYBGPlGtYWCSsGAQQB2kcPAQEH QIO0pe9j/3u853twGgG1CVK3g9SytDdw/i94umEExMbcAAD+OqsXOsky+0KUALOK d0X6bXElePGMHjYUySwNNt4fkZ8OuMLAAAQYFgoAcgWCY+Ua1gkQ42LUXH/3tlRH FAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnlKNI5wDCnCTp jVDFKO5eSYqxhk0fCNg8Jk2dqnvukt8CmyAWIQTpxu/A45zm+d9SdOfjYtRcf/e2 VAAA1QUA/2EUWkTsVcQsFOs2jqnH5TYYk8dS8ytGEGT4CDnh3h7VAQDXfTW3rRDI MA0aKgKzAtabfE1TIfmq++9ok2NVY6VmCA== =9u3W -----END PGP PRIVATE KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/carol.pgp000064400000000000000000000027241046102023000172540ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- Comment: E9C6 EFC0 E39C E6F9 DF52 74E7 E362 D45C 7FF7 B654 Comment: Comment: xjMEY+Ua1hYJKwYBBAHaRw8BAQdAY65xupenq6zarpL8+IP4ET8SMHNc2uB5lyRk SkP9o7PCwAsEHxYKAH0FgmPlGtYDCwkHCRDjYtRcf/e2VEcUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcme3GwnIbq1lq5eiq7jAOt9PjsK5NISF xkZTnRtq1PfJtQMVCggCmwECHgEWIQTpxu/A45zm+d9SdOfjYtRcf/e2VAAA8SUA /jV5PDLI59yy4dOsVuPceJYfQqVU42+FQdAwnApjDMbOAPsEczOgvX+dUnJou9R7 9ESTtsESWhbJYxsdgH4KpP5nDs0QPGNhcm9sQGNsdWIub3JnPsLADgQTFgoAgAWC Y+Ua1gMLCQcJEONi1Fx/97ZURxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVv aWEtcGdwLm9yZwEyWjvqIuFUOAsnbvuMS6flbYtpABRXyhNK0jFVDbZAAxUKCAKZ AQKbAQIeARYhBOnG78DjnOb531J05+Ni1Fx/97ZUAAA4NQEA5VfeVHrIGwDXsij3 3CoETGtOGcFk/66FimWkrrgT4nABANOJ3U9Tngajwq03bdPf25yX33q3dWwgxFUU Wp2qEL8JzRE8Y2Fyb2xAdmVyZWluLmRlPsLACwQTFgoAfQWCY+Ua1gMLCQcJEONi 1Fx/97ZURxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ89/ oSa0VOupyoAJKcZmHT+F+81q71e1YPAwAsDnbgTEAxUKCAKbAQIeARYhBOnG78Dj nOb531J05+Ni1Fx/97ZUAABRfAEAr8SBbJyUHMo/N8G1/CmFG0vMASIcOwpfM3Iu yt56i1sBAPFzg6o8gbQ3vUAgF1pAL+Xw9EPpamTJRY8c6FxT040AzjMEY+Ua1hYJ KwYBBAHaRw8BAQdAg7Sl72P/e7zne3AaAbUJUreD1LK0N3D+L3i6YQTExtzCwAAE GBYKAHIFgmPlGtYJEONi1Fx/97ZURxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl cXVvaWEtcGdwLm9yZ5SjSOcAwpwk6Y1QxSjuXkmKsYZNHwjYPCZNnap77pLfApsg FiEE6cbvwOOc5vnfUnTn42LUXH/3tlQAANUFAP9hFFpE7FXELBTrNo6px+U2GJPH UvMrRhBk+Ag54d4e1QEA1301t60QyDANGioCswLWm3xNUyH5qvvvaJNjVWOlZgg= =EKJk -----END PGP PUBLIC KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/david-priv.pgp000064400000000000000000000023671046102023000202240ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: A82B C944 220B D5EB ECC4 D428 83F7 4A0E AC20 7446 Comment: xVgEY+Ua5hYJKwYBBAHaRw8BAQdAXAh2K8fjAUO0OgzlDev4uNbX5BGK3Esw4/0J XI1JyzoAAQDkIRWTuiPoOD7sBdGWVpou8lTmj8QQ4a9Szkr2QgacrxBxwsALBB8W CgB9BYJj5RrmAwsJBwkQg/dKDqwgdEZHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu c2VxdW9pYS1wZ3Aub3JnaQnCtjdzUum2zN5rDlJ5oAubbIlB22DQi9u3RCozk2ID FQoIApsBAh4BFiEEqCvJRCIL1evsxNQog/dKDqwgdEYAADcHAQC+pct6ehrApldr muFXouCHhgNJY/kluxb1Sm86nbOMBgD/WB1z2zyClo/ih/C4W2lID9/DyiES0Qxx HyTtnVKkBQjNEzxkYXZpZEBleGFtcGxlLm9yZz7CwA4EExYKAIAFgmPlGuYDCwkH CRCD90oOrCB0RkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5v cmcpieSeQNbpWGyU3MABKsAQsYerarlIivPGeZEeWiaitQMVCggCmQECmwECHgEW IQSoK8lEIgvV6+zE1CiD90oOrCB0RgAAfyEBAJPHJI7fgqwKbNRNxQkX9MiB3cRF 3E/PUPzG5j5s9OPuAQClPa4DMSU+eq6R2eKhqRnvVXu0xhjq9X5YSCZNItgkD8dY BGPlGuYWCSsGAQQB2kcPAQEHQEcWx/3XYvqCV2gZckVmuswudsq9SJLCGxqOC39Q UpGlAAEA7hA6/QxuHZ+v0wWaXqoAiMJEGVjQbteS3HyfrOJgFFgPjMLAAAQYFgoA cgWCY+Ua5gkQg/dKDqwgdEZHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9p YS1wZ3Aub3JnXiZfmH9Jd4GM1HP00xfTTv6MYOlIEdNjDvq8wAM1GDkCmyAWIQSo K8lEIgvV6+zE1CiD90oOrCB0RgAAO8YA/1ZbLxcWywe3Jusa1qlXMOlg45BpYKwS wwDXOL2luG8SAPwK69yqLy9ScYsAEL4OhmDL7yM1Uilj6dqj5wf5J2c2Dw== =jWEy -----END PGP PRIVATE KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/david.pgp000064400000000000000000000022001046102023000172300ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- xjMEY+Ua5hYJKwYBBAHaRw8BAQdAXAh2K8fjAUO0OgzlDev4uNbX5BGK3Esw4/0J XI1JyzrCwAsEHxYKAH0FgmPlGuYDCwkHCRCD90oOrCB0RkcUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdpCcK2N3NS6bbM3msOUnmgC5tsiUHb YNCL27dEKjOTYgMVCggCmwECHgEWIQSoK8lEIgvV6+zE1CiD90oOrCB0RgAANwcB AL6ly3p6GsCmV2ua4Vei4IeGA0lj+SW7FvVKbzqds4wGAP9YHXPbPIKWj+KH8Lhb aUgP38PKIRLRDHEfJO2dUqQFCM0TPGRhdmlkQGV4YW1wbGUub3JnPsLADgQTFgoA gAWCY+Ua5gMLCQcJEIP3Sg6sIHRGRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl cXVvaWEtcGdwLm9yZymJ5J5A1ulYbJTcwAEqwBCxh6tquUiK88Z5kR5aJqK1AxUK CAKZAQKbAQIeARYhBKgryUQiC9Xr7MTUKIP3Sg6sIHRGAAB/IQEAk8ckjt+CrAps 1E3FCRf0yIHdxEXcT89Q/MbmPmz04+4BAKU9rgMxJT56rpHZ4qGpGe9Ve7TGGOr1 flhIJk0i2CQPzjMEY+Ua5hYJKwYBBAHaRw8BAQdARxbH/ddi+oJXaBlyRWa6zC52 yr1IksIbGo4Lf1BSkaXCwAAEGBYKAHIFgmPlGuYJEIP3Sg6sIHRGRxQAAAAAAB4A IHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ14mX5h/SXeBjNRz9NMX007+ jGDpSBHTYw76vMADNRg5ApsgFiEEqCvJRCIL1evsxNQog/dKDqwgdEYAADvGAP9W Wy8XFssHtybrGtapVzDpYOOQaWCsEsMA1zi9pbhvEgD8Cuvcqi8vUnGLABC+DoZg y+8jNVIpY+nao+cH+SdnNg/OMwRj5RrWFgkrBgEEAdpHDwEBB0CDtKXvY/97vOd7 cBoBtQlSt4PUsrQ3cP4veLphBMTG3A== =qvwl -----END PGP PUBLIC KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/ed-priv.pgp000064400000000000000000000015771046102023000175270ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 0C34 6B2B 6241 263F 64E9 C7CF 1EA3 0079 7258 A74E Comment: xVgEY+ik9BYJKwYBBAHaRw8BAQdAObLfcRUmRi9GlRVnuTclWR3NJHksXjREY3cH JDyH1voAAQDORtejD38d+8iWdYv3M63QpoyPL/L3+TvN3SbXnUJphBK6wsARBB8W CgCDBYJj6KT0BYkFpI+9AwsJBwkQHqMAeXJYp05HFAAAAAAAHgAgc2FsdEBub3Rh dGlvbnMuc2VxdW9pYS1wZ3Aub3Jnh7+35183eD+a3pvIXkdz7DCCR2ISYaxyaHOD 3WtribIDFQoIApsBAh4BFiEEDDRrK2JBJj9k6cfPHqMAeXJYp04AAJ26AP4ovN3U DSrlm7jGTB+tf8JMdwgcQcUKgnoDBuMJvkLccwEAoRtHRXNH+sYHURU/jahToikJ Zfgzh8sf+uCiL5szGQrNEDxlZEBleGFtcGxlLm9yZz7CwBQEExYKAIYFgmPopPQF iQWkj70DCwkHCRAeowB5clinTkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1 b2lhLXBncC5vcmfyK5UQkGketo/v4NZTwsJ0zVJ3ELy8grkWl8XLxGytqAMVCggC mQECmwECHgEWIQQMNGsrYkEmP2Tpx88eowB5clinTgAAfqYA/R9/VwnlCaLf3t5I /xVwdg1Ts0YfyV03/A4o2hPFGcOtAP4j0mlIKP819VEtQOR0bGQJoefzbjRRpXPB muVIbumoDA== =rB7Y -----END PGP PRIVATE KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/ed.pgp000064400000000000000000000026351046102023000165450ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- Comment: 0C34 6B2B 6241 263F 64E9 C7CF 1EA3 0079 7258 A74E Comment: xjMEY+ik9BYJKwYBBAHaRw8BAQdAObLfcRUmRi9GlRVnuTclWR3NJHksXjREY3cH JDyH1vrCwBEEHxYKAIMFgmPopPQFiQWkj70DCwkHCRAeowB5clinTkcUAAAAAAAe ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeHv7fnXzd4P5rem8heR3Ps MIJHYhJhrHJoc4Pda2uJsgMVCggCmwECHgEWIQQMNGsrYkEmP2Tpx88eowB5clin TgAAnboA/ii83dQNKuWbuMZMH61/wkx3CBxBxQqCegMG4wm+QtxzAQChG0dFc0f6 xgdRFT+NqFOiKQll+DOHyx/64KIvmzMZCs0QPGVkQGV4YW1wbGUub3JnPsLAFAQT FgoAhgWCY+ik9AWJBaSPvQMLCQcJEB6jAHlyWKdORxQAAAAAAB4AIHNhbHRAbm90 YXRpb25zLnNlcXVvaWEtcGdwLm9yZ/IrlRCQaR62j+/g1lPCwnTNUncQvLyCuRaX xcvEbK2oAxUKCAKZAQKbAQIeARYhBAw0aytiQSY/ZOnHzx6jAHlyWKdOAAB+pgD9 H39XCeUJot/e3kj/FXB2DVOzRh/JXTf8DijaE8UZw60A/iPSaUgo/zX1US1A5HRs ZAmh5/NuNFGlc8Ga5Uhu6agMzjMEY+ik9BYJKwYBBAHaRw8BAQdAObLfcRUmRi9G lRVnuTclWR3NJHksXjREY3cHJDyH1vrCwL8EGBYKATEFgmPopZYJEB6jAHlyWKdO RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ2DwGaTs9JDD fiZEnYLt1gnn9Y1BPath3jMWJVieWnMiApsBvqAEGRYKAG8FgmPopZYJEB6jAHly WKdORxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZykmDJ78 rKWJ5keAV7xA/cXU/fILUT42Pl4hKER8n+dpFiEEDDRrK2JBJj9k6cfPHqMAeXJY p04AAFWuAQCgRxcn47GVYgjnCTtM2nR65cDP2tD8Asi17yTK9B4hQQD+NeIuSbjw nJZ20Lc775PN966+4yvmw0yIcqy78W6/qgYWIQQMNGsrYkEmP2Tpx88eowB5clin TgAAHtcA/1Z6hgFz6HduwMGO8pU1UdZOHN9tyof10as2/77QvadlAQCx5HBD+d5P NkOu/RHHPp5w26LRENwzmeWxF7/WwAulAw== =h26d -----END PGP PUBLIC KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/halfling-encryption.pgp000064400000000000000000000033151046102023000221250ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- xjMEY/8uIxYJKwYBBAHaRw8BAQdAA9aW4B1xTRkp7iBYjGFguvS4QSBtDECaz5IC 3W7Si6vCwAsEHxYKAH0FgmP/LiMDCwkHCRCmenMxJ7voBEcUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdPYAQhUEP91O1VnH3HoKmNbyxdQj3b 9oC2B/UsIp4yAQMVCggCmwECHgEWIQTVjgR8BdEV6k89GpimenMxJ7voBAAA/tEA /ArkGemMlBvohv/fimDmlg2zkt/Nw5oM2okjWJHphji4AP94j9uSMI3GAEoOLaxI kus5vG90Ud7/qVLpWj+9ychXCM0PPHJlZ2lzQHB1cC5jb20+wsALBBMWCgB9BYJj /y4jAwsJBwkQpnpzMSe76ARHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9p YS1wZ3Aub3JnGsjX3qd44eMJYm2mbl8MwYPkxEXNI19YCMKg2sveytoDFQoIApsB Ah4BFiEE1Y4EfAXRFepPPRqYpnpzMSe76AQAAEtPAQC/ah2Y+9ZEASbXJi+qU/+o vdDlZlLe6ebEhXSC+IbQBwEA9yls7wgOdo/Hd2ObuBZ4yVsYl3mWWiyaHEiBUWaj dAvNIkhhbGZsaW5nIDxlbmNyeXB0aW9uQGhhbGZsaW5nLm9yZz7CwAsEExYKAH0F gmP/LiMDCwkHCRCmenMxJ7voBEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1 b2lhLXBncC5vcmcSil+Zw+IntviqsJbeZxDhvtQYNpnCz9ZFvRN21TFxdAMVCggC mwECHgEWIQTVjgR8BdEV6k89GpimenMxJ7voBAAAh0EA/0ByKwDxhH+/vv1M5RbO og3XIVwZHNcvMAgeP3xc8cv8AQCj8NuNWA69IqmIL6wA3fk2sA8+vsaPeiv5V663 p0FWDM4zBGP/LiMWCSsGAQQB2kcPAQEHQEKgATvrVSfdcu3z8dRVmSYNvRY4WiI4 VqDnXdVfPw6zwsAABBgWCgByBYJj/y4jCRCmenMxJ7voBEcUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmf9Zi+LJShZF85Q891p2IEzeanqA2l4 3XJl5/Rb/qRaIgKbIBYhBNWOBHwF0RXqTz0amKZ6czEnu+gEAAAgeQEA7Q8yHq28 RY3PAcSTDua7ggtz3G9DFcsE6fUQNWu47vYBAI+IYKsqLqQM6HKyNhQOAAxQy9mK eL+hrmAeQPam+e0MzjgEY/8uIxIKKwYBBAGXVQEFAQEHQCVvNNnL1FUU6TCMyUPF e1jUiulQT/zau/KV4KPofFYnAwEIB8LAAAQYFgoAcgWCY/8uIwkQpnpzMSe76ARH FAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnYZaiMhnJCms6 gTancmz+JeZeLV2cZ3S13gA1nOQMckICmwwWIQTVjgR8BdEV6k89GpimenMxJ7vo BAAAJsAA/AnJSCYRCloktWNPvfOUszKy4+R7CZ/IAIiCniCk/hIGAP9uf6mJIpnb /bIcfuDmUm5aWgAVUE+FpvNIjY9+mjoNAQ== =98QG -----END PGP PUBLIC KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/halfling-priv.pgp000064400000000000000000000057321046102023000207200ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: D58E 047C 05D1 15EA 4F3D 1A98 A67A 7331 27BB E804 Comment: Comment: Halfling Comment: Halfling xVgEY/8uIxYJKwYBBAHaRw8BAQdAA9aW4B1xTRkp7iBYjGFguvS4QSBtDECaz5IC 3W7Si6sAAQDpLGjeWahSE6psgYSmz7OTAisgRPTCas0HZDlHJrVZQw54wsALBB8W CgB9BYJj/y4jAwsJBwkQpnpzMSe76ARHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu c2VxdW9pYS1wZ3Aub3JnT2AEIVBD/dTtVZx9x6CpjW8sXUI92/aAtgf1LCKeMgED FQoIApsBAh4BFiEE1Y4EfAXRFepPPRqYpnpzMSe76AQAAP7RAPwK5BnpjJQb6Ib/ 34pg5pYNs5LfzcOaDNqJI1iR6YY4uAD/eI/bkjCNxgBKDi2sSJLrObxvdFHe/6lS 6Vo/vcnIVwjNDzxyZWdpc0BwdXAuY29tPsLACwQTFgoAfQWCY/8uIwMLCQcJEKZ6 czEnu+gERxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZxrI 196neOHjCWJtpm5fDMGD5MRFzSNfWAjCoNrL3sraAxUKCAKbAQIeARYhBNWOBHwF 0RXqTz0amKZ6czEnu+gEAABLTwEAv2odmPvWRAEm1yYvqlP/qL3Q5WZS3unmxIV0 gviG0AcBAPcpbO8IDnaPx3djm7gWeMlbGJd5llosmhxIgVFmo3QLzSJIYWxmbGlu ZyA8ZW5jcnlwdGlvbkBoYWxmbGluZy5vcmc+wsALBBMWCgB9BYJj/y4jAwsJBwkQ pnpzMSe76ARHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn EopfmcPiJ7b4qrCW3mcQ4b7UGDaZws/WRb0TdtUxcXQDFQoIApsBAh4BFiEE1Y4E fAXRFepPPRqYpnpzMSe76AQAAIdBAP9AcisA8YR/v779TOUWzqIN1yFcGRzXLzAI Hj98XPHL/AEAo/DbjVgOvSKpiC+sAN35NrAPPr7Gj3or+Veut6dBVgzNH0hhbGZs aW5nIDxzaWduaW5nQGhhbGZsaW5nLm9yZz7CwA4EExYKAIAFgmP/LiMDCwkHCRCm enMxJ7voBEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmef WyvuawQMSACunre9VWwnwXCShhv05eNmAIx4hDt6CAMVCggCmQECmwECHgEWIQTV jgR8BdEV6k89GpimenMxJ7voBAAAUWIA/jFrXrOw4h6iOJovCvGD9zoJfyZWhCEt t7x/+6amswwgAP0fYkSVoKfBFYKaBjTYRKF3THLsNBeF5YiRQFqG4MknCMdYBGP/ LiMWCSsGAQQB2kcPAQEHQEKgATvrVSfdcu3z8dRVmSYNvRY4WiI4VqDnXdVfPw6z AAEA5teNruqH9A7+PSQEK3dXgr6Fr4ghOKYoNye4GEfhIuoPTcLAAAQYFgoAcgWC Y/8uIwkQpnpzMSe76ARHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1w Z3Aub3Jn/WYviyUoWRfOUPPdadiBM3mp6gNpeN1yZef0W/6kWiICmyAWIQTVjgR8 BdEV6k89GpimenMxJ7voBAAAIHkBAO0PMh6tvEWNzwHEkw7mu4ILc9xvQxXLBOn1 EDVruO72AQCPiGCrKi6kDOhysjYUDgAMUMvZini/oa5gHkD2pvntDMdYBGP/LiMW CSsGAQQB2kcPAQEHQLwKdrFqa5xqwwWWc/XcOJ1lHWcafCeupBQYG9cKzh5MAAEA ro5+ebXHuOPjY/pujqygi0th6+Fol06kvQ7S6SVFW/0TD8LAvwQYFgoBMQWCY/8u IwkQpnpzMSe76ARHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Au b3JnzfCsG7qn234cMZF4CFsc4ONjr3AJlHydRYL8Qy1bIs0CmwK+oAQZFgoAbwWC Y/8uIwkQ/1xlguTRS2hHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1w Z3Aub3JnQoH/CrlAhnFSborWNoHT9TYvuRiBNEDQEYxnNfAWUekWIQSdzaKpWhe3 KNalEV7/XGWC5NFLaAAAQbABAJWxv0yuD0OoNgdD1yJBjGdP+VbNkBppuH8kmFzs KQu2AP49z+hhBHCj6fw9IcFZUPoMH29Sn8ZQJ8uiPUK718fmCBYhBNWOBHwF0RXq Tz0amKZ6czEnu+gEAACFcAEA9/VC7suSJyisJ64qeBryGm9sX2KOm8F3BQlD1ck7 h2IA/i330B58jaJ8JOhaoLqIfNx5OaNx3CNlpkXakPoG+RoLx10EY/8uIxIKKwYB BAGXVQEFAQEHQCVvNNnL1FUU6TCMyUPFe1jUiulQT/zau/KV4KPofFYnAwEIBwAA /1rxV+ZmdBzBr1NKc5q54iqv5/XNCosvy3KIZ7hYswNYEcLCwAAEGBYKAHIFgmP/ LiMJEKZ6czEnu+gERxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdw Lm9yZ2GWojIZyQprOoE2p3Js/iXmXi1dnGd0td4ANZzkDHJCApsMFiEE1Y4EfAXR FepPPRqYpnpzMSe76AQAACbAAPwJyUgmEQpaJLVjT73zlLMysuPkewmfyACIgp4g pP4SBgD/bn+piSKZ2/2yHH7g5lJuWloAFVBPhabzSI2Pfpo6DQE= =GU9G -----END PGP PRIVATE KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/halfling-signing.pgp000064400000000000000000000037111046102023000213710ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- xjMEY/8uIxYJKwYBBAHaRw8BAQdAA9aW4B1xTRkp7iBYjGFguvS4QSBtDECaz5IC 3W7Si6vCwAsEHxYKAH0FgmP/LiMDCwkHCRCmenMxJ7voBEcUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdPYAQhUEP91O1VnH3HoKmNbyxdQj3b 9oC2B/UsIp4yAQMVCggCmwECHgEWIQTVjgR8BdEV6k89GpimenMxJ7voBAAA/tEA /ArkGemMlBvohv/fimDmlg2zkt/Nw5oM2okjWJHphji4AP94j9uSMI3GAEoOLaxI kus5vG90Ud7/qVLpWj+9ychXCM0PPHJlZ2lzQHB1cC5jb20+wsALBBMWCgB9BYJj /y4jAwsJBwkQpnpzMSe76ARHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9p YS1wZ3Aub3JnGsjX3qd44eMJYm2mbl8MwYPkxEXNI19YCMKg2sveytoDFQoIApsB Ah4BFiEE1Y4EfAXRFepPPRqYpnpzMSe76AQAAEtPAQC/ah2Y+9ZEASbXJi+qU/+o vdDlZlLe6ebEhXSC+IbQBwEA9yls7wgOdo/Hd2ObuBZ4yVsYl3mWWiyaHEiBUWaj dAvNH0hhbGZsaW5nIDxzaWduaW5nQGhhbGZsaW5nLm9yZz7CwA4EExYKAIAFgmP/ LiMDCwkHCRCmenMxJ7voBEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lh LXBncC5vcmefWyvuawQMSACunre9VWwnwXCShhv05eNmAIx4hDt6CAMVCggCmQEC mwECHgEWIQTVjgR8BdEV6k89GpimenMxJ7voBAAAUWIA/jFrXrOw4h6iOJovCvGD 9zoJfyZWhCEtt7x/+6amswwgAP0fYkSVoKfBFYKaBjTYRKF3THLsNBeF5YiRQFqG 4MknCM4zBGP/LiMWCSsGAQQB2kcPAQEHQEKgATvrVSfdcu3z8dRVmSYNvRY4WiI4 VqDnXdVfPw6zwsAABBgWCgByBYJj/y4jCRCmenMxJ7voBEcUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmf9Zi+LJShZF85Q891p2IEzeanqA2l4 3XJl5/Rb/qRaIgKbIBYhBNWOBHwF0RXqTz0amKZ6czEnu+gEAAAgeQEA7Q8yHq28 RY3PAcSTDua7ggtz3G9DFcsE6fUQNWu47vYBAI+IYKsqLqQM6HKyNhQOAAxQy9mK eL+hrmAeQPam+e0MzjMEY/8uIxYJKwYBBAHaRw8BAQdAvAp2sWprnGrDBZZz9dw4 nWUdZxp8J66kFBgb1wrOHkzCwL8EGBYKATEFgmP/LiMJEKZ6czEnu+gERxQAAAAA AB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ83wrBu6p9t+HDGReAhb HODjY69wCZR8nUWC/EMtWyLNApsCvqAEGRYKAG8FgmP/LiMJEP9cZYLk0UtoRxQA AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ0KB/wq5QIZxUm6K 1jaB0/U2L7kYgTRA0BGMZzXwFlHpFiEEnc2iqVoXtyjWpRFe/1xlguTRS2gAAEGw AQCVsb9Mrg9DqDYHQ9ciQYxnT/lWzZAaabh/JJhc7CkLtgD+Pc/oYQRwo+n8PSHB WVD6DB9vUp/GUCfLoj1Cu9fH5ggWIQTVjgR8BdEV6k89GpimenMxJ7voBAAAhXAB APf1Qu7LkicorCeuKnga8hpvbF9ijpvBdwUJQ9XJO4diAP4t99AefI2ifCToWqC6 iHzceTmjcdwjZaZF2pD6BvkaCw== =RcKT -----END PGP PUBLIC KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/hans-puny-code-priv.pgp000064400000000000000000000030221046102023000217540ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: F667 5D0E 4DA4 0823 715C 4811 B894 91F0 7D08 E4F8 Comment: Hans xVgEY+oqshYJKwYBBAHaRw8BAQdAT3RaMo+Mm0kzj5kQhqNQWwHzsxsXFC10ZWBf tdIAKWMAAP9//c94gjDUS6nzSSxBTkqH7CQPZS5Lfh1x01ZYWkGmXg8twsALBB8W CgB9BYJj6iqyAwsJBwkQuJSR8H0I5PhHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu c2VxdW9pYS1wZ3Aub3JnTonT+wh1nQ6+SGZM/3caxq3V5hflG/hX3IrX/Q/NR3YD FQoIApsBAh4BFiEE9mddDk2kCCNxXEgRuJSR8H0I5PgAAJ4xAP9y4me1jRRfxrnW 3jnV+jB7n6m3xxpn/Y0Tmh/EhZsrGwEAvrMBIyXycmSLB1giDnmeLi8U+z3cx5+x kRJGHi/cvAnNHUhhbnMgPGhhbnNAeG4tLWJjaGVyLWt2YS50bGQ+wsAOBBMWCgCA BYJj6iqyAwsJBwkQuJSR8H0I5PhHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2Vx dW9pYS1wZ3Aub3Jn6ABJrqKC61PG6PamtZM1YuRmoxFWwuBcEadTWjH6VYYDFQoI ApkBApsBAh4BFiEE9mddDk2kCCNxXEgRuJSR8H0I5PgAANV2AP47sc59UcqS0woi p3aqR9rhgQ0PlGR1dlKCved1ScfU4AD7BOlXuMdm9rkPHsMWUVv2zBWHsfOzU9Tr Vt6OsaTrpAjHWARj6iqyFgkrBgEEAdpHDwEBB0AZMvj3dmsZZS+1bL+WX6HDv+gN 8i+QyhyBjB0s8wUjkwAA/RFddhggq/2q9zpOfb0C/mhdZgBCPcZMNz/UbxVzzZuB DwrCwL8EGBYKATEFgmPqKrIJELiUkfB9COT4RxQAAAAAAB4AIHNhbHRAbm90YXRp b25zLnNlcXVvaWEtcGdwLm9yZyuXD0xCb8liVrUy7F6WOBJOxfEELZY6ysI1tpUL Tb3YApsCvqAEGRYKAG8FgmPqKrIJENsfXxHBfLLURxQAAAAAAB4AIHNhbHRAbm90 YXRpb25zLnNlcXVvaWEtcGdwLm9yZw9UPiixBgUO/0gAwW41/f5qoJTwNSRxtQ3N 2gZw/PPMFiEEP2DqCuvBPikJOaCA2x9fEcF8stQAALtoAP9N9fc0d+mq59zflgdM QSljALYqug0xOECe15Z01FHuPAEAy2VMaBiv140AkT+AoUgFjT5UGbttbAGR5PaT z0f9ZQAWIQT2Z10OTaQII3FcSBG4lJHwfQjk+AAAOB0BAOE6N1BWh63R0bozConx 708Xahe/6434g1ypTKLKrPiaAP9qK8oK38FKpoSjL2tLRzDhqUvBsRYPvYZ070tV sKPqCw== =Patr -----END PGP PRIVATE KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/hans-puny-code.pgp000064400000000000000000000026521046102023000210060ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- Comment: F667 5D0E 4DA4 0823 715C 4811 B894 91F0 7D08 E4F8 Comment: Hans xjMEY+oqshYJKwYBBAHaRw8BAQdAT3RaMo+Mm0kzj5kQhqNQWwHzsxsXFC10ZWBf tdIAKWPCwAsEHxYKAH0FgmPqKrIDCwkHCRC4lJHwfQjk+EcUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdOidP7CHWdDr5IZkz/dxrGrdXmF+Ub +Ffcitf9D81HdgMVCggCmwECHgEWIQT2Z10OTaQII3FcSBG4lJHwfQjk+AAAnjEA /3LiZ7WNFF/GudbeOdX6MHufqbfHGmf9jROaH8SFmysbAQC+swEjJfJyZIsHWCIO eZ4uLxT7PdzHn7GREkYeL9y8Cc0dSGFucyA8aGFuc0B4bi0tYmNoZXIta3ZhLnRs ZD7CwA4EExYKAIAFgmPqKrIDCwkHCRC4lJHwfQjk+EcUAAAAAAAeACBzYWx0QG5v dGF0aW9ucy5zZXF1b2lhLXBncC5vcmfoAEmuooLrU8bo9qa1kzVi5GajEVbC4FwR p1NaMfpVhgMVCggCmQECmwECHgEWIQT2Z10OTaQII3FcSBG4lJHwfQjk+AAA1XYA /juxzn1RypLTCiKndqpH2uGBDQ+UZHV2UoK953VJx9TgAPsE6Ve4x2b2uQ8ewxZR W/bMFYex87NT1OtW3o6xpOukCM4zBGPqKrIWCSsGAQQB2kcPAQEHQBky+Pd2axll L7Vsv5ZfocO/6A3yL5DKHIGMHSzzBSOTwsC/BBgWCgExBYJj6iqyCRC4lJHwfQjk +EcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcrlw9MQm/J Yla1MuxeljgSTsXxBC2WOsrCNbaVC0292AKbAr6gBBkWCgBvBYJj6iqyCRDbH18R wXyy1EcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcPVD4o sQYFDv9IAMFuNf3+aqCU8DUkcbUNzdoGcPzzzBYhBD9g6grrwT4pCTmggNsfXxHB fLLUAAC7aAD/TfX3NHfpqufc35YHTEEpYwC2KroNMThAnteWdNRR7jwBAMtlTGgY r9eNAJE/gKFIBY0+VBm7bWwBkeT2k89H/WUAFiEE9mddDk2kCCNxXEgRuJSR8H0I 5PgAADgdAQDhOjdQVoet0dG6MwqJ8e9PF2oXv+uN+INcqUyiyqz4mgD/aivKCt/B SqaEoy9rS0cw4alLwbEWD72GdO9LVbCj6gs= =zJtz -----END PGP PUBLIC KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/peter-priv.pgp000064400000000000000000000050251046102023000202460ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 692E 359E FB0B E115 373F 6CEA D5D8 BA16 C805 A81F Comment: Peter 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 =2/un -----END PGP PRIVATE KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/peter.pgp000064400000000000000000000050361046102023000172720ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- xjMEZitRShYJKwYBBAHaRw8BAQdAfch6qYuSZjyAxB0kd4uERbpxItJdvUmi0ZHy rPBbB8LCwAsEHxYKAH0FgmYrUUoDCwkHCRDV2LoWyAWoH0cUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfscXvsw6LnXkWt6PBWZ8tQV8hyvFr6 iJz0mTQ+WsIidQMVCggCmwECHgEWIQRpLjWe+wvhFTc/bOrV2LoWyAWoHwAAzXAB AMfWCyLGA6knJxrZK/QR/WUmSb5jnOdkdGsdlXWik+g5AP9qQstQn0PE2diXK7Dc tW2ZC/UjAfo1vo4bTP3w+xIVD80aRGFkIDxwZXRlckBleGFtcGxlLmZhbWlseT7C wAMEEBYKAHUFgmYrUV0FgwlnmjsJEHFCSt4+xhu8RxQAAAAAAB4AIHNhbHRAbm90 YXRpb25zLnNlcXVvaWEtcGdwLm9yZ/zclAkuPM9+PHfXBpkWIhWpTb2yPER1yjMP 41e1eU+KFiEEEZsBRgZZ2O83Mr7CcUJK3j7GG7wAAMrdAQCHabjPugiOSNJeIfkV jGU5/MxmAwHN1pCZ4c+xT/ddIwEAm03iqwhnaionr5yU3m1+4dzlIZ0CVr2Sp6bR jCGJxwvNGVBldGVyIDxwZXRlckBleGFtcGxlLmNvbT7CwA4EExYKAIAFgmYrUUoD CwkHCRDV2LoWyAWoH0cUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBn cC5vcmftDrRqfDLoEu3qCLDVc/aKgQmt69pfOE30XqXhkPmu4wMVCggCmQECmwEC HgEWIQRpLjWe+wvhFTc/bOrV2LoWyAWoHwAAfTMA/0To1wdestrfd1WexWXcpsro 96XQeMROYniDnfhCWHXUAQD54C4NTDccWIcZrblnPWkse1rYLTJqH2kKrJYoqlyi As4zBGYrUUoWCSsGAQQB2kcPAQEHQB3h014ilCSIgHF3TQeC6lr1a3L7s2zRA5T1 FaWsD7AywsC/BBgWCgExBYJmK1FKCRDV2LoWyAWoH0cUAAAAAAAeACBzYWx0QG5v dGF0aW9ucy5zZXF1b2lhLXBncC5vcmcraJwDDOL5ibHcNg6vzpQ7vhAfmCVaSxDB jJH+BIrlHQKbIL6gBBkWCgBvBYJmK1FKCRCm3jSGfeiSfkcUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcZjLj/6EU751q94HyXdKWBvnnguuQW AEShpqHssIhQwhYhBGnTJeINpNQEJ5plNKbeNIZ96JJ+AACLDwD/fbGnFfjQs4De dJ8faGncn5V8gweFdJ/9OIqFtYqZLQMBANfd3xHuvmMZXARxlTGWmgPnHdI+XrV/ UD5Rsl4O0e0PFiEEaS41nvsL4RU3P2zq1di6FsgFqB8AAIt0APwMAxAT6tjRa5dx XTe4JP8VoxsvDqne84JmXotth5HHKAD/carelpPr+OM5+tQ3HTF3OTWGmQXAHEDS LGKOgoBsIwHOMwRmK1FKFgkrBgEEAdpHDwEBB0DMS+S39vIjEjWcjbvZCFmpU7UV XJ1KIciMuIvCkbU+3sLAvwQYFgoBMQWCZitRSgkQ1di6FsgFqB9HFAAAAAAAHgAg c2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnFpB9xcXnF2n2ucB1w0cBjErS LA0b0dwwdZZEpHZzaoUCmwK+oAQZFgoAbwWCZitRSgkQjOT+a9ASgt5HFAAAAAAA HgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JndkALqxfHOMJ8dINy2fR3 pQLmDEzt4Ilxguhp8/r3p30WIQRi0tl/AN+W0t/thfmM5P5r0BKC3gAANGQA/2wZ PZQNJvHM+uduVKj9wMFnwxYwmi/CMVp8FXgydgLkAQD6IfO7tGAaGFyURlXSOfzz bKaAJvSUIW9swAsnmponABYhBGkuNZ77C+EVNz9s6tXYuhbIBagfAABWzgD/cn/V 0CU/9eP5+jX8GvJl5VjyU3r8TfLEUNZ933naCZsBAO0zK402z6QYh1d2KYBXuoYA fv5XWyt7Rax8ryw39b4IzjgEZitRShIKKwYBBAGXVQEFAQEHQB4Nh/EWGIuvxxIy 7nf6loOBg5d9hPeTn27RSE6NtP5YAwEIB8LAAAQYFgoAcgWCZitRSgkQ1di6FsgF qB9HFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnzI1VKKf0 spgFecKS1tW0Plt98+QdFU97L3BepZEKpNwCmwwWIQRpLjWe+wvhFTc/bOrV2LoW yAWoHwAAAkgA/A67IJvzCsvzXvlLttfzdYTv/+RhINF4vhbiSz/GmkyqAQCWBW6r Qx0bgIeIkAz25ShuOOjDu9eRWLXJYoo7l7NIAw== =z2JX -----END PGP PUBLIC KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/steve-priv.pgp000064400000000000000000000030221046102023000202500ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 217E 256E 1767 19A5 452E DFF9 35AA DEC6 6B56 585B Comment: Steve xVgEY+oyohYJKwYBBAHaRw8BAQdAvt5KOKjBeor71rpom6N/6+TbHhXDIa7qQ0Ht zQneFeQAAQDAzUbk5663nYv1gGFw2L4lix8PKG/VAlTJ60U+WuOSPhDswsALBB8W CgB9BYJj6jKiAwsJBwkQNarexmtWWFtHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu c2VxdW9pYS1wZ3Aub3JnB5GN1ytb4OBEuWTGbe6YLQERrMyH25xx+ROabT1IlpMD FQoIApsBAh4BFiEEIX4lbhdnGaVFLt/5NarexmtWWFsAAD1FAQC3lqYfMNO+16bQ kczC9nFpKXR0w31T0Y5gcTTUFEPf5wEAsE3qZgUSk5jbAKdApQjae/L7Fg/n3rBT oxcJobSEIQDNHVN0ZXZlIDxzdGV2ZUBzdWIuY29tcGFueS5jb20+wsAOBBMWCgCA BYJj6jKiAwsJBwkQNarexmtWWFtHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2Vx dW9pYS1wZ3Aub3JnpHmJbxKKXD9L7rb6Nm4QO7MAwEL9tAij4pRJ5nLW3RoDFQoI ApkBApsBAh4BFiEEIX4lbhdnGaVFLt/5NarexmtWWFsAACRKAQComL8m5O1VFzkQ ppd3FiNZ55JFriChzKE4l/PDJtFWKwD+K1v/rPt8xoluiXkcCzCwy5bVTcxhAGD3 yG/kFty8Gw7HWARj6jKiFgkrBgEEAdpHDwEBB0CUDl/MXB9n4zhULNYtn4AvcLYJ p7VEwrrO4SEWTg3SQwABAMUq0/p6feCGZGQBviCqhhOCIn6HIs9NjnDev5dwBn8r Dz3CwL8EGBYKATEFgmPqMqIJEDWq3sZrVlhbRxQAAAAAAB4AIHNhbHRAbm90YXRp b25zLnNlcXVvaWEtcGdwLm9yZyBzXATYY9Bisl9qX3bybDnuHdvr2i0LuDdkCyeH WAjlApsCvqAEGRYKAG8FgmPqMqIJELWWtlb9j3ALRxQAAAAAAB4AIHNhbHRAbm90 YXRpb25zLnNlcXVvaWEtcGdwLm9yZ3ctrGVdHZWTXgkQgDuARO7gqTuwb/rNDolL avWFIE3cFiEEMsWCBUAwh1K3CS7ltZa2Vv2PcAsAAL/oAQDdllWylqthNy+e5OYl 4mr1GWUSOCfDlFaH146drBGyPAEArRTopl2w0KiDbo92kfrx+y3L4cLSK85byPOL swX4eAsWIQQhfiVuF2cZpUUu3/k1qt7Ga1ZYWwAAw3oBAMVzhKa/ZmUQmNX8zk6v GK0vbOw47hPaVRCG1RRrSGrwAQC5Tteh+McKoFQzKea/CreuPbaqDaMfEUarKO1b E0wPCA== =M99G -----END PGP PRIVATE KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/steve.pgp000064400000000000000000000026521046102023000173020ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- Comment: 217E 256E 1767 19A5 452E DFF9 35AA DEC6 6B56 585B Comment: Steve xjMEY+oyohYJKwYBBAHaRw8BAQdAvt5KOKjBeor71rpom6N/6+TbHhXDIa7qQ0Ht zQneFeTCwAsEHxYKAH0FgmPqMqIDCwkHCRA1qt7Ga1ZYW0cUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcHkY3XK1vg4ES5ZMZt7pgtARGszIfb nHH5E5ptPUiWkwMVCggCmwECHgEWIQQhfiVuF2cZpUUu3/k1qt7Ga1ZYWwAAPUUB ALeWph8w077XptCRzML2cWkpdHTDfVPRjmBxNNQUQ9/nAQCwTepmBRKTmNsAp0Cl CNp78vsWD+fesFOjFwmhtIQhAM0dU3RldmUgPHN0ZXZlQHN1Yi5jb21wYW55LmNv bT7CwA4EExYKAIAFgmPqMqIDCwkHCRA1qt7Ga1ZYW0cUAAAAAAAeACBzYWx0QG5v dGF0aW9ucy5zZXF1b2lhLXBncC5vcmekeYlvEopcP0vutvo2bhA7swDAQv20CKPi lEnmctbdGgMVCggCmQECmwECHgEWIQQhfiVuF2cZpUUu3/k1qt7Ga1ZYWwAAJEoB AKiYvybk7VUXORCml3cWI1nnkkWuIKHMoTiX88Mm0VYrAP4rW/+s+3zGiW6JeRwL MLDLltVNzGEAYPfIb+QW3LwbDs4zBGPqMqIWCSsGAQQB2kcPAQEHQJQOX8xcH2fj OFQs1i2fgC9wtgmntUTCus7hIRZODdJDwsC/BBgWCgExBYJj6jKiCRA1qt7Ga1ZY W0cUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcgc1wE2GPQ YrJfal928mw57h3b69otC7g3ZAsnh1gI5QKbAr6gBBkWCgBvBYJj6jKiCRC1lrZW /Y9wC0cUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmd3Laxl XR2Vk14JEIA7gETu4Kk7sG/6zQ6JS2r1hSBN3BYhBDLFggVAMIdStwku5bWWtlb9 j3ALAAC/6AEA3ZZVsparYTcvnuTmJeJq9RllEjgnw5RWh9eOnawRsjwBAK0U6KZd sNCog26PdpH68fsty+HC0ivOW8jzi7MF+HgLFiEEIX4lbhdnGaVFLt/5NarexmtW WFsAAMN6AQDFc4Smv2ZlEJjV/M5OrxitL2zsOO4T2lUQhtUUa0hq8AEAuU7XofjH CqBUMynmvwq3rj22qg2jHxFGqyjtWxNMDwg= =ueVE -----END PGP PUBLIC KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/una-priv.pgp000064400000000000000000000027751046102023000177230ustar 00000000000000-----BEGIN PGP PRIVATE KEY BLOCK----- Comment: 119B 0146 0659 D8EF 3732 BEC2 7142 4ADE 3EC6 1BBC Comment: Una xVgEY+oykhYJKwYBBAHaRw8BAQdAkHZelZnKgyDf93+x3dEjxFGvfNZ2mzWxh4MM mp+V4YAAAP9c7yFPEUEo8s17G0UvwGdnR5dP7cARfMk9oOXvNpTv4REGwsALBB8W CgB9BYJj6jKSAwsJBwkQcUJK3j7GG7xHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu c2VxdW9pYS1wZ3Aub3JnNkxF9jqWQFtCThu+2My8F4VNsMqIowBzbnMTtzLVFJUD FQoIApsBAh4BFiEEEZsBRgZZ2O83Mr7CcUJK3j7GG7wAAAiYAP0c90sDWGs5jsBv 03H5UkLZvw2uvMrKJd47pT3aloXpOQD8C1vt7PTgkzdr79G8h6nRhpg/FNrW/+pX Cq7qjUZ+/gnNFVVuYSA8dW5hQGNvbXBhbnkuY29tPsLADgQTFgoAgAWCY+oykgML CQcJEHFCSt4+xhu8RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdw Lm9yZ/PoQvY1cIKPZjLqb3HWViJXtrONf9GXxSMIr3z8o4kCAxUKCAKZAQKbAQIe ARYhBBGbAUYGWdjvNzK+wnFCSt4+xhu8AACMJQD+IkXutGYCxJMqwrbD18polWLx IhojofUTcwg9Qlmz3UUBAIP8bKENVhOCwA0hT/3GmOvMRSjosyNhiAMtPd09OIAC x1gEY+oykhYJKwYBBAHaRw8BAQdAINC4y2VLb5XeTxBjK7Pf/53xUXl0OJe/87tE ixluoMoAAQDhNvenwXoaY+Fw2iw1taVy5cZZ2ke/0crBn6RXMSUAnBGSwsC/BBgW CgExBYJj6jKSCRBxQkrePsYbvEcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1 b2lhLXBncC5vcmcMvtVQROXLGIVC8DAhtcW6jbmlokmR9MMngd58P2KuVgKbAr6g BBkWCgBvBYJj6jKSCRDQvkR785tDn0cUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z ZXF1b2lhLXBncC5vcmdCjEo62Cqb/N5XxGb1Tdnu225hTSuRiwXz3ss1+N7YwhYh BO5Ywy49IzbyI72JztC+RHvzm0OfAACnMwEAmPaEWKPd3hpJqdAph8KAdRYeVwhm rCwAi8NaE917S4sA/AzyJubT/4Sp/vK+SPa+d/VTNEApS1TRf9SQbkAjp40HFiEE EZsBRgZZ2O83Mr7CcUJK3j7GG7wAADLCAQC2h/atUVO/QhD6sg8uGFMj68/+QBy1 Qg/7f9TezlE2KgD+K6YyFlIZVuScF9xKkKGiQpxBiekcDoVdGev0jxE86QY= =UR1a -----END PGP PRIVATE KEY BLOCK----- sequoia-cert-store-0.7.1/tests/data/una.pgp000064400000000000000000000026261046102023000167400ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- Comment: 119B 0146 0659 D8EF 3732 BEC2 7142 4ADE 3EC6 1BBC Comment: Una xjMEY+oykhYJKwYBBAHaRw8BAQdAkHZelZnKgyDf93+x3dEjxFGvfNZ2mzWxh4MM mp+V4YDCwAsEHxYKAH0FgmPqMpIDCwkHCRBxQkrePsYbvEcUAAAAAAAeACBzYWx0 QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmc2TEX2OpZAW0JOG77YzLwXhU2wyoij AHNucxO3MtUUlQMVCggCmwECHgEWIQQRmwFGBlnY7zcyvsJxQkrePsYbvAAACJgA /Rz3SwNYazmOwG/TcflSQtm/Da68ysol3julPdqWhek5APwLW+3s9OCTN2vv0byH qdGGmD8U2tb/6lcKruqNRn7+Cc0VVW5hIDx1bmFAY29tcGFueS5jb20+wsAOBBMW CgCABYJj6jKSAwsJBwkQcUJK3j7GG7xHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMu c2VxdW9pYS1wZ3Aub3Jn8+hC9jVwgo9mMupvcdZWIle2s41/0ZfFIwivfPyjiQID FQoIApkBApsBAh4BFiEEEZsBRgZZ2O83Mr7CcUJK3j7GG7wAAIwlAP4iRe60ZgLE kyrCtsPXymiVYvEiGiOh9RNzCD1CWbPdRQEAg/xsoQ1WE4LADSFP/caY68xFKOiz I2GIAy093T04gALOMwRj6jKSFgkrBgEEAdpHDwEBB0Ag0LjLZUtvld5PEGMrs9// nfFReXQ4l7/zu0SLGW6gysLAvwQYFgoBMQWCY+oykgkQcUJK3j7GG7xHFAAAAAAA HgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnDL7VUETlyxiFQvAwIbXF uo25paJJkfTDJ4HefD9irlYCmwK+oAQZFgoAbwWCY+oykgkQ0L5Ee/ObQ59HFAAA AAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnQoxKOtgqm/zeV8Rm 9U3Z7ttuYU0rkYsF897LNfje2MIWIQTuWMMuPSM28iO9ic7QvkR785tDnwAApzMB AJj2hFij3d4aSanQKYfCgHUWHlcIZqwsAIvDWhPde0uLAPwM8ibm0/+Eqf7yvkj2 vnf1UzRAKUtU0X/UkG5AI6eNBxYhBBGbAUYGWdjvNzK+wnFCSt4+xhu8AAAywgEA tof2rVFTv0IQ+rIPLhhTI+vP/kActUIP+3/U3s5RNioA/iumMhZSGVbknBfcSpCh okKcQYnpHA6FXRnr9I8RPOkG =TWWI -----END PGP PUBLIC KEY BLOCK----- sequoia-cert-store-0.7.1/tests/keyring.pgp000064400000000000000000000425231046102023000167140ustar 00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- xjMEY+ik9BYJKwYBBAHaRw8BAQdAObLfcRUmRi9GlRVnuTclWR3NJHksXjREY3cH JDyH1vrCwBEEHxYKAIMFgmPopPQFiQWkj70DCwkHCRAeowB5clinTkcUAAAAAAAe ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeHv7fnXzd4P5rem8heR3Ps MIJHYhJhrHJoc4Pda2uJsgMVCggCmwECHgEWIQQMNGsrYkEmP2Tpx88eowB5clin TgAAnboA/ii83dQNKuWbuMZMH61/wkx3CBxBxQqCegMG4wm+QtxzAQChG0dFc0f6 xgdRFT+NqFOiKQll+DOHyx/64KIvmzMZCs0QPGVkQGV4YW1wbGUub3JnPsLAFAQT FgoAhgWCY+ik9AWJBaSPvQMLCQcJEB6jAHlyWKdORxQAAAAAAB4AIHNhbHRAbm90 YXRpb25zLnNlcXVvaWEtcGdwLm9yZ/IrlRCQaR62j+/g1lPCwnTNUncQvLyCuRaX xcvEbK2oAxUKCAKZAQKbAQIeARYhBAw0aytiQSY/ZOnHzx6jAHlyWKdOAAB+pgD9 H39XCeUJot/e3kj/FXB2DVOzRh/JXTf8DijaE8UZw60A/iPSaUgo/zX1US1A5HRs ZAmh5/NuNFGlc8Ga5Uhu6agMzjMEY+ik9BYJKwYBBAHaRw8BAQdAObLfcRUmRi9G lRVnuTclWR3NJHksXjREY3cHJDyH1vrCwL8EGBYKATEFgmPopZYJEB6jAHlyWKdO RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ2DwGaTs9JDD fiZEnYLt1gnn9Y1BPath3jMWJVieWnMiApsBvqAEGRYKAG8FgmPopZYJEB6jAHly WKdORxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZykmDJ78 rKWJ5keAV7xA/cXU/fILUT42Pl4hKER8n+dpFiEEDDRrK2JBJj9k6cfPHqMAeXJY p04AAFWuAQCgRxcn47GVYgjnCTtM2nR65cDP2tD8Asi17yTK9B4hQQD+NeIuSbjw nJZ20Lc775PN966+4yvmw0yIcqy78W6/qgYWIQQMNGsrYkEmP2Tpx88eowB5clin TgAAHtcA/1Z6hgFz6HduwMGO8pU1UdZOHN9tyof10as2/77QvadlAQCx5HBD+d5P NkOu/RHHPp5w26LRENwzmeWxF7/WwAulA8YzBGPqMpIWCSsGAQQB2kcPAQEHQJB2 XpWZyoMg3/d/sd3RI8RRr3zWdps1sYeDDJqfleGAwsALBB8WCgB9BYJj6jKSAwsJ BwkQcUJK3j7GG7xHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Au b3JnNkxF9jqWQFtCThu+2My8F4VNsMqIowBzbnMTtzLVFJUDFQoIApsBAh4BFiEE EZsBRgZZ2O83Mr7CcUJK3j7GG7wAAAiYAP0c90sDWGs5jsBv03H5UkLZvw2uvMrK Jd47pT3aloXpOQD8C1vt7PTgkzdr79G8h6nRhpg/FNrW/+pXCq7qjUZ+/gnNFVVu YSA8dW5hQGNvbXBhbnkuY29tPsLADgQTFgoAgAWCY+oykgMLCQcJEHFCSt4+xhu8 RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ/PoQvY1cIKP ZjLqb3HWViJXtrONf9GXxSMIr3z8o4kCAxUKCAKZAQKbAQIeARYhBBGbAUYGWdjv NzK+wnFCSt4+xhu8AACMJQD+IkXutGYCxJMqwrbD18polWLxIhojofUTcwg9Qlmz 3UUBAIP8bKENVhOCwA0hT/3GmOvMRSjosyNhiAMtPd09OIACzjMEY+oykhYJKwYB BAHaRw8BAQdAINC4y2VLb5XeTxBjK7Pf/53xUXl0OJe/87tEixluoMrCwL8EGBYK ATEFgmPqMpIJEHFCSt4+xhu8RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVv aWEtcGdwLm9yZwy+1VBE5csYhULwMCG1xbqNuaWiSZH0wyeB3nw/Yq5WApsCvqAE GRYKAG8FgmPqMpIJENC+RHvzm0OfRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl cXVvaWEtcGdwLm9yZ0KMSjrYKpv83lfEZvVN2e7bbmFNK5GLBfPeyzX43tjCFiEE 7ljDLj0jNvIjvYnO0L5Ee/ObQ58AAKczAQCY9oRYo93eGkmp0CmHwoB1Fh5XCGas LACLw1oT3XtLiwD8DPIm5tP/hKn+8r5I9r539VM0QClLVNF/1JBuQCOnjQcWIQQR mwFGBlnY7zcyvsJxQkrePsYbvAAAMsIBALaH9q1RU79CEPqyDy4YUyPrz/5AHLVC D/t/1N7OUTYqAP4rpjIWUhlW5JwX3EqQoaJCnEGJ6RwOhV0Z6/SPETzpBsYzBGPq MqIWCSsGAQQB2kcPAQEHQL7eSjiowXqK+9a6aJujf+vk2x4VwyGu6kNB7c0J3hXk wsALBB8WCgB9BYJj6jKiAwsJBwkQNarexmtWWFtHFAAAAAAAHgAgc2FsdEBub3Rh dGlvbnMuc2VxdW9pYS1wZ3Aub3JnB5GN1ytb4OBEuWTGbe6YLQERrMyH25xx+ROa bT1IlpMDFQoIApsBAh4BFiEEIX4lbhdnGaVFLt/5NarexmtWWFsAAD1FAQC3lqYf MNO+16bQkczC9nFpKXR0w31T0Y5gcTTUFEPf5wEAsE3qZgUSk5jbAKdApQjae/L7 Fg/n3rBToxcJobSEIQDNHVN0ZXZlIDxzdGV2ZUBzdWIuY29tcGFueS5jb20+wsAO BBMWCgCABYJj6jKiAwsJBwkQNarexmtWWFtHFAAAAAAAHgAgc2FsdEBub3RhdGlv bnMuc2VxdW9pYS1wZ3Aub3JnpHmJbxKKXD9L7rb6Nm4QO7MAwEL9tAij4pRJ5nLW 3RoDFQoIApkBApsBAh4BFiEEIX4lbhdnGaVFLt/5NarexmtWWFsAACRKAQComL8m 5O1VFzkQppd3FiNZ55JFriChzKE4l/PDJtFWKwD+K1v/rPt8xoluiXkcCzCwy5bV TcxhAGD3yG/kFty8Gw7OMwRj6jKiFgkrBgEEAdpHDwEBB0CUDl/MXB9n4zhULNYt n4AvcLYJp7VEwrrO4SEWTg3SQ8LAvwQYFgoBMQWCY+oyogkQNarexmtWWFtHFAAA AAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnIHNcBNhj0GKyX2pf dvJsOe4d2+vaLQu4N2QLJ4dYCOUCmwK+oAQZFgoAbwWCY+oyogkQtZa2Vv2PcAtH FAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jndy2sZV0dlZNe CRCAO4BE7uCpO7Bv+s0OiUtq9YUgTdwWIQQyxYIFQDCHUrcJLuW1lrZW/Y9wCwAA v+gBAN2WVbKWq2E3L57k5iXiavUZZRI4J8OUVofXjp2sEbI8AQCtFOimXbDQqINu j3aR+vH7LcvhwtIrzlvI84uzBfh4CxYhBCF+JW4XZxmlRS7f+TWq3sZrVlhbAADD egEAxXOEpr9mZRCY1fzOTq8YrS9s7DjuE9pVEIbVFGtIavABALlO16H4xwqgVDMp 5r8Kt649tqoNox8RRqso7VsTTA8IxjMEY+UaGxYJKwYBBAHaRw8BAQdAEHlpai+g E3eN9Sewy6RIwvaHi2OEIDB7fQr6pram3vPCwAsEHxYKAH0FgmPlGhsDCwkHCRB+ cW/+d98XCkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdx UEETzwaTQoyKKOfr4PZgTCdQd4z/ODS8AsjAOXZGtAMVCggCmwECHgEWIQQjz+Sd S7egqoNhnBR+cW/+d98XCgAA4yUA/0P9kPooU7x3VMEqLae8pKab9HBfppU8MRVL S4+72Iy3AP9CFXNKwPrDNx3zRSVr0gNP4LvIRnYIylwh6IgDZYZDCM0TPGFsaWNl QGV4YW1wbGUub3JnPsLADgQTFgoAgAWCY+UaGwMLCQcJEH5xb/533xcKRxQAAAAA AB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ2ihxev0+5HPAMi1PYwh MDfoYgFwF+pd51kBtHs/meouAxUKCAKZAQKbAQIeARYhBCPP5J1Lt6Cqg2GcFH5x b/533xcKAACpjQEAshMOpiGc3d0tmUrfJusiaK9DzcNvXvOz8VoMFTyszswA+wQ+ LwG0gOHeyByyOfrTS3SGUSzO94ccVCCgye5hzeMEzRE8YWxpY2VAdmVyZWluLmRl PsLACwQTFgoAfQWCY+UaGwMLCQcJEH5xb/533xcKRxQAAAAAAB4AIHNhbHRAbm90 YXRpb25zLnNlcXVvaWEtcGdwLm9yZyQm3s5x1vVKZC4DrB5FjOd/huKii45jn6v8 VKC2k9z7AxUKCAKbAQIeARYhBCPP5J1Lt6Cqg2GcFH5xb/533xcKAAD2FQEA9Ar+ /KwYtDCM3R1kPZYgs+XNu3uqfpUZz2EMIc0+xQMA/iQJ0wbIDRpxWITUAhzlPDCf ewQ8hcmKHON+xTK6TYUNzjMEY+UZ6hYJKwYBBAHaRw8BAQdA7Xh768P0ELWKVha7 jvlrf96N6bK8+ISBFSWBwQeTLanCwL8EGBYKATEFgmPlGoAJEH5xb/533xcKRxQA AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ6jKFT9D3TotuXf6 nPLnk+Y7fWhd95cY0roDW3BEOGuMApsCvqAEGRYKAG8FgmPlGrwJENWlBIpxzQEq RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ0aATZPHvO82 7KrIHm+uCgWSM5fV507JC5peoBZrUZ+sFiEEZi8D/EfAXQcLU6k61aUEinHNASoA ALtKAP9CQ70prkjoo4bz1zMvINIV9DI4mpn4lTU65OT9T3Jy3wD8Dc0ddirxKJHq ifpG2bJfU9alajDH0dv3RRIzD3OmrQoWIQQjz+SdS7egqoNhnBR+cW/+d98XCgAA HSUA/jmW7hAY8xgXrVD/Aj3qsnJA1mUAntHQjcOgQafSVHALAQCslWSDUEOMUVxG kq/498hF13iwJLcHgckPa6u+WzExAs44BGPlGeoSCisGAQQBl1UBBQEBB0Bmef3e ckyLiMO5kJG/hqnAIh0qub81lsLLTCcNfswTCAMBCAfCwAAEGBYKAHIFgmPlGoAJ EH5xb/533xcKRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9y ZzKM37EwuBHlB0dJ4Ljo9MIUDIEaVEFyJ+l+659sYwQsApsMFiEEI8/knUu3oKqD YZwUfnFv/nffFwoAANJrAP0aUjgxFCD789m2w0nAry0SA5JlLjuxWOJ42ziWEYAY UgEAzJwWRDgabsLC7VZ+v+TZ7wEh0JhGk6dX2lbH0v1apwHGMwRj5RnqFgkrBgEE AdpHDwEBB0CnHzEBU2NxSQZbfXBnpD2+7efLEla1eYdm/ex82W1gEMLACwQfFgoA fQWCY+UZ6gMLCQcJEL++Y1Z7S6V6RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNl cXVvaWEtcGdwLm9yZ72uBSWycce7Cu193YRatsDb3GbBXo2+xkN5QvFSwGIKAxUK CAKbAQIeARYhBDBQW863QDobv6nb8L++Y1Z7S6V6AAD+XAEA9OtQfoEN4xDqcOkP 8I5w6M7mLBsyxwRVhBVYvyGyLS8A/01iD6ovLvq4Oos4ORclilCpjjOGkCdVX9fe 8LJ0uKMGzRM8YWxpY2VAYmVpc3BpZWwuZGU+wsALBBMWCgB9BYJj5RnqAwsJBwkQ v75jVntLpXpHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn R/kzyvS4FqYD3ZnSnWrhxrGJ5pqlC4IxWqCoNYArrDcDFQoIApsBAh4BFiEEMFBb zrdAOhu/qdvwv75jVntLpXoAAHmBAQC0KJzxaq+2m6BaYNFyWAz/O2I/6dqXUK/x LJn6ugft6wD9FbmY0o94xkHYQDUBGo84Lfoz4gXR4l2MoNRC922CfAnNEzxhbGlj ZUBleGFtcGxlLm9yZz7CwA4EExYKAIAFgmPlGeoDCwkHCRC/vmNWe0ulekcUAAAA AAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmelV5HO9nFO4vWG42QB P/bSlE1pa5I3s0OETTIwgBY1uQMVCggCmQECmwECHgEWIQQwUFvOt0A6G7+p2/C/ vmNWe0ulegAAtLMBAI3Jebp3KBySat0wxXfXY8aG37f7BtX0Xw2ZAm50g0mhAP0Y 3OENXjIM9XDPFtsKr23YHZRVa+5M05Ha7ClGjfWaCM4zBGPlGeoWCSsGAQQB2kcP AQEHQO14e+vD9BC1ilYWu475a3/ejemyvPiEgRUlgcEHky2pwsC/BBgWCgExBYJj 5RnqCRC/vmNWe0ulekcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBn cC5vcmdtYYQ11XlNWLv9gxhoTYet69Lh8X77HOWBpzAdtJBmXwKbAr6gBBkWCgBv BYJj5RnqCRDVpQSKcc0BKkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lh LXBncC5vcmdQlNFzuInCVzUbKl51HCYaZFdTs2LNrQh8L9q/1owAWhYhBGYvA/xH wF0HC1OpOtWlBIpxzQEqAADLjAD/QUyfRGsEP2hWDBpFpCtUdGfP6A1jeKQn+9Xw 9DtxbKQA/3R4DWT92tQVKhxACYZ0fwonKES3O/LoXUNlAiopk3oHFiEEMFBbzrdA Ohu/qdvwv75jVntLpXoAAApDAP0aJdpJydTAcca0k3PXSbkesKGt6FzRX2FfaVYs F344PgEA53WgI0BP1JaXYVluRw/e810Cs6WU1mfx8+es89IjMg/OMwRj5RnqFgkr BgEEAdpHDwEBB0D5ukYoevx2XvwlWHTFXAwCOan5xlwnfSGOMQ1zfxvZN8LAAAQY FgoAcgWCY+UZ6gkQv75jVntLpXpHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2Vx dW9pYS1wZ3Aub3Jn6Vy/tb+n81qlxASiBepjIFkzPytq8OEl/TksAAXm1oUCmyAW IQQwUFvOt0A6G7+p2/C/vmNWe0ulegAAEn0BAJZuczP/Fo4SOc9U4YMv7Mvbh+fS nSGxp6iqDe+Ll9bHAPwItUiYCG60wijlFWmskw+28H8diiFsTMqEZ8+eMMaMAs44 BGPlGeoSCisGAQQBl1UBBQEBB0Bmef3eckyLiMO5kJG/hqnAIh0qub81lsLLTCcN fswTCAMBCAfCwAAEGBYKAHIFgmPlGeoJEL++Y1Z7S6V6RxQAAAAAAB4AIHNhbHRA bm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ8vh6XJJLTFgljfa4QNqIwwhBxaIXXAC /zqmVQyOAO3UApsMFiEEMFBbzrdAOhu/qdvwv75jVntLpXoAAAH8AQCOe4UXausm +h0C4h9c3eFbir6gsKRz7zYLZ4sjfIlisAEAkh8gFV9D94PCKWK/ZBq3GjiZWQS+ 5RmnMlp6juV/KwDGMwRmK1FKFgkrBgEEAdpHDwEBB0B9yHqpi5JmPIDEHSR3i4RF unEi0l29SaLRkfKs8FsHwsLACwQfFgoAfQWCZitRSgMLCQcJENXYuhbIBagfRxQA AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ+xxe+zDoudeRa3o 8FZny1BXyHK8WvqInPSZND5awiJ1AxUKCAKbAQIeARYhBGkuNZ77C+EVNz9s6tXY uhbIBagfAADNcAEAx9YLIsYDqScnGtkr9BH9ZSZJvmOc52R0ax2VdaKT6DkA/2pC y1CfQ8TZ2JcrsNy1bZkL9SMB+jW+jhtM/fD7EhUPzRpEYWQgPHBldGVyQGV4YW1w bGUuZmFtaWx5PsLAAwQQFgoAdQWCZitRXQWDCWeaOwkQcUJK3j7GG7xHFAAAAAAA HgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn/NyUCS48z348d9cGmRYi FalNvbI8RHXKMw/jV7V5T4oWIQQRmwFGBlnY7zcyvsJxQkrePsYbvAAAyt0BAIdp uM+6CI5I0l4h+RWMZTn8zGYDAc3WkJnhz7FP910jAQCbTeKrCGdqKievnJTebX7h 3OUhnQJWvZKnptGMIYnHC80ZUGV0ZXIgPHBldGVyQGV4YW1wbGUuY29tPsLADgQT FgoAgAWCZitRSgMLCQcJENXYuhbIBagfRxQAAAAAAB4AIHNhbHRAbm90YXRpb25z LnNlcXVvaWEtcGdwLm9yZ+0OtGp8MugS7eoIsNVz9oqBCa3r2l84TfRepeGQ+a7j AxUKCAKZAQKbAQIeARYhBGkuNZ77C+EVNz9s6tXYuhbIBagfAAB9MwD/ROjXB16y 2t93VZ7FZdymyuj3pdB4xE5ieIOd+EJYddQBAPngLg1MNxxYhxmtuWc9aSx7Wtgt MmofaQqsliiqXKICzjMEZitRShYJKwYBBAHaRw8BAQdAHeHTXiKUJIiAcXdNB4Lq WvVrcvuzbNEDlPUVpawPsDLCwL8EGBYKATEFgmYrUUoJENXYuhbIBagfRxQAAAAA AB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZytonAMM4vmJsdw2Dq/O lDu+EB+YJVpLEMGMkf4EiuUdApsgvqAEGRYKAG8FgmYrUUoJEKbeNIZ96JJ+RxQA AAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZxmMuP/oRTvnWr3g fJd0pYG+eeC65BYARKGmoeywiFDCFiEEadMl4g2k1AQnmmU0pt40hn3okn4AAIsP AP99sacV+NCzgN50nx9oadyflXyDB4V0n/04ioW1ipktAwEA193fEe6+YxlcBHGV MZaaA+cd0j5etX9QPlGyXg7R7Q8WIQRpLjWe+wvhFTc/bOrV2LoWyAWoHwAAi3QA /AwDEBPq2NFrl3FdN7gk/xWjGy8Oqd7zgmZei22HkccoAP9xqt6Wk+v44zn61Dcd MXc5NYaZBcAcQNIsYo6CgGwjAc4zBGYrUUoWCSsGAQQB2kcPAQEHQMxL5Lf28iMS NZyNu9kIWalTtRVcnUohyIy4i8KRtT7ewsC/BBgWCgExBYJmK1FKCRDV2LoWyAWo H0cUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmcWkH3FxecX afa5wHXDRwGMStIsDRvR3DB1lkSkdnNqhQKbAr6gBBkWCgBvBYJmK1FKCRCM5P5r 0BKC3kcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmd2QAur F8c4wnx0g3LZ9HelAuYMTO3giXGC6Gnz+venfRYhBGLS2X8A35bS3+2F+Yzk/mvQ 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 XFPTjQDOMwRj5RrWFgkrBgEEAdpHDwEBB0CDtKXvY/97vOd7cBoBtQlSt4PUsrQ3 cP4veLphBMTG3MLAAAQYFgoAcgWCY+Ua1gkQ42LUXH/3tlRHFAAAAAAAHgAgc2Fs dEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnlKNI5wDCnCTpjVDFKO5eSYqxhk0f CNg8Jk2dqnvukt8CmyAWIQTpxu/A45zm+d9SdOfjYtRcf/e2VAAA1QUA/2EUWkTs VcQsFOs2jqnH5TYYk8dS8ytGEGT4CDnh3h7VAQDXfTW3rRDIMA0aKgKzAtabfE1T Ifmq++9ok2NVY6VmCMYzBGPqKrIWCSsGAQQB2kcPAQEHQE90WjKPjJtJM4+ZEIaj UFsB87MbFxQtdGVgX7XSACljwsALBB8WCgB9BYJj6iqyAwsJBwkQuJSR8H0I5PhH FAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnTonT+wh1nQ6+ SGZM/3caxq3V5hflG/hX3IrX/Q/NR3YDFQoIApsBAh4BFiEE9mddDk2kCCNxXEgR uJSR8H0I5PgAAJ4xAP9y4me1jRRfxrnW3jnV+jB7n6m3xxpn/Y0Tmh/EhZsrGwEA vrMBIyXycmSLB1giDnmeLi8U+z3cx5+xkRJGHi/cvAnNHUhhbnMgPGhhbnNAeG4t LWJjaGVyLWt2YS50bGQ+wsAOBBMWCgCABYJj6iqyAwsJBwkQuJSR8H0I5PhHFAAA AAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn6ABJrqKC61PG6Pam tZM1YuRmoxFWwuBcEadTWjH6VYYDFQoIApkBApsBAh4BFiEE9mddDk2kCCNxXEgR uJSR8H0I5PgAANV2AP47sc59UcqS0woip3aqR9rhgQ0PlGR1dlKCved1ScfU4AD7 BOlXuMdm9rkPHsMWUVv2zBWHsfOzU9TrVt6OsaTrpAjOMwRj6iqyFgkrBgEEAdpH DwEBB0AZMvj3dmsZZS+1bL+WX6HDv+gN8i+QyhyBjB0s8wUjk8LAvwQYFgoBMQWC Y+oqsgkQuJSR8H0I5PhHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1w Z3Aub3JnK5cPTEJvyWJWtTLsXpY4Ek7F8QQtljrKwjW2lQtNvdgCmwK+oAQZFgoA bwWCY+oqsgkQ2x9fEcF8stRHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9p YS1wZ3Aub3JnD1Q+KLEGBQ7/SADBbjX9/mqglPA1JHG1Dc3aBnD888wWIQQ/YOoK 68E+KQk5oIDbH18RwXyy1AAAu2gA/0319zR36arn3N+WB0xBKWMAtiq6DTE4QJ7X lnTUUe48AQDLZUxoGK/XjQCRP4ChSAWNPlQZu21sAZHk9pPPR/1lABYhBPZnXQ5N pAgjcVxIEbiUkfB9COT4AAA4HQEA4To3UFaHrdHRujMKifHvTxdqF7/rjfiDXKlM osqs+JoA/2orygrfwUqmhKMva0tHMOGpS8GxFg+9hnTvS1Wwo+oL =YwIU -----END PGP PUBLIC KEY BLOCK----- sequoia-cert-store-0.7.1/tests/keyring.rs000064400000000000000000000153611046102023000165520ustar 00000000000000#[allow(non_upper_case_globals, dead_code)] mod keyring { use std::path::Path; use anyhow::Result; use sequoia_openpgp as openpgp; use openpgp::parse::Parse; pub struct Cert { pub filename: &'static str, pub base: &'static str, pub fingerprint: &'static str, pub subkeys: &'static [&'static str], pub userids: &'static [&'static str], } impl Cert { pub fn bytes(&self) -> Vec { let filename = Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests") .join(self.filename); std::fs::read(filename).expect("exists") } pub fn to_cert(&self) -> Result { openpgp::Cert::from_bytes(&self.bytes()) } } pub const certs: &[Cert] = &[ // alice Cert { filename: "data/alice.pgp", base: "alice", fingerprint: "30505BCEB7403A1BBFA9DBF0BFBE63567B4BA57A", subkeys: &[ "662F03FC47C05D070B53A93AD5A5048A71CD012A", "A6D92948A7ADEB809F04202F1CF1943DFE153D1E", "5989D7BE9908AE24799DF6CFBE678043781349F1", ], userids: &[ "", "", ], }, // alice2 Cert { filename: "data/alice2.pgp", base: "alice2", fingerprint: "23CFE49D4BB7A0AA83619C147E716FFE77DF170A", subkeys: &[ ], userids: &[ "", "", ], }, // alice2_adopted_alice Cert { filename: "data/alice2-adopted-alice.pgp", base: "alice2_adopted_alice", fingerprint: "23CFE49D4BB7A0AA83619C147E716FFE77DF170A", subkeys: &[ "662F03FC47C05D070B53A93AD5A5048A71CD012A", "5989D7BE9908AE24799DF6CFBE678043781349F1", ], userids: &[ "", "", ], }, // bob Cert { filename: "data/bob.pgp", base: "bob", fingerprint: "9994DBF9D34E88E2A21D0CE8E79C9395A1004BB0", subkeys: &[ "7E01441CBF6FAB5C4AB457E2FBD6F5322354B331", ], userids: &[ "", ], }, // carol Cert { filename: "data/carol.pgp", base: "carol", fingerprint: "E9C6EFC0E39CE6F9DF5274E7E362D45C7FF7B654", subkeys: &[ "CD22D4BD99FF10FDA11A83D4213DCB92C95346CE", ], userids: &[ "", "", ], }, // david Cert { filename: "data/david.pgp", base: "david", fingerprint: "A82BC944220BD5EBECC4D42883F74A0EAC207446", subkeys: &[ "DF674FBAC52E00F0E6E48436481D2E18158FB594", "CD22D4BD99FF10FDA11A83D4213DCB92C95346CE", ], userids: &[ "", ], }, // ed Cert { filename: "data/ed.pgp", base: "ed", fingerprint: "0C346B2B6241263F64E9C7CF1EA300797258A74E", subkeys: &[ "0C346B2B6241263F64E9C7CF1EA300797258A74E", ], userids: &[ "", ], }, // halfling_signing Cert { filename: "data/halfling-signing.pgp", base: "halfling_signing", fingerprint: "D58E047C05D115EA4F3D1A98A67A733127BBE804", subkeys: &[ "69669E91C8D5C546D442FB246FE6D4751AC09E15", "9DCDA2A95A17B728D6A5115EFF5C6582E4D14B68", ], userids: &[ "", "Halfling ", ], }, // halfling_encryption Cert { filename: "data/halfling-encryption.pgp", base: "halfling_encryption", fingerprint: "D58E047C05D115EA4F3D1A98A67A733127BBE804", subkeys: &[ "69669E91C8D5C546D442FB246FE6D4751AC09E15", "CC4EFA3BFAB8E92A54CDEA3F3DC7543293DD4E53", ], userids: &[ "", "Halfling ", ], }, // hans_puny_code Cert { filename: "data/hans-puny-code.pgp", base: "hans_puny_code", fingerprint: "F6675D0E4DA40823715C4811B89491F07D08E4F8", subkeys: &[ "3F60EA0AEBC13E290939A080DB1F5F11C17CB2D4", ], userids: &[ "Hans ", ], }, // una Cert { filename: "data/una.pgp", base: "una", fingerprint: "119B01460659D8EF3732BEC271424ADE3EC61BBC", subkeys: &[ "EE58C32E3D2336F223BD89CED0BE447BF39B439F", ], userids: &[ "Una ", ], }, // peter Cert { filename: "data/peter.pgp", base: "peter", fingerprint: "692E359EFB0BE115373F6CEAD5D8BA16C805A81F", subkeys: &[ "69D325E20DA4D404279A6534A6DE34867DE8927E", "62D2D97F00DF96D2DFED85F98CE4FE6BD01282DE", "DBEF1760AA2631194301E60F485BDDEF4BD2AD9A", ], userids: &[ "Dad ", "Peter ", ], }, // steve Cert { filename: "data/steve.pgp", base: "steve", fingerprint: "217E256E176719A5452EDFF935AADEC66B56585B", subkeys: &[ "32C5820540308752B7092EE5B596B656FD8F700B", ], userids: &[ "Steve ", ], }, ]; pub const alice: &Cert = &certs[0]; pub const alice2: &Cert = &certs[1]; pub const alice2_adopted_alice: &Cert = &certs[2]; pub const bob: &Cert = &certs[3]; pub const carol: &Cert = &certs[4]; pub const david: &Cert = &certs[5]; pub const ed: &Cert = &certs[6]; pub const halfling_signing: &Cert = &certs[7]; pub const halfling_encryption: &Cert = &certs[8]; pub const hans_puny_code: &Cert = &certs[9]; pub const una: &Cert = &certs[10]; pub const peter: &Cert = &certs[11]; pub const steve: &Cert = &certs[12]; } // mod keyring sequoia-cert-store-0.7.1/tests/pep/keys.db000064400000000000000000005400001046102023000165730ustar 00000000000000SQLite format 3@ ,.O|   - # } 'Eindexuserids_indexuserids CREATE INDEX userids_index ON userids (userid COLLATE EMAIL, primary_key)g%tableuseridsuseridsCREATE TABLE userids ( userid TEXT NOT NULL COLLATE EMAIL, primary_key TEXT NOT NULL, UNIQUE(userid, primary_key), FOREIGN KEY (primary_key) REFERENCES keys(primary_key) ON DELETE CASCADE )-Aindexsqlite_autoindex_userids_1userids o')indexsubkeys_indexsubkeysCREATE INDEX subkeys_index ON subkeys (subkey, primary_key)S}tablesubkeyssubkeysCREATE TABLE subkeys ( subkey TEXT NOT NULL, primary_key TEXT NOT NULL, UNIQUE(subkey, primary_key), FOREIGN KEY (primary_key) REFERENCES keys(primary_key) ON DELETE CASCADE )-Aindexsqlite_autoindex_subkeys_1subkeysc!indexkeys_indexkeysCREATE INDEX keys_index ON keys (primary_key, secret)5MtablekeyskeysCREATE TABLE keys ( primary_key TEXT UNIQUE PRIMARY KEY, secret BOOLEAN, tpk BLOB )';indexsqlite_autoindex_keys_1keys,*$)##"       %   ?M P   1 ^ 4 } l  o B # a z ,]AAB978A882B9A6E793960B071ADFC82AC3586C14%,]10E37AA3BBFD3348CF9AE3698EF85F1B1E37396F$,]507212E4796EFBF4FF8E4B1BF411A72A5C89092C#,]08C6A9408241E6ED99A0A2767A6B35253722954D",]6C36B5E1ED2B4B1D06CD03FF5D5025F89BBA7A07!,]E4C1FB98268170769154C21BBD1637EEB34E933B ,]2533E4E13EC84A784DE6F2962C3E64162620C978,]7A757276DFF48471EA032D2B98611755153523E6,]4925E278E468D55BA68B3AC49E1F03BA787E31EA,]B5AAD4575B2988D99E3FB3EE973EE71028459E9A,]D27B4F82C717A04DFF3A986489929D075908994E,]CDAE31CA330249BC5284C1F96033761A09D517DF,]9BC60CF498E584E5620014340C099ECDA62431FE,]CB4287D637F9BC0CB38F978AEB64F54F552F0AB7,]A2439F4C712EA9FA6B65BC17DECD473509E96847,]E1C12525BFAF4F092D109F2152B7186506DCC483,]2349DF0D7DBD60C6C20453350553D1E9E9AE5C54,]8A0293871D97E954B8397DFD072889CB0E82D77B,]F749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3,]7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76,]EB4750A0B0A0F558ED5F768F8B893A26133B3F66,]5C55764028BE2DD718F9DAAAD8D14CE515A32801,]68A4E57878501CF89B9844039B3560C27904221F,]912E63F55388D9886151101CDA4FE8ECD8365D6E,]8049F106768CD5D6374645F3B0C0ABEFB3892D73 ,]FBD19974E304C95589F976BD71059020F2CC257C ,]C11BCCBB3F843593B8975AFB958E85734EF0ADE0 ,]1CF1202EC58B5514EADC477AAA9CAC9C7B935A45 ,]9C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1 ,]A20EF4E353FF61FF6B8B401AC48BF850A5E9C611,]2F8EB6F51987B06706ED6E45064ABE0B9108844E,]04880CB55875B6548C25C729A00E4CD660454746,]43C5C721DCFF1D7D8FD1372F49B0B5F169F745F3,]22A589136F68CC46076FFA6071B2EDA40DC29CC7,]F29B751A3123A1502E5C0665745FB6564FC5F7DC,]4FDC731D26FAB6E8EAE6993C637DE61020C5DD1B+] B828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79 % [ 5I ; o  ! O  i } c [  U ' A w -]AAB978A882B9A6E793960B071ADFC82AC3586C14%-]10E37AA3BBFD3348CF9AE3698EF85F1B1E37396F$-]507212E4796EFBF4FF8E4B1BF411A72A5C89092C#-]08C6A9408241E6ED99A0A2767A6B35253722954D"-]6C36B5E1ED2B4B1D06CD03FF5D5025F89BBA7A07!-]E4C1FB98268170769154C21BBD1637EEB34E933B -]2533E4E13EC84A784DE6F2962C3E64162620C978-]7A757276DFF48471EA032D2B98611755153523E6-]4925E278E468D55BA68B3AC49E1F03BA787E31EA-]B5AAD4575B2988D99E3FB3EE973EE71028459E9A-]D27B4F82C717A04DFF3A986489929D075908994E-]CDAE31CA330249BC5284C1F96033761A09D517DF-]9BC60CF498E584E5620014340C099ECDA62431FE-]CB4287D637F9BC0CB38F978AEB64F54F552F0AB7-]A2439F4C712EA9FA6B65BC17DECD473509E96847-]E1C12525BFAF4F092D109F2152B7186506DCC483-]2349DF0D7DBD60C6C20453350553D1E9E9AE5C54-]8A0293871D97E954B8397DFD072889CB0E82D77B-]F749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3-]7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76-] EB4750A0B0A0F558ED5F768F8B893A26133B3F66-]5C55764028BE2DD718F9DAAAD8D14CE515A32801-]68A4E57878501CF89B9844039B3560C27904221F-]912E63F55388D9886151101CDA4FE8ECD8365D6E-]8049F106768CD5D6374645F3B0C0ABEFB3892D73 -]FBD19974E304C95589F976BD71059020F2CC257C -]C11BCCBB3F843593B8975AFB958E85734EF0ADE0 -]1CF1202EC58B5514EADC477AAA9CAC9C7B935A45 -]9C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1 -]A20EF4E353FF61FF6B8B401AC48BF850A5E9C611-]2F8EB6F51987B06706ED6E45064ABE0B9108844E-]04880CB55875B6548C25C729A00E4CD660454746-]43C5C721DCFF1D7D8FD1372F49B0B5F169F745F3-]22A589136F68CC46076FFA6071B2EDA40DC29CC7-]F29B751A3123A1502E5C0665745FB6564FC5F7DC-]4FDC731D26FAB6E8EAE6993C637DE61020C5DD1B,] B828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79$@|"|=-]BD1637EEB34E933BE4C1FB98268170769154C21BBD1637EEB34E933B}=-]6033761A09D517DFCDAE31CA330249BC5284C1F96033761A09D517DFe|!|=-]BD1637EEB34E933BE4C1FB98268170769154C21BBD1637EEB34E933B}=-]6033761A09D517DFCDAE31CA330249BC5284C1F96033761A09D517DFe 3av0[ B s / ]  @ i "O }5j$d+^f(aE3A]volker.birk@pep.foundationAAB978A882B9A6E793960B071ADFC82AC3586C14F2C]volker.birk@pep-project.orgAAB978A882B9A6E793960B071ADFC82AC3586C1461#]vb@pibit.chAAB978A882B9A6E793960B071ADFC82AC3586C14<0/]vb@pep.foundationAAB978A882B9A6E793960B071ADFC82AC3586C14=/1]vb@pep-project.orgAAB978A882B9A6E793960B071ADFC82AC3586C149.)]vb@dingens.orgAAB978A882B9A6E793960B071ADFC82AC3586C14=-1]dingens@bumens.orgAAB978A882B9A6E793960B071ADFC82AC3586C14=,1]bumens@dingens.orgAAB978A882B9A6E793960B071ADFC82AC3586C14@+7]pep3@ageinghacker.net10E37AA3BBFD3348CF9AE3698EF85F1B1E37396F@*7]pep2@ageinghacker.net507212E4796EFBF4FF8E4B1BF411A72A5C89092CG)E]luca-mobile@ageinghacker.net08C6A9408241E6ED99A0A2767A6B35253722954D7(%]luca@saiu.ch08C6A9408241E6ED99A0A2767A6B35253722954D?'5]saiu@univ-paris13.fr08C6A9408241E6ED99A0A2767A6B35253722954D;&-]positron@gnu.org08C6A9408241E6ED99A0A2767A6B35253722954D@%7]luca@ageinghacker.net08C6A9408241E6ED99A0A2767A6B35253722954DD$?]luca.saiu@univ-paris13.fr08C6A9408241E6ED99A0A2767A6B35253722954D?#5]iut@ageinghacker.net08C6A9408241E6ED99A0A2767A6B35253722954DB";]positron@pep.foundation08C6A9408241E6ED99A0A2767A6B35253722954DD!?]ziggy@fake.pep.foundation6C36B5E1ED2B4B1D06CD03FF5D5025F89BBA7A07F C]zenobia@fake.pep.foundationE4C1FB98268170769154C21BBD1637EEB34E933BFC]zachary@fake.pep.foundation2533E4E13EC84A784DE6F2962C3E64162620C978C=]yves@fake.pep.foundation7A757276DFF48471EA032D2B98611755153523E6C=]yoko@fake.pep.foundation4925E278E468D55BA68B3AC49E1F03BA787E31EAD?]xenia@fake.pep.foundationB5AAD4575B2988D99E3FB3EE973EE71028459E9AEA]xavier@fake.pep.foundationD27B4F82C717A04DFF3A986489929D075908994ED?]wyatt@fake.pep.foundationCDAE31CA330249BC5284C1F96033761A09D517DFEA]victor@fake.pep.foundation9BC60CF498E584E5620014340C099ECDA62431FEFC]valeria@fake.pep.foundationCB4287D637F9BC0CB38F978AEB64F54F552F0AB7FC]ulysses@fake.pep.foundationA2439F4C712EA9FA6B65BC17DECD473509E96847EA]tamara@fake.pep.foundationE1C12525BFAF4F092D109F2152B7186506DCC483FC]sabrina@fake.pep.foundation2349DF0D7DBD60C6C20453350553D1E9E9AE5C54D?]randy@fake.pep.foundation8A0293871D97E954B8397DFD072889CB0E82D77BHG]quasimodo@fake.pep.foundationF749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3C=]paul@fake.pep.foundation7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76C=]owen@fake.pep.foundationEB4750A0B0A0F558ED5F768F8B893A26133B3F66FC]ophelia@fake.pep.foundation5C55764028BE2DD718F9DAAAD8D14CE515A32801C=]nkls@fake.pep.foundation68A4E57878501CF89B9844039B3560C27904221FB;]ned@fake.pep.foundation912E63F55388D9886151101CDA4FE8ECD8365D6EC =]mary@fake.pep.foundation8049F106768CD5D6374645F3B0C0ABEFB3892D73D ?]louis@fake.pep.foundationFBD19974E304C95589F976BD71059020F2CC257CB ;]ken@fake.pep.foundationC11BCCBB3F843593B8975AFB958E85734EF0ADE0C =]john@fake.pep.foundation1CF1202EC58B5514EADC477AAA9CAC9C7B935A45D ?]irene@fake.pep.foundation9C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1D?]henry@fake.pep.foundationA20EF4E353FF61FF6B8B401AC48BF850A5E9C611FC]gilbert@fake.pep.foundation2F8EB6F51987B06706ED6E45064ABE0B9108844EFC]francis@fake.pep.foundation04880CB55875B6548C25C729A00E4CD660454746EA]ernest@fake.pep.foundation43C5C721DCFF1D7D8FD1372F49B0B5F169F745F3D?]david@fake.pep.foundation22A589136F68CC46076FFA6071B2EDA40DC29CC7D?]carol@fake.pep.foundationF29B751A3123A1502E5C0665745FB6564FC5F7DCB;]bob@fake.pep.foundation4FDC731D26FAB6E8EAE6993C637DE61020C5DD1BD?]alice@fake.pep.foundationB828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79 3/u.V H 9  g " M w4 u ,> Rx8 w/{4_GA]volker.birk@pep.foundationAAB978A882B9A6E793960B071ADFC82AC3586C143HC]volker.birk@pep-project.orgAAB978A882B9A6E793960B071ADFC82AC3586C1428#]vb@pibit.chAAB978A882B9A6E793960B071ADFC82AC3586C141>/]vb@pep.foundationAAB978A882B9A6E793960B071ADFC82AC3586C140?1]vb@pep-project.orgAAB978A882B9A6E793960B071ADFC82AC3586C14/;)]vb@dingens.orgAAB978A882B9A6E793960B071ADFC82AC3586C14.?1]dingens@bumens.orgAAB978A882B9A6E793960B071ADFC82AC3586C14-?1]bumens@dingens.orgAAB978A882B9A6E793960B071ADFC82AC3586C14,B7]pep3@ageinghacker.net10E37AA3BBFD3348CF9AE3698EF85F1B1E37396F+B7]pep2@ageinghacker.net507212E4796EFBF4FF8E4B1BF411A72A5C89092C*IE]luca-mobile@ageinghacker.net08C6A9408241E6ED99A0A2767A6B35253722954D)9%]luca@saiu.ch08C6A9408241E6ED99A0A2767A6B35253722954D(A5]saiu@univ-paris13.fr08C6A9408241E6ED99A0A2767A6B35253722954D'=-]positron@gnu.org08C6A9408241E6ED99A0A2767A6B35253722954D&B7]luca@ageinghacker.net08C6A9408241E6ED99A0A2767A6B35253722954D%F?]luca.saiu@univ-paris13.fr08C6A9408241E6ED99A0A2767A6B35253722954D$A5]iut@ageinghacker.net08C6A9408241E6ED99A0A2767A6B35253722954D#D;]positron@pep.foundation08C6A9408241E6ED99A0A2767A6B35253722954D"F?]ziggy@fake.pep.foundation6C36B5E1ED2B4B1D06CD03FF5D5025F89BBA7A07!HC]zenobia@fake.pep.foundationE4C1FB98268170769154C21BBD1637EEB34E933B HC]zachary@fake.pep.foundation2533E4E13EC84A784DE6F2962C3E64162620C978E=]yves@fake.pep.foundation7A757276DFF48471EA032D2B98611755153523E6E=]yoko@fake.pep.foundation4925E278E468D55BA68B3AC49E1F03BA787E31EAF?]xenia@fake.pep.foundationB5AAD4575B2988D99E3FB3EE973EE71028459E9AGA]xavier@fake.pep.foundationD27B4F82C717A04DFF3A986489929D075908994EF?]wyatt@fake.pep.foundationCDAE31CA330249BC5284C1F96033761A09D517DFGA]victor@fake.pep.foundation9BC60CF498E584E5620014340C099ECDA62431FEHC]valeria@fake.pep.foundationCB4287D637F9BC0CB38F978AEB64F54F552F0AB7HC]ulysses@fake.pep.foundationA2439F4C712EA9FA6B65BC17DECD473509E96847GA]tamara@fake.pep.foundationE1C12525BFAF4F092D109F2152B7186506DCC483HC]sabrina@fake.pep.foundation2349DF0D7DBD60C6C20453350553D1E9E9AE5C54F?]randy@fake.pep.foundation8A0293871D97E954B8397DFD072889CB0E82D77BJG]quasimodo@fake.pep.foundationF749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3E=]paul@fake.pep.foundation7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76E=]owen@fake.pep.foundationEB4750A0B0A0F558ED5F768F8B893A26133B3F66HC]ophelia@fake.pep.foundation5C55764028BE2DD718F9DAAAD8D14CE515A32801E=]nkls@fake.pep.foundation68A4E57878501CF89B9844039B3560C27904221FD;]ned@fake.pep.foundation912E63F55388D9886151101CDA4FE8ECD8365D6EE=]mary@fake.pep.foundation8049F106768CD5D6374645F3B0C0ABEFB3892D73 F?]louis@fake.pep.foundationFBD19974E304C95589F976BD71059020F2CC257C D;]ken@fake.pep.foundationC11BCCBB3F843593B8975AFB958E85734EF0ADE0 E=]john@fake.pep.foundation1CF1202EC58B5514EADC477AAA9CAC9C7B935A45 F?]irene@fake.pep.foundation9C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1 F?]henry@fake.pep.foundationA20EF4E353FF61FF6B8B401AC48BF850A5E9C611HC]gilbert@fake.pep.foundation2F8EB6F51987B06706ED6E45064ABE0B9108844EHC]francis@fake.pep.foundation04880CB55875B6548C25C729A00E4CD660454746GA]ernest@fake.pep.foundation43C5C721DCFF1D7D8FD1372F49B0B5F169F745F3F?]david@fake.pep.foundation22A589136F68CC46076FFA6071B2EDA40DC29CC7F?]carol@fake.pep.foundationF29B751A3123A1502E5C0665745FB6564FC5F7DCD;]bob@fake.pep.foundation4FDC731D26FAB6E8EAE6993C637DE61020C5DD1BE?] alice@fake.pep.foundationB828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79 3/u.V H 9  g " M w4 u ,> Rx8 w/{4_GA]volker.birk@pep.foundationAAB978A882B9A6E793960B071ADFC82AC3586C143HC]volker.birk@pep-project.orgAAB978A882B9A6E793960B071ADFC82AC3586C1428#]vb@pibit.chAAB978A882B9A6E793960B071ADFC82AC3586C141>/]vb@pep.foundationAAB978A882B9A6E793960B071ADFC82AC3586C140?1]vb@pep-project.orgAAB978A882B9A6E793960B071ADFC82AC3586C14/;)]vb@dingens.orgAAB978A882B9A6E793960B071ADFC82AC3586C14.?1]dingens@bumens.orgAAB978A882B9A6E793960B071ADFC82AC3586C14-?1]bumens@dingens.orgAAB978A882B9A6E793960B071ADFC82AC3586C14,B7]pep3@ageinghacker.net10E37AA3BBFD3348CF9AE3698EF85F1B1E37396F+B7]pep2@ageinghacker.net507212E4796EFBF4FF8E4B1BF411A72A5C89092C*IE]luca-mobile@ageinghacker.net08C6A9408241E6ED99A0A2767A6B35253722954D)9%]luca@saiu.ch08C6A9408241E6ED99A0A2767A6B35253722954D(A5]saiu@univ-paris13.fr08C6A9408241E6ED99A0A2767A6B35253722954D'=-]positron@gnu.org08C6A9408241E6ED99A0A2767A6B35253722954D&B7]luca@ageinghacker.net08C6A9408241E6ED99A0A2767A6B35253722954D%F?]luca.saiu@univ-paris13.fr08C6A9408241E6ED99A0A2767A6B35253722954D$A5]iut@ageinghacker.net08C6A9408241E6ED99A0A2767A6B35253722954D#D;]positron@pep.foundation08C6A9408241E6ED99A0A2767A6B35253722954D"F?]ziggy@fake.pep.foundation6C36B5E1ED2B4B1D06CD03FF5D5025F89BBA7A07!HC]zenobia@fake.pep.foundationE4C1FB98268170769154C21BBD1637EEB34E933B HC]zachary@fake.pep.foundation2533E4E13EC84A784DE6F2962C3E64162620C978E=]yves@fake.pep.foundation7A757276DFF48471EA032D2B98611755153523E6E=]yoko@fake.pep.foundation4925E278E468D55BA68B3AC49E1F03BA787E31EAF?]xenia@fake.pep.foundationB5AAD4575B2988D99E3FB3EE973EE71028459E9AGA]xavier@fake.pep.foundationD27B4F82C717A04DFF3A986489929D075908994EF?]wyatt@fake.pep.foundationCDAE31CA330249BC5284C1F96033761A09D517DFGA]victor@fake.pep.foundation9BC60CF498E584E5620014340C099ECDA62431FEHC]valeria@fake.pep.foundationCB4287D637F9BC0CB38F978AEB64F54F552F0AB7HC]ulysses@fake.pep.foundationA2439F4C712EA9FA6B65BC17DECD473509E96847GA]tamara@fake.pep.foundationE1C12525BFAF4F092D109F2152B7186506DCC483HC]sabrina@fake.pep.foundation2349DF0D7DBD60C6C20453350553D1E9E9AE5C54F?]randy@fake.pep.foundation8A0293871D97E954B8397DFD072889CB0E82D77BJG]quasimodo@fake.pep.foundationF749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3E=]paul@fake.pep.foundation7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76E=]owen@fake.pep.foundationEB4750A0B0A0F558ED5F768F8B893A26133B3F66HC]ophelia@fake.pep.foundation5C55764028BE2DD718F9DAAAD8D14CE515A32801E=]nkls@fake.pep.foundation68A4E57878501CF89B9844039B3560C27904221FD;]ned@fake.pep.foundation912E63F55388D9886151101CDA4FE8ECD8365D6EE=]mary@fake.pep.foundation8049F106768CD5D6374645F3B0C0ABEFB3892D73 F?]louis@fake.pep.foundationFBD19974E304C95589F976BD71059020F2CC257C D;]ken@fake.pep.foundationC11BCCBB3F843593B8975AFB958E85734EF0ADE0 E=]john@fake.pep.foundation1CF1202EC58B5514EADC477AAA9CAC9C7B935A45 F?]irene@fake.pep.foundation9C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1 F?]henry@fake.pep.foundationA20EF4E353FF61FF6B8B401AC48BF850A5E9C611HC]gilbert@fake.pep.foundation2F8EB6F51987B06706ED6E45064ABE0B9108844EHC]francis@fake.pep.foundation04880CB55875B6548C25C729A00E4CD660454746GA]ernest@fake.pep.foundation43C5C721DCFF1D7D8FD1372F49B0B5F169F745F3F?]david@fake.pep.foundation22A589136F68CC46076FFA6071B2EDA40DC29CC7F?]carol@fake.pep.foundationF29B751A3123A1502E5C0665745FB6564FC5F7DCD;]bob@fake.pep.foundation4FDC731D26FAB6E8EAE6993C637DE61020C5DD1BE?] alice@fake.pep.foundationB828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79 l 5lF]>4FDC731D26FAB6E8EAE6993C637DE61020C5DD1B3d- +G@ T8s*ng2wLˮTlAރ  }d-  c} G salt@notations.sequoia-pgp.orgD^n!-pPggw)# !Os&p-I5zuT /: g#?>SN !Os&@2y%u­T/*/=H]BB828EF1F203645DCDE9C37B0D0D1AB8DA16D8D793d- +G@$Ji(;+ꞘCzj  }d-  ѫmyG salt@notations.sequoia-pgp.orgO J Y6^#ЅiP( !( 6Eޜ7ѫmyTiym?}NcX@L8 XAncQ#G ҴEXǸ&W(b alice@fake.pep.foundation d-  ѫmyG salt@notations.sequoia-pgp.orgRwpN'e[K'fjwPH !( 6Eޜ7ѫmy\ҹ;OI7   tIϹneoeKvKȽO&8d/u8t!+9 3d- +G@67_"6*kڬH?n| 1d- ѫmyG salt@notations.sequoia-pgp.org&KSGT0Œ r+4Xo< od- =Gx,8OG salt@notations.sequoia-pgp.orgmO{%}`.w+y!:s*y=Gx,8Oh?wyTk~mJm X"O8 3֐pvbycPdNOkc;X 8d- +U@/иxy8Uzq)a^]7a rd- ѫmyG salt@notations.sequoia-pgp.orguW dj/}dL)ֹ- !( 6Eޜ7ѫmy56sjhome<V=]ɱ@HSh:IZT@ j 5jH]B22A589136F68CC46076FFA6071B2EDA40DC29CC73d- +G@$m@=,ʻMCObwC  }d-  q œG salt@notations.sequoia-pgp.orgdٴqc0O+SjE4& !"ohFo`q œmnylYd)ygy<( }e@B3J_̿Eu :_͘[k.4HS!fdavid@fake.pep.foundation d-  q œG salt@notations.sequoia-pgp.org'9sZN"ho7csGt !"ohFo`q œT5zihN2 *.;xwl|jFi<ɍ (x"7։q13d- +G@sd0D٪Q,öT>k rd- q œG salt@notations.sequoia-pgp.org02w?0=ADZޤk !"ohFo`q œNgN _GzXsԖ#B>X &4Ww=\A$E#uGJTg3d- +G@廉! / {,}s̕'Q "L2Jd{ 1d- q œG salt@notations.sequoia-pgp.org8^VE ZÊ6n$,/(tBĩ od- GM·G%G salt@notations.sequoia-pgp.orgֳxS䊪24%1w^|ߌ!RT;p5F1GM·G% Kx,˶.DzH2UsLZD02x6_f }M'.J$ !"ohFo`q œH=Qm!oZAMq8BޢԈyi: sw⚭"MPNy$H]BF29B751A3123A1502E5C0665745FB6564FC5F7DC3d- +G@xލu{Ek6*vKt  }d-  t_VOG salt@notations.sequoia-pgp.org#O/`/͡NjH)_Bå !u1#P.\et_VO+pKEZ2to$ V?FR Eg${3O ݑd^pP1carol@fake.pep.foundation d-  t_VOG salt@notations.sequoia-pgp.orgy ABs ^86_:Mls !u1#P.\et_VO ~ :;=m>/J#i#nKmT2Q#wgaw3d- +G@jhQ~7Xrh gc#1FWj rd- t_VOG salt@notations.sequoia-pgp.org>W fi? ?ziA|G X !u1#P.\et_VO4k]fQɋ/nl@yoe*}AaL&H}8T$3d- +G@jK+!d*Va_ D+︿d 8tr 1d- t_VOG salt@notations.sequoia-pgp.orgjCzD05eqzS 4u\{_7 od- hfG salt@notations.sequoia-pgp.orgtQ[ o,Y~ zV!5eBmt hf /6!h#b_m(fd$D''="E Qcբ!u1#P.\et_VO%Pj{Wٞ8C$X+\B}1m:0gdt)#&ۏE>\\38d- +U@z][cq`e5RDi{Tm rd- t_VOG salt@notations.sequoia-pgp.orgװ:&PMlVt`WJҳ H_̣` !u1#P.\et_VO~TOLOQ-1A VP JLR}/QN\PGK$X i&g\a_̈́ g 4gJ]F04880CB55875B6548C25C729A00E4CD6604547463d- +G@Q< ;lR KQ7francis@fake.pep.foundation d-  L`EGFG salt@notations.sequoia-pgp.org:"W0떘Stb([.ڷ ! XuT%)L`EGFf`H# ķ&iVEqs?G^~Cvk)TLvE(džQe3d- +G@IAp]~G=_Ax;+o2 1d- L`EGFG salt@notations.sequoia-pgp.orgxccQ~8)FڍM+uR^ݪ od- gQG salt@notations.sequoia-pgp.orgf%^>ZW?x7aq!˼NQ"wgQH qv&策Ƥ:׊HT9'(HhǤaLTD92P ! XuT%)L`EGF_ۮ,mgO/qw> '~Kv/ł/?eګD[Zr ! XuT%)L`EGFa!7jyȑ,Q`0N)Afq7M&!,MAtXڗ7xyc I]D43C5C721DCFF1D7D8FD1372F49B0B5F169F745F33d- +G@dh)` {8 ׅ=lÌƺl  }d-  IiEG salt@notations.sequoia-pgp.orghN$lܰ{? ûi Jv !C!}7/IiE_Հ/i>iz0-7@X]ۏ4& 8.RQ"E4ernest@fake.pep.foundation d-  IiEG salt@notations.sequoia-pgp.org!2A2/^H{5ߠ$Ye !C!}7/IiE]V^xwir\4ԍs}[3vV-U9[9.?FPx)s3d- +G@fTIAIs.;;Ɏ֥x 1d- IiEG salt@notations.sequoia-pgp.org]B[9Li jХf+ od- ({GG salt@notations.sequoia-pgp.orgLGVL$*M"cН%!p({GKL3z]x"T7<JXBkq={>08^о!C!}7/IiE 22IJaѵPȠ:38zhXF eB0Zwϙ;\-3d- +G@7.*|@! W͋!;P< rd- IiEG salt@notations.sequoia-pgp.orgU{3z8rMWr ?v1Cy !C!}7/IiE)v~ ~?8-*ǘH4G[* >_0K%DD+8d- +U@4RXw(@щN d3p rd- IiEG salt@notations.sequoia-pgp.orgV)چR4tȪnd<%m)Mo7P':Q !C!}7/IiE!%׽rG-BFuQ6pLsF!sVO5^ ͽKM|> h 3hH]BA20EF4E353FF61FF6B8B401AC48BF850A5E9C6113d- +G@@Sl^tlk(b{_'4)>  }d-  ċPG salt@notations.sequoia-pgp.orgrѼ5:D0dd9Z(e$b\ !Sak@ċPjH1ە4yx\/8Zj| P9ǯ0[L@.ɯF henry@fake.pep.foundation d-  ċPG salt@notations.sequoia-pgp.org_eSW JRUZ􊚬/ !Sak@ċP1pX@B91C@d% ㊀CH> "|{4@ =ͭطd F*3d- +G@} =Qr%⣳bt rd- ċPG salt@notations.sequoia-pgp.org5X1_Z4]E\!JЃ !Sak@ċPJΟqFmSbn[GN-bo7_uaM<[ w]"J 3d- +G@/]0phzdS{f$.! 1d- ċPG salt@notations.sequoia-pgp.org:6+W(7DaGo~ od- QNJG salt@notations.sequoia-pgp.orgdhuImKs[uIr⦜؀!ERkQNJHC5>Q).j 24czy>q(F _yMqCjK->WfU !Sak@ċP1Mv>ŀp2Ͽ.LvZ?ب⤂w`. QG1kX2 8d- +U@}Xz6v> S,HOd~ rd- ċPG salt@notations.sequoia-pgp.org*= Ǔ3OacG죵.&͉ !Sak@ċPG<n|~m дjKq7ah'B9;*~uFGL#bXJ]F2F8EB6F51987B06706ED6E45064ABE0B9108844E3d- +G@}{X.M Ӌ6k]{!٭  }d-  J NG salt@notations.sequoia-pgp.orgaâ)$0Njs* W_  !/gnEJ N:{Y2Zc]Vu! tjSQm 5=,aEgilbert@fake.pep.foundation d-  J NG salt@notations.sequoia-pgp.orgVj,a d0XF   !/gnEJ NјI疁L>F2 #D^SPYw&KPzu$'džy.q8g7 3d- +G@*+Az처qr"M||hkFف 1d- J NG salt@notations.sequoia-pgp.org>kR8{jCJH )`aIC od- -km}G salt@notations.sequoia-pgp.org"AJrd6}L s!8r`Îi-km}RÊs,b 2'.Y_fJBF\iTi^p@4uȬ@6Pl2J0!/gnEJ NGh>I1Wđ_cƏa4;bb *ڋB6xfSԭгᰖ3d- +G@zT =χguҁ\ )nA%|t rd- J NG salt@notations.sequoia-pgp.org0a9x%ɏ*ø:7 [mȑ !/gnEJ NyvGMi,bQ~˜ C«빽 ^#HX:ƾqMW5!Kc_azQf8d- +U@Պo$PP k 5kG ]@1CF1202EC58B5514EADC477AAA9CAC9C7B935A453d- +G@D4Q5Ho+3iNߔLP  }d-  {ZEG salt@notations.sequoia-pgp.orgGo- >0{C_[ ! .ŋUGz{ZE~6Aq(2`B@+.V{\n̛ rLY|􅮍#X=)U/dmfjohn@fake.pep.foundation d-  {ZEG salt@notations.sequoia-pgp.orgCӢO!TdDV聥% ! .ŋUGz{ZEyU7e5+ltlaer;1ڦn@;,5I"S]eW 3d- +G@&!a;M5biblXLL 1d- {ZEG salt@notations.sequoia-pgp.orgAT1fRh]L>rUX0b od- I1c+UvG salt@notations.sequoia-pgp.org}JmuN$Tդ؎ãşa!B=AvVʱI1c+Uv9ajQj8 E_ةAȚN>(~8/@»("tF;B (A! .ŋUGz{ZEX7xӰ"dJ/ĭ IwMj_K7 !$O[:{Mo~bUF:3d- +G@/mGXٸk+QM)):rAė rd- {ZEG salt@notations.sequoia-pgp.org"NA2xuKy) G6U/[?b! ! .ŋUGz{ZEHo2Pdoj{$ڨtm`pZpQɓlRK[h@m_6q9^0{Úgn ڶ 8d- +U@WhKtҘ`n6P@VX rd- {ZEG salt@notations.sequoia-pgp.orgjЬi)C7C-FTKܹ̳ Z( ! .ŋUGz{ZE]WeՃ1sؚlq(SB턱{^]yis9q 1H ]B9C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C13d- +G@YnF_nɌ/~a:i'3m  }d-  [2[G salt@notations.sequoia-pgp.org?)mWΛ` ږ/d !H'[2[]i疨C3HլAdb#p.^#l}O3d- +G@UZizZ20ERhv rd- [2[G salt@notations.sequoia-pgp.orgǖ姁GWE%6V] 1ߛze !H'[2[_B@ݔA^#6KoUr κB̵|0Z?6WQ 7B _> P|r-Rn~48d- +U@Q\̕xu*@+MeN-U-ce rd- [2[G salt@notations.sequoia-pgp.org#l͜nEF92إ{MANRUO+ !H'[2[z撬)2ဴzS+Ȗ$B<AWiP{N`0~$R0 l 7lH ]BFBD19974E304C95589F976BD71059020F2CC257C3d- +G@ >[Xy8n81+^F{  }d-  q %|G salt@notations.sequoia-pgp.orgNmX W*T>cٓ !љtUvq %|/Ȯ2-!6E1e8R}V3j=Slouis@fake.pep.foundation d-  q %|G salt@notations.sequoia-pgp.orgdm) }jy4GH@s LmϜ !љtUvq %|)K=wH{4h.B+֏[/LfgmZC_9/B.683d- +G@xFw5svYmm  rd- q %|G salt@notations.sequoia-pgp.org\EMcvaث'Nk'K͕J !љtUvq %|"JNjOM:t!E _1-XEPFHAwxVar6LM+tr Jw3d- +G@^|Xa45`B5 1d- q %|G salt@notations.sequoia-pgp.org'3myOڍAb<**- od- s̴hG salt@notations.sequoia-pgp.orgfXCC".fur'*GT!2)H6vIXs̴h6igIa촉N^~ǘ*S$xnrԣ4&LVn_'(} ΩE2!љtUvq %|5eEKJQnV6j(-.KYY` OJAA@+ GE88d- +U@pY1ՔH~muc`]| rd- q %|G salt@notations.sequoia-pgp.orgC&܄T D[Df{㠢K !љtUvq %|35R J!_"JJЁd6CT=4F6Gˉ?R]?,!P8mGXr F ]>C11BCCBB3F843593B8975AFB958E85734EF0ADE03d- +G@1 %mr&;HX'%lCw  }d-  sNG salt@notations.sequoia-pgp.orgW3ܐZ:@HB/%Ѧ=@ !̻?5ZsNGYo_tyck^r.5 2UtU7Ėt)fRiP}-+$ken@fake.pep.foundation d-  sNG salt@notations.sequoia-pgp.org/.+ˬ70 w}/胞A=Z  !̻?5ZsNo}UHlLH 낶tfVjָTIjgpl>Ңa%INNW ^s3d- +G@^S+o7bAz$ I-0_? 1d- sNG salt@notations.sequoia-pgp.orgrm<pL4Ȭ"|Mcis od- &(G salt@notations.sequoia-pgp.org 2ïH&㕙 )(ؼc鼖M!81qFFO~ &(JRX-Q=&31yq,ZM*BkX[.?q* Vc3 !̻?5ZsNf'*Й3\EB7 0@N(ͲY\Bw@*\S(0APU!"qۿšI 3d- +G@.u߫%-ϚTp+Q|  rd- sNG salt@notations.sequoia-pgp.orgpK|jZ^#81v3 !̻?5ZsNXO܏&&O(;5Jcr4yEqn6/jҍMtlؓP 8d- +U@rokXvj%=ƶxK rd- sNG salt@notations.sequoia-pgp.org?̼WYz갓+;hʫe> !̻?5ZsN0wPj0dg<@\&=A8I*veINPA$1KI R2^{ m 6mF]>912E63F55388D9886151101CDA4FE8ECD8365D6E3d- +G@ҙ-a=q&q`^|C2{J`  }d-  O6]nG salt@notations.sequoia-pgp.org}`|EVe_:x/%  !.cSوaQO6]nHYMs*qfɅ6Rm2f\vɫ)Hs GQhywaXt#j ned@fake.pep.foundation d-  O6]nG salt@notations.sequoia-pgp.org)s _c.\Laq !.cSوaQO6]n>kUU8iXd'Rbbu͉%.cu75޿TsI3oL V)1F{[q=3d- +G@W"a="X] +Σ ;ڟ 1d- O6]nG salt@notations.sequoia-pgp.orgM`wO3i&% t{y od- w0*yG salt@notations.sequoia-pgp.org፻*'m4UּLݚt}9`8 _W!"s05_Ww0*yO??qcL \x*(ߟֺZgi'rҕ(XjZf V6(!.cSوaQO6]n`L_E 6Q*Nx>0p Qw:#SPF&HԲEjM7bس 3d- +G@P*!M"R͋ZHO_`H rd- O6]nG salt@notations.sequoia-pgp.org-KDQ/m~%DT_ !.cSوaQO6]n_z4jzR\:_7ʱ(#IO !Yqz. 18d- +U@B︄ң,O"ws5Q rd- O6]nG salt@notations.sequoia-pgp.orgםb~"PeyazWWq4Mp !.cSوaQO6]nĠfs \ "V<A7 _ ^is \BUFZrX$I*^=w G ]@8049F106768CD5D6374645F3B0C0ABEFB3892D733d- +G@cVFw]uPtv2gn""@Hq  }d-  ﳉ-sG salt@notations.sequoia-pgp.orgooeu(~z`zb !Iv7FEﳉ-s+#kAgAg߻k|%O;*e5 zD²q}G&Gmary@fake.pep.foundation d-  ﳉ-sG salt@notations.sequoia-pgp.orgwb(M,Lu=DBDyQކ*vh !Iv7FEﳉ-s y”w\SWPRk(R!w; v8n0KbH߷WL}}Asʳyq3d- +G@@-㘂jDv&S| 1d- ﳉ-sG salt@notations.sequoia-pgp.org H>>?kz}N ,X-ry od- fFmG salt@notations.sequoia-pgp.org7n)!.=?+Mw_!i_%{E.fFmfL:Z>ͽbTG9  U7;EqkW7 !Iv7FEﳉ-s65IZh^򊊕d֞ @yTEKl3/e4jsV""E1)E O3d- +G@9$ YFJt ^pms rd- ﳉ-sG salt@notations.sequoia-pgp.orgY*lMVogKO  !Iv7FEﳉ-sihY}dREo tXǾM"=½Ht@ʾ +ATx.2Xۑ 8d- +U@Rׇėoi=/ £`oRoJ} rd- ﳉ-sG salt@notations.sequoia-pgp.org} / =HJ7҆K !Iv7FEﳉ-sD-ȁWJͺp\sx:DX;2|C@+4F0aV@D i 6iJ]F5C55764028BE2DD718F9DAAAD8D14CE515A328013d- +G@2O_7S5/E IOQ  }d-  L(G salt@notations.sequoia-pgp.orgAG;ۇdqyt/iz %=cophelia@fake.pep.foundation d-  L(G salt@notations.sequoia-pgp.org&@W,wel5={COi!@t !\Uv@(-ڪL(GCbas- |Wg47~1Յ.azx]Cԙ~ Ld<(Q_:b\ 3d- +G@Rׄ&- ǫ'](&Jb*B 1d- L(G salt@notations.sequoia-pgp.org;WX 1d- 5`y"G salt@notations.sequoia-pgp.orgX5sЄ-rf Va?( ~$ od- mQ]oG salt@notations.sequoia-pgp.org޽_fZ)ČZ7GV63!wmI27mQ]o !R?c7hB\hdlߨOUS?{C,#&3Cn9!hxxPD5`y"}G;7*>8X 9 K+NuXorhiz' q?|t",3d- +G@8£a<= PXЯ;?9M rd- 5`y"G salt@notations.sequoia-pgp.orgXx6я\Ƃ j  U1 !hxxPD5`y"$7 SU291`eߎD&yn~xl5Th긵SZ鿟Ȼp8d- +U@qؐ$ iFUA$ rd- 5`y"G salt@notations.sequoia-pgp.orgTJ L!WgW)gtp !hxxPD5`y"7 U{/[ch~O-dU4 h[4#H]}Aj㻐F 8hFN% V  ^ h e ' l . t 6|>FNVd^ >-]1ADFC82AC3586C14AAB978A882B9A6E793960B071ADFC82AC3586C14>-]5A826CAA7026DB6F10E37AA3BBFD3348CF9AE3698EF85F1B1E37396F>-]4A869829AAB70CBE10E37AA3BBFD3348CF9AE3698EF85F1B1E37396F>-]0AC73BFE55E17D2208C6A9408241E6ED99A0A2767A6B35253722954D>-]53D8C1B7F801D54D08C6A9408241E6ED99A0A2767A6B35253722954D=-]5DFAD7D10C678DBED27B4F82C717A04DFF3A986489929D075908994Ej>-]5D5025F89BBA7A076C36B5E1ED2B4B1D06CD03FF5D5025F89BBA7A07=-]5609A4F9A4344957A2439F4C712EA9FA6B65BC17DECD473509E96847\=-]54E2200EE3627373CB4287D637F9BC0CB38F978AEB64F54F552F0AB7_=-]52B7186506DCC483E1C12525BFAF4F092D109F2152B7186506DCC483U=-]51074EA7D24AA490A20EF4E353FF61FF6B8B401AC48BF850A5E9C611=-]50B9CB75C21518725C55764028BE2DD718F9DAAAD8D14CE515A32801@=-]4D2476E6EDCA3BE22533E4E13EC84A784DE6F2962C3E64162620C978|=-]4C34BA7BC16F86B3F749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3I=-]4B8313F3BF069E6A7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76G=-]4A4DF6C29EDD1F767D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76E=-]49B0B5F169F745F343C5C721DCFF1D7D8FD1372F49B0B5F169F745F3=-]47D04DC2B74725E122A589136F68CC46076FFA6071B2EDA40DC29CC7=-]42916177BEAA88747A757276DFF48471EA032D2B98611755153523E6x=-]3F0586841937BDFE912E63F55388D9886151101CDA4FE8ECD8365D6E8=-]3D4778FC2C38084FB828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79=-]3BDE948165BE18002F8EB6F51987B06706ED6E45064ABE0B9108844E=-]3AD07BB3F42BD08E2F8EB6F51987B06706ED6E45064ABE0B9108844E=-]38E7A3645666C76922A589136F68CC46076FFA6071B2EDA40DC29CC7=-]38CBCE44873FE1A74925E278E468D55BA68B3AC49E1F03BA787E31EAr=-]34450D8FA47BE5781CF1202EC58B5514EADC477AAA9CAC9C7B935A45(=-]33DB91F8AAECA2FDA20EF4E353FF61FF6B8B401AC48BF850A5E9C611=-]2F8A666BE91A400F2349DF0D7DBD60C6C20453350553D1E9E9AE5C54S=-]2DD7EC6B89966D7D2F8EB6F51987B06706ED6E45064ABE0B9108844E=-]2C7C1FA3369E4A4D7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76H=-]2C45488C98085D61CDAE31CA330249BC5284C1F96033761A09D517DFh=-]2C3E64162620C9782533E4E13EC84A784DE6F2962C3E64162620C978y=-]2B45B386DCB3A0CFE1C12525BFAF4F092D109F2152B7186506DCC483V=-]28967B47191899F843C5C721DCFF1D7D8FD1372F49B0B5F169F745F3>-]1DB2D0B226A87A276C36B5E1ED2B4B1D06CD03FF5D5025F89BBA7A07=-]1D7FA83415FD0C9CE4C1FB98268170769154C21BBD1637EEB34E933B=-]1C23477C835E5CEB7A757276DFF48471EA032D2B98611755153523E6w>-]1B23CEF521884A3F6C36B5E1ED2B4B1D06CD03FF5D5025F89BBA7A07=-]19BAAA837AA0EDE3F29B751A3123A1502E5C0665745FB6564FC5F7DC =-]1981CA07B08A2AA34FDC731D26FAB6E8EAE6993C637DE61020C5DD1B=-]1949FBA7A090B71D4925E278E468D55BA68B3AC49E1F03BA787E31EAt=-]1908736247B42C7C2349DF0D7DBD60C6C20453350553D1E9E9AE5C54R=-]185B7A6512A4839143C5C721DCFF1D7D8FD1372F49B0B5F169F745F3=-]1743133E219C1EB19BC60CF498E584E5620014340C099ECDA62431FEc=-]14180E23218FFEDAEB4750A0B0A0F558ED5F768F8B893A26133B3F66B=-]132E928709CB19568A0293871D97E954B8397DFD072889CB0E82D77BP=-]0DCD1F051766107FF749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3J=-]0D6DF17AFEF1FFBAB5AAD4575B2988D99E3FB3EE973EE71028459E9Ao=-]0C099ECDA62431FE9BC60CF498E584E5620014340C099ECDA62431FEa=-]072889CB0E82D77B8A0293871D97E954B8397DFD072889CB0E82D77BM=-]064ABE0B9108844E2F8EB6F51987B06706ED6E45064ABE0B9108844E=-]0553D1E9E9AE5C542349DF0D7DBD60C6C20453350553D1E9E9AE5C54Q=-]054118196B963B7DCDAE31CA330249BC5284C1F96033761A09D517DFf=-]0509657A6BAC730C68A4E57878501CF89B9844039B3560C27904221F;=-]04EEFDB17B04F78B5C55764028BE2DD718F9DAAAD8D14CE515A32801?=-]01076D890F515D6F68A4E57878501CF89B9844039B3560C27904221F: 3;3a &I  ^ y;i+ iqYF#N Q V  e  w 9A3|=-]8451B62D9873AE16A20EF4E353FF61FF6B8B401AC48BF850A5E9C611 =-]83C69AF0F851437AB5AAD4575B2988D99E3FB3EE973EE71028459E9An=-]6286EF138550E5C09C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1$=-]98611755153523E67A757276DFF48471EA032D2B98611755153523E6u=-]637DE61020C5DD1B4FDC731D26FAB6E8EAE6993C637DE61020C5DD1B=-]9079AA6B02A61A8B8049F106768CD5D6374645F3B0C0ABEFB3892D733=-]8F07A6D986845B5AEB4750A0B0A0F558ED5F768F8B893A26133B3F66C=-]8B893A26133B3F66EB4750A0B0A0F558ED5F768F8B893A26133B3F66A=-]89E01368B3CFEA66F29B751A3123A1502E5C0665745FB6564FC5F7DC =-]89929D075908994ED27B4F82C717A04DFF3A986489929D075908994Ei=-]884DFEDE8DF8A5B19BC60CF498E584E5620014340C099ECDA62431FEb=-]848F73B0CCB468D9FBD19974E304C95589F976BD71059020F2CC257C/=-]9E1F03BA787E31EA4925E278E468D55BA68B3AC49E1F03BA787E31EAq=-]64BF7EACFA7E07D3CB4287D637F9BC0CB38F978AEB64F54F552F0AB7`=-]973EE71028459E9AB5AAD4575B2988D99E3FB3EE973EE71028459E9Am=-]A685E0D6F5530952D27B4F82C717A04DFF3A986489929D075908994El=-]650BEEAE8BE0BA784FDC731D26FAB6E8EAE6993C637DE61020C5DD1B=-]94D1A2CC4A9787279BC60CF498E584E5620014340C099ECDA62431FEd=-]6C9FF639C2D20ED28A0293871D97E954B8397DFD072889CB0E82D77BO=-]66CC46F715A66DBC8049F106768CD5D6374645F3B0C0ABEFB3892D732=-]B974664B34752AFCA2439F4C712EA9FA6B65BC17DECD473509E96847[=-]AA761009D6D131A0A2439F4C712EA9FA6B65BC17DECD473509E96847Z=-]780F3DCE8BA84184FBD19974E304C95589F976BD71059020F2CC257C0=-]773082B0012A791A912E63F55388D9886151101CDA4FE8ECD8365D6E6=-]745FB6564FC5F7DCF29B751A3123A1502E5C0665745FB6564FC5F7DC =-]71B2EDA40DC29CC722A589136F68CC46076FFA6071B2EDA40DC29CC7 =-]71059020F2CC257CFBD19974E304C95589F976BD71059020F2CC257C-=-]9C4396FA8EE7BB36F749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3Lf>-]8EF85F1B1E37396F10E37AA3BBFD3348CF9AE3698EF85F1B1E37396F>-]A9173FFEBD40DA28507212E4796EFBF4FF8E4B1BF411A72A5C89092C>-]7A6B35253722954D08C6A9408241E6ED99A0A2767A6B35253722954D>-]69AF5A6299EC8A606C36B5E1ED2B4B1D06CD03FF5D5025F89BBA7A07>-]B929651D1EDAD644E4C1FB98268170769154C21BBD1637EEB34E933B=-]8134877075548DB72533E4E13EC84A784DE6F2962C3E64162620C978z=-]7F6B5258C086DC6B7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76F=-]7AB77AECC3703ED52533E4E13EC84A784DE6F2962C3E64162620C978{=-]7903174192105E48D27B4F82C717A04DFF3A986489929D075908994Ek=-]B4E5A2B2EF97E40568A4E57878501CF89B9844039B3560C27904221F<=-]B0C0ABEFB3892D738049F106768CD5D6374645F3B0C0ABEFB3892D731=-]B058180EE35C8BE05C55764028BE2DD718F9DAAAD8D14CE515A32801>=-]AF26C6724E7E40F3912E63F55388D9886151101CDA4FE8ECD8365D6E7=-]ADF2379966C7497222A589136F68CC46076FFA6071B2EDA40DC29CC7=-]AC9255C490B722199C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1#=-]AA9CAC9C7B935A451CF1202EC58B5514EADC477AAA9CAC9C7B935A45%=-]A7CB62B3422503EE04880CB55875B6548C25C729A00E4CD660454746=-]A00E4CD66045474604880CB55875B6548C25C729A00E4CD660454746=-]9F5599183124F72D43C5C721DCFF1D7D8FD1372F49B0B5F169F745F3=-]9B3560C27904221F68A4E57878501CF89B9844039B3560C27904221F9=-]994931632B55CF761CF1202EC58B5514EADC477AAA9CAC9C7B935A45&=-]958E85734EF0ADE0C11BCCBB3F843593B8975AFB958E85734EF0ADE0)=-]94E766EE6359246DFBD19974E304C95589F976BD71059020F2CC257C. 8hFN% V  ^ h e ' l . t 6|>FNVd^ >-]1ADFC82AC3586C14AAB978A882B9A6E793960B071ADFC82AC3586C14>-]5A826CAA7026DB6F10E37AA3BBFD3348CF9AE3698EF85F1B1E37396F>-]4A869829AAB70CBE10E37AA3BBFD3348CF9AE3698EF85F1B1E37396F>-]0AC73BFE55E17D2208C6A9408241E6ED99A0A2767A6B35253722954D>-]53D8C1B7F801D54D08C6A9408241E6ED99A0A2767A6B35253722954D=-]5DFAD7D10C678DBED27B4F82C717A04DFF3A986489929D075908994Ej>-]5D5025F89BBA7A076C36B5E1ED2B4B1D06CD03FF5D5025F89BBA7A07=-]5609A4F9A4344957A2439F4C712EA9FA6B65BC17DECD473509E96847\=-]54E2200EE3627373CB4287D637F9BC0CB38F978AEB64F54F552F0AB7_=-]52B7186506DCC483E1C12525BFAF4F092D109F2152B7186506DCC483U=-]51074EA7D24AA490A20EF4E353FF61FF6B8B401AC48BF850A5E9C611=-]50B9CB75C21518725C55764028BE2DD718F9DAAAD8D14CE515A32801@=-]4D2476E6EDCA3BE22533E4E13EC84A784DE6F2962C3E64162620C978|=-]4C34BA7BC16F86B3F749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3I=-]4B8313F3BF069E6A7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76G=-]4A4DF6C29EDD1F767D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76E=-]49B0B5F169F745F343C5C721DCFF1D7D8FD1372F49B0B5F169F745F3=-]47D04DC2B74725E122A589136F68CC46076FFA6071B2EDA40DC29CC7=-]42916177BEAA88747A757276DFF48471EA032D2B98611755153523E6x=-]3F0586841937BDFE912E63F55388D9886151101CDA4FE8ECD8365D6E8=-]3D4778FC2C38084FB828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79=-]3BDE948165BE18002F8EB6F51987B06706ED6E45064ABE0B9108844E=-]3AD07BB3F42BD08E2F8EB6F51987B06706ED6E45064ABE0B9108844E=-]38E7A3645666C76922A589136F68CC46076FFA6071B2EDA40DC29CC7=-]38CBCE44873FE1A74925E278E468D55BA68B3AC49E1F03BA787E31EAr=-]34450D8FA47BE5781CF1202EC58B5514EADC477AAA9CAC9C7B935A45(=-]33DB91F8AAECA2FDA20EF4E353FF61FF6B8B401AC48BF850A5E9C611=-]2F8A666BE91A400F2349DF0D7DBD60C6C20453350553D1E9E9AE5C54S=-]2DD7EC6B89966D7D2F8EB6F51987B06706ED6E45064ABE0B9108844E=-]2C7C1FA3369E4A4D7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76H=-]2C45488C98085D61CDAE31CA330249BC5284C1F96033761A09D517DFh=-]2C3E64162620C9782533E4E13EC84A784DE6F2962C3E64162620C978y=-]2B45B386DCB3A0CFE1C12525BFAF4F092D109F2152B7186506DCC483V=-]28967B47191899F843C5C721DCFF1D7D8FD1372F49B0B5F169F745F3>-]1DB2D0B226A87A276C36B5E1ED2B4B1D06CD03FF5D5025F89BBA7A07=-]1D7FA83415FD0C9CE4C1FB98268170769154C21BBD1637EEB34E933B=-]1C23477C835E5CEB7A757276DFF48471EA032D2B98611755153523E6w>-]1B23CEF521884A3F6C36B5E1ED2B4B1D06CD03FF5D5025F89BBA7A07=-]19BAAA837AA0EDE3F29B751A3123A1502E5C0665745FB6564FC5F7DC =-]1981CA07B08A2AA34FDC731D26FAB6E8EAE6993C637DE61020C5DD1B=-]1949FBA7A090B71D4925E278E468D55BA68B3AC49E1F03BA787E31EAt=-]1908736247B42C7C2349DF0D7DBD60C6C20453350553D1E9E9AE5C54R=-]185B7A6512A4839143C5C721DCFF1D7D8FD1372F49B0B5F169F745F3=-]1743133E219C1EB19BC60CF498E584E5620014340C099ECDA62431FEc=-]14180E23218FFEDAEB4750A0B0A0F558ED5F768F8B893A26133B3F66B=-]132E928709CB19568A0293871D97E954B8397DFD072889CB0E82D77BP=-]0DCD1F051766107FF749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3J=-]0D6DF17AFEF1FFBAB5AAD4575B2988D99E3FB3EE973EE71028459E9Ao=-]0C099ECDA62431FE9BC60CF498E584E5620014340C099ECDA62431FEa=-]072889CB0E82D77B8A0293871D97E954B8397DFD072889CB0E82D77BM=-]064ABE0B9108844E2F8EB6F51987B06706ED6E45064ABE0B9108844E=-]0553D1E9E9AE5C542349DF0D7DBD60C6C20453350553D1E9E9AE5C54Q=-]054118196B963B7DCDAE31CA330249BC5284C1F96033761A09D517DFf=-]0509657A6BAC730C68A4E57878501CF89B9844039B3560C27904221F;=-]04EEFDB17B04F78B5C55764028BE2DD718F9DAAAD8D14CE515A32801?=-]01076D890F515D6F68A4E57878501CF89B9844039B3560C27904221F: 3;3a &I  ^ y;i+ iqYF#N Q V  e  w 9A3|=-]8451B62D9873AE16A20EF4E353FF61FF6B8B401AC48BF850A5E9C611 =-]83C69AF0F851437AB5AAD4575B2988D99E3FB3EE973EE71028459E9An=-]6286EF138550E5C09C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1$=-]98611755153523E67A757276DFF48471EA032D2B98611755153523E6u=-]637DE61020C5DD1B4FDC731D26FAB6E8EAE6993C637DE61020C5DD1B=-]9079AA6B02A61A8B8049F106768CD5D6374645F3B0C0ABEFB3892D733=-]8F07A6D986845B5AEB4750A0B0A0F558ED5F768F8B893A26133B3F66C=-]8B893A26133B3F66EB4750A0B0A0F558ED5F768F8B893A26133B3F66A=-]89E01368B3CFEA66F29B751A3123A1502E5C0665745FB6564FC5F7DC =-]89929D075908994ED27B4F82C717A04DFF3A986489929D075908994Ei=-]884DFEDE8DF8A5B19BC60CF498E584E5620014340C099ECDA62431FEb=-]848F73B0CCB468D9FBD19974E304C95589F976BD71059020F2CC257C/=-]9E1F03BA787E31EA4925E278E468D55BA68B3AC49E1F03BA787E31EAq=-]64BF7EACFA7E07D3CB4287D637F9BC0CB38F978AEB64F54F552F0AB7`=-]973EE71028459E9AB5AAD4575B2988D99E3FB3EE973EE71028459E9Am=-]A685E0D6F5530952D27B4F82C717A04DFF3A986489929D075908994El=-]650BEEAE8BE0BA784FDC731D26FAB6E8EAE6993C637DE61020C5DD1B=-]94D1A2CC4A9787279BC60CF498E584E5620014340C099ECDA62431FEd=-]6C9FF639C2D20ED28A0293871D97E954B8397DFD072889CB0E82D77BO=-]66CC46F715A66DBC8049F106768CD5D6374645F3B0C0ABEFB3892D732=-]B974664B34752AFCA2439F4C712EA9FA6B65BC17DECD473509E96847[=-]AA761009D6D131A0A2439F4C712EA9FA6B65BC17DECD473509E96847Z=-]780F3DCE8BA84184FBD19974E304C95589F976BD71059020F2CC257C0=-]773082B0012A791A912E63F55388D9886151101CDA4FE8ECD8365D6E6=-]745FB6564FC5F7DCF29B751A3123A1502E5C0665745FB6564FC5F7DC =-]71B2EDA40DC29CC722A589136F68CC46076FFA6071B2EDA40DC29CC7 =-]71059020F2CC257CFBD19974E304C95589F976BD71059020F2CC257C-=-]9C4396FA8EE7BB36F749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3Lf>-]8EF85F1B1E37396F10E37AA3BBFD3348CF9AE3698EF85F1B1E37396F>-]A9173FFEBD40DA28507212E4796EFBF4FF8E4B1BF411A72A5C89092C>-]7A6B35253722954D08C6A9408241E6ED99A0A2767A6B35253722954D>-]69AF5A6299EC8A606C36B5E1ED2B4B1D06CD03FF5D5025F89BBA7A07>-]B929651D1EDAD644E4C1FB98268170769154C21BBD1637EEB34E933B=-]8134877075548DB72533E4E13EC84A784DE6F2962C3E64162620C978z=-]7F6B5258C086DC6B7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76F=-]7AB77AECC3703ED52533E4E13EC84A784DE6F2962C3E64162620C978{=-]7903174192105E48D27B4F82C717A04DFF3A986489929D075908994Ek=-]B4E5A2B2EF97E40568A4E57878501CF89B9844039B3560C27904221F<=-]B0C0ABEFB3892D738049F106768CD5D6374645F3B0C0ABEFB3892D731=-]B058180EE35C8BE05C55764028BE2DD718F9DAAAD8D14CE515A32801>=-]AF26C6724E7E40F3912E63F55388D9886151101CDA4FE8ECD8365D6E7=-]ADF2379966C7497222A589136F68CC46076FFA6071B2EDA40DC29CC7=-]AC9255C490B722199C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1#=-]AA9CAC9C7B935A451CF1202EC58B5514EADC477AAA9CAC9C7B935A45%=-]A7CB62B3422503EE04880CB55875B6548C25C729A00E4CD660454746=-]A00E4CD66045474604880CB55875B6548C25C729A00E4CD660454746=-]9F5599183124F72D43C5C721DCFF1D7D8FD1372F49B0B5F169F745F3=-]9B3560C27904221F68A4E57878501CF89B9844039B3560C27904221F9=-]994931632B55CF761CF1202EC58B5514EADC477AAA9CAC9C7B935A45&=-]958E85734EF0ADE0C11BCCBB3F843593B8975AFB958E85734EF0ADE0)=-]94E766EE6359246DFBD19974E304C95589F976BD71059020F2CC257C.  G]@7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F763d- +G@`aj6`s zesOS  }d-  JMžvG salt@notations.sequoia-pgp.orgUqnW7q;|2`b7nN !}mNXaJMžv+Yfd_/vuE'9r3wet-/ZsWIbo8(='^paul@fake.pep.foundation d-  JMžvG salt@notations.sequoia-pgp.orghu"iPueVa !}mNXaJMžv^Z>6,:C$ r©ڲTN0h A`* [|}4hwY43d- +G@$T.XaV#HnQaZz^1 rd- JMžvG salt@notations.sequoia-pgp.org,'yĔd7:k7h/GI`3! !}mNXaJMžv |mH J\>s]m`_|ﵺQ@g.YX3d- +G@>ډswrW^'DqWpS¶&: 1d- JMžvG salt@notations.sequoia-pgp.orgLfQ#E6 ٵa`~Wds od- KjG salt@notations.sequoia-pgp.org\;*a,bD=s3;eS460 !MAI ~$L͑U>\3v< dɆg!}mNXaJMžvDS+PM=/`  }d-  :&;?fG salt@notations.sequoia-pgp.org ~:&jԳ,(.l  !GPX_v:&;?f~غ^[ZB4Ko&K4]z^nN6ǐ] l4pxowen@fake.pep.foundation d-  :&;?fG salt@notations.sequoia-pgp.org 0{yoZNJUp !GPX_v:&;?fIlaϊ+5ud\s$}9e\k 2={!$FFx >Knw傣Xd- +G@!r(zCF|{8[x;ݤIIu0_,7¼9ȗ-K 1d- :&;?fG salt@notations.sequoia-pgp.org^*"uĚ6 \s=il /?d od- #!G salt@notations.sequoia-pgp.org}M7?0};f-V!m`Xe2#!|Hk/i W$pnKL tyR㱙k4z~uDDrFmb!GPX_v:&;?ffRh!Á1 Ĩ4a]HOlS1bRu.@r F˶3=Xd- +G@|p')u5O"-5&]}>aBAI':ںJTX&=-&~ rd- :&;?fG salt@notations.sequoia-pgp.org|W75ϘYi||6iZz !GPX_v:&;?fE͕CA1zqjPRSmj]n(rޕPupqpI]d- +U@F@鰘RӆA)EkS}?A.<BB~\k,Ihgߞ rd- :&;?fG salt@notations.sequoia-pgp.orgHiePdq*]W X%t%P, !GPX_v:&;?fPL,gїň~O3-_ݣ,mBYK$W'ʝϝģlQ! @I U a $ m 0 y < H T `#l/x;G S_"k.w:;@-]50B9CB75C21518725C55764028BE2DD718F9DAAAD8D14CE515A32801;?-]04EEFDB17B04F78B5C55764028BE2DD718F9DAAAD8D14CE515A32801;>-]B058180EE35C8BE05C55764028BE2DD718F9DAAAD8D14CE515A32801;=-]D8D14CE515A328015C55764028BE2DD718F9DAAAD8D14CE515A32801;<-]B4E5A2B2EF97E40568A4E57878501CF89B9844039B3560C27904221F;;-]0509657A6BAC730C68A4E57878501CF89B9844039B3560C27904221F;:-]01076D890F515D6F68A4E57878501CF89B9844039B3560C27904221F;9-]9B3560C27904221F68A4E57878501CF89B9844039B3560C27904221F;8-]3F0586841937BDFE912E63F55388D9886151101CDA4FE8ECD8365D6E;7-]AF26C6724E7E40F3912E63F55388D9886151101CDA4FE8ECD8365D6E;6-]773082B0012A791A912E63F55388D9886151101CDA4FE8ECD8365D6E;5-]DA4FE8ECD8365D6E912E63F55388D9886151101CDA4FE8ECD8365D6E;4-]FD289E27C690EE4E8049F106768CD5D6374645F3B0C0ABEFB3892D73;3-]9079AA6B02A61A8B8049F106768CD5D6374645F3B0C0ABEFB3892D73;2-]66CC46F715A66DBC8049F106768CD5D6374645F3B0C0ABEFB3892D73;1-]B0C0ABEFB3892D738049F106768CD5D6374645F3B0C0ABEFB3892D73;0-]780F3DCE8BA84184FBD19974E304C95589F976BD71059020F2CC257C;/-]848F73B0CCB468D9FBD19974E304C95589F976BD71059020F2CC257C;.-]94E766EE6359246DFBD19974E304C95589F976BD71059020F2CC257C;--]71059020F2CC257CFBD19974E304C95589F976BD71059020F2CC257C;,-]C0D868F1963180D8C11BCCBB3F843593B8975AFB958E85734EF0ADE0;+-]E572969A4BF0EAA1C11BCCBB3F843593B8975AFB958E85734EF0ADE0;*-]DD26BFF997B128D0C11BCCBB3F843593B8975AFB958E85734EF0ADE0;)-]958E85734EF0ADE0C11BCCBB3F843593B8975AFB958E85734EF0ADE0;(-]34450D8FA47BE5781CF1202EC58B5514EADC477AAA9CAC9C7B935A45;'-]DA2E401BDCC261401CF1202EC58B5514EADC477AAA9CAC9C7B935A45;&-]994931632B55CF761CF1202EC58B5514EADC477AAA9CAC9C7B935A45;%-]AA9CAC9C7B935A451CF1202EC58B5514EADC477AAA9CAC9C7B935A45;$-]6286EF138550E5C09C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1;#-]AC9255C490B722199C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1;"-]DE99B5AACE8010449C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1;!-]DC5BFE32065BD8C19C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1; -]8451B62D9873AE16A20EF4E353FF61FF6B8B401AC48BF850A5E9C611;-]51074EA7D24AA490A20EF4E353FF61FF6B8B401AC48BF850A5E9C611;-]33DB91F8AAECA2FDA20EF4E353FF61FF6B8B401AC48BF850A5E9C611;-]C48BF850A5E9C611A20EF4E353FF61FF6B8B401AC48BF850A5E9C611;-]3AD07BB3F42BD08E2F8EB6F51987B06706ED6E45064ABE0B9108844E;-]3BDE948165BE18002F8EB6F51987B06706ED6E45064ABE0B9108844E;-]2DD7EC6B89966D7D2F8EB6F51987B06706ED6E45064ABE0B9108844E;-]064ABE0B9108844E2F8EB6F51987B06706ED6E45064ABE0B9108844E;-]DAC435B561D44E7B04880CB55875B6548C25C729A00E4CD660454746;-]A7CB62B3422503EE04880CB55875B6548C25C729A00E4CD660454746;-]CADEEF67FAADD95104880CB55875B6548C25C729A00E4CD660454746;-]A00E4CD66045474604880CB55875B6548C25C729A00E4CD660454746;-]185B7A6512A4839143C5C721DCFF1D7D8FD1372F49B0B5F169F745F3;-]9F5599183124F72D43C5C721DCFF1D7D8FD1372F49B0B5F169F745F3;-]28967B47191899F843C5C721DCFF1D7D8FD1372F49B0B5F169F745F3;-]49B0B5F169F745F343C5C721DCFF1D7D8FD1372F49B0B5F169F745F3;-]38E7A3645666C76922A589136F68CC46076FFA6071B2EDA40DC29CC7;-]47D04DC2B74725E122A589136F68CC46076FFA6071B2EDA40DC29CC7;-]ADF2379966C7497222A589136F68CC46076FFA6071B2EDA40DC29CC7; -]71B2EDA40DC29CC722A589136F68CC46076FFA6071B2EDA40DC29CC7; -]DF466C19D1E09129F29B751A3123A1502E5C0665745FB6564FC5F7DC; -]89E01368B3CFEA66F29B751A3123A1502E5C0665745FB6564FC5F7DC; -]19BAAA837AA0EDE3F29B751A3123A1502E5C0665745FB6564FC5F7DC; -]745FB6564FC5F7DCF29B751A3123A1502E5C0665745FB6564FC5F7DC;-]E8F03F5A4682799E4FDC731D26FAB6E8EAE6993C637DE61020C5DD1B;-]650BEEAE8BE0BA784FDC731D26FAB6E8EAE6993C637DE61020C5DD1B;-]1981CA07B08A2AA34FDC731D26FAB6E8EAE6993C637DE61020C5DD1B;-]637DE61020C5DD1B4FDC731D26FAB6E8EAE6993C637DE61020C5DD1B;-]CDD45401DD0E759FB828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79;-]DF5F5C00DA897592B828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79;-]3D4778FC2C38084FB828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79;-]D0D1AB8DA16D8D79B828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79 @I U a $ m 0 y < H T `#l/x;G S_"k.w:;-]B929651D1EDAD644E4C1FB98268170769154C21BBD1637EEB34E933B;-]1D7FA83415FD0C9CE4C1FB98268170769154C21BBD1637EEB34E933B;~-]E7D3FED35EAF68B9E4C1FB98268170769154C21BBD1637EEB34E933B;}-]BD1637EEB34E933BE4C1FB98268170769154C21BBD1637EEB34E933B;|-]4D2476E6EDCA3BE22533E4E13EC84A784DE6F2962C3E64162620C978;{-]7AB77AECC3703ED52533E4E13EC84A784DE6F2962C3E64162620C978;z-]8134877075548DB72533E4E13EC84A784DE6F2962C3E64162620C978;y-]2C3E64162620C9782533E4E13EC84A784DE6F2962C3E64162620C978;x-]42916177BEAA88747A757276DFF48471EA032D2B98611755153523E6;w-]1C23477C835E5CEB7A757276DFF48471EA032D2B98611755153523E6;v-]EEE9E2E4D0403BA27A757276DFF48471EA032D2B98611755153523E6;u-]98611755153523E67A757276DFF48471EA032D2B98611755153523E6;t-]1949FBA7A090B71D4925E278E468D55BA68B3AC49E1F03BA787E31EA;s-]C96ADEF6A2CCFA984925E278E468D55BA68B3AC49E1F03BA787E31EA;r-]38CBCE44873FE1A74925E278E468D55BA68B3AC49E1F03BA787E31EA;q-]9E1F03BA787E31EA4925E278E468D55BA68B3AC49E1F03BA787E31EA;p-]CEFC4DCD7DD09BF9B5AAD4575B2988D99E3FB3EE973EE71028459E9A;o-]0D6DF17AFEF1FFBAB5AAD4575B2988D99E3FB3EE973EE71028459E9A;n-]83C69AF0F851437AB5AAD4575B2988D99E3FB3EE973EE71028459E9A;m-]973EE71028459E9AB5AAD4575B2988D99E3FB3EE973EE71028459E9A;l-]A685E0D6F5530952D27B4F82C717A04DFF3A986489929D075908994E;k-]7903174192105E48D27B4F82C717A04DFF3A986489929D075908994E;j-]5DFAD7D10C678DBED27B4F82C717A04DFF3A986489929D075908994E;i-]89929D075908994ED27B4F82C717A04DFF3A986489929D075908994E;h-]2C45488C98085D61CDAE31CA330249BC5284C1F96033761A09D517DF;g-]EA301A7C9A893815CDAE31CA330249BC5284C1F96033761A09D517DF;f-]054118196B963B7DCDAE31CA330249BC5284C1F96033761A09D517DF;e-]6033761A09D517DFCDAE31CA330249BC5284C1F96033761A09D517DF;d-]94D1A2CC4A9787279BC60CF498E584E5620014340C099ECDA62431FE;c-]1743133E219C1EB19BC60CF498E584E5620014340C099ECDA62431FE;b-]884DFEDE8DF8A5B19BC60CF498E584E5620014340C099ECDA62431FE;a-]0C099ECDA62431FE9BC60CF498E584E5620014340C099ECDA62431FE;`-]64BF7EACFA7E07D3CB4287D637F9BC0CB38F978AEB64F54F552F0AB7;_-]54E2200EE3627373CB4287D637F9BC0CB38F978AEB64F54F552F0AB7;^-]C25C74A408C599D8CB4287D637F9BC0CB38F978AEB64F54F552F0AB7;]-]EB64F54F552F0AB7CB4287D637F9BC0CB38F978AEB64F54F552F0AB7;\-]5609A4F9A4344957A2439F4C712EA9FA6B65BC17DECD473509E96847;[-]B974664B34752AFCA2439F4C712EA9FA6B65BC17DECD473509E96847;Z-]AA761009D6D131A0A2439F4C712EA9FA6B65BC17DECD473509E96847;Y-]DECD473509E96847A2439F4C712EA9FA6B65BC17DECD473509E96847;X-]BDE25B8E80237EE2E1C12525BFAF4F092D109F2152B7186506DCC483;W-]DB24D05A4CFF4204E1C12525BFAF4F092D109F2152B7186506DCC483;V-]2B45B386DCB3A0CFE1C12525BFAF4F092D109F2152B7186506DCC483;U-]52B7186506DCC483E1C12525BFAF4F092D109F2152B7186506DCC483;T-]CDFBE06BBC5F6D292349DF0D7DBD60C6C20453350553D1E9E9AE5C54;S-]2F8A666BE91A400F2349DF0D7DBD60C6C20453350553D1E9E9AE5C54;R-]1908736247B42C7C2349DF0D7DBD60C6C20453350553D1E9E9AE5C54;Q-]0553D1E9E9AE5C542349DF0D7DBD60C6C20453350553D1E9E9AE5C54;P-]132E928709CB19568A0293871D97E954B8397DFD072889CB0E82D77B;O-]6C9FF639C2D20ED28A0293871D97E954B8397DFD072889CB0E82D77B;N-]D49FD2CB8EEF91038A0293871D97E954B8397DFD072889CB0E82D77B;M-]072889CB0E82D77B8A0293871D97E954B8397DFD072889CB0E82D77B;L-]9C4396FA8EE7BB36F749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3;K-]C1B27AC998ABABB8F749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3;J-]0DCD1F051766107FF749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3;I-]4C34BA7BC16F86B3F749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3;H-]2C7C1FA3369E4A4D7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76;G-]4B8313F3BF069E6A7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76;F-]7F6B5258C086DC6B7D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76;E-]4A4DF6C29EDD1F767D8A6D4E9F5804C3F161D2FC4A4DF6C29EDD1F76;D-]F300560992B840A8EB4750A0B0A0F558ED5F768F8B893A26133B3F66;C-]8F07A6D986845B5AEB4750A0B0A0F558ED5F768F8B893A26133B3F66;B-]14180E23218FFEDAEB4750A0B0A0F558ED5F768F8B893A26133B3F66;A-]8B893A26133B3F66EB4750A0B0A0F558ED5F768F8B893A26133B3F66 f 1fH]B8A0293871D97E954B8397DFD072889CB0E82D77B3d- +G@dQi\nj @ dnAwU*  }d-  ({G salt@notations.sequoia-pgp.org&79k_o)&,-) !T9}({؃OO `oȯʉ5DD- 0'2<To1<٢dkү:C%vg).&randy@fake.pep.foundation d-  ({G salt@notations.sequoia-pgp.orgDCk2Y?? xh{%f !T9}({$^g,Sě1'/"aQٺҢEWcL0l7"Q0Ozso 3d- +G@ɻ.gU>8.T`̴!D8Bo 1d- ({G salt@notations.sequoia-pgp.org% t1!߾p%͛n 6 od- ԟˎG salt@notations.sequoia-pgp.orgO;`{ @srC5sK|!aO-}#sԟˎ#Z4(*H jCm#[ɿ;CrlH _ف^ٖ `o tA{`LOoP !T9}({NJT:GphY4ļo[VTO^V/#1_+ho 3d- +G@Lxf4+>,:Y (EmJ rd- ({G salt@notations.sequoia-pgp.orgP7d `GcJKIperKzt !T9}({ibEIXWJ= QZ+]@MN7tgJչfHag)8d- +U@4"ԍʤXl%#IXt rd- ({G salt@notations.sequoia-pgp.orgCM#ނ]TpnC\4EFcAayv !T9}({'>D.{5'qnX|`̀b2GG]id*3\2uJn8Q L]JF749E746EAAFEB7A634BCE8A4C34BA7BC16F86B33d- +G@Jΰ%'(^oW5_<6]  }d-  L4{oG salt@notations.sequoia-pgp.orgLQ"H}0 e") !IFzcKΊL4{oL(t@NJI|'#\B?iX!E`Ҿ2 zɘ-q_i^, ªu[hp9Q\l8x4|4"@,@[6M !IFzcKΊL4{ojL} F*NPq8 Q#PʒZ'$W[;R2y ؋"f8d- +U@]吂edjlB[kl(pM rd- L4{oG salt@notations.sequoia-pgp.org g7bPYdx^3&yn!wG !IFzcKΊL4{om*. ; ۏՂ5%l_\IyJ(Gw<3f |R g 3gI]DE1C12525BFAF4F092D109F2152B7186506DCC4833d- +G@bÐk&0N)~ZKpv2  }d-  ReăG salt@notations.sequoia-pgp.org -mQ6FcنF$< !%%O -!Reă[6Χ<] צm 'DRen9"F yqg 3 tamara@fake.pep.foundation d-  ReăG salt@notations.sequoia-pgp.org#:̟CxxGA c"1u~Pu !%%O -!Reă`͍37/OY8=ygҜ.m]ךлNg&RF3d- +G@Wn#ϗk&gӅ%/ _ϋT rd- ReăG salt@notations.sequoia-pgp.org&_gƌ3nk`^OkOx@t _ !%%O -!Reă%k@?6l̬x"1{Ҙcsޮ].b,*LV94DUS'̄WHmQ !%%O -!ReăѡVOT#jdKԓT,R7}y^۽;A z Cwdk> 8d- +U@J:<,l**I~F+5[nF$p^  rd- ReăG salt@notations.sequoia-pgp.orgPT+hTD7?$o7} !%%O -!Reăn;@AMh/3;b;>1$LߊE4蘈?I4>3\:&|J]F2349DF0D7DBD60C6C20453350553D1E9E9AE5C543d- +G@8(0 w&KZ\ mR  }d-  S\TG salt@notations.sequoia-pgp.orgB"ioI`P%Iw,+$ !#I }`S5S\Tϟ*w6ɣ/)G+.$Be` jE,iW?sabrina@fake.pep.foundation d-  S\TG salt@notations.sequoia-pgp.orgaj7bOm.GbWݾ%;c|9W !#I }`S5S\TSSU B/%e8xU҈ahjj H[O(E3d- +G@>սEC`Hc2 rd- S\TG salt@notations.sequoia-pgp.orgG f]PrqJ+Z^ 9OsV).S !#I }`S5S\TQ?^03@d ʐ:3c\U)a*3S6|WI3d- +G@`,b;<%3J|KP*"eKuN 1d- S\TG salt@notations.sequoia-pgp.orgz/9ȤkT{Z͘wō5:6;%s od- /fk@G salt@notations.sequoia-pgp.org6FXN0o|qhG`.RMR!ྴq@/fk@yCɺfat}[$*{nuOh0@ IjJ>`y !#I }`S5S\T^$#b J)հ՘Y&9 V9~";Y]8d- +U@. ׻S7$amզ*͖g/V>5= rd- S\TG salt@notations.sequoia-pgp.orgF()AjyJUC;8 G3  !#I }`S5S\TPG?5zB12٦ T 3̆r K^c^UU.ΘQ' f 3fJ]FCB4287D637F9BC0CB38F978AEB64F54F552F0AB73d- +G@6 R!tŲub0 x  }d-  dOU/ G salt@notations.sequoia-pgp.orgeb>COyОR"dׂ !B7 dOU/ =4w5ݕ Z|:_f (LSHvԫ?d새q= \G{펡rmul17l !CLq.keG5 hG}DvA()1s =ڞUu,x |mc]<|]p_6QP!4TI>, ulysses@fake.pep.foundation d-  G5 hGG salt@notations.sequoia-pgp.orgÏJ/lcǻZP#-} !CLq.keG5 hG&ˮoΠz%bT;/2=`(#b3PN2X4Gl4>#mycat'TkA3d- +G@du#T`}ZoșI"U_NB rd- G5 hGG salt@notations.sequoia-pgp.org%0R+d/۵wk(uߔߍ !CLq.keG5 hG!ȓod7V:ـA& f E;ӧX{3d- +G@/rP?Yoz=.i$ 1d- `3v G salt@notations.sequoia-pgp.orgcݮϻV& `+K od- Ak;}G salt@notations.sequoia-pgp.orgV ݥOe4#l(^4=|,!K ן7Ak;}JH.$ˆr9՘Պy5_Ō dض: }.!ͮ13IR`3v K8PK %%Rr{4m!_M2ͤq/šrus$E3d- +G@ ~jxIH K3 !ͮ13IR`3v {d*I3}2}(o@7]>16+ šrA`y||a9 8d- +U@#BCf^[uYѰ[`|3gxF- rd- `3v G salt@notations.sequoia-pgp.org[4YQ/#wCE70`^< !ͮ13IR`3v -y6'Wj$KwZ\dzg1G+kPJDzn/t)I]D9BC60CF498E584E5620014340C099ECDA62431FE3d- +G@;'!PRH) \S13  }d-   ͦ$1G salt@notations.sequoia-pgp.org0@iDž"xG3d- +G@3] 2bQt&$iicP 1d-  ͦ$1G salt@notations.sequoia-pgp.orgyl3k̽VY k J^E| od- C>!G salt@notations.sequoia-pgp.orgm*9CL(]G4N͓؄Yz3!fP4e3C>!5 ~ X@QD]h Ƞ?Ǯ}w3;\FEE!_e\EXo ! b4 ͦ$1cgnA}&J0O'9coG6o*aDB4cYT]8d- +U@4E :q j9l[iTD޿ZG rd-  ͦ$1G salt@notations.sequoia-pgp.org$ 쪟2a'O&_4a< ! b4 ͦ$1W)IYaI{ sIX{ 3AC r v OhXN%ob? i 4iH]BB5AAD4575B2988D99E3FB3EE973EE71028459E9A3d- +G@˜lng>hjN]ٿ;g}mTwo:^  }d-  >(EG salt@notations.sequoia-pgp.org+6 ##h1uUOz !W[)ٞ?>(EW+.owL 'u/B"5 Hc73OR0,(Kjxenia@fake.pep.foundation d-  >(EG salt@notations.sequoia-pgp.orgu,|Eɢ0CO:~*2w!Z !W[)ٞ?>(EX\̝] u Mpf&X6ưNi5% I-넦 3d- +G@ef|m2[Ӌx\2/žQ rd- >(EG salt@notations.sequoia-pgp.orgb@|q>dj,8_buW_T !W[)ٞ?>(E._ VѩOLZsmB%+|F{1-Ԗ\,~'|:3-]մ 3d- +G@]lcbJ2cHK!3+vRͶ 1d- >(EG salt@notations.sequoia-pgp.org;f]4Z"AvI od-  mzG salt@notations.sequoia-pgp.orgzcZ b*:"P'Cp`J{ [!n | mz`k0gਬf  n=,mH(x_A2! 0T> !W[)ٞ?>(Eoum"gvM4{D"stUP2(~ñ6]!=PP;{rX v8d- +U@;KPZ|g1Un<WDON rd- >(EG salt@notations.sequoia-pgp.org:Ĩ7 h;3-مap !W[)ٞ?>(Ep\Z`αO1/`1@b .VRfALը$e}J7K1CDI]DD27B4F82C717A04DFF3A986489929D075908994E3d- +G@{R8&`cʼnny>oU? !{OM:dYN o\1vң32` uwa]́sfp:BV/gZ dp E.2+鑣pxavier@fake.pep.foundation d-  YNG salt@notations.sequoia-pgp.org֠(nYiR~Y-6^Z*ECEm !{OM:dYNKuZLyOXF%;lY=x\2ܫ ear p 3d- +G@S%gK:a#ޖCL*1# rd- YNG salt@notations.sequoia-pgp.orgIoF~",'ՐD !{OM:dYNehh@Ny|ʋ|¦f*ma st0^D(0$Zy0 3d- +G@܈N?]lyAp5ܓNB$[Or^ 1d- YNG salt@notations.sequoia-pgp.org<ġXyS}7  od- yA^HG salt@notations.sequoia-pgp.org=|3 ..` izZ"@~ !%YqyA^H/!#tE?d1ƨ=yȷ@&zZ'-يZ_]m/⺉z!{OM:dYNA^Z#Z~bS#><Y4㘏CH!nHHھ7߅8d- +U@-]q8ncqYnZ']]7 rd- YNG salt@notations.sequoia-pgp.orgVȓm2~)DY6tQ}*00 !{OM:dYNDWKǑo7\`"UMTjޡZ;&Mլ@E$3d- +G@Û;,IΚsfވ}oUӓO 1d- aU5#G salt@notations.sequoia-pgp.orgI,q$uWIL|{#qU] od- #G|^\G salt@notations.sequoia-pgp.org7X]c*9w(ڌqBDO+XU !6FFv㲇D#G|^\QQ=T?iVokUI B̭e^9]1chCK v !zurvq-+aU5#qfWS2|KO}X!"b%>I*bWmdw  Bnch*2!eS 8d- +U@j1t2ɾvY s5 AL߄# rd- aU5#G salt@notations.sequoia-pgp.orgb8xDYYX{g"nxM0X t}&WO !zurvq-+aU5#LJ5T{` ;yDT=)# "|jUMK"$T@'4 G]@4925E278E468D55BA68B3AC49E1F03BA787E31EA3d- +G@\r@OT[3]^PQ}  }d-  x~1G salt@notations.sequoia-pgp.orgAƔRs u 4"o-kߩ~8 !I%xh[:Ğx~1`,5:巚ЎqLo<>;IU)P jݷwn LN0Pyoko@fake.pep.foundation d-  x~1G salt@notations.sequoia-pgp.org+nq}h[1M'_H}Bwz0J2 !I%xh[:Ğx~15ڊiУp4w!ǩg6sف ('iF+WlvE34V: Fab.3d- +G@*eK&YZ){SDWi -Cm rd- x~1G salt@notations.sequoia-pgp.orgf`\=# ʘ>ai4}dUA !I%xh[:Ğx~1 z7MaRŢUpPuzȲz n@Pt}A>lց~zR 3d- +G@7u[eτdyA\Ɛ  1d- x~1G salt@notations.sequoia-pgp.orgP#pfPϴw{鉿+* g od- jG salt@notations.sequoia-pgp.org͜GBX)VO`ym!P ~M bjTI!FD঱ψw5t̍?~; v b]J .5 N߳!I%xh[:Ğx~1x?7Śޙ.--:(wbP,k&Ģ [TR)=' mʝC8d- +U@:֢]G{ΘTw"y˾T*O rd- x~1G salt@notations.sequoia-pgp.orgRaSK{QV$M3Qz|  !I%xh[:Ğx~1.a<ɔ7;P.1rvW/;J_hsaD+i}H-1ĉIڕ( f 3fJ ]FE4C1FB98268170769154C21BBD1637EEB34E933B3d- +G@ +1iM*B.NbtCKya  }d-  7N;G salt@notations.sequoia-pgp.org[&ȬIJƷ9dgl !&pvT7N;" *1ùiv,a^ j[L1ߟ}e" _~dF L~y9+zenobia@fake.pep.foundation d-  7N;G salt@notations.sequoia-pgp.org$wia!/Qcݐ?n搇(QwR !&pvT7N;Qe+ O2+NNᗯ! 9/u{c¾ ԸCSHS喵Y3d- +G@*[:2b\-@>"r0  1d- 7N;G salt@notations.sequoia-pgp.orgvG6 =feAO/o"M?䧳Ʊm od- ^hG salt@notations.sequoia-pgp.orgB%&`F~]~r͞r/*!`i7 E,Y^hn=Fo~oqV7;PY n7(3\?ϯ`S ZOfi!&pvT7N;6@39o My]Bl#k`%'xok5a~;jW0V@I_dIb3d- +G@aX_`h3$m J rd- 7N;G salt@notations.sequoia-pgp.orgԢ Z.DM^MM#o Eۏ⿊ !&pvT7N;Eb \Mߏ( KZ sKY[2Zߵ"$5  JI@8d- +U@u6e܀ Nwu rd- 7N;G salt@notations.sequoia-pgp.orgNl'Wd>޽FxW;^&ܠP]O !&pvT7N;{rۜ}]#AСr^ K $2ΓW,bKinth="J]F2533E4E13EC84A784DE6F2962C3E64162620C9783d- +G@M+u VK%:Đ `:=Gqԓ  }d-  ,>d& xG salt@notations.sequoia-pgp.org?qCDH w@O;Do !%3>JxM,>d& xhv2+L~sh>7 R`1Wb(^ksjh~gHzachary@fake.pep.foundation d-  ,>d& xG salt@notations.sequoia-pgp.org|857i;B3B} LF !%3>JxM,>d& xV1_SůE]>;{¤Ɏ2'6H,Tc1,F 3d- +G@ϛ={ID K'uc rd- ,>d& xG salt@notations.sequoia-pgp.orgo=JozƈhtM#.F4+$p.{ !%3>JxM,>d& x|6r\&ҍn.)XtT#Ea9n{SGuko@jؠ9\3d- +G@P;=PƏ;  1d- ,>d& xG salt@notations.sequoia-pgp.orgwm~]> UR{B 印V od- zzp>G salt@notations.sequoia-pgp.orgǝRj]6[գ֧0?!>%!&={kvFzzp>&J0eudz*yW urO7!90LAԮM`x.c_9Y !%3>JxM,>d& x\Ҿ^g4tz7 `@ [H'îb/WlGyO.7,8d- +U@]t<&L'e  }n rd- ,>d& xG salt@notations.sequoia-pgp.org҆|glIٖ.l !%3>JxM,>d& xt%hGo19V汖0!__)U E\WC\bd^F,Y5˞j| #FN W  @ _ ! g ) o 1 w 9>-]EDF969810BA5194CAAB978A882B9A6E793960B071ADFC82AC3586C14>-]E6F0D74501D65927507212E4796EFBF4FF8E4B1BF411A72A5C89092C>-]F411A72A5C89092C507212E4796EFBF4FF8E4B1BF411A72A5C89092C>-]D9D4C006F09088D108C6A9408241E6ED99A0A2767A6B35253722954D=-]FD289E27C690EE4E8049F106768CD5D6374645F3B0C0ABEFB3892D734=-]F300560992B840A8EB4750A0B0A0F558ED5F768F8B893A26133B3F66D=-]EEE9E2E4D0403BA27A757276DFF48471EA032D2B98611755153523E6v=-]EB64F54F552F0AB7CB4287D637F9BC0CB38F978AEB64F54F552F0AB7]=-]EA301A7C9A893815CDAE31CA330249BC5284C1F96033761A09D517DFg=-]E8F03F5A4682799E4FDC731D26FAB6E8EAE6993C637DE61020C5DD1B=-]E7D3FED35EAF68B9E4C1FB98268170769154C21BBD1637EEB34E933B~=-]E572969A4BF0EAA1C11BCCBB3F843593B8975AFB958E85734EF0ADE0+=-]DF5F5C00DA897592B828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79=-]DF466C19D1E09129F29B751A3123A1502E5C0665745FB6564FC5F7DC =-]DECD473509E96847A2439F4C712EA9FA6B65BC17DECD473509E96847Y=-]DE99B5AACE8010449C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1"=-]DD26BFF997B128D0C11BCCBB3F843593B8975AFB958E85734EF0ADE0*=-]DC5BFE32065BD8C19C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1!=-]DB24D05A4CFF4204E1C12525BFAF4F092D109F2152B7186506DCC483W=-]DAC435B561D44E7B04880CB55875B6548C25C729A00E4CD660454746=-]DA4FE8ECD8365D6E912E63F55388D9886151101CDA4FE8ECD8365D6E5=-]DA2E401BDCC261401CF1202EC58B5514EADC477AAA9CAC9C7B935A45'=-]D8D14CE515A328015C55764028BE2DD718F9DAAAD8D14CE515A32801==-]D49FD2CB8EEF91038A0293871D97E954B8397DFD072889CB0E82D77BN<-] D0D1AB8DA16D8D79B828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79=-]CEFC4DCD7DD09BF9B5AAD4575B2988D99E3FB3EE973EE71028459E9Ap=-]CDFBE06BBC5F6D292349DF0D7DBD60C6C20453350553D1E9E9AE5C54T=-]CDD45401DD0E759FB828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79=-]CADEEF67FAADD95104880CB55875B6548C25C729A00E4CD660454746=-]C96ADEF6A2CCFA984925E278E468D55BA68B3AC49E1F03BA787E31EAs=-]C48BF850A5E9C611A20EF4E353FF61FF6B8B401AC48BF850A5E9C611=-]C25C74A408C599D8CB4287D637F9BC0CB38F978AEB64F54F552F0AB7^=-]C1B27AC998ABABB8F749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3K=-]C0D868F1963180D8C11BCCBB3F843593B8975AFB958E85734EF0ADE0,=-]BDE25B8E80237EE2E1C12525BFAF4F092D109F2152B7186506DCC483X #FN W  @ _ ! g ) o 1 w 9>-]EDF969810BA5194CAAB978A882B9A6E793960B071ADFC82AC3586C14>-]E6F0D74501D65927507212E4796EFBF4FF8E4B1BF411A72A5C89092C>-]F411A72A5C89092C507212E4796EFBF4FF8E4B1BF411A72A5C89092C>-]D9D4C006F09088D108C6A9408241E6ED99A0A2767A6B35253722954D=-]FD289E27C690EE4E8049F106768CD5D6374645F3B0C0ABEFB3892D734=-]F300560992B840A8EB4750A0B0A0F558ED5F768F8B893A26133B3F66D=-]EEE9E2E4D0403BA27A757276DFF48471EA032D2B98611755153523E6v=-]EB64F54F552F0AB7CB4287D637F9BC0CB38F978AEB64F54F552F0AB7]=-]EA301A7C9A893815CDAE31CA330249BC5284C1F96033761A09D517DFg=-]E8F03F5A4682799E4FDC731D26FAB6E8EAE6993C637DE61020C5DD1B=-]E7D3FED35EAF68B9E4C1FB98268170769154C21BBD1637EEB34E933B~=-]E572969A4BF0EAA1C11BCCBB3F843593B8975AFB958E85734EF0ADE0+=-]DF5F5C00DA897592B828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79=-]DF466C19D1E09129F29B751A3123A1502E5C0665745FB6564FC5F7DC =-]DECD473509E96847A2439F4C712EA9FA6B65BC17DECD473509E96847Y=-]DE99B5AACE8010449C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1"=-]DD26BFF997B128D0C11BCCBB3F843593B8975AFB958E85734EF0ADE0*=-]DC5BFE32065BD8C19C1CCC07B3BF48BEA0278E93DC5BFE32065BD8C1!=-]DB24D05A4CFF4204E1C12525BFAF4F092D109F2152B7186506DCC483W=-]DAC435B561D44E7B04880CB55875B6548C25C729A00E4CD660454746=-]DA4FE8ECD8365D6E912E63F55388D9886151101CDA4FE8ECD8365D6E5=-]DA2E401BDCC261401CF1202EC58B5514EADC477AAA9CAC9C7B935A45'=-]D8D14CE515A328015C55764028BE2DD718F9DAAAD8D14CE515A32801==-]D49FD2CB8EEF91038A0293871D97E954B8397DFD072889CB0E82D77BN<-] D0D1AB8DA16D8D79B828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79=-]CEFC4DCD7DD09BF9B5AAD4575B2988D99E3FB3EE973EE71028459E9Ap=-]CDFBE06BBC5F6D292349DF0D7DBD60C6C20453350553D1E9E9AE5C54T=-]CDD45401DD0E759FB828EF1F203645DCDE9C37B0D0D1AB8DA16D8D79=-]CADEEF67FAADD95104880CB55875B6548C25C729A00E4CD660454746=-]C96ADEF6A2CCFA984925E278E468D55BA68B3AC49E1F03BA787E31EAs=-]C48BF850A5E9C611A20EF4E353FF61FF6B8B401AC48BF850A5E9C611=-]C25C74A408C599D8CB4287D637F9BC0CB38F978AEB64F54F552F0AB7^=-]C1B27AC998ABABB8F749E746EAAFEB7A634BCE8A4C34BA7BC16F86B3K=-]C0D868F1963180D8C11BCCBB3F843593B8975AFB958E85734EF0ADE0,=-]BDE25B8E80237EE2E1C12525BFAF4F092D109F2152B7186506DCC483X  5S"]V08C6A9408241E6ED99A0A2767A6B35253722954DM]Mɥə*T7CSCo^kX^vBCX *iʿxY@lW͞/ܜ?_ݥ4Au90^&Ieةʚ>@RZo4bf%EwDյ Yta{SQhgVII{^D r74їtxC-z\uHX}}Mk_fq˜n韷tA,'l2Jg>)P-V(J0!_JNKf{H2 *]v dfL~}$-S 0cyX4]h2 i.UdJ`%8G07Щ.u/El'seݩ&i[W#q[Y~R~aq"7AB= 0h,VWkJ2w\ep1Y3W!C2So.@`Ox2uU?Ԙ-},bM{o*LLuca Saiu (free software hacker at pEp foundation)  8!Ʃ@A홠vzk5%7"Mah    zk5%7"MA'>j:.̌VXsaY=p%?(Jب;R*$p^B9Y8[g7#;A;A5pymb ni\oy\~A U`dg '{`Y製Et6[l+9-] e4Ѽ׶8dyo'*-Le}H9CCH[#Sw;뱸5`=m+(gu@lVa/!R=қ<7,w3 *6Y"@̔Wd'SM .28:j=CoAn!-GeujZa,91>ls.6S ]TMZ@zN,N(Oŝ`[5Gг/ƷIPFSJͫ<[Luca Saiu (free software hacker, GNU maintainer, computer scientist)  8!Ʃ@A홠vzk5%7"M]XS%H!]B6C36B5E1ED2B4B1D06CD03FF5D5025F89BBA7A073d- +G@BvL"#Ji.%.7^oO1q0eG<uziggy@fake.pep.foundation d-  ]P%zG salt@notations.sequoia-pgp.orgU( ,mEr Q|kSqܱ !l6+K]P%zXt:Z"\Z>PzNbwegf,W , 1d- ]P%zG salt@notations.sequoia-pgp.org`ۇCé\C2Gu od- #!J?G salt@notations.sequoia-pgp.org4?Nr$ޱ0G?af4_!}x%;#!J?ʭ^jzݽ_K1Q[RDԨ;D@_fOay9Ojxڶ̃q,[!l6+K]P%z.wdxI:j0Z̻lM1$4$Zd-ai! FC?23d- +G@Q}'ͤ/*#l~t rd- ]P%zG salt@notations.sequoia-pgp.orgi ڑzYcJ !l6+K]P%z@l@BҳZh퓱$z5{^OEAA_@bm_JS;5YM܁8d- +U@IqYi}"G rd- ]P%zG salt@notations.sequoia-pgp.orgZ,vہoJV ,/҉{pƦXA)#u{r (&q2MF^~z%vhUoHFA'IT(i:ҖWz|hjz2f5dPۖ8r{lu%0~$y8Mk{^x$soRԖ:q̌?ham2[rom"cg8W"4V.n6`"7oK _hK8kԈ[i,7ITsbjs|$%r`Y*?laMPLA^[7nG/!~Ӓ#J os6]}+Ȑn ?yIjU?Ԍr@uTq6a4p2#넱fלB&"8_V+lYq,MBH06\rx3uģi& Gϸ >) }1 fSkFYxsXm#D)uq&duȹJCmɆ떻݋)#*Te#.%xЗnig6ٴRs!W\,82>#] >#O -~' p ٜxlf`8R#U75c}>]9Q' ֏Ln$/B}=BEp XD]ZnvWHF|S% R^D:;{z1~]_ 7lt=m]~01&TM(ցsTQa.,xASpxהW/qWI, qj$T8? 8!Ʃ@A홠vzk5%7"M]U    zk5%7"M%HJ @n44wdw-V-=ITTH&.H43[Go-W2{@TU 3mjHC}[_eʈOĊ/8]Ծ4,ꢽ ?ڐYt (PteRXlw`tظDbqR-Vy>2CGzb^^9̆ hD7rFA1 +q #irP2)f!z seoJ`c樫60yHM:KD 5-cДjަɟ7ceFF,S o Sf[ٿ#oӄ =[B4OnGhk\NkX\ЄgO= < 'M'+-߂79 l>֚6m(fI. q%F' Z1 -PXԉ2H_/Пb!s !xE +*C5] *C5O몓oZ0BE5Vr"?y8K^2"pľCi@ &֡-QjI s 5iP$qVyh/a {B/G~7#j"5H G}!JmN1vx5Ay>r-e26:=qhүh /IA. } y{=x`8*KKvj'xG۷tqu}4en l;l׋^ a$ yJBL2c.lW246?AY7e(;!½ՌuRp-w'|"SQ۹Ѹ )SSs ~Egh xa!"֊$qSMUG=VzJVN0hp[`^.ЉLx aWbݔe'*bg+nFkv[JzP7Y:s!W\,82>#] >#Y7R[fH# .̲HAK+s'I>.c!]̠<)Cf4a;q_[/S5怱acYяK1gz/M l Ee`.7=xq W<{Zr5\eNv}(-<%3D6YngGPJF&˽ěXg v^J׻'P,/d+ ugMп-&p/rk-|ml S,!gߢ㏵TX>X85D7ݡm:<@;aǮy9ZQcӑڕ"s rUiiPɂ׭1Y2!v1tqqwqvf:=A5Ẅp!. 6RasDKupbNPFyjy(nˑ|Y?ezR6?svD)Wkj]!r.<3&҆]X 3&҆DGr&lj]/y5/!!eUFZ!"x༏_n\Luca Saiu (free software hacker, GNU maintainer, computer scientist)  8   !Ʃ@A홠vzk5%7"M] zk5%7"M)U H*ҽ#I1LM)|oMG֊%3X?e䜫(G7һaR`mXު)iF@v0cXU G _!GɈZK!տx$)R,еkjkK\TpPxg)sxkW9^qFԬz L/U6YFԴ-HFQ?'ILQPmtOSAOUT*`*ת <> 6z2#jHai!>sBGr,GK"P,XJp;$FƋlYПk*wEZ(糎{֝@JS TKۜ'y/DW;L{^=BJ)&kyf6O R +miJN*F1;RD̫>:q0?R,I8fdt.[P2~/GQ k5(o3OM"*B[W~ 8!Ʃ@A홠vzk5%7"M]M    zk5%7"MWڌmC^q9Gz#tIȴQ$M±\ J-(mPHXӈZXcY:F}lbeŝp8;sTP[юC-pdrւ/< U7ۣ1?3gG5K ," TX'0Rz%yNh>r }8{Pr$u#6d5c)3-wQ{j]W 'n*Yŀ?FPEEx͝}" \,qTI'$w;@6hxr)%(@忙$ioֺ?R>& Z4F%zD95Ր}cBgBg)=CM8@9wV6qoߠ>jϙNnkW |(.]ŀ/m?<_>ϻybI 7½ ^a6Ռ[s !xE +*C5] *C5l fn֠3qz}u`E ]:f~`*vtc)nw =㝅Q=IY0E܉Z7yDo[  ݾױZz4t|M pw es T9`zD:Sǀ-uV/W$p"  6ak+4qTMo[V ,sfM{܈ݽ˅ -xCgo76q&f9y(#_?q_+3 =oʲ@2ʺtqU7nUEi׈"Z4*=BmScO/K:/Ax>KHg2k ({=Ymހn ==$0M}Nd+z5-=蕾#ᒡV_=ף+{RèOt"Km]d2I|p%ٮ!]M#] >#0DJ]vҎ8s)ik#ޕW`~77 ;WF/(-DUJl_<5u/2wg^U@ &W2`]1D,3>Ḧ́@-SLa2!4"5CwBQO l,ڹ/շ,mYXӗŴ6ya;bRm X_ qjW\\}wW 2|0e(5Vc^M(]1X8ß:Vd?Y`w<׼=Zt}*\{'U^Kn)f85\FIN4 !R~l7F/h A>Biu$zgWFۓ7Tzx ϼ)Q9,n,[ȲyUZEBhÐm:LvszBkufmE*ܥ_W3pMp '=)Z˪ *DUJ {O s WLuca Saiu (free software hacker, GNU maintainer, computer scientist)  ;   !Ʃ@A홠vzk5%7"M] zk5%7"MĀ'DLt]ބ4,j mbn?kZcIPc>YV >/@J!cQ =?cRI PKG=["zFvd J`-+%Yi٘Rx^ߟ~&6LK臥QzF<߹=H%Cwg'1>e<%L(oЁxb>$ٵO6S|P'.3EF!&gW\1픽6^P)q2 Z;@1㨷p9H!MS#Ͽ }76#qHrP=]E74=} N\62DF/ySe9Rbqt -9C<*oicIx`nHp7,? 4 < }>~nIhrL;L!F|/۪]}QCxān=Kϵl1w4\kªۥv\6|CC[ ;   !Ʃ@A홠vzk5%7"M] zk5%7"MoPk&P[ w:׈^OPDə@N"NCI: $3 jpm!Ym C]!(]hҠ%j: Bzᙈ컑*-uA>3 ͩB!H`DQ :D!eYjqZt;^8S#RbTE&<,8":.=\yY>ˡN^8Py maC|iGɁ߀U@%=M[UܛfXq;eM)(Rn'7{?+8fj~+"&PUֈ4 RN-<˝˂c*q[nڊ?acÍb}](_E̯ w* YJ˙ZrfTij r'{at;lqb8I;i{[\ֱ̲CRzwC'=%UE lWξFL%|' Kl iVssƺowuJx$!CQ4ss !xE +*C5] *C5Khɗ{L[*8A$=O'.ar;R ծӎH̕=z_Ƞ(^p@흞e'[V"A*Fyv5|߭]vA/%}7wsO&; 0- Ggs!W\,82>#] >#u$ !傯-~%;&}PW0QXL*P uhT͒l$8scןbyt &M !Y$ٓi$xK#O!0ë(();gucFs@V9<%zy6~hʾA.;5Q3 8!Ʃ@A홠vzk5%7"M]U    zk5%7"M@sv&(EI?h`C֜ZL/.m4:Su-wT$4* ?LT(.1i #DeaHm 7Su^Q8c]J~չi#Qug-n"(p*}}(opGAve*MNݸz%y{`9|-/^¢<,%!ۘ)F:j yd\J J|*9Ieu\1ZqL"i(r:)MPEfܶ>H”%+5$9҃ӜcjLVȜWyaQgכ+e2^/1DF+s !xE +*C5] *C5F\1$lltH5 NAǗ=hΎLAR ~ut2H%=%|944]a@l",kH3 P)7 7\hb9 ?f4je{!wklzqWlR\t7=O tlK/Rhg%.J;/1lXW[$QlM]agX"y0ǫzdJ-\ m<BiϷ*炏@۠6?v0K kbmF`XO(JӉ|憌[U:0ըUnh65(]Z4([o->vDtB?r;-IsӒ Hx觍+`!+~_IơǗ ,9 f,/C}鼟(ww,mgw>V{P6Ғ䚘Eo7t2)MsI" s!W\,82>#] >#T2U)lǗT !m\]ڶg iE!heoptj׻itt @#b)YMv Kiv.MX1N{髙L8kSWgO a-L`9{]$yuTUQ!%4֟E3 )o VY ק?іZ!X\FS^7z$)Ԟ lLhVM<ɺ|s{u벓IIcz:f&ay [LU #5ͰE +l_ ^]LRɚѰߏ噲t q!֊4E|@h1AΞ _[FLxy@ JMÐsflTQ s66&ؘ|`UekOͺ ~֊g)*(\*H _,HSܣA+l>>:.\]!r.<3&҆]X 3&҆-xC.ߎs΋ARi0Bs\P^eIе Luca Saiu  8!Ʃ@A홠vzk5%7"Mb_    zk5%7"M5#am\ZLҶ|TZc[x'n@o6? oue:w6Sm ~׶`Qӌ[i[ZDP?A{nt4h3Q83In[ed.H C pwF0y8EWņO!T֣cտ[ r$80I.&bo zyM;FK'RY$ALK/瓛tC^G ٔ8({/47 IȉHS)@:og]!K)Ep hN W C2{&ͯшV Z}gy7ãț'7$cJ=^Z w~Nk6wr_\T Fvh;ilG;+d6_Փߟ9d׎[_+PughKq^~έ7rI!ؿ0挒 Ubw5O,PLuca Saiu on mobile (do not use: only for myself)  8!Ʃ@A홠vzk5%7"Mb    zk5%7"MAؒK,?l#%fߑ+,V{zzQ۝OZL 6p$Yۏ'ra􍖇qfYky;pVw0k(pm90ERhxEqGIZz2O ]r|ZV<\n^BV'F M{d>Q}sKb* lU5 繆&"V m_WB@lIK9mZl1I򥞙 8xwc9tWBȊ 9M]VkP4c^趎e2({=+Ѷwwqz.p50o⊘E# M)\oa > Ikj-&DS,ͤѹ̢7F'a̫Fذ@>AuC1VT#h3:i3Ց;g53^7AE*h)>`C:})'3WBG!) [PEc',VfC`v\ MH»9yZa=HRZӟ^H`yHNL^P|R޽q(.^\{ǍiL XE҉;}؆)S,QcÀʆn81PV~|P :%+499n7Ry譳ie_oQÎ}~7Y]U ]I|ވpn3YU)v4K}\m`g KxM73MxH=v !Ʃ@A홠vzk5%7"M]V zk5%7"M=(B|הt2mdD3scؕh#GU-4w_ÃU&Z%CCBz58"B1&x#~YXړF/'Į澄cuvS|).SvOgTU N}̗An="O;YМ[-H#"HNOe22ykSս04hۆ8Cq؜JҘNZ ڃboXn~m{ʉ?"X?/ O.J_ O+9I,<:z/?"13NBx!jkU(:: 6MRi$Q᪆|̪Bj}F` LQ1o^Ep&/߾ۑ!SI>vmt@[Z#OYU;Xh/bo &LY%'H:p){l5D&+-S*\n}6y^6r+5+ J`WAG;EPM]VǔكnK7І%(z\bvBmTy1 N@+}IJK:P}T,q^-5ӡm O,Tşʑ&x6#G d(hŽ@#,ql8;0x [$}RN9$AŊqr À͉q0C0q#o>X5Sì !Ʃ@A홠vzk5%7"M]V@ zk5%7"Mt  !ۀh9a ]V /SF(facH;Fb*![wfzM/ya(WspxX}MЬ=9l<'Q֙?.B xʉ ʓYºfǞEѨ<&'hIG"p x P5&7H4Ԍ; Bj Ӝ.)j"R=>*PS|n}@Q¹"h9 Tr-^MNC wq;Hľ0k O!*s<42:RGeM@ $VlK{ږho{4bPrM,A:}u(fAʤPf$5rKtnoɛE<7XSd_WrG&uO[NJgϯ*d+%+-J~4f4`dƙF(1o3E;h&jup&ۏ@Uf2臖50 miBQb}lq\Őv υLG]>|KU{'Vxtޅ S(4_a*x1Qxol74noC4cftt5,s k)xq}0+~ Epr`l^*q`duLRb" 歠8D]lFDr[)cHșf" c<ozm8ʮЬs٘}[M7~ V)"T* z(!~jXGr7l:Ov(^sX7ýOƸ)5A&N!n+[Y5D0uр t%WZ"q RDg/&IHcDXz|K㑚b(7PLh>_&1 j&B{]=  hfTRʵ'TzZl\{91"_qI*T_+ u6gXlF8M]Me1[A[ɭI\#LҴES\܇n{m]xdV: zw@ +IeQM~ߑZkNa֛$*VR1Oc$ P&Hu r\#ѴעCQ< ` !eJ#y![B&J!/GvXh>ߦ"?\,y_Wڒ5JnUzZ9TEs Wa}J+d9$&~+j&h7[oH$+n1egc>%uAkڋP5߀5v:A4{@9M(R%2n quiݕML=U_~/IP!("-BFa83z?V$h? 3=NIZ_mOyjFU~By3?$Ó/\&ى`4Si%)fiQRUmxv !Ʃ@A홠vzk5%7"M]M zk5%7"MpjUXkb|sWl${"aE^Sw]@ Sz"Z~@*ժiXOlۿ"ߠ1Q-5F(}:]4e΃om46eekp8d,']>7|i4v|,J~ S b^ynBą*>Nb)Mv.\=נ_vT`钂J1X [q@Eq"` zp h+USq{__MjН >"in  -o 4#]507212E4796EFBF4FF8E4B1BF411A72A5C89092CMb59ɀXRJ]j_%t=\3LSgx R\wAy.GXGe--P_벙="`v p<74L82&Y<`K,԰{Nc$Ƌ|FD쏥v$KݮvA3yK,՘BX?FaώO I7.ecm MMkLsI* &Ϻj3EsGf^sΩ| b59  *\ ,G salt@notations.sequoia-pgp.orgEktB'$@Z?oҜΞʃo’WTƸl֘ے)v({)ဏS$WobEVLHW2,Y["b$fJ06nG& ^!@L*un0"pEpUserTwo  b59  *\ ,G salt@notations.sequoia-pgp.orgn8 Qrwį$9K_L=C !PrynK*\ ,6$bȺ,ɼ|xh(]j߿M|L6zhF;蝬Ai=^se$ ;JD#)^#iIfg|-*B4 }bU{:7ܚ(aںLPŽgCɽ> b#K`C€,$u3mO"pArtrgtR !HBgnD= egjp`v FTҽ(Mb59X۩ +Lч @_-x,ŐՇY~%h>[g^pPFrU٨S)@8u|'n /$ɉ'"0Ξ|e}~/[ZdLO~1BbsvIVB[f2~qXi# fXe1b|z*l ̽SxshwX/Bi[!M(Jy;ވXnçȋES~{?t!B b59 *\ ,G salt@notations.sequoia-pgp.org)#b_}u\A@{Q Um膢 ob59 ?@(G salt@notations.sequoia-pgp.org>Pdf;} "=pX؝zE!(.{9`Qy9b?@(h#@ ?NqGALcl-Qt=A `$ 7|7'.̽Cyφ9 %!PrynK*\ ,LC`Gd:UG01*sfJX7T)l:iFEt* l'$Nr.LXX\D",d~jْBD+bIVr3'ͥ_ɮoҤ"D[D=#6:^nyqa}: F{QI7qG31L ޒd)+4o;zg e!s c^$*(bF Mb59&Q;؜H# ,CSYR>X&Հ9:^lEMs ՛f&߀O>>otvCCcG_@xyugEHO4W`փrC3ia4V:|sG U-cOT2拂›0ˋ;£ F嶷tW +ߙSxC/P/3p?PMBlqUoJ(0 &S|!|POm>1Ak xb59 *\ ,G salt@notations.sequoia-pgp.org#c}TڗFj't>pJ !PrynK*\ ,J @x:wWP8m^iy\oӻfG]j8I"۔j)KfZ޷y爀=O)Ԟ/Kv/6^LSU# z"xĦ㰄lx`(l7*+OKF.Fc˷VpH&AhyP#r͙?BU1Owvwn`[#Bd:NM 6$]10E37AA3BBFD3348CF9AE3698EF85F1B1E37396FMb5:E"zKZ M N]֨S8fBPh6$@$Vx+]d5LN1nB-x\N¹G]礛=!$- Z0-65$7^>߇gd~RZbJ( m^F f3|ʔ1ZLMgdaCez|n8,6cwhL`#*=,ׄx`#3Eth b5:  _79oG salt@notations.sequoia-pgp.org>Cg7G zz| oKw !z3HϚi_79o#aHqXޒ26vilVd\W(p1fSHwt3=?u:bgB{xСl(ЈC>G椰 5Kò*FF\ F7/4ǞшZN9* >#ݪGgeca^L$pEpUserThree  b5:  _79oG salt@notations.sequoia-pgp.orgjW 8moD1\a–L pL+n !z3HϚi_79oяQUz;C/c !(yMty<+(+v^Mq,XeH=^n> ۽Pݚs;sɆam F(p{O!9d,U7PNt/5^3NٮlՇ;bzs7ʪ$^Q![KK* r^usޭ;Q 4)Ԟ޵<]Ts.[H @+âr SӸ؊{XD\Sߤ@A'Mb5:b9G7uV!.'T˳1%aQfWw \nqa$32}tj'9$" ˑB%6n - QխQk-֞sNC?9 䄸Z.L4$1ϻm{Wcm9F^a̵Q~z(m2_0} N&7-WQ xb5: _79oG salt@notations.sequoia-pgp.org)P攌@lBv$x; zP, !z3HϚi_79ob3mVaYЋ,xXk&'.C> %;r{t( ŋ )s`CkSR$BEI*s:(ўy7 J3D$9dKOum͛/cNRklWX?,E Hy~#A%o .dV19;|BӪOm=3/a%SI>v+rB#֓0SE+Ub  PMb5:Vv {҇QgY8OBċ;~* g -h"aDB |ɝs ΋]-՝h ,5Gpޖx뺦;둔B mi qz,D߭gr{>yFcJ(MM/ʅY! KU GK_/byjm.N]r.5f p?m*Q:rךv=3z.6>{ wiB.,B b5: _79oG salt@notations.sequoia-pgp.orgE0BtP>Lδ:*!h_ ob5: Zlp&oG salt@notations.sequoia-pgp.orgyf`A[3 P:!?9(Zlp&ozmM\4b&eQFZ[!εW::HL O_}⃋DCB2 %wu<~f\@ڻg>[,tD'efKB$}Uh}J[ ¶IZKQv،Su78\y> l>85aly^`sHb[}a%p*c ^B]W~1RCdAҙZ9rzgt^,;RUJmA4;"OI8t Js("CXK ;:m6m:rNvzx6k)^~Q&dy'jFDk4*Xl<{ LW//?hRQ+6/M\ESvb;/--,NVf]'L 5s ̃ڈF=mLC? M %~؟SBo=J(ֿ!_ZMvJpFN!x瓖 *Xl]D g    *Xl[ڢy)<ӁM 0>   !x瓖 *XlcA( M *Xln'Cäd^J78'Dݧ,JW^H,V/<?9;14F%|ǶB7Tڄ~+{NZ.28 ZqmF S!=Q(rDgךI& {+CkݼG!O'ud nn95rCW,H@oVBY .Xv8nX#qp;|R#ЎZVECv¢Pu#BR%2>!x瓖 *Xl] g    *XlxE!j+JEqeKES̝E2q[/uN>6x *|t ~zȒVl0$Ew_gO_lٻA(pԄZ^'Ǽ&5O.$՝qhuԔNݲ$`qxڬua A2mdIm|-zq.\7 qҬ!P ubicfRP^\u$ >4;"Volker Birk >   !x瓖 *XlcA( M *Xl)N 3l|G>3*% imO#'v%x5G{%߭w.㵘ѨN[Y ~#(X f,{QGGʮ!p\I~)ֶ=%*fF/b3 &+%Ry hsS~V9]^b`o>!E.裑56gf"x`RR?Qsi/g*/uǎGZ~>!x瓖 *Xl]: g    *XlX s;f,K!DtE[z*-"K\NskԢtC*d #ZSPU 26,Ik%}‰¶aGaK؋d2,R=w&AٰpqM] b*Â|8`aPmq%N *nalE gIbI{zy ;5W?9?8D<U&O)cSE,5=̎h*NMx)Volker Birk >   !x瓖 *XlcA( M *XlպYt X!%lhswF#:\=j~oF5(ԃX쪐B#ݥfdKh3>!x瓖 *Xl] g    *Xl(1Sl3 {xc1zIqBŊe[l$?SGX~OBH[Sҩry 4-7HH¦;(^xG$ٌia{_\ wL܅jٔϬr%fg&831]OK}]q^ߌi8MOɴgAx4o\حHb胵/A:X@:=\1%1(Volker Birk >   !x瓖 *XlcA( M *Xlěa}Cy= N U9F#y-f#>AX@w⦡ʙX/$. &cݿݑ7uGqƀptYuŸQ*zAcnL;_8= "Y>5w&`]K) =7B!5r=k?'KJosKYaJd养xMx!L T==a-\f>g^oODj`Q`|[->!x瓖 *Xl] g    *Xlp(xISq-hq&OD5 d\J5 8:Zt62xobO&IV }R2C)}cjtr8l" D]0?$B'Gbǒ=j N!$Wߊ-!pC?IYnKČ~`M]Dfa͹ ;(YiڞAa=x̼Xp޵wQ-XY1cOy̗;qgsMxMw{R-%? +2Q=@cN\yYAk>   !x瓖 *XlcA( M *Xl#tOLa曾t L}^`0jV rpln @"XM޳69K·}$_VV̦]qFj_Aq u "4'o<3"5?+_b9tNe =-(uY F*,ŸgRY:mLv.ewKp.~*\& tunEdhg|ׇ [iIpѽ*Iua1 X3>!x瓖 *Xl] g    *XlU8;&:3ԝM`7k"MrQ=_?gW*bIb4;u $ ?^cƞ#dPaޏ2l7t%i43ϗ)8cbMDVb"ea S%40\hqGecްnn>ײDOB: j='R<,Bιy'JT?覦Lz~ olhO:0 Volker Birk >   !x瓖 *XlcA( M *Xl5٪&?Y|z`RN˂[w%OQs*?;M15i6Rt܀7i1 ´=J~X؜Җa]F (+?81] ]r"`NZ[^oC_{!jJ?"ѕ;w䬓n oߏ TtcL, /v}ƅթ]PS_6D!&Hq=3L uD>!x瓖 *Xl]' g    *XlE0e_)ʑ#ť1p)OYmgVǢpcRzx2nΝs6#搣kdцzJʮT!(XbDQWn <$ jC tOU3&.48=@r̈́&+i-ר(ߕWH]7R #JNYjO VPqmqXN dR%)/mxkuKrA]C1I[0NTŭQs~.ةxVolker Birk >   !x瓖 *XlcA( M *Xll\Z隒_p}䇈.`J#Rԕ$oA􌪶xUi=&<_kk6qƻTph:4LP2IAYd{8O5ynBPP5o oܾ6goz/_2{l fH+3wG!˥!\뼋F1l߀zw\SuI=|_TlPDyv Yp6: I(re5& qs;I>!x瓖 *Xl] g    *Xl#Qf!<񼍏EnLKbk$||́xNr,]3Xw}cCM; ;4Vs8yp*WK[ |/"tz}Kɖt^wd]ZZa1凐7̍~աzvU=ۏ^AWV]mXaCʞQMX[50K?g\6(?ZaA$'y2n&SRf5) wjtg1R+aeȊ Volker Birk >   !x瓖 *XlcA( M +