sequoia-git-0.1.0/.cargo_vcs_info.json0000644000000001360000000000100132610ustar { "git": { "sha1": "a684c131ef2f6ee188a71feace4d3becf890e694" }, "path_in_vcs": "" }sequoia-git-0.1.0/.ci/all_commits.sh000075500000000000000000000022711046102023000153660ustar 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-git-0.1.0/.codespellrc000064400000000000000000000004301046102023000143460ustar 00000000000000[codespell] skip = *.bin,*.gpg,*.pgp,./.git,data,highlight.js,*/target,Makefile,*.html,*/cargo,*.xml,*.xmlv2,./openpgp-policy.toml, ignore-words-list = crate,ede,iff,mut,nd,te,uint,KeyServer,keyserver,Keyserver,keyservers,Keyservers,keypair,keypairs,KeyPair,fpr,dedup,ba,ser,ist, sequoia-git-0.1.0/.gitignore000064400000000000000000000000101046102023000140300ustar 00000000000000/target sequoia-git-0.1.0/.gitlab-ci.yml000064400000000000000000000151311046102023000145060ustar 00000000000000# Only ever create pipelines for tags or branches. # Avoid creation of detached pipelines for merge requests. workflow: rules: - if: $CI_COMMIT_TAG - if: $CI_COMMIT_BRANCH stages: - lint - test - deploy # These stanzas do some common management tasks before and after the # job-specific before_script and after_script stanzas are run. # before_script_start configures any default global state. The # job-specific before_script can override this state, if required. # before_script_end prints out information about the environment to # improve debugging; it does not modify the environment. # after_script_end does some common management tasks after the # job-specific after_script is run. It prints information about the # environment, and does some clean up. # # Add this to your stanza as follows: # # before_script: # - *before_script_start # - *** YOUR CODE HERE *** # - *before_script_end # after_script: # - *** YOUR CODE HERE *** # - *after_script_end .before_script_start: &before_script_start - 'if test "x${RUSTFLAGS+SET}" = xSET; then echo "\$RUSTFLAGS is set ($RUSTFLAGS)"; exit 1; fi' # The test rely on gpg. Make sure it is available. - apt update -y -qq && apt install -y gpg .before_script_end: &before_script_end - 'if test "x${RUSTFLAGS+SET}" = xSET; then echo "WARNING: before_script set \$RUSTFLAGS ($RUSTFLAGS)"; fi' - rustc --version --verbose - cargo --version - clang -v - if [ -d $CARGO_TARGET_DIR ]; then find $CARGO_TARGET_DIR | wc --lines; du -sh $CARGO_TARGET_DIR; fi - if [ -d $CARGO_HOME ]; then find $CARGO_HOME | wc --lines; du -sh $CARGO_HOME; fi .after_script_end: &after_script_end - if [ -d $CARGO_TARGET_DIR ]; then du -sh $CARGO_TARGET_DIR; fi - if [ -d $CARGO_HOME ]; then du -sh $CARGO_HOME; fi before_script: - *before_script_start - *before_script_end after_script: - *after_script_end cache: &general_cache_config # default key is default # default policy is pull-push paths: - $CARGO_TARGET_DIR - $CARGO_HOME .rust-stable: image: 192.168.122.1:5000/sequoia-pgp/build-docker-image/rust-stable:latest before_script: - *before_script_start - *before_script_end after_script: - *after_script_end cache: # inherit all general cache settings <<: *general_cache_config # override the key key: "rust-stable" .bookworm: image: 192.168.122.1:5000/sequoia-pgp/build-docker-image/bookworm:latest before_script: - *before_script_start - *before_script_end after_script: - *after_script_end cache: # inherit all general cache settings <<: *general_cache_config # override the key key: "bookworm" codespell: stage: lint interruptible: true extends: .bookworm before_script: - *before_script_start - codespell --version - *before_script_end script: - codespell --config .codespellrc --summary after_script: [] authenticate-commits: stage: test needs: [] interruptible: true image: 192.168.122.1:5000/sequoia-pgp/build-docker-image/sq-git:a60e5c0e12de56640f6d254d4c9d0bf6d805349c script: - sq-git policy describe - ./scripts/gitlab.sh rules: # TODO: We currently only authenticate the changes on non-merged # branches where we use the default branch as the trust root. For # the default branch, the project needs to set an explicit trust # root. - if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH' test-rust-stable: stage: test interruptible: true extends: .rust-stable script: - cargo test --all deny: stage: lint interruptible: true extends: .rust-stable script: - cargo deny check rules: - if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH' allow_failure: true - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' allow_failure: false cache: [] coverage: stage: test # Disabled by default since it is slow and the output is not # normally inspected. when: manual interruptible: true extends: .rust-stable script: - cargo tarpaulin --version - cargo tarpaulin --all --follow-exec --out Xml coverage: '/^\d+.\d+% coverage/' rules: - allow_failure: true cache: # inherit all general cache settings <<: *general_cache_config # override the key key: "tarpaulin" artifacts: reports: coverage_report: coverage_format: cobertura path: cobertura.xml all_commits: # Test each commit up to main, to facilitate bisecting. stage: test interruptible: true extends: .rust-stable script: - .ci/all_commits.sh variables: GIT_STRATEGY: clone sq-git-image: stage: deploy tags: - docker - self-hosted services: - name: docker:dind command: ["--insecure-registry=192.168.122.1:5000"] image: docker:stable before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - | if [ "$CI_COMMIT_BRANCH" != "$CI_DEFAULT_BRANCH" -o "$CI_PIPELINE_SOURCE" == schedule ] then docker build --no-cache --file=Containerfile -t $CI_REGISTRY/sequoia-pgp/sequoia-git:$CI_COMMIT_SHA . docker push $CI_REGISTRY/sequoia-pgp/sequoia-git:$CI_COMMIT_SHA fi - | if [ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ] then docker pull $CI_REGISTRY/sequoia-pgp/sequoia-git:$CI_COMMIT_SHA docker tag $CI_REGISTRY/sequoia-pgp/sequoia-git:$CI_COMMIT_SHA $CI_REGISTRY/sequoia-pgp/sequoia-git:latest docker push $CI_REGISTRY/sequoia-pgp/sequoia-git:latest fi only: refs: - /force-rebuild/ - branches - schedules pages: stage: deploy image: 192.168.122.1:5000/sequoia-pgp/build-docker-image/rust-stable:latest before_script: - sed -i '/Components:/ s/$/ non-free/' /etc/apt/sources.list.d/debian.sources # xml2rfc is non-free - apt update -y -qq && apt install -y -qq --no-install-recommends weasyprint xml2rfc ruby-kramdown-rfc2629 - if [ -d target ]; then find target | wc --lines; du -sh target; fi - if [ -d cargo ]; then find cargo | wc --lines; du -sh cargo; fi - rustc --version - cargo --version script: - cargo doc --no-deps - mv target/doc public - make -Cspec draft-sequoia-git.html - cp spec/draft-sequoia-git.html public/index.html - echo "/sequoia-git/api /sequoia-git/sequoia_git/index.html 302" > public/_redirects cache: # inherit all general cache settings <<: *general_cache_config # override the key key: "bookworm" artifacts: paths: - public rules: - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' variables: CARGO_HOME: cargo/ CARGO_FLAGS: --color always CARGO_INCREMENTAL: 0 CARGO_TARGET_DIR: target/ sequoia-git-0.1.0/Cargo.lock0000644000002034410000000000100112400ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if", "once_cell", "version_check", ] [[package]] name = "aho-corasick" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" [[package]] name = "anstream" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is-terminal", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" [[package]] name = "anstyle-parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" dependencies = [ "anstyle", "windows-sys", ] [[package]] name = "anyhow" version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[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.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", "syn 2.0.37", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "base64" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "bindgen" version = "0.63.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885" dependencies = [ "bitflags 1.3.2", "cexpr", "clang-sys", "lazy_static", "lazycell", "peeking_take_while", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "syn 1.0.109", ] [[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 = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "buffered-reader" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66d3bea5bcc3ecc38fe5388e6bc35e6fe7bd665eb3ae9a44283e15b91ad3867d" dependencies = [ "bzip2", "flate2", "lazy_static", "libc", ] [[package]] name = "bumpalo" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "bzip2" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" dependencies = [ "bzip2-sys", "libc", ] [[package]] name = "bzip2-sys" version = "0.1.11+1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" dependencies = [ "cc", "libc", "pkg-config", ] [[package]] name = "cc" version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", "libc", ] [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "js-sys", "num-traits", "wasm-bindgen", ] [[package]] name = "clang-sys" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" dependencies = [ "glob", "libc", "libloading", ] [[package]] name = "clap" version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb690e81c7840c0d7aade59f242ea3b41b9bc27bcd5997890e7702ae4b32e487" dependencies = [ "clap_builder", "clap_derive", "once_cell", ] [[package]] name = "clap_builder" version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e" dependencies = [ "anstream", "anstyle", "clap_lex", "once_cell", "strsim", "terminal_size", ] [[package]] name = "clap_complete" version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" dependencies = [ "clap", ] [[package]] name = "clap_derive" version = "4.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" dependencies = [ "heck", "proc-macro2", "quote", "syn 2.0.37", ] [[package]] name = "clap_lex" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "clap_mangen" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f2e32b579dae093c2424a8b7e2bea09c89da01e1ce5065eb2f0a6f1cc15cc1f" dependencies = [ "clap", "roff", ] [[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "core-foundation" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "crc32fast" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" dependencies = [ "cfg-if", "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", "crossbeam-queue", "crossbeam-utils", ] [[package]] name = "crossbeam-channel" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", "scopeguard", ] [[package]] name = "crossbeam-queue" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] [[package]] name = "crunchy" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "data-encoding" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ "generic-array", ] [[package]] name = "dirs" version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" dependencies = [ "dirs-sys 0.3.7", ] [[package]] name = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ "dirs-sys 0.4.1", ] [[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.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", "redox_users", "winapi", ] [[package]] name = "dirs-sys" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", "redox_users", "windows-sys", ] [[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", "winapi", ] [[package]] name = "dyn-clone" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "ena" version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c533630cf40e9caa44bd91aadc88a75d75a4c3a12b4cfde353cbed41daa1e1f1" dependencies = [ "log", ] [[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.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" dependencies = [ "heck", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", "windows-sys", ] [[package]] name = "errno-dragonfly" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" dependencies = [ "cc", "libc", ] [[package]] name = "fallible-iterator" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[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.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" [[package]] name = "fd-lock" version = "3.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" dependencies = [ "cfg-if", "rustix 0.38.13", "windows-sys", ] [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] [[package]] name = "futures-channel" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", ] [[package]] name = "futures-core" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-io" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-macro" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", "syn 2.0.37", ] [[package]] name = "futures-sink" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", "futures-macro", "futures-task", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ "cfg-if", "js-sys", "libc", "wasi 0.9.0+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "gimli" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "git2" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12ef350ba88a33b4d524b1d1c79096c9ade5ef8c59395df0e60d1e1889414c0e" dependencies = [ "bitflags 2.4.0", "libc", "libgit2-sys", "log", "openssl-probe", "openssl-sys", "url", ] [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", "http", "indexmap 1.9.3", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" dependencies = [ "ahash", "allocator-api2", ] [[package]] name = "hashlink" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ "hashbrown 0.14.0", ] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "hostname" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" dependencies = [ "libc", "match_cfg", "winapi", ] [[package]] name = "http" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", "pin-project-lite", ] [[package]] name = "httparse" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", "socket2 0.4.9", "tokio", "tower-service", "tracing", "want", ] [[package]] name = "hyper-tls" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", "hyper", "native-tls", "tokio", "tokio-native-tls", ] [[package]] name = "idna" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ "matches", "unicode-bidi", "unicode-normalization", ] [[package]] name = "idna" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "idna" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "indexmap" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", ] [[package]] name = "indexmap" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", "hashbrown 0.14.0", ] [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", "windows-sys", ] [[package]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ "socket2 0.5.4", "widestring", "windows-sys", "winreg", ] [[package]] name = "ipnet" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "is-terminal" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", "rustix 0.38.13", "windows-sys", ] [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jobserver" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" dependencies = [ "libc", ] [[package]] name = "js-sys" version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] [[package]] name = "lalrpop" version = "0.19.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a1cbf952127589f2851ab2046af368fd20645491bb4b376f04b7f94d7a9837b" dependencies = [ "ascii-canvas", "bit-set", "diff", "ena", "is-terminal", "itertools", "lalrpop-util", "petgraph", "regex", "regex-syntax 0.6.29", "string_cache", "term", "tiny-keccak", "unicode-xid", ] [[package]] name = "lalrpop-util" version = "0.19.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3c48237b9604c5a4702de6b824e02006c3214327564636aef27c1028a8fa0ed" [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lazycell" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libgit2-sys" version = "0.16.1+1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2a2bb3680b094add03bb3732ec520ece34da31a8cd2d633d1389d0f0fb60d0c" dependencies = [ "cc", "libc", "libssh2-sys", "libz-sys", "openssl-sys", "pkg-config", ] [[package]] name = "libloading" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if", "winapi", ] [[package]] name = "libsqlite3-sys" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ "pkg-config", "vcpkg", ] [[package]] name = "libssh2-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" dependencies = [ "cc", "libc", "libz-sys", "openssl-sys", "pkg-config", "vcpkg", ] [[package]] name = "libz-sys" version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" dependencies = [ "cc", "libc", "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.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" [[package]] name = "lock_api" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[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 = "match_cfg" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "matches" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memoffset" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] [[package]] name = "memsec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa0916b001582d253822171bd23f4a0229d32b9507fae236f5da8cad515ba7c" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "mio" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] [[package]] name = "native-tls" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ "lazy_static", "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework", "security-framework-sys", "tempfile", ] [[package]] name = "nettle" version = "7.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9fdccf3eae7b161910d2daa2f0155ca35041322e8fe5c5f1f2c9d0b12356336" dependencies = [ "getrandom 0.2.10", "libc", "nettle-sys", "thiserror", "typenum", ] [[package]] name = "nettle-sys" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e81c347b9002da0b6b0c4060993c280e99eb14b42ecf65a2fefcd6eb3d8a73" dependencies = [ "bindgen", "cc", "libc", "pkg-config", "tempfile", "vcpkg", ] [[package]] name = "new_debug_unreachable" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" [[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-traits" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", ] [[package]] name = "num_threads" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" dependencies = [ "libc", ] [[package]] name = "object" version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "openpgp-cert-d" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccaa2a2e4502a5daf19c5753250fc6e37daa1d06f866ec97cd5e3416e6d05883" dependencies = [ "anyhow", "dirs 4.0.0", "fd-lock", "sha1collisiondetection", "tempfile", "thiserror", ] [[package]] name = "openssl" version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ "bitflags 2.4.0", "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 2.0.37", ] [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" version = "0.9.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" 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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", "redox_syscall 0.3.5", "smallvec", "windows-targets", ] [[package]] name = "peeking_take_while" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "percent-encoding" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "petgraph" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", "indexmap 2.0.0", ] [[package]] name = "phf_shared" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" dependencies = [ "siphasher", ] [[package]] name = "pin-project-lite" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[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.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" 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.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[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.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom 0.1.16", "libc", "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", ] [[package]] name = "rand_chacha" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ "ppv-lite86", "rand_core 0.5.1", ] [[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 0.6.4", ] [[package]] name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ "getrandom 0.1.16", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.10", ] [[package]] name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ "rand_core 0.5.1", ] [[package]] name = "rayon" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", "num_cpus", ] [[package]] name = "redox_syscall" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_syscall" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_users" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom 0.2.10", "redox_syscall 0.2.16", "thiserror", ] [[package]] name = "regex" version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax 0.7.5", ] [[package]] name = "regex-automata" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" dependencies = [ "aho-corasick", "memchr", "regex-syntax 0.7.5", ] [[package]] name = "regex-syntax" version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "resolv-conf" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" dependencies = [ "hostname", "quick-error", ] [[package]] name = "roff" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" [[package]] name = "rusqlite" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ "bitflags 2.4.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", "smallvec", ] [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" version = "0.37.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys 0.3.8", "windows-sys", ] [[package]] name = "rustix" version = "0.38.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" dependencies = [ "bitflags 2.4.0", "errno", "libc", "linux-raw-sys 0.4.7", "windows-sys", ] [[package]] name = "rustversion" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "schannel" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ "windows-sys", ] [[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.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "sequoia-cert-store" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02cc866249ad73d5350e7c52ef1b3939129a7a1b52d9ca25ef220ee184c5b33e" dependencies = [ "anyhow", "crossbeam", "dirs 5.0.1", "num_cpus", "once_cell", "openpgp-cert-d", "rayon", "rusqlite", "sequoia-net", "sequoia-openpgp", "smallvec", "thiserror", "tokio", ] [[package]] name = "sequoia-git" version = "0.1.0" dependencies = [ "anyhow", "buffered-reader", "clap", "clap_complete", "clap_mangen", "dirs 5.0.1", "git2", "once_cell", "sequoia-cert-store", "sequoia-net", "sequoia-openpgp", "serde", "serde_json", "tempfile", "thiserror", "tokio", "toml", "vergen", ] [[package]] name = "sequoia-net" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960f1ea77bf8b6f455285424257574d66fa29223e4286268017a4458cc29d33f" dependencies = [ "anyhow", "base64 0.13.1", "futures-util", "http", "hyper", "hyper-tls", "libc", "native-tls", "percent-encoding", "sequoia-openpgp", "tempfile", "thiserror", "tokio", "trust-dns-client", "trust-dns-resolver", "url", "zbase32", ] [[package]] name = "sequoia-openpgp" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30efff3f9930e85b4284e76bbdad741f36412dfb1e370efd0de5866ae1a11dfc" dependencies = [ "anyhow", "base64 0.21.4", "buffered-reader", "bzip2", "chrono", "dyn-clone", "flate2", "getrandom 0.2.10", "idna 0.3.0", "lalrpop", "lalrpop-util", "lazy_static", "libc", "memsec", "nettle", "once_cell", "rand 0.7.3", "regex", "regex-syntax 0.6.29", "sha1collisiondetection", "thiserror", "xxhash-rust", ] [[package]] name = "serde" version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", "syn 2.0.37", ] [[package]] name = "serde_json" version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "serde_spanned" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" dependencies = [ "serde", ] [[package]] name = "sha1collisiondetection" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b20793cf8330b2c7da4c438116660fed24e380bcb8a1bcfff2581b5593a0b38e" dependencies = [ "digest", "generic-array", ] [[package]] name = "shlex" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" [[package]] name = "signal-hook-registry" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "socket2" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", ] [[package]] name = "socket2" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" dependencies = [ "libc", "windows-sys", ] [[package]] name = "string_cache" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" dependencies = [ "new_debug_unreachable", "once_cell", "parking_lot", "phf_shared", "precomputed-hash", ] [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", "redox_syscall 0.3.5", "rustix 0.38.13", "windows-sys", ] [[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 = "terminal_size" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ "rustix 0.37.23", "windows-sys", ] [[package]] name = "thiserror" version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", "syn 2.0.37", ] [[package]] name = "time" version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" dependencies = [ "itoa", "libc", "num_threads", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" dependencies = [ "time-core", ] [[package]] name = "tiny-keccak" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" dependencies = [ "crunchy", ] [[package]] name = "tinyvec" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 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.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2 0.5.4", "tokio-macros", "windows-sys", ] [[package]] name = "tokio-macros" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", "syn 2.0.37", ] [[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-util" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", "tracing", ] [[package]] name = "toml" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c226a7bba6d859b63c92c4b4fe69c5b6b72d0cb897dbc8e6012298e6154cb56e" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ff63e60a958cefbb518ae1fd6566af80d9d4be430a33f3723dfc47d1d411d95" dependencies = [ "indexmap 2.0.0", "serde", "serde_spanned", "toml_datetime", "winnow", ] [[package]] name = "tower-service" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", "syn 2.0.37", ] [[package]] name = "tracing-core" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", ] [[package]] name = "trust-dns-client" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c408c32e6a9dbb38037cece35740f2cf23c875d8ca134d33631cec83f74d3fe" dependencies = [ "cfg-if", "data-encoding", "futures-channel", "futures-util", "lazy_static", "radix_trie", "rand 0.8.5", "thiserror", "time", "tokio", "tracing", "trust-dns-proto", ] [[package]] name = "trust-dns-proto" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" dependencies = [ "async-trait", "cfg-if", "data-encoding", "enum-as-inner", "futures-channel", "futures-io", "futures-util", "idna 0.2.3", "ipnet", "lazy_static", "openssl", "rand 0.8.5", "smallvec", "thiserror", "tinyvec", "tokio", "tracing", "url", ] [[package]] name = "trust-dns-resolver" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" dependencies = [ "cfg-if", "futures-util", "ipconfig", "lazy_static", "lru-cache", "parking_lot", "resolv-conf", "smallvec", "thiserror", "tokio", "tracing", "trust-dns-proto", ] [[package]] name = "try-lock" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-xid" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "url" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna 0.4.0", "percent-encoding", ] [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" version = "8.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85e7dc29b3c54a2ea67ef4f953d5ec0c4085035c0ae2d325be1c0d2144bd9f16" dependencies = [ "anyhow", "git2", "rustversion", "time", ] [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[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.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn 2.0.37", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", "syn 2.0.37", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "widestring" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" [[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-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[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_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[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_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[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_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] [[package]] name = "winreg" version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", "windows-sys", ] [[package]] name = "xxhash-rust" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9828b178da53440fa9c766a3d2f73f7cf5d0ac1fe3980c1e5018d899fd19e07b" [[package]] name = "zbase32" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f9079049688da5871a7558ddacb7f04958862c703e68258594cb7a862b5e33f" sequoia-git-0.1.0/Cargo.toml0000644000000043500000000000100112610ustar # 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.66" name = "sequoia-git" version = "0.1.0" authors = [ "Neal H. Walfield ", "Justus Winter ", ] build = "build.rs" description = "A tool for managing and enforcing a commit signing policy." homepage = "https://sequoia-pgp.org/" documentation = "https://docs.rs/sequoia-git" readme = "README.md" keywords = [ "cryptography", "openpgp", "pgp", "signing", "git", ] categories = [ "cryptography", "authentication", "command-line-utilities", "development-tools", ] license = "LGPL-2.0-or-later" repository = "https://gitlab.com/sequoia-pgp/sequoia-git" [[bin]] name = "sq-git" path = "src/main.rs" [dependencies.anyhow] version = "1" [dependencies.buffered-reader] version = "1" [dependencies.clap] version = "4.0" features = [ "cargo", "derive", "env", "string", "wrap_help", ] [dependencies.dirs] version = "5" [dependencies.git2] version = "0.18" [dependencies.once_cell] version = "1" [dependencies.sequoia-cert-store] version = "0.3" [dependencies.sequoia-net] version = "0.27" [dependencies.sequoia-openpgp] version = "1.15" [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.serde_json] version = "1" [dependencies.tempfile] version = "3" [dependencies.thiserror] version = "1" [dependencies.tokio] version = "1" features = ["full"] [dependencies.toml] version = "0.8" [build-dependencies.clap] version = "4.0" features = [ "cargo", "derive", "env", "string", "wrap_help", ] [build-dependencies.clap_complete] version = "4" [build-dependencies.clap_mangen] version = "0.2" [build-dependencies.dirs] version = "5" [build-dependencies.vergen] version = "8" features = [ "git", "git2", ] sequoia-git-0.1.0/Cargo.toml.orig000064400000000000000000000024661046102023000147500ustar 00000000000000[package] name = "sequoia-git" description = "A tool for managing and enforcing a commit signing policy." authors = [ "Neal H. Walfield ", "Justus Winter ", ] documentation = "https://docs.rs/sequoia-git" homepage = "https://sequoia-pgp.org/" repository = "https://gitlab.com/sequoia-pgp/sequoia-git" readme = "README.md" keywords = ["cryptography", "openpgp", "pgp", "signing", "git"] categories = ["cryptography", "authentication", "command-line-utilities", "development-tools"] version = "0.1.0" license = "LGPL-2.0-or-later" edition = "2021" build = "build.rs" rust-version = "1.66" [dependencies] anyhow = "1" buffered-reader = "1" clap = { version = "4.0", features = [ "cargo", "derive", "env", "string", "wrap_help" ] } dirs = "5" git2 = "0.18" once_cell = "1" toml = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1" sequoia-net = "0.27" sequoia-openpgp = "1.15" sequoia-cert-store = "0.3" tempfile = "3" thiserror = "1" tokio = { version = "1", features = ["full"] } [build-dependencies] clap = { version = "4.0", features = [ "cargo", "derive", "env", "string", "wrap_help" ] } clap_complete = "4" clap_mangen = "0.2" dirs = "5" vergen = { version = "8", features = [ "git", "git2" ] } [[bin]] name = "sq-git" path = "src/main.rs" sequoia-git-0.1.0/Containerfile000064400000000000000000000010121046102023000145500ustar 00000000000000ARG TAG=latest FROM 192.168.122.1:5000/sequoia-pgp/build-docker-image/rust-stable:$TAG AS build RUN cargo install --root=/usr --git https://gitlab.com/sequoia-pgp/sequoia-git.git sequoia-git FROM docker.io/debian:bookworm-slim COPY --from=build /usr/bin/sq-git /usr/bin/sq-git RUN apt update -y -qq && \ apt install -y -qq --no-install-recommends \ libsqlite3-0 libssl3 && \ apt clean && \ rm -fr /var/lib/lists/* /var/cache/* /usr/share/doc/* /usr/share/locale/* CMD ["/usr/bin/sq-git", "--help"] sequoia-git-0.1.0/LICENSE.txt000064400000000000000000000627341046102023000137100ustar 00000000000000Sequoia-git 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-git 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-git-0.1.0/README.md000064400000000000000000000273051046102023000133370ustar 00000000000000# Sequoia `git` `sequoia-git` is tool that can be used to improve a project's supply chain security. ## Introduction A version control system like git doesn't just track changes, it also provides a record of who made those changes. This information can be used to check that commits are authorized, which can improve software supply chain security. In particular, checking a change's provenance can be used to remove intermediaries like forges, and package registries from a user's trusted computing base. But, authorship information can easily be forged. An obvious solution to prevent forgeries would be to require that commits are digitally signed. But by itself a valid digital signature doesn't prevent forgeries. The certificate that was used to make the signature could claim to be one of the project's maintainers. What is needed is not only a list of entities who are allowed to modify the repository, but also the keys they use to sign the commits. In other words, to authenticate a commit we need a signing policy, which says what keys are authorized to make changes. Creating a policy isn't complicated. A project's maintainers could curate a list of entities who are allowed to add commits, and enumerate the certificates they use to sign them. The tricky part is applying the policy. There are a number of edge cases that need to be handled like how merge changes from external contributions, who is allowed to change the policy, and how to deal with compromised keys. Sequoia git is a project that specifies a set of semantics, defines a policy language, and provides a set of tools to manage a policy file, and authenticate commits. Using Sequoia git is relatively straightforward. You start by adding a policy file, `openpgp-policy.toml`, to your project's repository. The policy is maintained in band to allow it to evolve, just like the rest of the project. The `openpgp-policy.toml` file is basically a list of OpenPGP certificates and the type of changes they are authorized to make. `sq-git` can help you create it. Then, before you merge a pull request, you check that commits are authorized by the policy. Locally, this is done by running `sq-git log` on the range of commits that you want to push. A commit is considered authorized if the commit has a valid signature, and at least one immediate parent's policy allows the signer to make that type of change. Projects hosted on GitHub can use this action to automatically check that a pull request is authorized when it is opened, or updated. Downstream users can use Sequoia git to check that there is a chain of trust from an older, known-good version of the software to a new version. This helps prevent the use of versions that include modifications that weren't authorized by the project's maintainers. See [the specification] for an in-depth discussion of semantics and implementation. [the specification]: https://sequoia-pgp.gitlab.io/sequoia-git/ ## Deploying `sq-git` To start using Sequoia git in a git repository, you first add one or more certificates to the project's policy, and grant them some rights. The policy is called `openpgp-policy.toml`, and is stored in the root of the repository. It is a [toml file](https://toml.io), which means it can be edited by hand, but `sq-git` provides tools that make it easier to examine and modify it. There are six different rights: `add-user`, `retire-user`, `audit`, `sign-tag`, `sign-archive`, and `sign-commit`. Only users who have the `add-user` right can add new users to the policy. Similarly, the `retire-user` right is needed to remove users from the policy. The `audit` right is needed to good list (using `sq-git policy goodlist`) a commit that was signed by a certificate that was subsequently hard revoked. The `sign-tag`, `sign-archive`, and `sign-commit` rights are needed to sign tags, archives, and commits, respectively. You can use `sq-git policy authorize` to grant a specific right to a user. For instance, you could run: ``` $ sq-git policy authorize --sign-commit 'Neal H. Walfield ' F7173B3C7C685CD9ECC4191B74E445BA0E15C957 ``` This says that the specified certificate can be used to sign commits. The name is purely decorative. To make assigning rights easier, `sq-git policy authorize` knows about three roles: the project maintainer (who gets all rights), the release manager (who can sign tags, archives, and commits), and the committer (who can only sign commits). These can be passed to `sq-git policy authorized`. For instance: ``` $ sq-git policy authorize --project-maintainer 'Neal H. Walfield ' F7173B3C7C685CD9ECC4191B74E445BA0E15C957 ``` `sq-git policy authorize` immediately expands the roles to the corresponding rights; the roles do not appear in the policy file. You can use the `sq-git init` subcommand to get a quick overview of who has contributed to the project, and what certificates they used to sign their commits, if any. `sq-git init` looks at commits from the last half year, or the last 10 commits, whichever is more. As such it focuses on contributors who have been active recently; it doesn't make sense to authorize someone has left the project. Although `sq-git init` usually provides a good starting point, you should not trust it. It is essential to verify a contributor's certificate by, e.g., reaching out to them, and asking what their certificate's fingerprint is. As you can always modify the policy later, it is better to only add the certificates that you are certain about, from contributors who are active. Here's how you might initialize a policy file: ```shell ../sequoia-git$ sq-git init # Examined the 136 commits in the last 183 days. # Stopped at commit 83ce12f617c9e1dd90f812825707337f8787f69e. # Encountered 0 unsigned commits # Neal H. Walfield added 66 commits (48%). # # After checking that they really control the following OpenPGP keys: # # 6863C9AD5B4D22D3 (66 commits) # # You can make them a project maintainer (someone who can add and # remove committers) by running: sq-git policy authorize --project-maintainer "Neal H. Walfield " 6863C9AD5B4D22D3 # Justus Winter added 44 commits (32%). # # After checking that they really control the following OpenPGP keys: # # 686F55B4AB2B3386 (44 commits) # # You can make them a committer by running: sq-git policy authorize --committer "Justus Winter " 686F55B4AB2B3386 ... ../sequoia-git$ sq-git policy authorize --project-maintainer "Neal H. Walfield " 6863C9AD5B4D22D3 - User "Neal H. Walfield " was added. - User "Neal H. Walfield " was granted the right sign-commit. - User "Neal H. Walfield " was granted the right sign-tag. - User "Neal H. Walfield " was granted the right sign-archive. - User "Neal H. Walfield " was granted the right add-user. - User "Neal H. Walfield " was granted the right retire-user. - User "Neal H. Walfield " was granted the right audit. ../sequoia-git$ sq-git policy authorize --committer "Justus Winter " 686F55B4AB2B3386 - User "Justus Winter " was added. - User "Justus Winter " was granted the right sign-commit. ``` `sq-git` reads the certificates from the user's certificate store. Use `sq import < FILE` to import certificates in a file, `sq keyserver get FINGERPRINT` to fetch certificates from a key server, etc. Alternatively, you can provide the certificate to `sq-git policy authorize` using the `--cert-file` argument. The policy file can be viewed as follows: ```shell $ sq-git policy describe # OpenPGP policy file for git, version 0 ## Commit Goodlist ## Authorizations 0. Justus Winter - may sign commits - has OpenPGP cert: D2F2C5D45BE9FDE6A4EE0AAF31855247603831FD 1. Neal H. Walfield - may sign commits - may sign tags - may sign archives - may add users - may retire users - may goodlist commits - has OpenPGP cert: F7173B3C7C685CD9ECC4191B74E445BA0E15C957 ``` If you are happy, you can add it to your `git` repository in the usual manner. Don't forget to tell `git` to sign commits by adding something like the following to your repository's `.git/config` file: ```text [user] signingkey = F7173B3C7C685CD9ECC4191B74E445BA0E15C957 email = 'neal@pep.foundation' name = 'Neal H. Walfield' [commit] gpgsign = true ``` Then run: ```shell ../sequoia-git$ git add openpgp-policy.toml ../sequoia-git$ git commit -m 'Add a commit policy.' [main 911c4eb] Add a commit policy. 1 file changed, 119 insertions(+), 1831 deletions(-) rewrite openpgp-policy.toml (94%) ``` Create a new commit, and verify the new version: ```shell ../sequoia-git$ echo 'hello world' > greeting ../sequoia-git$ git add greeting ../sequoia-git$ git commit -m 'Say hello.' [main 698876a] Say hello. 1 file changed, 1 insertion(+) create mode 100644 greeting ../sequoia-git$ sq-git log --trust-root 911c4eb1e9832d6df8e733bf103ca4c9f4637eb9 911c4eb1e9832d6df8e733bf103ca4c9f4637eb9..698876a7ff11fff2f8cd0df55bbe8fc5c5d224d9: Neal H. Walfield [74E445BA0E15C957] ``` Instead of entering the trust root manually, which is error prone, you can set the trust root in the repository's git config file: ```shell ../sequoia-git$ git config sequoia.trust-root 911c4eb1e9832d6df8e733bf103ca4c9f4637eb9 ../sequoia-git$ sq-git log 911c4eb1e9832d6df8e733bf103ca4c9f4637eb9..698876a7ff11fff2f8cd0df55bbe8fc5c5d224d9: Cached positive verification ``` You can also use tags or branches, however, you must be careful as these may be updated when you fetch from a remote repository using, e.g., `git fetch`. ## Rejecting Unauthorized Commits Insert the following line into `hooks/update` on a shared `git` server to make it enforce the policy embedded in the repository starting at the given trust root (``), which is specified as a hash: ```text sq-git update-hook --trust-root= "$@" ``` ## Using `sq-git` in CI `sequoia-git` is available in an OCI image for ease of use inside of CI pipelines. ### Gitlab To authenticate commits from a Gitlab CI pipeline, there is a script included at `scripts/gitlab.sh` which may be run as a job inside a project's `.gitlab-ci.yml` manifest: ``` authenticate-commits: stage: test image: registry.gitlab.com/sequoia-pgp/sequoia-git:latest script: - sq-git policy describe - ./scripts/gitlab.sh rules: # TODO: We currently only authenticate the changes on non-merged # branches where we use the default branch as the trust root. For # the default branch, the project needs to set an explicit trust # root. - if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH' ``` ### GitHub To use `sq-git` to authenticate a pull request in GitHub, you can use the [`sequoia-pgp/authenticate-commits` Action](https://github.com/sequoia-pgp/authenticate-commits). This action checks that the commits are authorized by the last commit of the merge base. [This video](https://www.youtube.com/watch?v=KdDbU9u5X-Q) shows a demonstration of the action. Note: GitHub's interface for merging pull requests offers three merge strategies, but unfortunately none of them are appropriate for use with Sequoia git, because they all modify the commits. With Sequoia git, it is necessary to either rebase and fast forward the change, or to add a signed merge commit. It is possible to use the [`sequoia-pgp/fast-forward` action](https://github.com/sequoia-pgp/fast-forward) to fast forward pull requests. When enabled for a repository, an authorized user can add a comment containing `/fast-forward` to the pull request, and the action will fast forward the merge base. sequoia-git-0.1.0/build.rs000064400000000000000000000065731046102023000135310ustar 00000000000000use std::path::Path; use std::path::PathBuf; use clap::CommandFactory; use clap_complete::Shell; pub mod cli { include!("src/cli/mod.rs"); } // To avoid adding a build dependency on sequoia-openpgp, we mock the // bits of openpgp that the CLI module uses. pub mod openpgp { #[derive(Clone, Debug)] pub struct KeyHandle { } impl From<&str> for KeyHandle { fn from(_: &str) -> KeyHandle { KeyHandle {} } } } fn main() { git_version(); completions(); man_pages(); } fn git_version() { // Emit the "cargo:" instructions including // "cargo:rustc-env=VERGEN_GIT_DESCRIBE=". // // If the source directory does not contain a git repository, // e.g., because the code was extracted from a tarball, this // produces an `Error` result, which we ignore, and // `VERGEN_GIT_DESCRIBE` is not set. That's okay, because we are // careful to only use `VERGEN_GIT_DESCRIBE` if it is actually // set. let _ = vergen::EmitBuilder::builder() // VERGEN_GIT_DESCRIBE .git_describe(/* dirty */ true, /* tags */ false, None) // Don't emit placeholder values for the git version if the // git repository is not present. .fail_on_error() .emit(); } fn completions() { // Generate shell completions let outdir = match std::env::var_os("CARGO_TARGET_DIR") { None => { println!("cargo:warning=Not generating completion files, \ environment variable CARGO_TARGET_DIR not set"); return; } Some(outdir) => outdir, }; std::fs::create_dir_all(&outdir).unwrap(); let mut cli = cli::Cli::command(); for shell in &[Shell::Bash, Shell::Fish, Shell::Zsh, Shell::PowerShell, Shell::Elvish] { let path = clap_complete::generate_to( *shell, &mut cli, "sq-git", &outdir).unwrap(); println!("cargo:warning=generated completion file {:?}", path); }; } fn man_pages() { // Man page support. let outdir = match std::env::var_os("CARGO_TARGET_DIR") { None => { println!("cargo:warning=Not generating man pages, \ environment variable CARGO_TARGET_DIR not set"); return; } Some(outdir) => PathBuf::from(outdir), }; std::fs::create_dir_all(&outdir).unwrap(); let cli = cli::Cli::command(); let man = clap_mangen::Man::new(cli.clone()); let mut buffer: Vec = Default::default(); man.render(&mut buffer).unwrap(); let filename = outdir.join("sq-git.1"); println!("cargo:warning=writing man page to {}", filename.display()); std::fs::write(filename, buffer).unwrap(); fn doit(outdir: &Path, prefix: &str, command: &clap::Command) { let man = clap_mangen::Man::new(command.clone()); let mut buffer: Vec = Default::default(); man.render(&mut buffer).unwrap(); let filename = outdir.join(format!("{}-{}.1", prefix, command.get_name())); println!("cargo:warning=writing man page to {}", filename.display()); std::fs::write(filename, buffer).unwrap(); for sc in command.get_subcommands() { doit(outdir, &format!("{}-{}", prefix, command.get_name()), sc); } } for sc in cli.get_subcommands() { doit(&outdir, "sq-git", sc); } } sequoia-git-0.1.0/deny.toml000064400000000000000000000006471046102023000137140ustar 00000000000000[advisories] ignore = [ ] unmaintained = "deny" 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", "BSD-3-Clause", "BSL-1.0", "CC0-1.0", "GPL-2.0", "GPL-3.0", "ISC", "LGPL-2.0", "LGPL-3.0", "MIT", "MPL-2.0", "Unicode-DFS-2016", ] sequoia-git-0.1.0/openpgp-policy.toml000064400000000000000000003416311046102023000157230ustar 00000000000000version = 0 commit_goodlist = [] [authorization."Justus Winter "] sign_commit = true sign_tag = true sign_archive = true add_user = true retire_user = true audit = true keyring = """ -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: CBCD 8F03 0588 653E EDD7 E265 9B7D D433 F254 904A Comment: Comment: Justus Winter Comment: Justus Winter Comment: Justus Winter Comment: Justus Winter Comment: Justus Winter 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 =uq6w -----END PGP PUBLIC KEY BLOCK----- -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: D2F2 C5D4 5BE9 FDE6 A4EE 0AAF 3185 5247 6038 31FD Comment: Justus Winter (Code Signing Key) "] sign_commit = true sign_tag = true sign_archive = true add_user = true retire_user = true audit = 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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 HCL+AFMw7YtSwqT2TxbLYPc/7bU+pWAe1YT6pk0OLZhuRzyG3UuZaMVFHcp26sLP rGROgdo4R79MUUDMLYkuYKRcTrdi4bceZlmIpJeUt+SY8zcAP3m4epeEOu3vrJER 1xglFmeNDEcjD32iTJHlkGcyHw+NWhl6l8LvFov4ZVyW4pXiYjnjbOHq8ukbPIDP eafMkishG18ekELGoGuLpDqnPYG9AYXiy03D6ZwduLI770bMkG5zhKzDksIGQ8x0 JUPvtroSlgNQwe1PFlDS7c2bIs3JqwCpuJClEEqKkvg7samKXWNCO23vl9ucmOwk xMm2w4v09WooFlG28hSsH+FL+ROEU2Px1MJbUahReTbsho828NstqZ46UWmcWe9q 9YDwnHBj2sKEy47f2mZ3ZoR1wchRIxze7dcPLUTPMoEyjAFKzsoozpE+Ah/hUwIj PSEK0kfPaQx4jR9QKCwTnMrHNs16nVUM9zhXLNlwb2rdG6Y0dAyBllLvE4UHAF8x 3Gv/6O3xC/PU6LrAUitH0mViV+GHHn3jWV7oO3NmV27x+Vw1DqZO9XZMPqmgRoRz MEQXzSROZWFsIEguIFdhbGZpZWxkIDxuZWFsQHdhbGZpZWxkLm9yZz7CwWgEEwEK AD4CGwMICwkIBw0MCwoFFQoJCAsCHgECF4ACGQEWIQSPF3dxGKM92pukjmKqyzJD YwBS2QUCZCarHgUJEORDtQAKCRCqyzJDYwBS2YLaDp0fMZrwfLGqC8LJiRfI+HwV +29E7EWaJmOF6S24sewr1t//2Vz4mc41E6bDprHsHte4JEmzyCnXnXlPaobyONWU IbYcnM8723myJml0Du4EvvvJPUieMiwFWzRdmFxc/eQ+6aeTTUknNJKVhY18/qsA pvYTIkvmlePgLQVGvpnYqQ1ElgMf768KJ4+lVMmIT2+fHlLe5RyJRlZcjVryV5H9 fOy+TQqhLLoTFhxAiJBACYIUCglGgjTQejivXXRXjhvMpvlBzSxwI9iZGZUL4FE1 kjK99epoictN9iv1O5XTZYTqkdBf1PD79GicHAbG6Io8F07xR/CijZl27l6rLb55 9/33Iswi6Ma8mdnmeTJ1d+io9XbtAGhPZqKxM2XtKbVO8uJli6YCqeEpVP+/Wy2c g4RSj42b5yrplbqVvnbddx8ZYCm5SXD6Pb/ZrrUmKlayuM2UnMs9+snIGG/SMRUv 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 =MESu -----END PGP PUBLIC KEY BLOCK----- """ [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.wiktor] sign_commit = true sign_tag = true sign_archive = true add_user = true retire_user = true audit = true keyring = """ -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: 6539 09A2 F0E3 7C10 6F5F AF54 6C88 57E0 D8E8 F074 Comment: Wiktor Kwapisiewicz xsFNBFhoYHoBEADzmg9UuwDrtvyejU01gDY1J1iJiCi4XGJ4lCfYeLC2jSagIxU/ 5Lu0lRft0Loi2tsjpo0c8docP7HFxafEEvnnt/iabd6I536llMuw0uno4PgnD3lj cCMZLT+vn+amIDtalzVoMnSqzoNUotMNMtjIFuAaQ/wr4/Mp9CIgJdviGUc3Pscq UiiUVVtk6uF0x657NULZgSIT/Mrqlr2i4RuyPwXe2Qt0uEA3KWWjF0l2NpAMVrqz +nHsLoNOaAsfdx94bzKQrrSeSQqEO2f+/eO/hbUAFAmEhrotmUO8wJNygo8Tgkdl zFI+UE4p8/KW0aCgGGgR8YkCvHq2OQhAAYFNJoNzHqw0FGxdsY8qWFkYpoSB8zKs pNy8KliofCamMYXoPF7eVIxIiKvxrAykGP4jNnzSoV0cn+bYfXnox1IhnqbnoJIT 7kTmXv4JmWoYm8ThHqpEgcQOUUQzSRXb9OiNwiXT71ijeO1qswMRpsgk6AGKSZGW xa3c4ive/p8z1Ax27BFZSh2FceIcMCcGLrDjnQYgeFsAJ1jSxZQXkGuJFHfb4nff Big7aq/vyKrQFQXG0NQQL7rZAdk/s665vifos0yPmRDu7yDT1ggdyBp4Pa4re+ZJ cNRNzNHozU9al+CoImCQjnTtKMXmOe/BzGrpHI4QR3NNzVa423WCIWkHfwARAQAB zSlXaWt0b3IgS3dhcGlzaWV3aWN6IDx3aWt0b3JAbWV0YWNvZGUuYml6PsLDbgQT AQoCGAIbAQgLCQgHDQwLCgUVCgkICwIeAQIXgDQUgAAAAAASABlwcm9vZkBtZXRh Y29kZS5iaXpkbnM6bWV0YWNvZGUuYml6P3R5cGU9VFhUXBSAAAAAABIAQXByb29m QG1ldGFjb2RlLmJpemh0dHBzOi8vZ2lzdC5naXRodWIuY29tL3dpa3Rvci1rLzM4 OWQ1ODlkZDE5MjUwZTFmOWE0MmJjM2Q1ZDQwYzE2VRSAAAAAABIAOnByb29mQG1l dGFjb2RlLmJpemh0dHBzOi8vd3d3LnJlZGRpdC5jb20vdXNlci93aWt0b3Itay9j b21tZW50cy9ibzVvaWgvdGVzdC9zFIAAAAAAKgBAdGltZXN0YW1wK2JpdGNvaW4t dHJhbnNhY3Rpb25AbWV0YWNvZGUuYml6YWZjYjA5MmM1Y2E2NDA5NTI2ZDE4YWU5 Y2YyMmQzYjU1ZDM3ZTcyM2ViMWI3NGUzZjg0ZjdlNmIwNTJhMTYyYUgUgAAAAAAS AC1wcm9vZkBtZXRhY29kZS5iaXpodHRwczovL25ld3MueWNvbWJpbmF0b3IuY29t L3VzZXI/aWQ9d2lrdG9yLWs3FIAAAAAAEgAccHJvb2ZAbWV0YWNvZGUuYml6aHR0 cHM6Ly9tZXRhY29kZS5iaXovQHdpa3RvchYhBGU5CaLw43wQb1+vVGyIV+DY6PB0 BQJjjdcHBQkNKkjJAAoJEGyIV+DY6PB0crgQAOjVg6U3JckfSCCeUQphX5q9QShf 29WNNmZI7x3FO5Img2ZMbnrdq9gm3MhvDMTwJF0r7oCy+2BOem8HmLsu+nsOxMSe +dwZwl1pH4lkP4SCMYlemXnqZ/Mdzc2xZuzbjK36HmtqCe7yBzhpriTCvbiqBC6r yGc7BU/PRcfGh+0bG3Ux8sRJaJUrUFLABU+kvClPIj0xlmvpbxY9ND32Hid8Rpiy d7ur80NC8AtKzeJ88Zguv4CQG1hx8OdlzJVDDvhfhNw6EL7Ja2vhrrbsgIkw5IsW gFAj3Nj/P0FtMK+rG0Q4BCB4mYNrR20ooyvToJNJlx11ytRpFwSE5FkBQATFtnFX YsPMwDodwKR31ipuYlpWC1SscDXs2ngDQ95G2ncogUN5rKdjTqwVO14SKtK92kGV YVMbyHIEHAg/nf3YlSrI23Pw8JGgUMaHV9Cd1/9T4Sz4wyDxGzH+2cjlBKf17/XZ yAIipvhPPPFLRX+3F2Kg0u986Tgm55ylWWvra0vqFjM11aDB3ZM/yYXrrxKI1dL9 9TBWeb/sbRCV69MvazSRRhO707L9/UmfrROAXJEEPXG1YusW7D6d7e5oGyyZ8tvq DOJWzStJcfgtD9QNd9Q/Ga09qzH7KTZmvnPCivZ7a3RSEsRRNDPylAY/fmMjEYNA qVSN03TBLM3+hirjwsNuBBMBCgIYAhsBCAsJCAcNDAsKBRUKCQgLAh4BAheANBSA AAAAABIAGXByb29mQG1ldGFjb2RlLmJpemRuczptZXRhY29kZS5iaXo/dHlwZT1U WFRcFIAAAAAAEgBBcHJvb2ZAbWV0YWNvZGUuYml6aHR0cHM6Ly9naXN0LmdpdGh1 Yi5jb20vd2lrdG9yLWsvMzg5ZDU4OWRkMTkyNTBlMWY5YTQyYmMzZDVkNDBjMTZV FIAAAAAAEgA6cHJvb2ZAbWV0YWNvZGUuYml6aHR0cHM6Ly93d3cucmVkZGl0LmNv bS91c2VyL3dpa3Rvci1rL2NvbW1lbnRzL2JvNW9paC90ZXN0L3MUgAAAAAAqAEB0 aW1lc3RhbXArYml0Y29pbi10cmFuc2FjdGlvbkBtZXRhY29kZS5iaXphZmNiMDky YzVjYTY0MDk1MjZkMThhZTljZjIyZDNiNTVkMzdlNzIzZWIxYjc0ZTNmODRmN2U2 YjA1MmExNjJhSBSAAAAAABIALXByb29mQG1ldGFjb2RlLmJpemh0dHBzOi8vbmV3 cy55Y29tYmluYXRvci5jb20vdXNlcj9pZD13aWt0b3ItazcUgAAAAAASABxwcm9v ZkBtZXRhY29kZS5iaXpodHRwczovL21ldGFjb2RlLmJpei9Ad2lrdG9yFiEEZTkJ ovDjfBBvX69UbIhX4Njo8HQFAmGwhocFCQtHw8gACgkQbIhX4Njo8HTKLxAAt3vP 4mfTgc8vXqu//ojfTLA9RCpqIDfV7G1kjdBbTaCTdQhbGrdzQygAg7ohIYcCCAX6 PyOVYFQkbas3o3rqTcPR6z5/+Hxp3iugrvhmZMSywi+gpC4FLi9536qlATjy+Ecw nmArzQgv7xpw/y4mB6t6n0Qz+TA3P43Y0R6a6k6ooiulSEJsNLNzJuRIWUch05+2 6228Wl/ldZxKuQKUQ2QI5710XQCAGXiZ0Q17BK/urKr054+Jb2T/rxoSIgH7sFxD 4U3VaCLyec+AEpPiee5CS/jejWyonniEZ4ZPjzYyfe3yFcyxu8uMDzBaEoP6I5jY k9eGOGgAZC7BqnLnXGsOaN8fstTR4GdnCxdWyv0Cy6aHTvNr8BUIgUqc53yXkaLW abXdBx7RrG0mfzYbr5e7VgaCXAtnMVzODtsmBydwTLqikl4N9BqAPfnnhcUfZOVa AODHYoYqjo9ILKduIug9Bfjfo5FWL/ODKwQDbXeAR7BrCpbXKEukCprwXYxmcdsY IQNyYdtYYuTAgz8G/S7/EuCnQg+A6+pbgRTvl4eZcFMS6nYD8WnrDEwJB0A1c6bT HqGQwg/fBY5kLZE+7qZwL4MWMoow51QBjUVMGwxTkV10aPuHF9OABp6ZMaLs/o3t OROWyeQJN3SdKKXgPDgLhDuvBVwCrKn/kvEprv3Cw24EEwEKAhgCGwEICwkIBw0M CwoFFQoJCAsCHgECF4A0FIAAAAAAEgAZcHJvb2ZAbWV0YWNvZGUuYml6ZG5zOm1l dGFjb2RlLmJpej90eXBlPVRYVFwUgAAAAAASAEFwcm9vZkBtZXRhY29kZS5iaXpo dHRwczovL2dpc3QuZ2l0aHViLmNvbS93aWt0b3Itay8zODlkNTg5ZGQxOTI1MGUx ZjlhNDJiYzNkNWQ0MGMxNlUUgAAAAAASADpwcm9vZkBtZXRhY29kZS5iaXpodHRw czovL3d3dy5yZWRkaXQuY29tL3VzZXIvd2lrdG9yLWsvY29tbWVudHMvYm81b2lo L3Rlc3QvcxSAAAAAACoAQHRpbWVzdGFtcCtiaXRjb2luLXRyYW5zYWN0aW9uQG1l dGFjb2RlLmJpemFmY2IwOTJjNWNhNjQwOTUyNmQxOGFlOWNmMjJkM2I1NWQzN2U3 MjNlYjFiNzRlM2Y4NGY3ZTZiMDUyYTE2MmFIFIAAAAAAEgAtcHJvb2ZAbWV0YWNv ZGUuYml6aHR0cHM6Ly9uZXdzLnljb21iaW5hdG9yLmNvbS91c2VyP2lkPXdpa3Rv ci1rNxSAAAAAABIAHHByb29mQG1ldGFjb2RlLmJpemh0dHBzOi8vbWV0YWNvZGUu Yml6L0B3aWt0b3IWIQRlOQmi8ON8EG9fr1RsiFfg2OjwdAUCXkCitwUJCWfhyQAK CRBsiFfg2OjwdGgoD/47O0ni6MGoh6mfJ927sT7snSispHz/WsJ2Y2R7h3fKz/Xn puKk8NqhW/FKLhZsfLj6vJJ0YQ1VA+LEOOIh2B/YHwizCfca1jw9KcYVrZy62waP tbIU+YDnUIpF8kEPiLzKCaLkZc6/xS/XC6a+7jzKUvfg7JQg+bM4RojCzDTeGKDT uYBy2kAt6hbBQBmGrJEHhUzZ9EguRsU6tOp1L/l8ZUjof9d8zs8fgghmC0GE8T2d iipZ7B/v25Q6VbwjmGrPqpgJhac8wVukbgyVuQrq0DQOWWw+MIr3Sqq+7TjCDao9 2StLUNBPP00pmQFEmZXnZzvTpzyrz4d+yz6DXkuWH+VcLgnGJjlJ9yzFVzlRUnls CgqzbGhDfKLzjZxYCtC8Ln7mcfU37Xuv97S7WseQU7TgS1qrhZEG5iMG/hjwWlwK qxRgS9IYT+ZRqSpnRPmnIuFyHEIDqM4zj+S3izZRQOVmJvC9+EW34NcLduR3jk9o lzu6C8LkG1231dG9PkUvtFB64rRsAiHR1HTAKD3PSkkVq4hT/J4+UXjZBaT7Ek71 wKgw3eiQZv7+egoHMcz4pyK3+HaY9J8ai/N/BEylRAgQuOdDqqxkRVrsM16pvs6w raBcPDnOfmp0hvnczh1kTSu+FHEjxnp7raZh90zzHzSrM7zSP2KU3ckyX9LtRMLD bgQTAQoCGAIbAQgLCQgHDQwLCgUVCgkICwIeAQIXgDQUgAAAAAASABlwcm9vZkBt ZXRhY29kZS5iaXpkbnM6bWV0YWNvZGUuYml6P3R5cGU9VFhUXBSAAAAAABIAQXBy b29mQG1ldGFjb2RlLmJpemh0dHBzOi8vZ2lzdC5naXRodWIuY29tL3dpa3Rvci1r LzM4OWQ1ODlkZDE5MjUwZTFmOWE0MmJjM2Q1ZDQwYzE2VRSAAAAAABIAOnByb29m QG1ldGFjb2RlLmJpemh0dHBzOi8vd3d3LnJlZGRpdC5jb20vdXNlci93aWt0b3It ay9jb21tZW50cy9ibzVvaWgvdGVzdC9zFIAAAAAAKgBAdGltZXN0YW1wK2JpdGNv aW4tdHJhbnNhY3Rpb25AbWV0YWNvZGUuYml6YWZjYjA5MmM1Y2E2NDA5NTI2ZDE4 YWU5Y2YyMmQzYjU1ZDM3ZTcyM2ViMWI3NGUzZjg0ZjdlNmIwNTJhMTYyYUgUgAAA AAASAC1wcm9vZkBtZXRhY29kZS5iaXpodHRwczovL25ld3MueWNvbWJpbmF0b3Iu Y29tL3VzZXI/aWQ9d2lrdG9yLWs3FIAAAAAAEgAccHJvb2ZAbWV0YWNvZGUuYml6 aHR0cHM6Ly9tZXRhY29kZS5iaXovQHdpa3RvchYhBGU5CaLw43wQb1+vVGyIV+DY 6PB0BQJdK4YGBQkHhq5HAAoJEGyIV+DY6PB0qPsQAIKTMUYx8RPHfLMM3F11XtLU obKO6CpU83TM894/uF06woM3OaHiajVqC8d6jBXcw2OLH9cCQ9oPQsfxns3YcKLp WLnSv6F46U9M1e1rZM7H/ooEsNWZNiTyZPaO0bBDsLtpEEOzo609IftKaP3+BFyE r4YGerHeXcmBzoGlxR84GVsoTzs+VLZn4zAxPMPSe+s9mTTU85uGAXDdhSjTvb5s KARVDQNAlrEo5tZ17/K0BcSztYBT+rnRVAROaxxsqvVQG8lGuohBQuv3BDaqSBwJ p/qcDHz3eOLNLfvanZvGtoXtRybimd8mDjzG18wd/V1DJOIzixdsBA2PHzPvFAoY zohjZrEjC7KPFXiUN1NN9B5PsTKXEWzZiqffjEQHCD8o3JO5tJwI04tN+g55HXxM 750639OFuZRGpBTysY7NSqkzDcDNuzkcPU7mXFfNZNG1+t54NlSaU9cwfZNdOd4y 6ClE3qZReKwZMiqgQPNF7h4FPpFzkR79z6CLWt5iHhMVJ1au00xuf1c+NDGXp6oK UbtlTRpmGnLjLn1z+7s9wUDdfvUf+aRRDXRLPcseI0wvk82mkBhSbX5ZDRgFqEB+ giNS7ydZw4ur5scXgMA2i6JUe3eAoDflygpB0+EWiJWv/Eyzwsoj1V/z9TXDeTME 1sQckXPpmspnuO0uogrEwsNuBBMBCgIYAhsBCAsJCAcNDAsKBRUKCQgLAh4BAheA BQkFotfKFiEEZTkJovDjfBBvX69UbIhX4Njo8HQFAlzavWg0FIAAAAAAEgAZcHJv b2ZAbWV0YWNvZGUuYml6ZG5zOm1ldGFjb2RlLmJpej90eXBlPVRYVFwUgAAAAAAS AEFwcm9vZkBtZXRhY29kZS5iaXpodHRwczovL2dpc3QuZ2l0aHViLmNvbS93aWt0 b3Itay8zODlkNTg5ZGQxOTI1MGUxZjlhNDJiYzNkNWQ0MGMxNlUUgAAAAAASADpw cm9vZkBtZXRhY29kZS5iaXpodHRwczovL3d3dy5yZWRkaXQuY29tL3VzZXIvd2lr dG9yLWsvY29tbWVudHMvYm81b2loL3Rlc3QvcxSAAAAAACoAQHRpbWVzdGFtcCti aXRjb2luLXRyYW5zYWN0aW9uQG1ldGFjb2RlLmJpemFmY2IwOTJjNWNhNjQwOTUy NmQxOGFlOWNmMjJkM2I1NWQzN2U3MjNlYjFiNzRlM2Y4NGY3ZTZiMDUyYTE2MmFI FIAAAAAAEgAtcHJvb2ZAbWV0YWNvZGUuYml6aHR0cHM6Ly9uZXdzLnljb21iaW5h dG9yLmNvbS91c2VyP2lkPXdpa3Rvci1rNxSAAAAAABIAHHByb29mQG1ldGFjb2Rl LmJpemh0dHBzOi8vbWV0YWNvZGUuYml6L0B3aWt0b3IACgkQbIhX4Njo8HQ37BAA zapYIsYALt5YRAir1nD+aOXO1L47/vEb2jx1sIE7+pVqcMNnMKt514gd0q6jqWXI qY+X79Cs8SkduyXave45AvaMhPyMcaOT7YuehcxLUELqBRmVS/cCBGIh/99XJgdy HCkBWIHxdCdc9IDpQwLh8cxWlejbUmGSVja/59bf3CVJoJHYrhPowFbLYCaC8Q1h OfzJ6d6qIiV2kPWTpkvM6YUvScwhJ3ftfF5Dit0TxsmpuJi4wmKS5a7m2CcxzS2j xTyafbBoFg93+t/aOsAR5+9A7/ZG31wuttq5oGWfwQvljj1ayYCxgpFxFZuRNUOe 4RcOELa63E4bWf+QBm6e7zuJHAEqBpjo3bLmeLOKvnImI2I0fUgCUq9LzutJRi7g DrCtmSLVye8t22ZKxcOVqCIqZZWqkVapsut88n/xsXyqX/I+17urYX0nQATQLR4X O45RiVSyXEfokd/wSYuZBUFrr/PhHsjgx1s+mB8yu2SP0cL89lRNu+SmXKEMWzN3 0npw+4w9FAO5/NeforKOuTCow9hZ58b7KcfF/3d4LVknqeJT9KRlwj2dZa2wirVa OaaWZ6CxO9YfTwu+WW/zibuvl/xsFl46Z6jlgnXLXSqTdZHYbo5DvKCkFGj5y7nm WkX2zrM1kT6KG7lguAmxWwcv0O8sIIFGj/fuhAf6fh7CwgUEEwEKAK8CGwEICwkI Bw0MCwoFFQoJCAsCHgECF4BzFIAAAAAAKgBAdGltZXN0YW1wK2JpdGNvaW4tdHJh bnNhY3Rpb25AbWV0YWNvZGUuYml6YWZjYjA5MmM1Y2E2NDA5NTI2ZDE4YWU5Y2Yy MmQzYjU1ZDM3ZTcyM2ViMWI3NGUzZjg0ZjdlNmIwNTJhMTYyYRYhBGU5CaLw43wQ b1+vVGyIV+DY6PB0BQJbtfdzBQkFotfKAAoJEGyIV+DY6PB0gE4QAKmqq0xju3jP pY8xgrSZua7nF2eO23cKvZ+GIfyimXeg8TNSAVKcopwXVFUib9330PCUhWxhn4Oc 0dr/9yy61b6UDB+KY9TsUvNXUVUnydvcCTyZjFv3e7/LAkhbuKGwBFcsP8tKNFuR xa9ov7/4tP1EiNrhY54Gdp6txOPuCZwYXjGnV/jOdgojCkcmksVWpvNSbwf6Xe+7 AfHZo2KI2efUaqiGIdVUXNPS6zRrnF9rtpqGZoiPlvcVfvTU0wHypFAwBUPba9G5 avt9tFcBDAmAxSRcw6jixddfZUU2oilcDaXwnKz93Uz06DqKxixWxayvTs6dyPzW U+R68PuckGVvjlzgI9k6GdqYhQWwlVDTRdq78Lfvlchx8jphdT5w9RePej+z89z1 EJZqZ4pRg/9s9U3p5jmm8xTRmVS/k6dMKSXNGogBktI9Rj1kDjRLltVfjKy3IhvU 6PdUTiB9E3a2Eubv5TSJ91Lq7CKp9GrLcV4GGCCdc3Ru6MRVW7XoCtx7yUGnXtlP 8O+2lr09jZJzc0QYNQUdLKyeUGmj9YnbE2hlDvoWYb6TWfQiIATXfAhd0i1js8DD qMaeVG0FpQNlMkdG9EKIlPeqDC8/JNOgSBiHkuAPx7u8c2F3ZGZ2ruIDQy+s/DIC FHKnS/NcQjq98EE7ivNw49Eq+nWnNNX8wsIFBBMBCgCYAhsBCAsJCAcNDAsKBRUK CQgLAh4BAheAcxSAAAAAACoAQHRpbWVzdGFtcCtiaXRjb2luLXRyYW5zYWN0aW9u QG1ldGFjb2RlLmJpemFmY2IwOTJjNWNhNjQwOTUyNmQxOGFlOWNmMjJkM2I1NWQz N2U3MjNlYjFiNzRlM2Y4NGY3ZTZiMDUyYTE2MmEFAloug90FCQPA+4YAIQkQbIhX 4Njo8HQWIQRlOQmi8ON8EG9fr1RsiFfg2OjwdAjZEAChkxR8xuGDusp3uY3zJRnB fCVGKtBBhxPQ0SuqAMhqPtUxhW39W0uMFC7wOixQTi3fmiO7rjdOtpA3bR1VH9Et AGfQcoyAXUCbKseO1url7EV/6QdGu9EKkujW0twC/33cGTo/0wZPG3bH4O2foR/s pDHDrBGC5nTC5q6mIJfRMj+BZrJ1PR/MeMiqdPyr+AUGH45+vAhb8jNjUFNzEfx9 7KrzP7mOQCcE6QPF9WWzfKY2TuWEU4kLTK13pDc4T96dHqdfnLtlMUDuo11eko3Z vA/EScxmsQroTf1YSzUr4XG9k+bHowQIVPCZijZXuPs+cGVgeG5eQ35INTaUhBHb +7KPilwp7M6PPVCSYo5X4ARbZuVoyru4AkfAlTnhkMEKJRM4tfM4oQsEcta4P8Sq JJO6umQRL8TNbAWDXfNZotT4KzcIB+Iz1C30JFCbVOxy0VTS256yMhEGyziXoAS2 lh4EeE+vW5QRoNaKoqv9Tx52biWrmYYTRAFYl+NlSiT9c8OjyH7244Nu8H+DoxA7 VVVwq8aqpYx0YpWoBUBxpVPoheiItgt6TaHddfpaiQtK4hcpcSv8Lsx6aGXRYNdI KjFiBNhMZu5yek6Cr+ZGaigve5lq5oehRRK+h5oww7qPjeEREd1Ro9HUnsuZ3HhU Nr68teyBjdZY1fU4gEQ4IsLB/wQTAQoAkgIbAQgLCQgHDQwLCgUVCgkICwIeAQIX gAUCWdt7wXMUgAAAAAAqAEB0aW1lc3RhbXArYml0Y29pbi10cmFuc2FjdGlvbkBt ZXRhY29kZS5iaXphZmNiMDkyYzVjYTY0MDk1MjZkMThhZTljZjIyZDNiNTVkMzdl NzIzZWIxYjc0ZTNmODRmN2U2YjA1MmExNjJhACEJEGyIV+DY6PB0FiEEZTkJovDj fBBvX69UbIhX4Njo8HTY2w/+PjQuW9QwnIIgekfNibeIMNXusCbizyYs3ZxVyYv0 58xNd+oP8/E0XY6lFfD407j8zbBAPFC3H5FmlVQzZlaBBKP/4bH5wHKIkYK/x/A9 wQTI3RrSgzJVZaJvvTbXGZ8klxtzQjDzuCUZJOMRypkw5Df04BsIQe0h2E1m1X9N +T5DvhULU1/iRIJe1zCRwgUj7aJ9Vb5na79eIViEtJImIv8YVbge9lL6Peq+7PRf kjihWW6hNA/sU5uVROSbLXp2O/CdgWn4GwvFkogcDd8fVIii5dLaa4/jTS/tNSTF cneSemxKEbasnewF5WmoB9z50S+53VYj/Ia8oB26SXswsoLyqVlrEjR/hfdqqk+j TbYGFXiqonmZv8CztxS9vLoF5M6mH5NkwEmcaYA1Aeh6dgkoLJpnfntYGZ2Wd5pb yf968N/v/9SYcpHIZD8xTI91wndeua637JaM48ntUI9wvl8qqiQ/fstGZ3I66niY 3YWUZ8ORa/XwL4WHh4GO469Zjc9q/uIT1s4i2aS3N9rBPgtL9jY4fkhEttlbjZ8d BR+jWmROdwvbHjQp6phBXDszJoGe38S0gudGKUseWxR73U26s5w+OwcVUDQl0+MK RRBoahIiRUoETO5CsvjNp434anE4zH5yrngAk6WPQ8O0ZjWDxhDu3sxSE5OPXLmq a23CwYsEEwEKAB4FAlhoYHoCGwEICwkIBw0MCwoFFQoJCAsCHgECF4AAIQkQbIhX 4Njo8HQWIQRlOQmi8ON8EG9fr1RsiFfg2OjwdNwtEADR2esveX0Z6qFBpjFrqJPx Rddlmn7EUJB3lwQAv3dQ6vgmEyL6UgIRVmjORPaNO4wAmW8to5JEvixKMY/YV5dU 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 XMr2NIJP2w6J828dI7xnJKim37ZDegmuFw== =D3yY -----END PGP PUBLIC KEY BLOCK----- """ sequoia-git-0.1.0/scripts/gitlab.sh000075500000000000000000000021241046102023000153400ustar 00000000000000#!/bin/bash set -exuo pipefail if test x$CI_DEFAULT_BRANCH = x then : Environment variable CI_DEFAULT_BRANCH not set. exit 1 fi : HEAD is $(git rev-parse HEAD). MAIN=refs/remotes/origin/$CI_DEFAULT_BRANCH : Default branch is $CI_DEFAULT_BRANCH, $(git rev-parse $MAIN). if git merge-base --is-ancestor $MAIN HEAD then : $CI_DEFAULT_BRANCH can be fast-forwarded. : : Commits since $CI_DEFAULT_BRANCH: git log --format=oneline $MAIN..HEAD : : Authenticating commits: sq-git log --keep-going --trust-root `git rev-parse $MAIN` else FORK=`git merge-base $MAIN HEAD || true` if test x$FORK = x then : Failing. HEAD does not appear to have a common fork point with $MAIN. exit 1 else : MR needs to be merged or rebased. Fork point is $FORK. : : Authenticating commits: git log --format=oneline $FORK..HEAD : : Authenticating commits, using fork point $FORK as trust root: sq-git log --keep-going --trust-root $FORK : Failing. Cannot fast-forward $MAIN. exit 1 fi fi sequoia-git-0.1.0/spec/Makefile000064400000000000000000000010001046102023000144320ustar 00000000000000#!/usr/bin/make -f # dependencies: # apt install weasyprint xml2rfc ruby-kramdown-rfc2629 draft = draft-sequoia-git OUTPUT = $(draft).txt $(draft).html $(draft).xml $(draft).pdf all: $(OUTPUT) %.xmlv2: sequoia-git.md kramdown-rfc2629 < $< > $@.tmp mv $@.tmp $@ %.xml: %.xmlv2 xml2rfc --v2v3 $< -o $@ %.html: %.xml xml2rfc $< --html -o $@ %.txt: %.xml xml2rfc $< --text -o $@ %.pdf: %.xml xml2rfc $< --pdf -o $@ clean: -rm -rf $(OUTPUT) $(draft).xmlv2 .PRECIOUS: $(draft).xmlv2 .PHONY: clean all sequoia-git-0.1.0/spec/sequoia-git.md000064400000000000000000001012611046102023000155550ustar 00000000000000--- title: Supply Chain Security for Version Control Systems abbrev: Supply Chain Security for VCSs docname: draft-nhw-openpgp-supply-chain-security-vcs-00 date: 2023-06-20 category: info submissiontype: independent ipr: trust200902 area: int workgroup: openpgp keyword: Internet-Draft stand_alone: yes pi: [toc, sortrefs, symrefs] venue: group: "OpenPGP" type: "Working Group" mail: "openpgp@ietf.org" arch: "https://mailarchive.ietf.org/arch/browse/openpgp/" repo: "https://gitlab.com/sequoia-pgp/sequoia-git" latest: "https://sequoia-pgp.gitlab.io/sequoia-git/" author: - ins: N.H. Walfield name: Neal H. Walfield org: Sequoia PGP email: neal@sequoia-pgp.org - ins: J. Winter name: Justus Winter org: Sequoia PGP email: justus@sequoia-pgp.org normative: RFC2119: RFC4880: RFC8174: toml: author: - ins: T. Preston Werner name: Tom Preston-Werner - ins: P. Gedam name: Pradyun Gedam title: TOML v1.0.0 date: 2021-01-12 target: https://toml.io/en/v1.0.0 informative: event-stream: author: - ins: T. Hunter name: Thomas Hunter II title: "Compromised npm Package: event-stream" date: 2018-11-27 target: https://medium.com/intrinsic-blog/compromised-npm-package-event-stream-d47d08605502 dependency-confusion: author: - ins: A. Birsan name: Alex Birsan title: "Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies" date: 2021-02-09 target: https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610 reflections-on-trusting-trust: DOI.10.1145/358198.358210 guix: author: - ins: L. Courtès name: Ludovic Courtès title: Building a Secure Software Supply Chain with GNU Guix date: 2022-06 doi: 10.48550/arXiv.2206.14606 target: https://arxiv.org/abs/2206.14606 --- abstract In a software supply chain attack, an attacker injects malicious code into some software, which they then leverage to compromise systems that depend on that software. A simple example of a supply chain attack is when SourceForge, a once popular open source software forge, injected advertising into the binaries that they delivered on behalf of the projects that they hosted. Software supply chain attacks are different from normal bugs in that the intent of the perpetrator is different: in the former case, bugs are added with the intent to harm, and in the latter they are added inadvertently, or due to negligence. Software supply chain security starts on a developer's machine. By signing a commit or a tag, a developer can assert that they wrote or approved the change. This allows users of a code base to determine whether a version has been approved, and by whom, and then make a policy decision based on that information. For instance, a packager may require that software releases be signed with a particular certificate. Version control systems such as git have long included support for signed commits and tags. Most developers don't sign their commits, and in the cases where they do, it is usually unclear what the semantics are. This document describes a set of semantics for signed commits and tags, and a framework to work with them in a version control system, in particular, in a git repository. The framework is designed to be self contained. That is, given a repository, it is possible to add changes, or authenticate a version without consulting any third parties; all of the relevant information is stored in the repository itself. By publishing this draft we hope to clarify and enrich the semantics of signing in version control system repositories thereby enabling a new tooling ecosystem, which can strengthen software supply chain security. --- middle # Introduction ## Requirements Language The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 {{RFC2119}} {{RFC8174}} when, and only when, they appear in all capitals, as shown here. ## Terminology - "Maintainer" is a software developer, who is responsible for a software project in the sense that they act as a gatekeeper, and decide with other maintainers what changes are acceptable, and should be added to the software. - "Contributor" is someone who contributes changes to a software project. Unlike a maintainer, a contributor cannot add their changes to a project on their own accord. - "Software supply chain" is the collection of software that something depends on. For instance, a software package depends on libraries, it is built by a compiler, it is distributed by a package registry, etc. - "Software supply chain attack" is an attack in which an attacker compromises a software supply chain. For instance, a maintainer or a contributor may stealthily insert malicious code into a software project in order to compromise the security of a system that depends on that software. - "Version control system" is a database, which contains versions of a software project. Each version includes links to preceding versions. - "git" is a popular version control system. Although "git" is distributed and does not rely on a central authority, it is often used with one to simplify collaboration. Examples of centralized authorities include gitea, GitHub, and Gitlab. - "Commit" is a version that is added to the "version control system". In git, commits are identified by their message digest. - "Branch" is a typically human readable name given to a particular commit. When a commit is superseded, the branch is updated to point to the new commit. Repositories normally have at least one branch called "main" or "master" where most work is done. - "Tag" is a name given to a particular commit. Tags are usually only added for significant versions like releases and are normally not changed once published. - "Change" is a commit or a tag. - "Forge" is a service which hosts software repositories, and often provides additional services like a bug tracker. Examples of forges are codeberg, GitHub, and GitLab. - "Registry" or "Package Registry" is a service that provides an index of software packages. Maintainers register their software there under a well-known name. Build tools like `cargo` fetch dependencies by looking up the software by its name. - "Authentication" is the process of determining whether something should be considered authentic. - "Trust model" is a process for determining what evidence to consider, and how to weigh it when doing authentication. - "OpenPGP certificate" or just "certificate" is the data structure that section 11.2 of {{RFC4880}} defines as a "Transferable Public Key". A certificate is sometimes called a key, but this is confusing, because a certificate contains components that are also called keys. - "Liveness" is a property of a certificate, a signature, etc. An object is considered live with respect to some reference time if, as of the reference time, its creation time is in the past, and it has not expired. # Problem Statement Consider the following scenario. Alice and Bob are developers. They are the primary maintainers of the Xyzzy project, which is a free and open source project. Although they do most of the work on the project, they also have occasional collaborators like Carol, and drive-by contributions from people like Dave. Paul packages their software for an operating system distribution. Ted from Ty Coon Corporation integrates it into his company's software. And, Mallory is an adversary who is trying to subvert the project. When someone updates their local copy of Xyzzy's source code repository, they want to authentic any changes before they use them. That is, they want to know that each change was made or approved by someone whom they consider authorized to make that change. In the Xyzzy project, Alice is willing to rely on Bob to check-in changes he makes, and to approve contributions from third parties without auditing the code herself. But, she doesn't want to rely on anyone else without checking their proposed changes manually. Bob feels the same way about Alice. In version control systems like `git`, the meta-data for a commit or tag includes `author` and `committer` fields. By themselves, these fields cannot be used to reliably determine who a change's author and committer are, because these fields are set by the committer and unauthenticated. That is, Mallory could author a commit, set both of these fields to "Bob," and push the malicious commit. No one would be able to tell that they came from Mallory and not Bob. There are two main ways to authenticate changes. First, changes to a repository or branch can be mediated by a trusted third party, which enforces a policy at the time a change is added to the repository. Second, individual changes can be signed, and a policy can be evaluated at any time. These two approaches can be mixed. ## Repositories Protected by a Trusted Third Party When using a trusted third party, only certain users are allowed to change the repository. This is often realized using access control lists: the trusted third party has a list of users who are allowed to do certain types of modifications. Before the trusted third party allows a user to modify the repository, the user has to authenticate themselves. When they attempt to make a change, the trusted third party checks that they are authorized. If they are, the third party allows the modification. If not, it is rejected. A user of this repository can now conclude that if they can authenticate the trusted third party, then the changes were approved. A drawback of using a trusted third party is that it relies on centralized infrastructure. This means the only way for a user to determine if a version of Xyzzy is authentic is to fetch it from the trusted third party; the repository is not self authenticating. If the third party ever disappears, users will no longer be able to authenticate the project's source code. Another disadvantage is that this approach doesn't expose the project's policy to its users. This means that both first-parties like Alice and third-parties like Paul are not able to audit the trusted third party. This is the case even if the set of users that are currently authorized to make changes are exposed via a separate API end point: because the set of authorized users changes with time, all updates to the ACLs would need to be exposed along with information about what user authorized each change. ## Self-Authenticating Repositories An alternative approach is to have authors and committers sign their changes. Users then check that the changes are signed correctly, and authenticate the signers. For instance, for the Xyzzy project, Paul might decide that Alice or Bob are allowed to make changes. So when Paul fetches changes, he checks whether Alice or Bob signed the new changes, and flags changes made by anyone else. If Alice and Bob later decide that Carol should also be allowed to directly commit her changes, Paul needs to update his policy. If Bob leaves the team, Paul needs to pay enough attention to notice, and then disallow changes made by Bob after a certain date. For projects that sign their commits today, this is more or less the status quo. Most users, however, do not want to maintain their own policy, and aren't even in a good position to do so. Since users are willing to rely on the maintainers to make changes to the project, they can just as well delegate the policy to them. Now, a user like Paul just needs to designate an initial policy. If he knows when the policy changes, and can authenticate changes to the policy based on the existing policy, then he is able to authenticate any subsequent changes to the repository. An easy way to manage the policy is to include it in the repository itself. Then changes to the policy can be authenticated in the same way as normal changes. This also makes the repository self authenticating, because it is self contained. One issue is how users should handle forks to a project. A fork in a project may occur due to a social or technical conflict, or because the project dies, and is later revived by a different party. In both cases, it may not be possible for there to be a clean hand off to the new maintainer. That is, Alice or Bob may not be willing or able to change the policy file to allow Dave to seamlessly continue the development of Xyzzy. Forks are straightforward to handle, but require user intervention: from the system's perspective, Dave is not authorized, so his changes are rejected. And that's good, as Dave may be an attacker; the system can't tell. Users opt in to a fork by changing their trust root to designate a version in which Dave is authorized to make changes. # Threat Model Consider an attacker, Mallory, who is trying to compromise a user, Ursula, by injecting a vulnerability into the software supply chain of a piece of software, Super Frob, that she uses. There are several different ways that Mallory could accomplish this. These include: - Mallory could pose as a contributor, and convince a develop to authorize a malicious change to one of Super Frob's dependencies, such as a library. - Mallory could take over an abandoned package that Super Frob depends on, and publish a new version with malicious code. - Mallory could use typo squatting to opportunistically or through social engineering inject malicious software into Super Frob's supply chain. For instance, Mallory could publish a library called `libevent`, which is a copy of `libevents`, but includes a malicious change, and Super Frob accidentally includes `libevent` as a dependency instead of `libevents`. - Mallory could publish a malicious package that has the same name as a package on another registry in order to confuse Super Frob's build tools. This type of attack is called a dependency confusion attack, {{dependency-confusion}}. It can be launched when an organization uses an internal registry and a public registry to find dependencies. As dependencies are often referenced by name, and that name does not include the registry, an attacker may trick the organization into using their malicious version of the package. - Mallory could sneak a change into one of Super Frob's build dependencies, like the compiler. Whereas software maintainers have a large degree of control over their direct dependencies, they have more limited control over the tools downstream users use to build their software. In the extreme, a software project may include a copy of a dependency in their version control system, or depend on a specific version of a dependency by cryptographic hash, but only specify a standard that the compiler needs, like C99. This attack is most well-known from Ken Thompson's Reflections on Trusting Trust Turning award lecture, {{reflections-on-trusting-trust}}. - Mallory could compromise the tools that a developer uses, e.g., by publishing a useful, but malicious plug-in for an editor, which detects certain code patterns, and quietly modifies them to insert malicious code. - Mallory could compromise the systems that the developers use, and modify their source code repositories. For instance, if Mallory gets access to a developer's machine, he could stealthy modify code before it is signed and committed. Or, he could exfiltrate the developer's signing key, or login credentials and imitate her. Similarly, if a software project uses a forge and Mallory is able to compromise the forge, he could modify the source code. - Mallory could compromise Super Frob or one of its dependencies as it is being downloaded. For instance, if a package registry like `crates.io` depends on a content delivery network (CDN) to distribute packages, a compromised node in the CDN may return a modified version of the software to the user. The setting is as follows. To protect herself from Mallory, Ursula has to make sure that versions of the software she obtains do not contain malicious code. Ursula cannot afford to audit every version of the software, but she is willing to rely on the maintainers of the project to not add malicious code, and to review contributions from third parties. The framework presented in this specification allows Ursula to audit a dependency and its developers once, and then to delegate decisions of what code and dependencies to include to the developers. Assuming the developers are reliable, this can protect Ursula from attacks where Mallory is not explicitly authorized to make a change. For instance, if the developers of an abandoned software package do not authorize a new maintainer, Ursula will be warned when a package has a new maintainer, as she can no longer authenticate it. She can then reaudit it. Similarly, when the software is modified in transit by a machine in the middle, Ursula will not be able to authenticate it. This can also stop dependency confusion attacks, because the software cannot be authenticated. It won't however, stop a downgrade attack, as older versions can still be authenticated. This framework cannot protect Ursula from mistakes that she or a developer of the software that she depends on makes. For instance, if Mallory is able to convince a developer to authorize a malicious change to their software, this framework consider the change to be legitimate. This framework can facilitate forensic analysis in these case by making it easier to identify changes approved by the same person (potentially across different projects) and thereby conduct a targeted audit. # Authentication This framework helps users authenticate three types of artifacts: commits, tags, and tarballs or other archives. ## Policy Every commit has an associated policy. If a commit contains the file `openpgp-policy.toml` in the root directory, then that file describes the commit's policy. If the commit does not contain that file, the void policy is used. The void policy rejects everything. `openpgp-policy.toml` is a TOML v1.0.0 file {{toml}}. Version 0 defines the following three top-level keys: `version`, `authorization`, and `commit_goodlist`. If a parser recognizes the version, but encounters keys that it does not know, then it must ignore the unknown keys. This allows a degree of forwards compatibility. ### version The value of the `version` key is an integer and must be `0`: version = 0 If the value of `version` is not recognized, the implementation SHOULD error out. It MAY instead treat the policy as the void policy. ### authorization `authorization` is a table of authorization entries. Each key in the `authorization` table is a free-form identifier, which is chosen by the user of the system. The identifier SHOULD be a UTF-8 encoded, human-readable string that identifies an entity. Examples of identifiers are `alice`, `Bob `, `Boty McBotface `. The value of each authorization entry is another table. The table has the following entries: - `keyring` - `sign_commit` - `sign_tag` - `sign_archive` - `audit` - `add_user` - `retire_user` #### keyring The value of `keyring` is a string. It contains one or more OpenPGP certificates. The OpenPGP certificates MUST be ASCII-armored. An ASCII-armored block MAY contain more than one OpenPGP certificate. The string MAY contain multiple ASCII-armored blocks. An implementation SHOULD ignore valid OpenPGP certificates that is does not support, and MAY emit a warning that a certificate, or component is not supported. An implementation SHOULD return an error if it encounters something other than an OpenPGP certificate encoded with ASCII armor. When adding a certificate, an implementation SHOULD only add components that are needed to validate the signatures. That is, an implementation SHOULD strip subkeys that are not signing capable, and third-party signatures. For components that are kept, an implementation SHOULD include all known self signatures, and not just the newest self signature. #### sign_commit The value of `sign_commit` is a boolean. If `true`, then the entity is authorized to sign commits. #### sign_tag The value of `sign_tag` is a boolean. If `true`, then the entity is authorized to sign tags. #### sign_archive The value of `sign_archive` is a boolean. If `true`, then the entity is authorized to sign tarballs or other archives. #### audit The value of `audit` is a boolean. If `true`, then the entity is authorized to add commits to the top-level `commit_goodlist` array. #### add_user The value of `add_user` is a boolean. If `true`, then the entity is authorized to add new entities to the authorization table, and to grant them any capabilities that they have. #### retire_user The value of `retire_user` is a boolean. If `true`, then the entity is authorized to retire capabilities from any entity. This includes capabilities that they do not have. #### Example The following is an example of an authorization entry. The user has been granted all the capabilities. The user is identified by two different OpenPGP certificates. The certificates are contained in two concatenated ASCII armored blocks. [authorization."Neal H. Walfield "] sign_commit = true sign_tag = true sign_archive = true add_user = true retire_user = true audit = 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 xsEhBFUjmukBDqCpmVI7Ve+2xTFSTG+mXMFHml63/Yai2nqxBk9gBfQfRFIjMt74 =MESu -----END PGP PUBLIC KEY BLOCK----- """ ### commit_goodlist The value of `commit_goodlist` is an array of strings where each string contains a commit identifier. The commit identifier MUST be a full hash. The commit identifier MUST NOT be a branch name, a tag name, or a truncated hash. Commits listed in the `commit_goodlist` are commits that have retroactively been marked as valid. This may be useful when a certificate's private key material has been compromised. ## Authenticating Commits Each commit in a `git` repository is part of a directed acyclic graph (DAG) where a node is a commit, and a directed edge shows how two commits are related. Specifically, the head of a directed edge is a commit that is derived from the tail. Except for the root commits, each commit has one or more parents. A commit that has multiple parents is derived from multiple commits. Conceptually, it merges multiple paths, and as such is called a merge commit. A commit is consider authenticated if at least one of its parent commits considers the commit to be authenticated. This rule is different from Guix's *authorization invariant* as described in {{guix}}, which states that all parent commits must consider the commit to be authenticated. The semantics described here allow a developer to add commits from unauthorized third-parties as-is using a merge commit. Using Guix's authorization invariant, the third party's commit would have to be resigned, which loses the third-party's signature, and consequently complicates forensic analysis. A commit's parent authenticates it as follows. First, the implementation looks up the signer's certificate in the parent commit's policy file. The implementation SHOULD then canonicalize the certificate so that the active self signatures are those that were active when the signature was made. A self signature is valid, if it is not revoked, and not expired. A self signature is active, if it is the most recent, valid self signature prior to a reference time. That is, if a new commit was made on June 9, 2023, then each component's most recent signature as of June 9, 2023, which is also not revoked, and not expired, is considered that component's active self signature. If the canonicalized certificate is valid as of the signature's time, not expired as of that time, not soft revoked as of that time, not hard revoked at any time, and the signature is correct, then the signature is considered verified. The implementation MAY consider certificate updates from other sources. If it does, it SHOULD only consider hard revocations. The implementation MUST then check that the type of change is authorized by the policy. The following capabilities allow the specified types of changes: - `sign_commit`: Needed for any change. - `add_user`: Needed to delegate a capability to another user. Updating `keyring` does not require this capability if a certificate is only updated, and not added. - `retire_user`: Needed to rescind a capability from another user. - `audit`: Needed to modify the `version` field, and the `commit_goodlist` list. If the signature is considered verified, and the signer is authorized to make the type of change that was made, then the commit is considered authenticated. If the commit is not considered authenticated, because the signer's certificate has been hard revoked, but the commit is included in a later commit's `commit_goodlist`, then the commit is considered to be authenticated. A commit is considered to occur later if when authenticating a range of commits, a commit is a direct descendant of the commit in question, and it is in the commit range. Consider the three commits `a`, `b`, and `c` where `a` is `b`'s parent, `b` is `c`'s parent, the certificate used to sign `b` has been hard revoked, and `c` includes `b` in its `commit_goodlist`. In this case, the hard revocation for the certificate to use `b` is ignored. All other criteria including the fact that the signature on `b` is valid are still checked. ## Authenticating Tags A tag is a special type of commit in `git`, which has no content, but assigns a name to a specific commit. A tag is usually used to mark release points. A tag is authenticated in the same way as a commit, as described in the previous section, with the following exceptions. First, the tagged commit is considered a parent commit, and the tag is considered its child commit. The entity that signed the tag needs the `sign_tag` capability, and only the `sign_tag` capability. ## Authenticating Archives Archives like tarballs are often generated as part of a software's release process. These may be signed. To authenticate an archive with respect to a signature, and a trust root, the trust root's policy is used to authenticate the tarball's signature. The entity that signed the tarball must have the `sign_archive` capability. Unlike a commit, an archive does not have a pointer to the commit that it was derived from. Thus, if an archive is derived from commit `c`, it may be possible to authenticate commit `c`, as well as tags referring to commit `c` using a given trust root, but to not authenticate an archive derived from commit `c` using the same trust root, because the policy changed in the meantime. If the signature includes the notation `commit@notations.sequoia-pgp.org`, then the value of the notation is interpreted as the commit that the archive is derived from. The value of the notation is a hexadecimal value corresponding to the commit's full hash. Truncated hashes MUST be considered erroneous. The commit identifier MUST NOT be a branch name, a tag name, or a truncated hash. Since archives are often verified outside of a repository, one or more repositories may be specified using the `repository@notations.sequoia-pgp.org` notation. In that case, each notation indicates a git repository. For example, the main repository of the reference implementation, `sq-git`, is `https://gitlab.com/sequoia-pgp/sequoia-git.git`. So, archives SHOULD include the `repository@notations.sequoia-pgp.org` notation with `https://gitlab.com/sequoia-pgp/sequoia-git.git` as the value. When `commit@notations.sequoia-pgp.org` is present in the signature, the implementation MUST use that commit's policy to authenticate the archive, and then authenticate that commit by chaining back to the trust root, as described above; in this case, it MUST NOT use the trust root's policy directly unless the specified commit is also the trust root. # Reference implementation A Rust implementation of this specification is part of Sequoia. See https://gitlab.com/sequoia-pgp/sequoia-git for the source code. # Security Concerns ## Malicious vs. Buggy Changes The scheme presented here can help mitigate malicious attacks on a code base, but it does nothing to prevent design flaws or code errors. That is, this scheme does not and cannot provide any protections from normal bugs. ## Trusted Developers The protections outlined in this document are mainly designed to stop third-parties from adding malicious code to a project. This system provides no protection from a developer who is authorized to make changes and turns out to be malicious. That said, because commits are signed, when malicious code is discovered, an audit is required to restore trust in the code base. Using this system, it is easier to identify other code added by the same person, and focus an audit on that code. ## Judging Code vs. Judging Humans The approach described in this document relies on transitive trust. The basic idea is that if a user is willing to run a developer's code, then they can reasonably rely on that developer to modify the code, and to delegate that capability to a third party. Yet, writing and reviewing code is fundamentally different from evaluating another person's intents. This is demonstrated quite well by the events surrounding the popular `event-stream` npm package, {{event-stream}}. In 2018, a new developer gained the trust of the package's maintainer by contributing a number of high-quality changes. The original developer eventually made the new developer the maintainer, and the new maintainer introduced malicious code to steal user's credentials. ## Operational Security Signing commits relies on each developer having a long-term identity key, which they keep safe. If the key is compromised, the attacker is able to impersonate the developer. It is possible to limit the damage by revoking the compromised key, or having another authorized user retire the developer's access. In this regard, sigstore appears to be better as it relies on ephemeral signing keys, which are issued by a central authority. However, in order to obtain a signing key, the user needs to log in. If they use a password, then if an attacker gets access to the password, an attacker can impersonate the developer. If the developer uses a second factor like a hardware token, then they are again using private key cryptography, and may as well put their private keys on a hardware token, and forego the centralized infrastructure. ## Dependencies This specification has concentrated on enabling a user of a software project to authenticate new versions. But most software has its own dependencies, and those also need to be authenticated. A user could identify all software that they are willing to rely on, but this is more work than most users are willing and able to do. But, just as developers are usually in a better position to evaluate who should be allowed to contribute to their project, they are also in a better position to designate a trust root for their dependencies. Enabling this functionality requires ecosystem-specific tooling. The developer needs to be able to specifying a trust root for each dependency, and the build infrastructure needs to authenticate the dependencies. For instance, the Rust ecosystem uses Cargo for building and dependency management. Currently, to add `sequoia-openpgp` as a dependency to a project, a developer would modify their `Cargo.toml` file as follows: [dependencies] sequoia-openpgp = { version = "1" } Instead, they would also specify a trust root, which they've presumably audited: [dependencies] sequoia-openpgp = { version = "1", trust-root = "HASH" } When downloading the dependency, `cargo` would make sure that the dependency can be authenticated from the specified trust root, and if not throw an error. ## Document History This is a first draft that has not been published. # Acknowledgments My thanks go---in particular, but not only---to the Sequoia PGP team for many fruitful discussions. Funding for this project was provided by the Sovereign Tech Fund. sequoia-git-0.1.0/src/cli/mod.rs000064400000000000000000000512771046102023000145500ustar 00000000000000use std::path::{ PathBuf, }; use clap::{ ArgGroup, Parser, }; // Note: this file is used both from main.rs and from build.rs. This // means that we can only use what is under the cli module! // // We also try to minimize the number of build dependencies to reduce // the build time. This includes not depending on sequoia-openpgp. // Since we do need a couple of types from sequoia-openpgp, we mock // them in build.rs. If you import another type here, you'll need to // mock that too. use crate::openpgp; use openpgp::{ KeyHandle, // Mock additional imports in build.rs! }; /// Returns the cert store's base directory. pub fn cert_store_base() -> PathBuf { // XXX: openpgp-cert-d doesn't yet export this: // https://gitlab.com/sequoia-pgp/pgp-cert-d/-/issues/34 // Remove this when it does. dirs::data_dir() .expect("Unsupported platform") .join("pgp.cert.d") } #[derive(Parser)] #[command( author, name = "sq-git", version = format!("{}{}", clap::crate_version!(), if let Some(v) = option_env!("VERGEN_GIT_DESCRIBE") { format!("-g{}", v) } else { "".into() }), about = "A tool to help protect a project's supply chain.", long_about = "\ \"sq-git\" is a tool that can help improve a project's supply chain security. To use \"sq-git\", you add a policy file (\"openpgp-policy.toml\") to the root of a \"git\" repository. The policy file includes a list of OpenPGP certificates, and the types of changes they are authorized to make. The capabilities include adding a commit, and authorizing a new certificate. See the \"sq-git init\" and \"sq-git policy\" subcommands for more details. A commit is considered authorized if the commit is signed, and at least one immediate parent commit's policy authorizes the signer's certificate to make that type of change. A downstream user authenticates a version of the project using the \"sq-git log\" subcommand. They specify a trust root (a commit), which they've presumably audited, and \"sq-git log\" looks for an authenticated path from the trust root to the current \"HEAD\". If there is an authenticated path, then there is evidence that the project's maintainers authorized all of the intermediate changes. To find an authenticated path, \"sq-git\" starts with the current commit, and tries to authenticate it using each of its parent commits. It repeats this process for each parent commit that authenticated it. If the trust root is reached, then the version is considered authenticated.", )] pub struct Cli { #[clap( long, help = "Disables the use of a certificate store", long_help = format!("\ Disables the use of a certificate store. By default, sq-git uses the \ user's standard cert-d, which is located in \"{}\".", cert_store_base().display()), )] pub no_cert_store: bool, #[clap( long, value_name = "PATH", env = "SQ_CERT_STORE", conflicts_with_all = &[ "no_cert_store" ], help = "Specifies the location of the certificate store", long_help = format!("\ Specifies the location of the certificate store. By default, sq-git \ uses the the user's standard cert-d, which is located in \"{}\".", cert_store_base().display()), )] pub cert_store: Option, /// Use POLICY instead of the repository's policy. /// /// The default policy is stored in the "openpgp-policy.toml" /// file in the root of the repository. #[arg(global = true, long, value_name = "POLICY")] pub policy_file: Option, #[arg( global = true, long = "output-format", value_name = "FORMAT", value_parser = ["human-readable", "json"], default_value = "human-readable", help = "Produces output in FORMAT, if possible", )] pub output_format: String, #[command(subcommand)] pub subcommand: Subcommand, } #[derive(Parser, Debug)] #[clap( name = "init", about = "Suggests how to create a policy", long_about = "\ Suggests how to create a policy by analyzing recent commits. The heuristic considers signed commits on the current branch that were made over the past half year. The heuristic suggests that the most frequent committer be made the project maintainer, and other committers be made committers. Note: This is a *simple* heuristic; its recommendations should be viewed as a starting point. In particular, you still need to do some due diligance. It is essential that you review the suggested roles, and check that people actually control the certificates. Ideally, you should ask each person for their OpenPGP fingerprint in person. But in the very least you should ask them via email.", after_help = "\ EXAMPLES: # Inspects the current branch and suggests how to create a policy. $ sq-git init", )] pub struct InitSubcommand { /// Show additional information. #[clap( short='v' )] pub verbose: bool, } /// Describe, update, and change the OpenPGP policy. #[derive(clap::Subcommand)] pub enum PolicySubcommand { /// Describes the policy. /// /// This reads in the policy default and dumps it in a more /// descriptive format on stdout. Describe { }, /// Changes the authorizations. /// /// A certificate can delegate any of its capabilities to another /// certificate without breaking an authentication chain. /// /// To fork a project, you create a new policy file. Authorize { name: String, #[command(flatten)] cert: CertArg, /// Grant the certificate the sign-commit capability. /// /// This capability allows the certificate to sign commits. /// That is, when authenticating a version of the repository, /// a commit is considered authenticated if it is signed by a /// certificate with this capability. #[arg(long, overrides_with = "no_sign_commit", default_value_ifs( [("committer", "true", Some("true")), ("release_manager", "true", Some("true")), ("project_maintainer", "true", Some("true"))])) ] sign_commit: bool, /// Rescind the sign-commit capability from a certificate. /// /// Removes the sign-commit capability for the certificate. /// Note: this operation is not retroactive; commits signed /// with the certificate, and made prior to the policy change /// are still considered authenticated. #[clap(long)] no_sign_commit: bool, /// Grant the certificate the sign-tag capability. /// /// This capability allows the certificate to sign tags. That /// is, when authenticating a tag, a tag is considered /// authenticated if it is signed by a certificate with this /// capability. #[arg(long, overrides_with = "no_sign_tag", default_value_ifs( [("release_manager", "true", Some("true")), ("project_maintainer", "true", Some("true"))])) ] sign_tag: bool, /// Rescind the sign-tag capability from a certificate. /// /// Removes the sign-tag capability for the certificate. /// Note: this operation is not retroactive; tags signed with /// the certificate, and made prior to the policy change are /// still considered authenticated. #[clap(long)] no_sign_tag: bool, /// Grant the certificate the sign-archive capability. /// /// This capability allows the certificate to sign tarballs or /// other archives. That is, when authenticating an archive, /// an archive is considered authenticated if it is signed by /// a certificate with this capability. #[arg(long, overrides_with = "no_sign_archive", default_value_ifs( [("release_manager", "true", Some("true")), ("project_maintainer", "true", Some("true"))])) ] sign_archive: bool, /// Rescind the sign-archive capability from a certificate. /// /// Removes the sign-archive capability for the certificate. /// Note: this operation is not retroactive; archives signed /// with the certificate prior to the policy change are still /// considered authenticated. #[clap(long)] no_sign_archive: bool, /// Grant the certificate the add-user capability. /// /// This capability allows the certificate add users to the /// policy file, and to grant them capabilities. A /// certificate that has this capability is only allowed to /// grant capabilities that it has. That is, if Alice has the /// "sign-commit" and "add-user" capability, she can grant Bob /// either of those capabilities, but she is can't grant him /// the "sign-tag" capability, because she does not have that /// capability. #[arg(long, overrides_with = "no_add_user", default_value_ifs( [("project_maintainer", "true", Some("true"))])) ] add_user: bool, /// Rescind the add-user capability from a certificate. /// /// Removes the add-user capability for the certificate. /// Note: this operation is not retroactive; operations that /// rely on this grant prior to the policy change are still /// considered authenticated. /// /// Rescinding the add-user capability from a certificate does /// not rescind any grants that that certificate made. That /// is, if Alice grants Bob the can-sign and add-user /// capability, Bob grants Carol the can-sign capability, and /// then Alice rescinds Bob's can-sign and add-user /// capabilities, Carol still has the can-sign capability. In /// this way, a grant is a copy of a capability. #[clap(long)] no_add_user: bool, /// Grants the certificate the retire-user capability. /// /// This capability allows the certificate to rescind /// arbitrary capabilities. That is, if Alice has the /// retire-user capability, she can rescind Bob's can-sign /// capability even if she didn't grant him that capability. #[arg(long, overrides_with = "no_retire_user", default_value_ifs( [("project_maintainer", "true", Some("true"))])) ] retire_user: bool, /// Rescind the retire-user capability from a certificate. /// /// Removes the retire-user capability from a certificate. /// The specified certificate cannot no longer rescind /// capabilities even those that they granted. #[clap(long)] no_retire_user: bool, /// Grants the certificate the audit capability. /// /// This capability allows the certificate to audit commits. /// If Alice has the audit capability, Bob has the can-sign /// capability, and then Bob revokes his key, because it was /// compromised, then all commits that Bob signed are /// considered invalid. Alice can recover from this situation /// by auditing Bob's commit. After auditing each commit, she /// marks it as good. #[arg(long, overrides_with = "no_audit", default_value_ifs( [("project_maintainer", "true", Some("true"))])) ] audit: bool, /// Rescind the audit capability from a certificate. /// /// Removes the audit capability from a certificate. The /// specified certificate cannot no longer mark arbitrary /// commits as good. #[clap(long)] no_audit: bool, /// Grants all capabilities relevant to a project maintainer. /// /// A project maintainer is a person who is responsible for /// maintaining the project. This options grants the /// certificate all capabilities. #[arg(long)] project_maintainer: bool, /// Grants all capabilities relevant to a release manager. /// /// A release manager is authorized to commit changes, and /// make releases. This options grants the certificate the /// "sign-tag", "sign-archive", and "sign-commit" /// capabilities. #[arg(long)] release_manager: bool, /// Grants all capabilities relevant to a committer. /// /// A committer is authorized to commit changes to the code. /// This options grants the certificate the "sign-commit" /// capability. #[arg(long)] committer: bool, }, /// Updates the OpenPGP certificates in the policy. /// /// "sq-git" looks for updates to the certificates listed in the /// policy file in the user's certificate store, and on /// the main public keyservers. /// /// # Examples /// /// # Download updates to the specified certificate from WKD to /// # the local certificate store. /// $ sq wkd get neal@walfield.org /// /// # Look for updates. /// $ sq-git policy sync Sync { /// Looks for updates on the specified keyservers. /// /// In addition to looking in the local certificate store, /// also looks for updates in the specified keyserver. #[clap( long, short='s', default_values_t = [ "hkps://keys.openpgp.org".to_string(), "hkps://keyserver.ubuntu.com".to_string(), "hkps://api.protonmail.ch".to_string(), ], )] keyserver: Vec, /// Don't look for updates on any keyservers. #[arg(long)] disable_keyservers: bool, }, /// Adds the given commit to the commit goodlist. /// /// This requires the audit capability to not break an /// authentication chain. Goodlist { commit: String, }, } #[derive(clap::Subcommand)] pub enum Subcommand { Init(InitSubcommand), Policy { #[command(subcommand)] command: PolicySubcommand, }, /// Lists and verifies commits. /// /// Lists and verifies that the commits from the given trust root /// to the target commit adhere to the policy. /// /// A version is considered authenticated if there is a path from /// the trust root to the target commit on which each commit can /// be authenticated by its parent. /// /// If the key used to sign a commit is hard revoked, then the /// commit is considered bad. "sq-git" looks for hard revocations /// in all of the commits that it examines. Thus, if a project /// maintainer adds a hard revocation to a commit's policy file, /// it will cause later *and* earlier commits signed with that key /// to be considered invalid. This is useful when a key has been /// compromised. /// /// When a key has been hard revoked, downstream users either need /// to start using a more recent trust root, or the upstream /// project maintainers need to audit the relevant commits. If /// the commits are considered benign, they can be added to a /// goodlist using "sq-git policy goodlist". When a commit is /// considered authenticated, but the certificate has been hard /// revoked, "sq-git" looks to see whether the commit has been /// goodlisted by a commit that is on an authenticated path from /// the commit in question to the target. If so, the commit is /// considered to be authenticated. Log { /// Specifies the trust root. /// /// If no policy is specified, then the value of the /// repository's "sequoia.trust-root" configuration key is /// used as the trust root. #[arg(long, value_name = "COMMIT")] trust_root: Option, /// Continues to check commits even when it is clear that the /// version cannot be authenticated. /// /// Causes "sq-git" to continue to check commits rather than /// stopping as soon as it is clear that the version can't be /// authenticated. #[arg(long)] keep_going: bool, /// After authenticating the current version, prunes the /// certificates. /// /// After authenticating the current version, prunes unused /// components of the certificates. In particular, subkeys /// that were not used to verify a signature, and User IDs /// that were never considered primary are removed. /// /// This does not remove unused certificates from the policy /// file; this just minimizes them. #[arg(long)] prune_certs: bool, /// The commits to check. /// /// If not specified, HEAD is authenticated with repsect to /// the trust root. /// /// If a single commit id is specified, the specified commit /// is authenticated with respect to the trust root. /// /// If a commit range like "3895a3a..3b388ae" is specified, /// the end of the range is authenticated with repsect to the /// trust root, and there must be an authenticated path from /// the trust root via the start of the range to the end of /// the range. commit_range: Option, }, /// Verifies signatures on archives like release tarballs. Verify { /// Read the policy from this commit. Falls back to using the /// value of the repository's "sequoia.trust-root" /// configuration key. Can be overridden using /// "--policy-file". #[arg(long, value_name = "COMMIT")] trust_root: Option, /// The signature to verify. #[arg(long, value_name = "FILENAME")] signature: PathBuf, /// The archive that the signature protects. #[arg(long, value_name = "FILENAME")] archive: PathBuf, }, /// git update hook that enforces an OpenPGP policy. /// /// Insert the following line into "hooks/update" on the shared /// git server to make it enforce the policy embedded in the /// repository starting at the given trust root "COMMIT". /// /// sq-git update-hook --trust-root= "$@" /// /// When a branch is pushed that is not previously known to the /// server, sq-git checks that all commits starting from the trust /// root to the pushed commit adhere to the policy. /// /// When a branch is pushed that is previously known to the /// server, i.e. the branch is updated, sq-git checks that /// all new commits starting from the commit previously known to /// the server to the pushed commit adhere to the policy. If /// there is no path from the previously known commit to the new /// one, the branch has been rebased. Then, we fall back to /// searching a path from the trust root. UpdateHook { /// The commit to use as a trust root. #[arg(long, value_name = "COMMIT", required = true)] trust_root: String, /// The name of the ref being updated (supplied as first /// argument to the update hook, see githooks(5)). ref_name: String, /// The old object name stored in the ref (supplied as second /// argument to the update hook, see githooks(5)). old_object: String, /// The new object name stored in the ref (supplied as third /// argument to the update hook, see githooks(5)). new_object: String, } } #[derive(clap::Args, Debug)] #[clap(group(ArgGroup::new("cert") .args(&["value", "cert_handle", "cert_file"]) .required(true)))] pub struct CertArg { /// The filename, fingerprint or Key ID of the certificate to /// authenticate. /// /// This is first interpreted as a filename. If that file does /// not exist, then it is interpreted as a fingerprint or Key ID, /// and read from the certificate store as described for the CERT /// argument. To avoid ambiguity, use "--cert" or "--cert-file". #[arg(value_name="FILE|FINGERPRINT|KEYID")] pub value: Option, /// The fingerprint or Key ID of the certificate to use.. /// /// This is read from the user's default certificate /// directory. On Unix-like systems, this is usually located /// in "$HOME/.local/share/pgp.cert.d". #[arg(long="cert", value_name="FINGERPRINT|KEYID")] pub cert_handle: Option, /// The file containing the certificate to authorize. /// /// The file must contain exactly one certificate. #[arg(long, value_name="FILE")] pub cert_file: Option, } sequoia-git-0.1.0/src/commands/init.rs000064400000000000000000000210701046102023000157520ustar 00000000000000use std::{ collections::{ btree_map::Entry, BTreeMap, BTreeSet, }, time::{ Duration, SystemTime, UNIX_EPOCH, }, }; use anyhow::Context; use sequoia_openpgp as openpgp; use openpgp::{ KeyID, Packet, parse::{ PacketParser, PacketParserResult, Parse, }, }; use crate::Result; use crate::git_repo; use crate::cli::InitSubcommand; // How far in the past to look to find active contributors. const HORIZON: Duration = Duration::new(183 * 24 * 60 * 60, 0); // The minimum number of commits to examine. const MIN_COMMITS: usize = 10; pub fn dispatch(c: InitSubcommand) -> Result<()> { let git = git_repo()?; let head_id = git.head().context("Looking up HEAD")? .resolve().context("Resolving HEAD to a commit")? .target().expect("resolved to direct reference"); // We do a breath-first search and examine the commits since // CUTOFF, but at least MIN_COMMITS. let now = SystemTime::now(); let cutoff = now - HORIZON; // Set to true if there aren't enough commits after the cutoff. let mut cutoff_exceeded = false; // Whether we've already processed a commit. let mut processed: BTreeSet = Default::default(); // The last commit that we processed. let mut last_processed = None; // Commits that we still need to process. let mut pending: BTreeSet = Default::default(); pending.insert(head_id.clone()); let mut unsigned: Vec = Vec::new(); struct Committer { commit_count: usize, // Keys used by this commit and the number of commits they // signed. signing_keys: BTreeMap, } let mut committers: BTreeMap = Default::default(); while let Some(commit_id) = pending.pop_first() { processed.insert(commit_id.clone()); last_processed = Some(commit_id.clone()); let commit = git.find_commit(commit_id) .with_context(|| { format!("Getting commit data for {}", commit_id) })?; if c.verbose { println!("{}: {}", commit_id, commit.summary().unwrap_or("")); } // See who signed it. let mut signing_keys: BTreeSet = Default::default(); let mut have_sig = false; if let Ok((sig, _data)) = git.extract_signature(&commit_id, None) { // We expect signature packets. Anything else is not ok. let mut ok = true; if let Ok(mut ppr) = PacketParser::from_bytes(&sig[..]) { while let PacketParserResult::Some(pp) = ppr { match pp.next() { Ok((packet, next_ppr)) => { ppr = next_ppr; if let Packet::Signature(sig) = packet { have_sig = true; let issuers = sig.get_issuers(); if c.verbose { for issuer in issuers.iter() { println!(" Allegedly signed by {}", issuer); } } signing_keys.extend( issuers.into_iter().map(KeyID::from)); } else { ok = false; break; } } Err(err) => { eprintln!("Warning: {} contains an invalid \ signature: {}", commit_id, err); ok = false; break; } } } } if ! ok { have_sig = false; signing_keys.clear(); } } if ! have_sig { unsigned.push( commit.as_object() .short_id().ok() .and_then(|id| id.as_str().map(|id| id.to_string())) .unwrap_or_else(|| commit.id().to_string())); } let committer = commit.committer(); let committer = format!( "{}{}{}", committer.name().unwrap_or(""), if committer.name().is_some() && committer.email().is_some() { " " } else { "" }, committer .email() .map(|e| format!("<{}>", e)) .unwrap_or("".to_string())); match committers.entry(committer) { Entry::Occupied(mut oe) => { let e = oe.get_mut(); e.commit_count += 1; for signing_key in signing_keys.into_iter() { e.signing_keys.entry(signing_key) .and_modify(|e| { *e += 1; }) .or_insert(1); } } Entry::Vacant(e) => { let mut info = Committer { commit_count: 1, signing_keys: BTreeMap::new(), }; for signing_key in signing_keys.into_iter() { info.signing_keys.insert(signing_key, 1); } e.insert(info); } } for parent in commit.parents() { let parent_id = parent.id(); if processed.contains(&parent_id) || pending.contains(&parent_id) { continue; } let commit_time = parent.time(); let commit_time = UNIX_EPOCH + Duration::new(commit_time.seconds() as u64, 0); if commit_time < cutoff { if processed.len() + pending.len() > MIN_COMMITS { continue; } else { cutoff_exceeded = true; } } pending.insert(parent_id.clone()); } } if cutoff_exceeded { println!("# Examined {} recent commits.", processed.len()); } else { println!("# Examined the {} commits in the last {} days.", processed.len(), HORIZON.as_secs() / 24 / 60 / 60); } if let Some(commit_id) = last_processed { println!("# Stopped at commit {}.", commit_id); } println!(); print!("# Encountered {} unsigned commits", unsigned.len()); if ! unsigned.is_empty() { print!(" ("); for (i, id) in unsigned.iter().enumerate() { if i == 3 && ! c.verbose { print!("..."); break; } if i > 0 { print!(" "); } print!("{}", id); } print!(")"); } println!(); let mut committers: Vec<(String, Committer)> = committers.into_iter().collect(); committers.sort_by_key(|(committer, info)| { (usize::MAX - info.commit_count, committer.clone()) }); for (i, (committer, info)) in committers.iter().enumerate() { println!(); println!("\ # {} added {} commits ({}%). #", committer, info.commit_count, (100 * info.commit_count) / processed.len()); if info.signing_keys.is_empty() { println!("\ # Never signed any commits. To authorize them, you'll need their OpenPGP # certificate."); continue; } println!("\ # After checking that they really control the following OpenPGP keys: # # {} #", info .signing_keys.iter() .map(|(keyid, count)| { format!("{} ({} commits)", keyid, count) }) .collect::>() .join("# ")); if i == 0 { println!("\ # You can make them a project maintainer (someone who can add and # remove committers) by running:"); } else { println!("\ # You can make them a committer by running:"); } for (keyid, _count) in info.signing_keys.iter() { println!("sq-git policy authorize {} {:?} {}", if i == 0 { " --project-maintainer" } else { "--committer" }, committer, keyid); } } Ok(()) } sequoia-git-0.1.0/src/commands/policy.rs000064400000000000000000000224441046102023000163140ustar 00000000000000use std::{ collections::btree_map::Entry, io, }; use anyhow::Context; use sequoia_openpgp as openpgp; use openpgp::{ Cert, KeyHandle, }; use sequoia_cert_store as cert_store; use cert_store::Store; use crate::Config; use crate::Result; use crate::cli::PolicySubcommand; use crate::git_repo; use crate::output; pub fn dispatch(config: &Config, command: PolicySubcommand) -> Result<()> { match command { PolicySubcommand::Describe { } => { let p = config.read_policy()?; match config.output_format { output::Format::HumanReadable => { output::describe_policy(&p)?; }, output::Format::Json => serde_json::to_writer_pretty(io::stdout(), &p)?, } }, PolicySubcommand::Authorize { name, cert, sign_commit, no_sign_commit, sign_tag, no_sign_tag, sign_archive, no_sign_archive, add_user, no_add_user, retire_user, no_retire_user, audit, no_audit, project_maintainer: _, release_manager: _, committer: _, } => { let cert = cert.get(&config)?; let fp = cert.fingerprint(); let old_policy = config.read_policy()?; let mut p = old_policy.clone(); let (new_entry, a) = match p.authorization.entry(name) { Entry::Occupied(oe) => (false, oe.into_mut()), Entry::Vacant(ve) => (true, ve.insert(Default::default())), }; if new_entry && (! sign_commit && ! sign_tag && ! sign_archive && ! add_user && ! retire_user && ! audit) { eprintln!("Warning: Adding new entry with no capabilities. \ You probably want to add some capabilities by \ running the command again, and specifying \ \"--committer\", \"--release-manager\", \ or \"--project-maintainer\". Refer to the \ help for details.\""); } let mut merged = false; let mut updated = Vec::new(); for c in a.certs()? { let mut c = Cert::try_from(c?)?; if c.fingerprint() == fp { c = c.merge_public(cert.clone())?; merged = true; } updated.push(c); } if ! merged { updated.push(cert); } a.set_certs(updated)?; a.sign_commit = (a.sign_commit | sign_commit) & !no_sign_commit; a.sign_tag = (a.sign_tag | sign_tag) & !no_sign_tag; a.sign_archive = (a.sign_archive | sign_archive) & !no_sign_archive; a.add_user = (a.add_user | add_user) & !no_add_user; a.retire_user = (a.retire_user | retire_user) & !no_retire_user; a.audit = (a.audit | audit) & !no_audit; let diff = old_policy.diff(&p)?; match config.output_format { output::Format::HumanReadable => { output::describe_diff(&diff)?; }, output::Format::Json => serde_json::to_writer_pretty(io::stdout(), &diff)?, } config.write_policy(&p)?; }, PolicySubcommand::Sync { keyserver: keyservers, disable_keyservers, } => { let old_policy = config.read_policy()?; let mut p = old_policy.clone(); let mut keyserver = keyservers .into_iter() .map(|keyserver| { sequoia_net::KeyServer::new( sequoia_net::Policy::Encrypted, &keyserver) .map(|instance| { (keyserver, instance) }) }) .collect::>>()?; let cert_update = |cert: Cert, update: Cert| -> (Cert, bool) { if cert.fingerprint() != update.fingerprint() { eprintln!("bad server response, \ wrong certificate ({}).", update.fingerprint()); (cert, false) } else { match cert.clone().insert_packets2(update.into_packets()) { Ok((cert, changed)) => { if changed { eprintln!("updated."); } else { eprintln!("unchanged."); } (cert, changed) } Err(err) => { eprintln!("{}", err); (cert, false) } } } }; // XXX: We should do this in parallel. tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap() .block_on(async { let mut any_changed = false; for (id, a) in p.authorization.iter_mut() { let mut changed = false; let mut updated = Vec::new(); for cert in a.certs()? { let mut cert = Cert::try_from(cert?)?; let fp = cert.fingerprint(); eprint!("Updating {} ({}) from the local \ certificate store... ", fp, id); if let Ok(c) = config.cert_store()? .lookup_by_cert_fpr(&fp) .and_then(|lc| lc.to_cert().cloned()) { let did_change; (cert, did_change) = cert_update(cert, c); changed |= did_change; } else { eprintln!("not found."); }; if ! disable_keyservers { let kh = KeyHandle::from(fp); for (uri, keyserver) in keyserver.iter_mut() { eprint!("Updating {} ({}) from {}... ", kh, id, uri); match keyserver.get(kh.clone()).await { Ok(c) => { let did_change; (cert, did_change) = cert_update(cert, c); changed |= did_change; } Err(err) => { eprintln!("{}.", err); } } } } updated.push(cert); } if changed { a.set_certs(updated)?; any_changed = true; } } if any_changed { eprintln!("Note: certificates are stripped so not \ all certificate updates may be relevant."); } Ok::<(), anyhow::Error>(()) })?; let diff = old_policy.diff(&p)?; match config.output_format { output::Format::HumanReadable => { output::describe_diff(&diff)?; }, output::Format::Json => serde_json::to_writer_pretty(io::stdout(), &diff)?, } config.write_policy(&p)?; }, PolicySubcommand::Goodlist { commit, } => { let git = git_repo()?; let object = git.revparse_single(&commit) .with_context(|| { format!("Looking up \"{}\"", commit) })?; let commit = object.peel_to_commit() .with_context(|| { format!("\"{}\" does not refer to a commit", commit) })?; let old_policy = config.read_policy()?; let mut p = old_policy.clone(); p.commit_goodlist.insert(commit.id().to_string()); let diff = old_policy.diff(&p)?; match config.output_format { output::Format::HumanReadable => { output::describe_diff(&diff)?; }, output::Format::Json => serde_json::to_writer_pretty(io::stdout(), &diff)?, } config.write_policy(&p)?; }, } Ok(()) } sequoia-git-0.1.0/src/commands.rs000064400000000000000000000000361046102023000150060ustar 00000000000000pub mod init; pub mod policy; sequoia-git-0.1.0/src/git.rs000064400000000000000000000141501046102023000137720ustar 00000000000000use std::{ collections::{ BTreeSet }, }; use anyhow::Result; use git2::{ Repository, Oid, }; use crate::{ Error, }; const TRACE: bool = false; /// Returns whether `ancestor` is an ancestor of `target`. /// /// A commit is considered to be an ancestor of another commit if /// there is a path from the first commit to the target commit. Note: /// this does not authenticate the path, or even check whether the /// commits are signed. Commits are considered their own ancestors. /// /// Returns `Ok(())` if there is a path. If there is no path, returns /// `Err(Error::NoPathConnecting)`. pub fn git_is_ancestor(git: &Repository, ancestor: Oid, target: Oid) -> Result<()> { tracer!(TRACE, "is_ancestor"); t!("Looking for {}..{}", ancestor, target); if ancestor.is_zero() { return Err(Error::NoPathConnecting(ancestor, target).into()); } if ancestor == target { return Ok(()); } // Whether we've already processed a commit. let mut processed: BTreeSet = Default::default(); // Commits that we still need to process. let mut pending: BTreeSet = Default::default(); pending.insert(target.clone()); while let Some(commit_id) = pending.pop_first() { t!("Visiting commit {:?}", commit_id); processed.insert(commit_id.clone()); let commit = git.find_commit(commit_id)?; for parent in commit.parents() { let parent_id = parent.id(); if processed.contains(&parent_id) || pending.contains(&parent_id) { // There is a valid path from PARENT to TARGET, which // is not via COMMIT. There is no need to find a // second path. continue; } if parent_id == ancestor { t!("Reached ancestor!"); return Ok(()); } pending.insert(parent_id.clone()); } } Err(Error::NoPathConnecting(ancestor, target).into()) } #[cfg(test)] mod test { use super::*; use std::path::Path; use tempfile::TempDir; use git2::Commit; use git2::Repository; fn commit_file<'repo, P>(repo: &'repo Repository, filename: P, content: &[u8], commit_message: &str, parents: &[&Commit<'repo>]) -> Commit<'repo> where P: AsRef { let filename = filename.as_ref(); let filename_abs = repo.workdir().unwrap().join(&filename); std::fs::write(&filename_abs, content).unwrap(); let mut index = repo.index().unwrap(); index.add_path(&filename).unwrap(); let oid = index.write_tree().unwrap(); let tree = repo.find_tree(oid).unwrap(); let sig = repo.signature().unwrap(); let commit_oid = repo.commit( None, &sig, &sig, commit_message, &tree, parents) .unwrap(); let commit = repo.find_commit(commit_oid).unwrap(); commit } #[test] fn ancestor() -> Result<()> { let dir = TempDir::new()?; let repo = Repository::init(&dir) .expect("Initialize git repository"); let mut config = repo.config().unwrap(); config.set_str("user.name", "name").unwrap(); config.set_str("user.email", "email").unwrap(); // root // / \ // l.0 r.0 // | | // l.1 r.1 // \ / // merge // | // c.0 // | // c.1 let root = commit_file( &repo, "root", b"root", "root", &[]); let l_0 = commit_file( &repo, "l", b"0", "commit l.0", &[ &root ]); let l_1 = commit_file( &repo, "l", b"1", "commit l.1", &[ &l_0 ]); let r_0 = commit_file( &repo, "r", b"0", "commit r.0", &[ &root ]); let r_1 = commit_file( &repo, "r", b"1", "commit r.1", &[ &r_0 ]); let m = commit_file( &repo, "m", b"merge!", "commit merge", &[ &l_1, &r_1 ]); let c_0 = commit_file( &repo, "c", b"0", "commit c.0", &[ &m ]); let c_1 = commit_file( &repo, "c", b"1", "commit c.1", &[ &c_0 ]); let all_commits = &[&root, &l_0, &l_1, &r_0, &r_1, &m, &c_0, &c_1 ]; // All the commits in a topological order. let paths = &[ // Via l. &[&root, &l_0, &l_1, &m, &c_0, &c_1 ], // Via r. &[&root, &r_0, &r_1, &m, &c_0, &c_1 ], ]; let t = |ancestor: &Commit, commit: &Commit, expect: bool| { match (expect, git_is_ancestor(&repo, ancestor.id(), commit.id()).is_ok()) { (true, true) => (), (false, false) => (), (true, false) => { panic!("Expected {} ({}) to be an ancestor of {} ({})", ancestor.summary().unwrap_or(""), ancestor.id(), commit.summary().unwrap_or(""), commit.id()); } (false, true) => { panic!("Expected {} ({}) to NOT be an ancestor of {} ({})", ancestor.summary().unwrap_or(""), ancestor.id(), commit.summary().unwrap_or(""), commit.id()); } } }; // Commits are their own ancestors. for c in all_commits.iter() { t(c, c, true); } for path in paths.iter() { for i in 0..(path.len() - 1) { for j in (i + 1)..path.len() { t(&path[i], &path[j], true); t(&path[j], &path[i], false); } } } t(&l_0, &r_0, false); t(&l_0, &r_1, false); t(&l_1, &r_0, false); t(&l_1, &r_1, false); t(&r_0, &l_0, false); t(&r_0, &l_1, false); t(&r_1, &l_0, false); t(&r_1, &l_1, false); Ok(()) } } sequoia-git-0.1.0/src/lib.rs000064400000000000000000000024701046102023000137570ustar 00000000000000use git2::Oid; use sequoia_openpgp::{ self as openpgp, }; #[macro_use] mod macros; mod policy; pub use policy::*; mod verify; pub use verify::*; mod git; pub use git::*; pub mod persistent_set; pub(crate) mod utils; /// Errors for this crate. #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Invalid operation: {0}")] InvalidOperation(String), #[error("Storage error: {0}")] StorageError(String), #[error("Commit {0} is not signed")] MissingSignature(Oid), #[error("{0} is not signed")] MissingDataSignature(String), #[error("Commit {0} has no policy")] MissingPolicy(Oid), #[error("The given range contains no commits")] EmptyCommitRange, #[error("There is no path from {0} to {1}")] NoPathConnecting(Oid, Oid), #[error("Key `{0}` missing")] MissingKey(openpgp::KeyHandle), #[error("Key `{0}` is bad: {1}")] BadKey(openpgp::KeyHandle, String), #[error("Bad signature: {0}")] BadSignature(String), #[error("Unauthorized: {0}")] Unauthorized(String), #[error("Io error")] Io(#[from] std::io::Error), #[error("Libgit2 error")] Git2(#[from] git2::Error), #[error("Other error: {0}")] Other(#[from] anyhow::Error), } /// Crate result specialization. pub type Result = ::std::result::Result; sequoia-git-0.1.0/src/macros.rs000064400000000000000000000142761046102023000145040ustar 00000000000000use std::cmp; #[allow(unused_macros)] macro_rules! trace { ( $TRACE:expr, $fmt:expr, $($pargs:expr),* ) => { if $TRACE { eprintln!($fmt, $($pargs),*); } }; ( $TRACE:expr, $fmt:expr ) => { trace!($TRACE, $fmt, ); }; } // Converts an indentation level to whitespace. #[allow(dead_code)] pub(crate) fn indent(i: isize) -> &'static str { let s = " "; &s[0..cmp::min(usize::try_from(i).unwrap_or(0), s.len())] } #[allow(unused_macros)] macro_rules! tracer { ( $TRACE:expr, $func:expr ) => { tracer!($TRACE, $func, 0) }; ( $TRACE:expr, $func:expr, $indent:expr ) => { // Currently, Rust doesn't support $( ... ) in a nested // macro's definition. See: // https://users.rust-lang.org/t/nested-macros-issue/8348/2 #[allow(unused_macros)] macro_rules! t { ( $fmt:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, $fmt) }; ( $fmt:expr, $a:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a)) }; ( $fmt:expr, $a:expr, $b:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h, $i)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h, $i, $j)) }; ( $fmt:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr, $f:expr, $g:expr, $h:expr, $i:expr, $j:expr, $k:expr ) => { trace!($TRACE, "{}{}: {}", crate::macros::indent($indent), $func, format!($fmt, $a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k)) }; } } } /// 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. /// /// ```ignore /// let result = time_it!("Some code", 10, { /// // Some code. /// 5 /// }); /// assert_eq!(result, 5); /// ``` // Note: We cannot test the macro in doctests, because the macro is // not public. We test the cases in the test module below, instead. // If you change the examples here, propagate the changes to the // module below. #[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) }; } /// We cannot test the macro in doctests, because the macro is not /// public. We test the cases here, instead. If you change the /// examples here, propagate the changes to the docstring above. #[cfg(test)] mod test_time_it { /// 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. #[test] fn time_it() { let result = time_it!("Some code", 10, { // Some code. 5 }); assert_eq!(result, 5); } } #[allow(unused_macros)] macro_rules! platform { { unix => { $($unix:tt)* }, windows => { $($windows:tt)* }, } => { if cfg!(unix) { #[cfg(unix)] { $($unix)* } #[cfg(not(unix))] { unreachable!() } } else if cfg!(windows) { #[cfg(windows)] { $($windows)* } #[cfg(not(windows))] { unreachable!() } } else { #[cfg(not(any(unix, windows)))] compile_error!("Unsupported platform"); unreachable!() } } } sequoia-git-0.1.0/src/main.rs000064400000000000000000000456661046102023000141530ustar 00000000000000use std::{ env, io, path::{ Path, PathBuf, } }; use anyhow::{anyhow, Context, Result}; use clap::Parser; use once_cell::sync::OnceCell; use sequoia_openpgp::{ self as openpgp, Cert, KeyHandle, parse::Parse, }; use sequoia_cert_store::{ CertStore, Store, }; use sequoia_git::*; #[macro_use] mod macros; mod cli; use cli::CertArg; mod commands; mod output; #[allow(dead_code)] mod utils; impl CertArg { fn get(&self, config: &Config) -> Result { let filename; let r: Result<(&Path, Vec), KeyHandle> = if let Some(value) = &self.value { // First try to open as a file. Only if the file does not // exist, interpret the value as a key handle. filename = PathBuf::from(value); match std::fs::read(&filename) { Ok(contents) => { Ok((&filename, contents)) } Err(err) => { if err.kind() == std::io::ErrorKind::NotFound { match value.parse::() { Ok(kh) => Err(kh), Err(err) => return Err( err.context( format!("File {} does not exist, \ and is not a valid fingerprint \ or Key ID", filename.display()))), } } else { return Err(anyhow::Error::from(err).context( format!("Opening {}", filename.display()))); } } } } else if let Some(kh) = &self.cert_handle { Err(kh.clone()) } else if let Some(filename) = &self.cert_file { let content = std::fs::read(&filename) .with_context(|| { format!("Opening {}", filename.display()) })?; Ok((filename, content)) } else { unreachable!("clap ensures that one argument is set") }; match r { Ok((filename, content)) => { // Parse content as a Cert and make sure content only // contains a single certificate. Cert::from_bytes(&content) .with_context(|| { format!("Parsing {}", filename.display()) }) } Err(kh) => { let certs = config.cert_store()?.lookup_by_key(&kh)?; let cert = match certs.len() { 0 => return Err(anyhow!("Key {} not found", kh)), 1 => certs[0].to_cert()?.clone(), n => return Err(anyhow!( "Key {} is part of {} certs, use cert \ fingerprint to resolve", kh, n)), }; Ok(cert) } } } } pub struct Config<'a> { policy_file: Option<&'a Path>, output_format: output::Format, no_cert_store: bool, cert_store: Option, cert_store_instance: OnceCell>, } impl<'a> Config<'a> { // Returns the cert store. // // If it is not yet open, opens it. // // If it does not exist, issues a warning and returns an empty // cert store. fn cert_store(&self) -> Result<&CertStore<'a>> { self.cert_store_instance.get_or_try_init(|| { if self.no_cert_store { Ok(CertStore::empty()) } else { let path = self.cert_store.clone() .unwrap_or_else(|| cli::cert_store_base().into()); match path.metadata() { Ok(metadata) => { if metadata.is_dir() { CertStore::open(&path) } else { Err(anyhow::anyhow!( "Not a certificate directory")) } } Err(err) => { if err.kind() == std::io::ErrorKind::NotFound { eprintln!("Warning: no certificate \ store found at {}", path.display()); Ok(CertStore::empty()) } else { Err(err.into()) } } } .with_context(|| { format!("While opening the certificate store at {:?}", &path) }) } }) } fn read_policy(&self) -> Result { if let Some(path) = self.policy_file { Policy::read(path) .with_context(|| { format!("Reading specified policy file: {}", path.display()) }) } else { Policy::read_from_working_dir() .with_context(|| { format!("Reading default policy file") }) } } fn write_policy(&self, p: &Policy) -> Result<()> { if let Some(path) = self.policy_file { p.write(path) .with_context(|| { format!("Updating the specified policy file: {}", path.display()) }) } else { p.write_to_working_dir() .with_context(|| { format!("Updating default policy file") }) } } } // Returns the current git repository. fn git_repo() -> Result { let cwd = env::current_dir() .context("Getting current working directory")?; let repo = git2::Repository::discover(&cwd) .with_context(|| { format!("Looking for git repository under {}", cwd.display()) })?; Ok(repo) } fn main() -> anyhow::Result<()> { let policy = openpgp::policy::StandardPolicy::new(); // XXX let cli = cli::Cli::parse(); let config = Config { policy_file: cli.policy_file.as_deref(), output_format: cli.output_format.parse()?, no_cert_store: cli.no_cert_store, cert_store: cli.cert_store, cert_store_instance: Default::default(), }; let commit_by_symbolic_name = |git: &git2::Repository, name: &str, trust_root: bool| -> Result { // Allow the zero oid. if let Ok(oid) = git2::Oid::from_str(name) { if oid.is_zero() { return Ok(oid); } } let (object, reference) = git.revparse_ext(name) .with_context(|| { format!("Looking up {:?}.", name) })?; // We won't get a reference if we are passed an OID. if let Some(reference) = reference { if trust_root { if reference.is_tag() { eprintln!("Warning: using a tag as the trust root \ could allow the remote repository to \ manipulate your trust root."); } else if reference.is_remote() { eprintln!("Warning: using a remote branch as the \ trust root could allow the remote repository \ to manipulate your trust root."); } } let commit = reference.peel_to_commit() .with_context(|| { format!("{:?} is not a commit", name) })?; Ok(commit.id()) } else { if let Ok(commit) = object.into_commit() { Ok(commit.id()) } else { Err(anyhow::anyhow!("{:?} is not a commit", name)) } } }; let lookup_trust_root = |git: &git2::Repository, trust_root: Option<&str>| -> Result { if let Some(trust_root) = trust_root { return commit_by_symbolic_name(git, trust_root, true); } // We only look in the repository's configuration file. let config = git.config()? .open_level(git2::ConfigLevel::Local)? .snapshot()?; let trust_root = match config.get_str("sequoia.trust-root") { Ok(trust_root) => trust_root, Err(err) => { if err.code() == git2::ErrorCode::NotFound { eprintln!("Warning: no trust root specified. Either \ pass the '--trust-root' option, or set \ the 'sequoia.trust-root' configuration \ key in your repository's local git config \ to reference a commit."); } return Err(anyhow::Error::from(err).context( "Reading 'sequoia.trust-root' from the repository's \ git config.")); } }; commit_by_symbolic_name(git, trust_root, true) }; match cli.subcommand { cli::Subcommand::Init(command) => { commands::init::dispatch(command)?; } cli::Subcommand::Policy { command } => { commands::policy::dispatch(&config, command)?; } cli::Subcommand::Log { trust_root, keep_going, prune_certs, commit_range, } => { if prune_certs && commit_range.is_some() && cli.policy_file.is_none() { return Err(anyhow!("--prune-certs can only modify \ HEAD or a shadow policy")); } let git = git_repo()?; let trust_root = lookup_trust_root(&git, trust_root.as_deref())?; let shadow_p = if let Some(s) = &cli.policy_file { Some(std::fs::read(s)?) } else { None }; let shadow_p = shadow_p.as_deref(); let head = git.head()?.target().unwrap(); let (start, target) = if let Some(commit_range) = commit_range { let mut s = commit_range.splitn(2, ".."); let first = s.next().expect("always one component"); if let Some(second) = s.next() { if second.is_empty() { (commit_by_symbolic_name(&git, first, false)?, head) } else { (commit_by_symbolic_name(&git, first, false)?, commit_by_symbolic_name(&git, second, false)?) } } else { (trust_root, commit_by_symbolic_name(&git, first, false)?) } } else { (trust_root, head) }; let mut cache = VerificationCache::new()?; let mut vresults = VerificationResult::default(); let result = match config.output_format { output::Format::HumanReadable => { verify(&git, trust_root, shadow_p, (start, target), &mut vresults, keep_going, |oid, parent_oid, result| { output::Commit::new( &git, oid, parent_oid, &cli.policy_file, result)? .describe(&mut io::stdout())?; Ok(()) }, &mut cache, ) }, output::Format::Json => { use serde::ser::{Serializer, SerializeSeq}; let mut serializer = serde_json::ser::Serializer::pretty( std::io::stdout()); let mut seq = serializer.serialize_seq(None)?; let r = verify(&git, trust_root, shadow_p, (start, target), &mut vresults, keep_going, |oid, parent_oid, result| { seq.serialize_element( &output::Commit::new( &git, oid, parent_oid, &cli.policy_file, result)? ).map_err(anyhow::Error::from)?; Ok(()) }, &mut cache, ); seq.end()?; r }, }; if prune_certs { let mut p = config.read_policy()?; for a in p.authorization.values_mut() { let certs = a.certs()? .map(|r| r.and_then(Cert::try_from)) .collect::>>()?; a.set_certs_filter( certs, // Keep all subkeys that made a signature, and // those that are alive now. |sk| { let fp = sk.fingerprint(); vresults.signer_keys.contains(&fp) || { // Slightly awkward, because we // cannot use sk.with_policy. let c = sk.cert(); c.with_policy(&policy, None) .map(|vka| vka.keys().key_handle(fp) .next().is_some()) .unwrap_or(false) } }, // Keep all user IDs that were primary user // IDs when a signature was made, and the ones // that are the primary userid now. |uid| vresults.primary_uids.contains(&uid) || { // Slightly awkward, because we // cannot use sk.with_policy. let c = uid.cert(); c.with_policy(&policy, None) .and_then(|vka| vka.primary_userid()) .map(|u| u.userid() == uid.userid()) .unwrap_or(false) } )?; } config.write_policy(&p)?; } let _ = cache.persist(); result?; }, cli::Subcommand::Verify { trust_root, signature, archive, } => { let git = git_repo()?; let policy = if let Some(s) = &cli.policy_file { Policy::read(s) .with_context(|| { format!("Reading specified policy file: {}", s.display()) })? } else { let trust_root = lookup_trust_root( &git, trust_root.as_deref())?; Policy::read_from_commit(&git, &trust_root) .with_context(|| { format!("Reading policy from commit {}", trust_root) })? }; // XXX: In the future, mmap the data. let signature = std::fs::read(&signature) .with_context(|| { format!("Reading signature data from {}", signature.display()) })?; let archive = std::fs::read(&archive) .with_context(|| { format!("Reading archive data from {}", archive.display()) })?; let r = policy.verify_archive(signature, archive); let o = output::Archive::new(r)?; match config.output_format { output::Format::HumanReadable => o.describe(&mut io::stdout())?, output::Format::Json => serde_json::to_writer_pretty(io::stdout(), &o)?, } }, cli::Subcommand::UpdateHook { trust_root, ref_name: _, old_object, new_object, } => { let git = git_repo()?; let trust_root = commit_by_symbolic_name(&git, &trust_root, true) .with_context(|| { format!("Looking up specified trust root ({})", trust_root) })?; let new_object = commit_by_symbolic_name(&git, &new_object, false) .with_context(|| { format!("Looking up new object ({})", new_object) })?; let old_object = commit_by_symbolic_name(&git, &old_object, false) .with_context(|| { format!("Looking up old object ({})", old_object) })?; // Fall back to the trust root if this is a new branch. let start = if let Err(err) = git_is_ancestor(&git, old_object, new_object) { if let Some(e) = err.downcast_ref::() { if matches!(e, Error::NoPathConnecting(_, _)) { // There's no path from old object to new // object. Use the trust root. trust_root } else { // Some other error: abort. return Err(err); } } else { // There's a path from old object to new object. old_object } } else { trust_root }; let mut cache = VerificationCache::new()?; let mut vresults = VerificationResult::default(); let result = verify(&git, trust_root, None, (start, new_object), &mut vresults, false, |oid, parent_oid, result| { output::Commit::new( &git, oid, parent_oid, &cli.policy_file, result)? .describe(&mut io::stdout())?; Ok(()) }, &mut cache, ); let _ = cache.persist(); result?; }, } Ok(()) } sequoia-git-0.1.0/src/output.rs000064400000000000000000000230661046102023000145550ustar 00000000000000use std::io; use std::sync::Mutex; use git2::{Oid, Repository}; use serde::Serialize; use openpgp::{ Cert, Fingerprint, packet::Signature, }; use super::*; /// What output format to prefer, when there's an option? #[derive(Clone)] pub enum Format { /// Output that is meant to be read by humans, instead of programs. /// /// This type of output has no version, and is not meant to be /// parsed by programs. HumanReadable, /// Output as JSON. Json, } impl std::str::FromStr for Format { type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s { "human-readable" => Ok(Self::HumanReadable), "json" => Ok(Self::Json), _ => Err(anyhow!("unknown output format {:?}", s)), } } } /// Emits a human-readable description of the policy to stdout. pub fn describe_policy(p: &Policy) -> Result<()> { println!("# OpenPGP policy file for git, version {}", p.version); println!(); println!("## Commit Goodlist"); println!(); for commit in &p.commit_goodlist { println!(" - {}", commit); } println!(); println!("## Authorizations"); println!(); for (i, (name, auth)) in p.authorization.iter().enumerate() { println!("{}. {}", i, name); let ident = vec![' '; i.to_string().len() + 2] .into_iter().collect::(); if auth.sign_commit { println!("{}- may sign commits", ident); } if auth.sign_tag { println!("{}- may sign tags", ident); } if auth.sign_archive { println!("{}- may sign archives", ident); } if auth.add_user { println!("{}- may add users", ident); } if auth.retire_user { println!("{}- may retire users", ident); } if auth.audit { println!("{}- may goodlist commits", ident); } for cert in auth.certs()? { println!("{}- has OpenPGP cert: {}", ident, cert?.fingerprint()); } } Ok(()) } /// Emits a human-readable description of a difference between two /// policies to stdout. pub fn describe_diff(p: &Diff) -> Result<()> { for change in &p.changes { use Change::*; match change { VersionChange { from, to } => println!(" - Version changed from {} to {}.", from, to), GoodlistCommit(oid) => println!(" - Commit {} was added to the goodlist.", oid), UngoodlistCommit(oid) => println!(" - Commit {} was removed from the goodlist.", oid), AddUser(name) => println!(" - User {:?} was added.", name), RetireUser(name) => println!(" - User {:?} was retired.", name), AddRight(name, right) => println!(" - User {:?} was granted the right {}.", name, right), RemoveRight(name, right) => println!(" - User {:?}'s {} right was revoked.", name, right), _ => todo!("the cert stuff"), } } Ok(()) } // The version of the commit output. This follows semantic // versioning. static COMMIT_JSON_VERSION: &'static str = "1.0.0"; #[derive(Serialize)] pub struct Commit<'a> { version: &'static str, #[serde(serialize_with = "crate::utils::serialize_oid")] id: &'a Oid, #[serde(serialize_with = "crate::utils::serialize_optional_oid")] parent_id: Option<&'a Oid>, results: Vec)>>, } static MISSING_SIGNATURE_HINT: Mutex = Mutex::new(false); static MISSING_KEY_HINT: Mutex = Mutex::new(false); static MALFORMED_MESSAGE_HINT: Mutex = Mutex::new(false); impl<'a> Commit<'a> { pub fn new(_git: &Repository, id: &'a Oid, parent_id: Option<&'a Oid>, shadow_policy: &Option, result: &'a sequoia_git::Result>>) -> Result { let hint = |e: &Error| -> Option { match (shadow_policy, e) { (None, _) => None, (Some(p), Error::MissingSignature(commit)) => { let mut shown = MISSING_SIGNATURE_HINT.lock().unwrap(); if ! *shown { *shown = true; Some(format!("when using an external policy, do\n\n\ git show {1} \n\ \n and verify that the commit is good. \ If satisfied, do\n\n\ sq-git policy goodlist --policy-file {0} {1}", p.display(), commit)) } else { None } } (Some(p), Error::MissingKey(handle)) => { let mut shown = MISSING_KEY_HINT.lock().unwrap(); if ! *shown { *shown = true; Some(format!("when using an external policy, do\n\n\ sq keyserver get {1} \n\ \n and verify that the cert belongs to the \ committer. If satisfied, do\n\n\ sq-git policy authorize --policy-file {} \ {} --sign-commit", p.display(), handle)) } else { None } } (_, Error::Other(e)) => { if let Some(e) = e.downcast_ref::() { if let openpgp::Error::MalformedMessage(_) = e { let mut shown = MALFORMED_MESSAGE_HINT.lock().unwrap(); if ! *shown { *shown = true; Some(format!("\ a signature is malformed. It was probably created by GitHub, which\n\ is known to created invalid signatures. See the following discussion for\n\ more information:\n\ \n\ https://github.com/orgs/community/discussions/27607")) } else { None } } else { None } } else { None } } _ => None, } }; let mut r = Vec::new(); match result { Ok(results) => { for e in results.iter() .filter_map(|r| r.as_ref().err()) { r.push(Err((e.to_string(), hint(e)))); } for (name, _s, c, _signer_fpr) in results.iter() .filter_map(|r| r.as_ref().ok()) { r.push(Ok(format!("{} [{}]", name, c.keyid()))); } }, Err(e) => { r.push(Err((e.to_string(), hint(e)))); }, } Ok(Commit { version: COMMIT_JSON_VERSION, id, parent_id, results: r, }) } pub fn describe(&self, sink: &mut dyn io::Write) -> Result<()> { for r in &self.results { let id = if let Some(parent_id) = self.parent_id { format!("{}..{}", parent_id, self.id) } else { self.id.to_string() }; match r { Err((e, hint)) => { writeln!(sink, "{}: {}", id, e)?; if let Some(h) = hint { writeln!(sink, "\n Hint: {}", h)?; } }, Ok(fp) => { writeln!(sink, "{}: {}", id, fp)?; }, } } Ok(()) } } // The version of the commit output. This follows semantic // versioning. static ARCHIVE_JSON_VERSION: &'static str = "1.0.0"; #[derive(Serialize)] pub struct Archive { version: &'static str, results: Vec>, } impl Archive { pub fn new(result: sequoia_git::Result>>) -> Result { let mut r = Vec::new(); match result { Ok(results) => { for e in results.iter() .filter_map(|r| r.as_ref().err()) { r.push(Err(e.to_string())); } for (name, _s, c, _signer_fpr) in results.iter() .filter_map(|r| r.as_ref().ok()) { r.push(Ok(format!("{} [{}]", name, c.keyid()))); } }, Err(e) => { r.push(Err(e.to_string())); }, } Ok(Self { version: ARCHIVE_JSON_VERSION, results: r, }) } pub fn describe(&self, sink: &mut dyn io::Write) -> Result<()> { for r in &self.results { match r { Err(e) => { writeln!(sink, "{}", e)?; }, Ok(fp) => { writeln!(sink, "{}", fp)?; }, } } Ok(()) } } sequoia-git-0.1.0/src/persistent_set.rs000064400000000000000000000174761046102023000163000ustar 00000000000000//! A set of uniformly distributed 32-byte values that can be //! persisted. //! //! The version 0 file format is: //! //! | Offset | Description | //! | -----: | ----------- | //! | | Header //! | 0 | 15-byte magic value `b"StoredSortedSet"` //! | 15 | Version (`0`) //! | 16 | Context, 12 bytes of application-specific, opaque data //! | | Content //! | 28 | Entry count - big endian 32-bit unsigned integer //! | 32 | Entry 0 - big endian 32-byte unsigned integer //! | 48 | Entry 1 //! | ... | ... //! //! The entries are sorted, which allows doing an in-place binary //! search. They are interpreted as big endian 32-byte unsigned //! integers. use std::{ collections::BTreeSet, io::{ Seek, SeekFrom, Write, }, path::Path, }; use buffered_reader::{BufferedReader, File}; const VALUE_BYTES: usize = 32; pub type Value = [u8; VALUE_BYTES]; pub struct Set { header: Header, store: File<'static, ()>, scratch: BTreeSet, } /// A set data type with 32-byte keys, which can be easily persisted. /// /// The entire data structure is `mmap`ed or read into memory. /// /// Currently, there is no way to remove entries. impl Set { /// Returns the number of entries. #[allow(dead_code)] fn len(&self) -> usize { usize::try_from(self.header.entries).expect("representable") + self.scratch.len() // XXX: overestimate b/c of how insert is implemented } /// Returns `true` if the set contains an element equal to the value. pub fn contains(&mut self, value: &Value) -> Result { Ok(self.stored_values()?.binary_search(value).is_ok() || self.scratch.contains(value)) } /// Adds a value to the set. pub fn insert(&mut self, value: Value) { // We insert it into our overlay without checking whether it // exists in the stored set to avoid the lookup overhead. We // sort this out when persisting any changes to disk. self.scratch.insert(value); } fn stored_values(&mut self) -> Result<&[Value]> { let entries = self.header.entries as usize; let bytes = self.store.data_hard(entries * VALUE_BYTES)?; unsafe { Ok(std::slice::from_raw_parts(bytes.as_ptr() as *const Value, entries)) } } pub fn read>(path: P, context: &str) -> Result { // We are going to read an array of values into memory, and // use them as is. Check that layout matches (i.e., that Rust // doesn't expect any padding). assert_eq!(VALUE_BYTES, std::mem::size_of::()); assert_eq!(std::mem::size_of::<[Value; 2]>(), 2 * VALUE_BYTES, "values are unpadded"); let context: [u8; CONTEXT_BYTES] = context.as_bytes() .try_into() .map_err(|_| Error::BadContext)?; let (header, reader) = match File::open(path) { Ok(mut f) => { let header = Header::read(&mut f, context)?; (header, f) }, Err(e) if e.kind() == std::io::ErrorKind::NotFound => { let t = tempfile::NamedTempFile::new()?; // XXX: Rather, we should be using t.reopen() here and // constructing a File from that: let f = File::open(t.path())?; (Header::new(context), f) }, Err(e) => return Err(e.into()), }; // XXX: check here if the number of entries is plausible by // looking at the file metadata. This is currently not // possible to do in a race free manner. Ok(Set { header, store: reader, scratch: Default::default(), }) } pub fn write>(&mut self, path: P) -> Result<()> { // If we didn't change anything, we're done. if self.scratch.is_empty() { return Ok(()); } let mut sink = tempfile::NamedTempFile::new_in( path.as_ref().parent().ok_or(Error::BadPath)?)?; // First update and write the header. let mut h = self.header.clone(); h.entries = 0; // Fill be fixed later. h.write(&mut sink)?; // Then, merge the two sets while writing them out. let mut entries = 0; let scratch = std::mem::replace(&mut self.scratch, Default::default()); let mut stored = self.stored_values()?; for new in scratch.iter() { let p = stored.partition_point(|v| v < new); let before = &stored[..p]; let before_bytes = unsafe { std::slice::from_raw_parts(before.as_ptr() as *const u8, before.len() * VALUE_BYTES) }; sink.write_all(before_bytes)?; entries += p; // See if this is actually new. if before.is_empty() || &before[p - 1] != new { sink.write_all(new)?; entries += 1; } // Now advance the stored "iterator". stored = &stored[p..]; } // Now write out the final chunk. { let stored_bytes = unsafe { std::slice::from_raw_parts(stored.as_ptr() as *const u8, stored.len() * VALUE_BYTES) }; sink.write_all(stored_bytes)?; entries += stored.len(); } // And put scratch back. self.scratch = scratch; // Finally, write the header again, this time with the correct // number of values. sink.as_file_mut().seek(SeekFrom::Start(0))?; h.entries = entries.try_into().map_err(|_| Error::TooManyEntries)?; h.write(&mut sink)?; sink.flush()?; sink.persist(path).map_err(|pe| pe.error)?; Ok(()) } } const CONTEXT_BYTES: usize = 12; #[derive(Debug, Clone)] struct Header { version: u8, context: [u8; CONTEXT_BYTES], entries: u32, } impl Header { const MAGIC: &'static [u8; 15] = b"StoredSortedSet"; fn new(context: [u8; CONTEXT_BYTES]) -> Self { Header { version: 1, context, entries: 0, } } fn read(reader: &mut File<'static, ()>, context: [u8; CONTEXT_BYTES]) -> Result { let m = reader.data_consume_hard(Self::MAGIC.len())?; if &m[..Self::MAGIC.len()] != &Self::MAGIC[..] { return Err(Error::BadMagic); } let v = reader.data_consume_hard(1)?; let version = v[0]; if version != 1 { return Err(Error::UnsupportedVersion(version)); } let c = &reader.data_consume_hard(context.len())?[..context.len()]; if &c[..] != &context[..] { return Err(Error::BadContext); } let e = &reader.data_consume_hard(4)?[..4]; let entries = u32::from_be_bytes(e.try_into().expect("we read 4 bytes")); Ok(Header { version, context, entries, }) } fn write(&self, sink: &mut dyn Write) -> Result<()> { sink.write_all(Self::MAGIC)?; sink.write_all(&[self.version])?; sink.write_all(&self.context)?; sink.write_all(&self.entries.to_be_bytes())?; Ok(()) } } /// Errors for this crate. #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Bad magic read from file")] BadMagic, #[error("Unsupported version: {0}")] UnsupportedVersion(u8), #[error("Bad context read from file")] BadContext, #[error("Too many entries")] TooManyEntries, #[error("Bad path")] BadPath, #[error("Io error")] Io(#[from] std::io::Error), } /// Result specialization. pub type Result = ::std::result::Result; sequoia-git-0.1.0/src/policy.rs000064400000000000000000000611541046102023000145140ustar 00000000000000use std::{ collections::{BTreeMap, BTreeSet}, env, fmt, fs, io::{self, Read, Write}, path::{Path, PathBuf}, time::SystemTime, }; use git2::{ Repository, Oid, }; use serde::{Deserialize, Serialize}; use sequoia_openpgp::{ self as openpgp, Cert, Fingerprint, KeyHandle, cert::{ amalgamation::ValidAmalgamation, prelude::{SubordinateKeyAmalgamation, UserIDAmalgamation}, raw::{RawCert, RawCertParser}, }, packet::{ Signature, UserID, key::PublicParts, }, parse::Parse, parse::{stream::*}, policy::StandardPolicy, serialize::Serialize as _, }; use crate::{ Error, Result, utils::prune_cert, }; /// Whether to trace execution by default (on stderr). const TRACE: bool = false; /// A policy for OpenPGP signatures in git. /// /// A `Policy` governs state changes in git repositories. A state /// change is a change from one git commit with a policy embedded into /// it to the next commit, which may change the policy, the source, or /// both. #[derive(Default, Clone, Deserialize, Serialize)] pub struct Policy { /// Policy version. /// /// We provide backwards-compatibility but not /// forward-compatibility, so that we can evolve the policy /// language. #[serde(default)] pub version: usize, /// Set of commits that is assumed to be good. /// /// The commits will pass verification even if it would fail for /// whatever reason. /// /// To change this set, you need the `audit` right. #[serde(default)] pub commit_goodlist: BTreeSet, /// Set of authorizations. /// /// The key is a free-form, human-readable identifier for the /// authorization. #[serde(default)] pub authorization: BTreeMap, } impl Policy { /// Returns the path to the policy file in the current git /// repository. fn working_dir_policy_file() -> Result { let git = git2::Repository::discover(env::current_dir()?)?; if let Some(wd) = git.workdir() { Ok(wd.join("openpgp-policy.toml")) } else { Err(Error::InvalidOperation("doesn't work on bare repos".into())) } } pub fn parse_bytes>(bytes: D) -> Result { let bytes = bytes.as_ref(); let s = std::str::from_utf8(bytes) .map_err(|e| Error::StorageError(e.to_string()))?; let policy = toml::from_str(s) .map_err(|e| Error::StorageError(e.to_string()))?; Ok(policy) } /// Reads the policy from the given path. pub fn read>(path: P) -> Result { let path = path.as_ref(); let mut f = match fs::File::open(path) { Ok(f) => f, Err(e) => if e.kind() == io::ErrorKind::NotFound { return Ok(Policy::default()); } else { return Err(e.into()); }, }; let mut s = String::new(); f.read_to_string(&mut s)?; let p: Policy = toml::from_str(&s).map_err(|e| Error::StorageError(e.to_string()))?; Ok(p) } /// Reads the policy from the current git working directory. pub fn read_from_working_dir() -> Result { Self::read(&Self::working_dir_policy_file()?) } /// Reads the policy from the given git commit. pub fn read_bytes_from_commit(git: &Repository, commit: &Oid) -> Result> { tracer!(TRACE, "Policy::read_bytes_from_commit"); t!("(_, {})", commit); let commit = git.find_commit(commit.clone())?; let tree = commit.tree()?; let result = if let Some(entry) = tree.get_name("openpgp-policy.toml") { Ok(entry.to_object(&git)?.peel_to_blob()?.content().to_vec()) } else { Err(Error::MissingPolicy(commit.id())) }; result } /// Reads the policy from the given git commit. pub fn read_from_commit(git: &Repository, commit: &Oid) -> Result { Self::parse_bytes(Self::read_bytes_from_commit(git, commit)?) } /// Writes the policy into a file with the given path. pub fn write>(&self, path: P) -> Result<()> { let path = path.as_ref(); let mut new = tempfile::NamedTempFile::new_in(path.parent().unwrap())?; new.write_all(toml::to_string_pretty(&self) .map_err(|e| Error::StorageError(e.to_string()))? .as_bytes())?; new.persist(path).map_err(|e| Error::StorageError(e.to_string()))?; Ok(()) } /// Writes the policy to the current git working directory. pub fn write_to_working_dir(&self) -> Result<()> { self.write(&Self::working_dir_policy_file()?) } /// Computes the difference between this policy and `other`. pub fn diff<'f, 't>(&'f self, other: &'t Policy) -> Result> { let mut changes = Vec::new(); // First, the version. if self.version != other.version { changes.push(Change::VersionChange { from: self.version, to: other.version, }); } // Then, the commit goodlist. for c in self.commit_goodlist.difference(&other.commit_goodlist) { changes.push(Change::UngoodlistCommit(c.parse()?)); } for c in other.commit_goodlist.difference(&self.commit_goodlist) { changes.push(Change::GoodlistCommit(c.parse()?)); } // This null authorization comes in handy when introducing // new users and removing users. let null_auth = Authorization::default(); // Now for the authorizations. First, see if some vanished. for (k, from) in self.authorization.iter() .filter(|(k, _)| ! other.authorization.contains_key(k.as_str())) { // First, remove all the rights and certs. from.diff(&null_auth, k.into(), &mut changes); // Finally, remove the user. changes.push(Change::RetireUser(k.into())); } // Then, compare the common ones. for (k, from, to) in self.authorization.iter() .filter_map(|(k, from)| other.authorization.get(k) .map(|to| (k, from, to))) { from.diff(to, k.into(), &mut changes); } // See if new users were introduced. for (k, to) in other.authorization.iter() .filter(|(k, _)| ! self.authorization.contains_key(k.as_str())) { // First introduce the new user. changes.push(Change::AddUser(k.into())); // Then, all the new rights and certs. null_auth.diff(to, k.into(), &mut changes); } Ok(Diff { version: DIFF_JSON_VERSION, from: self, changes, to: other, }) } /// Verifies that the given commit adheres to this policy. /// /// During verification, the key(s) used are stored in /// `signer_keys`, and the primary user id of the issuing cert at /// the time of the signing is stored in `primary_uids`. This /// information can be used to prune certs in a policy. /// /// If the commit is goodlisted, this function returns Ok with an /// empty vector of verification results. pub fn verify(&self, git: &Repository, commit_id: &Oid, commit_policy: &Policy, signer_keys: &mut BTreeSet, primary_uids: &mut BTreeSet) -> Result>> { tracer!(TRACE, "Policy::verify"); t!("verify(_, {})", commit_id); if self.commit_goodlist.contains(&commit_id.to_string()) { Ok(vec![]) } else { let Ok((sig, data)) = git.extract_signature(commit_id, None) else { return Ok(vec![Err(Error::MissingSignature(commit_id.clone()))]); }; t!("{} bytes of signature", sig.len()); //let commit = git.find_commit(commit_id.clone())?; //let commit_time = commit.time(); //let commit_time = std::time::UNIX_EPOCH // + std::time::Duration::new(commit_time.seconds() as u64, 0); // Note the commit time and the signature time will often // diverge. This is because the signature is created // after the commit is made (the signature is over the // commit, including the creation time). If we use the // commit's time as the reference time, then the signature // will appear to have been made in the future. Note: it // is not enough to allow a few seconds of divergence, // because commits can be resigned without changing the // commit's time. self.verify_(&sig[..], &data[..], commit_policy, None, signer_keys, primary_uids, Error::MissingSignature(commit_id.clone()), Right::SignCommit) } } pub fn verify_archive(&self, signature: S, archive: T) -> Result>> where T: AsRef<[u8]>, S: AsRef<[u8]>, { let mut signer_keys = Default::default(); let mut primary_uids = Default::default(); self.verify_(signature.as_ref(), archive.as_ref(), self, None, &mut signer_keys, &mut primary_uids, Error::MissingDataSignature("Tarball".into()), Right::SignArchive) } fn verify_(&self, signature: &[u8], data: &[u8], commit_policy: &Policy, commit_time: Option, signer_keys: &mut BTreeSet, primary_uids: &mut BTreeSet, missing_signature_error: Error, require_right: Right) -> Result>> { tracer!(TRACE, "Policy::verify_"); t!("verify_({} bytes, {} bytes, _, {:?}, _, _, {}, {})", signature.len(), data.len(), commit_time, missing_signature_error, require_right); let p = &StandardPolicy::new(); let h = Helper { policy: self, signer_keys, primary_uids, results: Default::default(), }; let mut v = DetachedVerifierBuilder::from_bytes(signature)? .with_policy(p, commit_time, h)?; v.verify_bytes(data)?; let h = v.into_helper(); let signature_results = h.results; if signature_results.is_empty() { t!("no signatures found!"); return Ok(vec![Err(missing_signature_error)]); } if signature_results.iter().all(|r| r.is_err()) { let e = signature_results.into_iter().find(|r| r.is_err()) .expect("not empty and not all were ok"); return Err(e.unwrap_err()); } // If we are here, there is at least one valid OpenPGP // signature. Compute the diff between the policies, and // check whether the authorization invariant is intact. let diff = self.diff(commit_policy)?; let mut results: Vec> = Vec::new(); for r in signature_results { match r { Ok((sig, cert, signer_fpr)) => { // Find all authorizations that contain a // certificate that did issue a valid signature. let cert_fp = cert.fingerprint(); for (name, a) in self.authorization.iter() .filter(|(_, a)| a.certs().into_iter() .flat_map(|r| r.into_iter()) .flat_map(|r| r.into_iter()) .any(|c| c.fingerprint() == cert_fp)) { t!("{}: valid signature", name); let r = a.rights(); t!("{}: {:?}", name, r); if let Err(e) = r.assert(require_right) .and_then(|_| diff.assert(&r)) { results.push(Err(e)); } else { results.push( Ok((name.into(), sig.clone(), cert.clone(), signer_fpr.clone()))); } } }, Err(e) => results.push(Err(e)), } } Ok(results) } } // This fetches keys and computes the validity of the verification. struct Helper<'p> { policy: &'p Policy, //signer_userids: &'p mut BTreeSet, signer_keys: &'p mut BTreeSet, primary_uids: &'p mut BTreeSet, results: Vec>, } impl Helper<'_> { fn handle_result(&mut self, r: VerificationResult) { tracer!(TRACE, "VerificationHelper::handle_result"); match r { Ok(sig) => { self.signer_keys.insert(sig.ka.fingerprint()); if let Ok(userid) = sig.ka.cert().primary_userid() { let u = userid.userid(); if ! self.primary_uids.contains(u) { self.primary_uids.insert(u.clone()); } } self.results.push( Ok((sig.sig.clone(), sig.ka.cert().cert().clone(), sig.ka.fingerprint().clone()))); }, Err(e) => { t!("Signature verification failed: {}", e); use VerificationError::*; self.results.push(Err(match e { MalformedSignature { error, .. } => Error::BadSignature(error.to_string()), MissingKey { sig } => { let mut issuers = sig.get_issuers(); if issuers.is_empty() { Error::BadSignature( "No issuer information".into()) } else { Error::MissingKey(issuers.remove(0)) } }, UnboundKey { cert, error, .. } => Error::BadKey(cert.key_handle(), error.to_string()), BadKey { ka, error, .. } => Error::BadKey(ka.cert().key_handle(), error.to_string()), BadSignature { error, .. } => Error::BadSignature(error.to_string()), })); }, } } } impl VerificationHelper for Helper<'_> { fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result> { tracer!(TRACE, "VerificationHelper::get_certs"); t!("get_certs({:?})", ids); let mut certs = vec![]; for (name, auth) in self.policy.authorization.iter() { for cert in auth.certs()? { let cert = cert?; if cert.keys().any( |k| ids.iter().any(|i| i.aliases(&k.key_handle()))) { t!("Signature appears to be from {}", name); certs.push(Cert::try_from(cert)?); } } } Ok(certs) } fn check(&mut self, structure: MessageStructure) -> openpgp::Result<()> { tracer!(TRACE, "VerificationHelper::get_certs"); if false { t!("check({:?})", structure); } for (i, layer) in structure.into_iter().enumerate() { match layer { MessageLayer::SignatureGroup { results } if i == 0 => { for r in results { self.handle_result(r); } }, _ => return Err(Error::BadSignature( "Unexpected signature structure".into()).into()), } } Ok(()) } } #[derive(Default, Clone, Deserialize, Serialize)] pub struct Authorization { #[serde(default, skip_serializing_if = "bool_is_false")] pub sign_commit: bool, #[serde(default, skip_serializing_if = "bool_is_false")] pub sign_tag: bool, #[serde(default, skip_serializing_if = "bool_is_false")] pub sign_archive: bool, #[serde(default, skip_serializing_if = "bool_is_false")] pub add_user: bool, #[serde(default, skip_serializing_if = "bool_is_false")] pub retire_user: bool, #[serde(default, skip_serializing_if = "bool_is_false")] pub audit: bool, pub keyring: String, } fn bool_is_false(b: &bool) -> bool { *b == false } impl Authorization { pub fn rights(&self) -> Rights { use Right::*; let mut r = BTreeSet::default(); if self.sign_commit { r.insert(SignCommit); } if self.sign_tag { r.insert(SignTag); } if self.sign_archive { r.insert(SignArchive); } if self.add_user { r.insert(AddUser); } if self.retire_user { r.insert(RetireUser); } if self.audit { r.insert(Audit); } Rights(r) } pub fn certs(&self) -> Result>> { Ok(RawCertParser::from_bytes(self.keyring.as_bytes())?) } pub fn set_certs(&mut self, certs: Vec) -> Result<()> { self.set_certs_filter(certs, |_| true, |_| true) } pub fn set_certs_filter(&mut self, certs: Vec, mut subkeys: S, mut userids: U) -> Result<()> where S: FnMut(&SubordinateKeyAmalgamation) -> bool, U: FnMut(&UserIDAmalgamation) -> bool, { let mut keyring = Vec::new(); for c in certs { let c = prune_cert(c, &mut subkeys, &mut userids)?; c.armored().export(&mut keyring)?; } self.keyring = String::from_utf8(keyring) .map_err(|e| Error::StorageError(e.to_string()))?; Ok(()) } /// Computes the difference between this authorization and `other` /// recording the changes in `changes`. fn diff(&self, other: &Authorization, name: String, changes: &mut Vec) { let (from, to) = (self, other); // First, see if rights were removed. if from.sign_commit && ! to.sign_commit { changes.push(Change::RemoveRight(name.clone(), Right::SignCommit)); } if from.sign_tag && ! to.sign_tag { changes.push(Change::RemoveRight(name.clone(), Right::SignTag)); } if from.sign_archive && ! to.sign_archive { changes.push(Change::RemoveRight(name.clone(), Right::SignArchive)); } if from.add_user && ! to.add_user { changes.push(Change::RemoveRight(name.clone(), Right::AddUser)); } if from.retire_user && ! to.retire_user { changes.push(Change::RemoveRight(name.clone(), Right::RetireUser)); } if from.audit && ! to.audit { changes.push(Change::RemoveRight(name.clone(), Right::Audit)); } // Then, see if rights were added. if ! from.sign_commit && to.sign_commit { changes.push(Change::AddRight(name.clone(), Right::SignCommit)); } if ! from.sign_tag && to.sign_tag { changes.push(Change::AddRight(name.clone(), Right::SignTag)); } if ! from.sign_archive && to.sign_archive { changes.push(Change::AddRight(name.clone(), Right::SignArchive)); } if ! from.add_user && to.add_user { changes.push(Change::AddRight(name.clone(), Right::AddUser)); } if ! from.retire_user && to.retire_user { changes.push(Change::AddRight(name.clone(), Right::RetireUser)); } if ! from.audit && to.audit { changes.push(Change::AddRight(name.clone(), Right::Audit)); } // XXX compare certs } } // The version of the commit output. This follows semantic // versioning. static DIFF_JSON_VERSION: &'static str = "1.0.0"; /// The difference between two [`Policy`]s. #[derive(Serialize)] pub struct Diff<'f, 't> { version: &'static str, pub from: &'f Policy, pub changes: Vec, pub to: &'t Policy, } impl Diff<'_, '_> { fn assert(&self, r: &Rights) -> Result<()> { for c in &self.changes { c.assert(r)?; } Ok(()) } } use crate::utils::{serialize_fp, serialize_oid}; #[derive(Clone, Serialize)] pub enum Change { VersionChange { from: usize, to: usize, }, GoodlistCommit( #[serde(serialize_with = "serialize_oid")] Oid), UngoodlistCommit( #[serde(serialize_with = "serialize_oid")] Oid), AddUser(String), RetireUser(String), AddRight(String, Right), RemoveRight(String, Right), AddCert(String, #[serde(serialize_with = "serialize_fp")] Fingerprint), AddUserID(String, #[serde(serialize_with = "serialize_fp")] Fingerprint, String), AddSubkey(String, #[serde(serialize_with = "serialize_fp")] Fingerprint, #[serde(serialize_with = "serialize_fp")] Fingerprint), AddSignatures(String, #[serde(serialize_with = "serialize_fp")] Fingerprint, usize), RemoveCert(String, #[serde(serialize_with = "serialize_fp")] Fingerprint), RemoveUserID(String, #[serde(serialize_with = "serialize_fp")] Fingerprint, String), RemoveSubkey(String, #[serde(serialize_with = "serialize_fp")] Fingerprint, #[serde(serialize_with = "serialize_fp")] Fingerprint), RemoveSignatures(String, #[serde(serialize_with = "serialize_fp")] Fingerprint, usize), } impl Change { fn assert(&self, r: &Rights) -> Result<()> { use Change::*; match self { VersionChange { .. } => r.assert(Right::Audit), GoodlistCommit(_) => r.assert(Right::Audit), UngoodlistCommit(_) => r.assert(Right::Audit), // Rights management. AddUser(_) => r.assert(Right::AddUser), RetireUser(_) => r.assert(Right::RetireUser), AddRight(_, right) => r.assert(Right::AddUser).and_then(|_| r.assert(*right)), RemoveRight(_, right) => r.assert(Right::RetireUser).and_then(|_| r.assert(*right)), // Cert management. AddCert(_, _) => r.assert(Right::AddUser), RemoveCert(_, _) => r.assert(Right::RetireUser), // Lenient cert updates. AddUserID(_, _, _) => Ok(()), AddSubkey(_, _, _) => Ok(()), AddSignatures(_, _, _) => Ok(()), // Strict cert trimmings. RemoveUserID(_, _, _) => r.assert(Right::RetireUser), RemoveSubkey(_, _, _) => r.assert(Right::RetireUser), RemoveSignatures(_, _, _) => r.assert(Right::RetireUser), } } } #[derive(Debug)] pub struct Rights(BTreeSet); impl Rights { fn assert(&self, r: Right) -> Result<()> { if ! self.0.contains(&r) { Err(Error::Unauthorized(format!("Right {} is missing", r))) } else { Ok(()) } } } #[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, PartialOrd, Ord)] pub enum Right { SignCommit, SignTag, SignArchive, AddUser, RetireUser, Audit, } impl fmt::Display for Right { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use Right::*; match self { SignCommit => f.write_str("sign-commit"), SignTag => f.write_str("sign-tag"), SignArchive => f.write_str("sign-archive"), AddUser => f.write_str("add-user"), RetireUser => f.write_str("retire-user"), Audit => f.write_str("audit"), } } } sequoia-git-0.1.0/src/utils.rs000064400000000000000000000054711046102023000143550ustar 00000000000000use git2::Oid; use sequoia_openpgp::{ Cert, Fingerprint, cert::{ prelude::{SubordinateKeyAmalgamation, UserIDAmalgamation}, }, packet::{ key::PublicParts, }, }; use crate::{ Result, }; pub fn prune_cert(c: Cert, subkeys: S, userids: U) -> Result where S: FnMut(&SubordinateKeyAmalgamation) -> bool, U: FnMut(&UserIDAmalgamation) -> bool, { // Remove all user attributes, keep only the subkeys that // are plausible signing subkeys. let c = c.retain_user_attributes(|_| false) .retain_subkeys(|s| s.self_signatures().any( |s| s.key_flags().map(|f| f.for_signing()) .unwrap_or(false))); // Filter out any third-party certifications. let mut acc = Vec::new(); // The primary key and related signatures. let pk_bundle = c.primary_key().bundle(); acc.push(pk_bundle.key().clone().into()); for s in pk_bundle.self_signatures() { acc.push(s.clone().into()) } for s in pk_bundle.self_revocations() { acc.push(s.clone().into()) } // The subkeys and related signatures. for skb in c.keys().subkeys().filter(subkeys) { acc.push(skb.key().clone().into()); for s in skb.self_signatures() { acc.push(s.clone().into()) } for s in skb.self_revocations() { acc.push(s.clone().into()) } } // The UserIDs. for uidb in c.userids().filter(userids) { acc.push(uidb.userid().clone().into()); for s in uidb.self_signatures() { acc.push(s.clone().into()) } for s in uidb.self_revocations() { acc.push(s.clone().into()) } } Ok(Cert::from_packets(acc.into_iter())?) } #[allow(dead_code)] pub fn deserialize_oid<'de, D>(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { use serde::de::{Deserialize, Error}; String::deserialize(deserializer) .and_then(|s| s.parse().map_err(|e| Error::custom(e))) } pub fn serialize_oid(v: &Oid, serializer: S) -> std::result::Result where S: serde::Serializer, { serializer.serialize_str(&v.to_string()) } #[allow(dead_code)] pub fn serialize_optional_oid(v: &Option<&Oid>, serializer: S) -> std::result::Result where S: serde::Serializer, { if let Some(v) = v { serializer.serialize_str(&v.to_string()) } else { serializer.serialize_none() } } pub fn serialize_fp(v: &Fingerprint, serializer: S) -> std::result::Result where S: serde::Serializer, { serializer.serialize_str(&v.to_string()) } sequoia-git-0.1.0/src/verify.rs000064400000000000000000000650741046102023000145260ustar 00000000000000//! Commit-tree traversal and verification. use std::{ path::{Path, PathBuf}, collections::{ BTreeMap, BTreeSet, }, }; use anyhow::{anyhow, Context, Result}; use git2::{ Repository, Oid, }; use sequoia_openpgp::{ self as openpgp, Cert, cert::{ amalgamation::ValidAmalgamation, CertParser, }, crypto::hash::Digest, Fingerprint, packet::{Signature, UserID}, parse::Parse, policy::StandardPolicy, types::{ HashAlgorithm, RevocationStatus, RevocationType, }, }; use crate::{ Policy, persistent_set, }; /// Whether to trace execution by default (on stderr). const TRACE: bool = false; #[derive(Default, Debug)] pub struct VerificationResult { pub signer_keys: BTreeSet, pub primary_uids: BTreeSet, } pub fn verify(git: &Repository, trust_root: Oid, shadow_policy: Option<&[u8]>, commit_range: (Oid, Oid), results: &mut VerificationResult, keep_going: bool, mut verify_cb: impl FnMut(&Oid, Option<&Oid>, &crate::Result>>) -> crate::Result<()>, cache: &mut VerificationCache) -> Result<()> { tracer!(TRACE, "verify"); t!("verify(_, {}, {}..{})", trust_root, commit_range.0, commit_range.1); if shadow_policy.is_some() { t!("Using a shadow policy to verify commits."); } else { t!("Using in-band policies to verify commits."); } // XXX: These should be passed in as arguments. let p: &StandardPolicy = &StandardPolicy::new(); let now = std::time::SystemTime::now(); // STRATEGY // // We want to determine if we can authenticate the target commit // starting from the trust root. A simple approach is to try each // possible path. Unfortunately, this is exponential in the // number of merges. Consider a project where development is done // on feature branches, and then merged using a merge commit. The // commit graph will look like: // // ``` // o <- Merge commit // | \ // | o <- Feature branch // | / // o <- Merge commit // | \ // | o <- Feature branch // | / // o <- Merge commit // | \ // ... // ``` // // After 100 such merges, there are 2**100 different paths. // That's intractable. // // Happily, we can do better. We can determine if there is an // authenticated path as a byproduct of a topological walk. This // approach limits both the work we have to do, and the space we // require to O(N), where N is the number of commits preceding the // target commit in the commit graph. (If the trust root is a // dominator, which is usually the case, then N is the number of // commits that precede target, and follow the trust root, which // is often very small.) // // Recall the following facts: // // - A commit is authenticated if it is the trust root, or at // least one parent's policy authenticates the commit. // - Unless the signer's certificate is hard revoked. // - Unless there is an authenticated suffix that // immediately follows the commit, and a commit on that // authenticated suffix goodlists commit. // // In order to check the last condition, we need to know the // goodlists of all the following, authenticated commits when we // process the commit. When we do a topological walk, we know // that when we visit a commit, we've already visited all of its // ancestors. This means that we can easily collect the goodlists // of all of the commits on authenticated paths from the commit to // the target during the walk by propagating good lists from // authenticated commits to their parents. Then, when we are // examining a commit, and we discover that it has been revoked, // we can immediately check if it is on a relevant goodlist. // // To do the topological walk, we first have to do a bit of // preparation. Since we don't know a commit's children by // looking at the commit (and not all children lead to the // target), we have to extract that information from the graph. // We do this by visiting all commits on paths from the target to // the trust root, which we can do in O(N) space and time by doing // a breath first search. When we visit a commit, we increment // the number of children each of its parents has. We also use // this opportunity to discover any certificates that have been // hard revoked. // // Then we do the topological walk. For each commit, we consider // the number of unprocessed children to be the number of recorded // children. We then visit commits that have no unprocessed // children. // // When we visit a commit, and there is an authenticated path from // it to the target, we try to authenticate the commit. That is, // for each parent, we check if the parent can authenticate the // commit. If a parent authenticates the commit, but the signer's // certificate has been revoked, we can immediately check whether // the commit is on a goodlist. For each parent that // authenticated the commit, we merge the commit's *aggregate* // goodlist into the parent's goodlist. Finally, for each parent, // we decrement the number of unprocessed children. If there are // no unprocessed children left, it is added to a pending queue, // so that it can be processed. // Return the policy associated with the commit. let read_policy = |commit: &Oid| -> Result> { if let Some(p) = &shadow_policy { Ok(p.to_vec()) } else { Ok(Policy::read_bytes_from_commit(git, commit)?) } }; // Whether we've seen the policy file before. The key is the // SHA512 digest of the policy file. let mut policy_files: BTreeSet> = Default::default(); // Any hard revoked certificates. let mut hard_revoked: BTreeSet = Default::default(); // Scans the specified commit's policy for hard revocations, and // adds them to hard_revoked. let mut scan_policy = |commit_id| -> Result<()> { let policy_bytes = read_policy(&commit_id)?; let policy_hash = sha512sum(&policy_bytes)?; if policy_files.contains(&policy_hash) { t!("Already scanned an identical copy of {}'s policy, skipping.", commit_id); return Ok(()); } t!("Scanning {}'s policy for hard revocations", commit_id); let policy = Policy::parse_bytes(&policy_bytes)?; policy_files.insert(policy_hash); // Scan for revoked certificates. for authorization in policy.authorization.values() { for cert in CertParser::from_bytes(&authorization.keyring)? { let cert = if let Ok(cert) = cert { cert } else { continue; }; let vc = if let Ok(vc) = cert.with_policy(p, Some(now)) { vc } else { continue; }; let is_hard_revoked = |rs| { if let RevocationStatus::Revoked(revs) = rs { revs.iter().any(|rev| { if let Some((reason, _)) = rev.reason_for_revocation() { reason.revocation_type() == RevocationType::Hard } else { true } }) } else { false } }; // Check if the certificate is hard revoked. if is_hard_revoked(vc.revocation_status()) { t!("Certificate {} is hard revoked, bad listing", cert.fingerprint()); hard_revoked.insert(vc.fingerprint()); for k in vc.keys().subkeys().for_signing() { hard_revoked.insert(k.fingerprint()); t!(" Badlisting signing key {}", k.fingerprint()); } continue; } // Check if any of the signing keys are hard revoked. for k in vc.keys().subkeys().for_signing() { if is_hard_revoked(k.revocation_status()) { hard_revoked.insert(k.fingerprint()); t!(" Signing key {} hard revoked, bad listing", k.fingerprint()); } } } } Ok(()) }; let middle = if trust_root == commit_range.0 { None } else { Some(commit_range.0) }; struct Commit { // Number of children (commits derived from this one). children: usize, // Whether there is an authenticated path from this node to // the target. authenticated_suffix: bool, // Whether we still need to go via MIDDLE. traversed_middle: bool, } impl Default for Commit { fn default() -> Self { Commit { children: 0, authenticated_suffix: false, traversed_middle: false, } } } let mut commits: BTreeMap = Default::default(); if trust_root != commit_range.1 { commits.insert( commit_range.1.clone(), Commit { children: 0, authenticated_suffix: true, traversed_middle: middle.is_none(), }); } // We walk the tree from the target to the trust root (using a // breath first search, but it doesn't matter), and fill in // COMMITS.CHILDREN. { // Commits that we haven't processed yet. let mut pending: BTreeSet = Default::default(); pending.insert(commit_range.1.clone()); // Commits that we've processed. let mut processed: BTreeSet = Default::default(); while let Some(commit_id) = pending.pop_first() { processed.insert(commit_id); let commit = git.find_commit(commit_id)?; // Don't fail if we can't parse the policy file. let _ = scan_policy(commit_id); if commit_id == trust_root { // This is the trust root. There is no need to go // further. continue; } for parent in commit.parents() { let parent_id = parent.id(); let info = commits.entry(parent_id).or_default(); info.children += 1; if ! processed.contains(&parent_id) && ! pending.contains(&parent_id) { pending.insert(parent_id); } } } } // The union of the commit goodlists of commits on an // authenticated suffix (not including this commit). We build // this up as we authenticate commits. Since we do a topological // walk, this will be complete for a given commit when we process // that commit. let mut descendant_goodlist: BTreeMap> = Default::default(); let mut errors = Vec::new(); let mut unauthenticated_commits: BTreeSet = Default::default(); let mut authenticated_commits: BTreeSet = Default::default(); // Authenticate the commit using the specified parent. // // NOTE: This must only be called on a commit, if the commit is on // an authenticated suffix!!! let mut authenticate_commit = |commit_id, parent_id| -> Result { let parent_policy = read_policy(&parent_id)?; let parent_id = if commit_id == parent_id { // This is only the case when verifying the trust root // using the shadow policy. assert_eq!(commit_id, trust_root); assert!(shadow_policy.is_some()); None } else { Some(&parent_id) }; // The current commit's good list. let mut commit_goodlist = BTreeSet::new(); // XXX: If we have some certificates that are hard revoked, // then we can't use the cache. This is because the cache // doesn't tell us what certificate was used to sign the // commit, which means we can't figure out if the signer's // certificate was revoked when the result is cached. let (vresult, cache_hit) = if hard_revoked.is_empty() && cache.contains(&parent_policy, commit_id)? { (Ok(vec![]), true) } else { let parent_policy = Policy::parse_bytes(&parent_policy)?; let commit_policy = Policy::parse_bytes(read_policy(&commit_id)?)?; commit_goodlist = commit_policy.commit_goodlist.clone(); (parent_policy.verify(git, &commit_id, &commit_policy, &mut results.signer_keys, &mut results.primary_uids), false) }; if let Err(err) = verify_cb(&commit_id, parent_id, &vresult) { t!("verify_cb -> {}", err); return Err(err.into()); } if cache_hit { // XXX: communicate this to the caller // instead of eprintln. let id = if let Some(parent_id) = parent_id { format!("{}..{}", parent_id, commit_id) } else { commit_id.to_string() }; eprintln!("{}: Cached positive verification", id); } match vresult { Ok(results) => { // Whether the parent authenticated the commit. let mut good = false; // Whether the commit was goodlisted by a later // commit. let mut goodlisted = false; // Whether the commit was goodlisted by the parent's // policy. (Because commits form a Merkle tree, this // is only possible when we are using a shadow // policy.) if ! cache_hit && results.is_empty() { // XXX: communicate this to the caller // instead of eprintln. eprintln!("{}: Explicitly goodlisted", commit_id); good = true; } for r in results { match r { Ok((_, _sig, cert, signer_fpr)) => { // It looks good, but make sure the // certificate was not revoked. if hard_revoked.contains(&signer_fpr) { t!("Cert {}{} used to sign {} is revoked.", cert.fingerprint(), if cert.fingerprint() != signer_fpr { format!(", key {}", signer_fpr) } else { "".to_string() }, commit_id); // It was revoked, but perhaps the // commit was goodlisted. if descendant_goodlist.get(&commit_id) .map(|goodlist| { t!(" Goodlist contains: {}", goodlist .iter().cloned() .collect::>() .join(", ")); goodlist.contains(&commit_id.to_string()) }) .unwrap_or(false) { t!("But the commit was goodlisted, \ so all is good."); goodlisted = true; } } else { t!("{} has a good signature from {}", commit_id, cert.fingerprint()); good = true; } } Err(e) => errors.push( anyhow::Error::from(e).context( format!("While verifying commit {}", commit_id))), } } // We do NOT insert into the cache if the commit was // goodlisted. The cache is a function of the parent // policy and the children policy; goodlisting is a // function of commit range. if ! cache_hit && good && ! goodlisted { cache.insert(&parent_policy, commit_id)?; } if cache_hit || good || goodlisted { // Merge the commit's goodlist into the parent's // goodlist. if let Some(descendant_goodlist) = descendant_goodlist.get(&commit_id) { commit_goodlist.extend(descendant_goodlist.iter().cloned()); }; if let Some(parent_id) = parent_id { if let Some(p_goodlist) = descendant_goodlist.get_mut(&parent_id) { p_goodlist.extend(commit_goodlist.into_iter()); } else if ! commit_goodlist.is_empty() { descendant_goodlist.insert( parent_id.clone(), commit_goodlist); } } } let authenticated = cache_hit || good || goodlisted; if authenticated { authenticated_commits.insert(commit_id); } else { unauthenticated_commits.insert(commit_id); } Ok(authenticated) }, Err(e) => { unauthenticated_commits.insert(commit_id); errors.push(anyhow::Error::from(e).context( format!("While verifying commit {}", commit_id))); Ok(false) }, } }; // We now do a topological walk from the target to the trust root. // Assume there is no path until we prove otherwise. (A // zero-length path is already valid.) let mut valid_path = trust_root == commit_range.0 && commit_range.0 == commit_range.1; // Commits that we haven't processed yet. let mut pending: BTreeSet = Default::default(); if trust_root != commit_range.1 { pending.insert(commit_range.1.clone()); } 'authentication: while let Some(commit_id) = pending.pop_first() { let commit = git.find_commit(commit_id)?; t!("Processing {}: {}", commit_id, commit.summary().unwrap_or("")); let commit_info = commits.get(&commit_id).expect("added"); assert_eq!(commit_info.children, 0); let authenticated_suffix = commit_info.authenticated_suffix; let traversed_middle = commit_info.traversed_middle; drop(commit_info); for (parent_i, parent) in commit.parents().enumerate() { let parent_id = parent.id(); t!("Considering {} -> {} (parent #{})", commit_id, parent_id, parent_i); let parent_info = commits.get_mut(&parent_id).expect("added"); t!(" Parent has {} unprocessed children", parent_info.children); assert!(parent_info.children > 0); parent_info.children -= 1; if authenticated_suffix { t!(" Child IS on an authenticated suffix"); } else { t!(" Child IS NOT on an authenticated suffix."); } let authenticated = if keep_going || authenticated_suffix { authenticate_commit(commit_id, parent_id) .with_context(|| { format!("Authenticating {} with {}", commit_id, parent_id) })? } else { false }; if authenticated_suffix && authenticated { t!(" Parent authenticates child"); parent_info.authenticated_suffix = true; if traversed_middle { parent_info.traversed_middle = true; } else if middle == Some(commit_id) { t!(" Traversed {} on way to trust root.", commit_id); parent_info.traversed_middle = true; } if parent_id == trust_root { t!(" Parent is the trust root."); // This is the trust root. There is no need to go // further. if ! parent_info.traversed_middle { t!(" but path was not via {}", middle.unwrap()); } else { valid_path = true; } if ! keep_going { break 'authentication; } } } if parent_info.children == 0 { t!(" No other unprocessed children."); if parent_id == trust_root { t!(" Parent is the start of the commit range, \ so it doesn't need to be processed."); } else if ! keep_going && ! parent_info.authenticated_suffix { t!(" Parent does not authenticate any child, \ so it doesn't need to be processed."); } else { t!(" Adding parent to pending."); pending.insert(parent_id); } } } } // When using a shadow policy, we also authenticate the trust // root with it. if (keep_going || valid_path) && shadow_policy.is_some() { t!("Verifying trust root ({}) using the shadow policy", trust_root); // We verify the trust root using itself? Not quite. We know // that authenticate_commit prefers the shadow policy, and we // know that a shadow policy is set. So this will actually // check that the shadow policy verifies the trust root. if ! authenticate_commit(trust_root, trust_root)? { valid_path = false; if let Some(e) = errors.pop() { errors.push( e.context(format!("Could not verify trust root {} \ using the specified policy", trust_root))); } } } if valid_path { Ok(()) } else { if errors.is_empty() { Err(anyhow!("Could not verify commits {}..{}{}", trust_root, if let Some(middle) = middle { format!("{}..", middle) } else { "".to_string() }, commit_range.1)) } else { let mut e = errors.swap_remove(0) .context(format!("Could not verify commits {}..{}{}", commit_range.0, if let Some(middle) = middle { format!("{}..", middle) } else { "".to_string() }, commit_range.1)); if ! errors.is_empty() { e = e.context( format!("{} errors occurred while verifying the commits. \ {} commits couldn't be authenticated. \ Note: not all errors are fatal. \ The first error is shown:", errors.len() + 1, unauthenticated_commits.difference( &authenticated_commits).count())); } Err(e) } } } // Returns the SHA512 digest of the provided bytes. fn sha512sum(bytes: &[u8]) -> Result> { let mut digest = HashAlgorithm::SHA512.context()?; digest.update(bytes); let mut key = Vec::with_capacity(32); digest.digest(&mut key)?; Ok(key) } pub struct VerificationCache { path: PathBuf, set: persistent_set::Set, } impl VerificationCache { const CONTEXT: &'static str = "SqGitVerify0"; pub fn new() -> Result { let p = dirs::cache_dir().ok_or(anyhow::anyhow!("No cache dir"))? .join("sq-git.verification.cache"); Self::open(p) } pub fn open>(path: P) -> Result { let path = path.as_ref(); Ok(VerificationCache { path: path.into(), set: persistent_set::Set::read(&path, Self::CONTEXT)?, }) } fn key(&self, policy: &[u8], commit: Oid) -> Result { let mut digest = HashAlgorithm::SHA512.context()?; digest.update(policy); digest.update(commit.as_bytes()); let mut key = [0; 32]; digest.digest(&mut key)?; Ok(key) } /// Returns whether (policy, commit id) is in the cache. /// /// If (policy, commit id) is in the cache, then it was previously /// determined that the policy authenticated the commit. pub fn contains(&mut self, policy: &[u8], commit: Oid) -> Result { Ok(self.set.contains(&self.key(policy, commit)?)?) } /// Add (policy, commit id) to the cache. /// /// If (policy, commit id) is in the cache, this means that the /// policy considers the commit to be authenticated. Normally, /// the policy comes from the parent commit, but it may be a /// shadow policy. pub fn insert(&mut self, policy: &[u8], commit: Oid) -> Result<()> { self.set.insert(self.key(policy, commit)?); Ok(()) } pub fn persist(&mut self) -> Result<()> { self.set.write(&self.path)?; Ok(()) } } sequoia-git-0.1.0/tests/basics.rs000064400000000000000000000054461046102023000150360ustar 00000000000000use std::fs; mod common; use common::Environment; #[test] fn create_environment() -> anyhow::Result<()> { let e = Environment::new()?; assert!(e.gnupg_state().exists()); assert!(e.git_state().exists()); Ok(()) } #[test] fn make_commit() -> anyhow::Result<()> { let e = Environment::new()?; let p = e.git_state(); fs::write(p.join("a"), "Aller Anfang ist schwer.")?; e.git(&["add", "a"])?; e.git(&[ "commit", "-m", "Initial commit.", ])?; Ok(()) } #[test] fn sign_commit() -> anyhow::Result<()> { let e = Environment::new()?; let p = e.git_state(); fs::write(p.join("a"), "Aller Anfang ist schwer.")?; e.git(&["add", "a"])?; e.git(&[ "commit", "-m", "Initial commit.", &format!("-S{}", e.willow.fingerprint), ])?; Ok(()) } #[test] fn verify_commit() -> anyhow::Result<()> { let e = Environment::new()?; let p = e.git_state(); e.sq_git(&[ "policy", "authorize", e.willow.petname, &e.willow.fingerprint.to_string(), "--sign-commit" ])?; e.git(&["add", "openpgp-policy.toml"])?; e.git(&[ "commit", "-m", "Initial commit.", &format!("-S{}", e.willow.fingerprint), ])?; let root = e.git_current_commit()?; fs::write(p.join("a"), "Aller Anfang ist schwer.")?; e.git(&["add", "a"])?; e.git(&[ "commit", "-m", "First change.", &format!("-S{}", e.willow.fingerprint), ])?; e.sq_git(&["log", "--trust-root", &root])?; fs::write(p.join("a"), "Und es bleibt schwer.")?; e.git(&["add", "a"])?; e.git(&[ "commit", "-m", "Second change.", &format!("-S{}", e.willow.fingerprint), ])?; e.sq_git(&["log", "--trust-root", &root])?; Ok(()) } #[test] fn shadow_verify_commit() -> anyhow::Result<()> { let e = Environment::new()?; let p = e.git_state(); e.sq_git(&[ "policy", "authorize", "--policy-file", "shadow-policy.toml", e.willow.petname, &e.willow.fingerprint.to_string(), "--sign-commit" ])?; fs::write(p.join("a"), "Aller Anfang ist schwer.")?; e.git(&["add", "a"])?; e.git(&[ "commit", "-m", "Initial commit.", &format!("-S{}", e.willow.fingerprint), ])?; let root = e.git_current_commit()?; e.sq_git(&[ "log", "--trust-root", &root, "--policy-file", "shadow-policy.toml", ])?; fs::write(p.join("a"), "Und es bleibt schwer.")?; e.git(&["add", "a"])?; e.git(&[ "commit", "-m", "Second change.", &format!("-S{}", e.willow.fingerprint), ])?; e.sq_git(&[ "log", "--trust-root", &root, "--policy-file", "shadow-policy.toml", ])?; Ok(()) } sequoia-git-0.1.0/tests/common.rs000064400000000000000000000255051046102023000150600ustar 00000000000000use std::{ process::{Child, Command, Output, Stdio}, path::Path, path::PathBuf, time::{Duration, SystemTime}, }; use anyhow::{anyhow, Result}; use sequoia_openpgp::{ Fingerprint, cert::{ Cert, CertBuilder, }, packet::Signature, serialize::Serialize, }; use sequoia_cert_store::{ StoreUpdate, store::certd::CertD, }; pub struct Identity { pub email: &'static str, pub petname: &'static str, pub fingerprint: Fingerprint, pub cert: Cert, pub rev: Signature, } impl Identity { fn new(email: &'static str, petname: &'static str) -> Result { let (cert, rev) = CertBuilder::general_purpose(None, Some(format!("<{}>", email))) .set_creation_time(SystemTime::now() - Duration::new(24 * 3600, 0)) .generate()?; Ok(Identity { email, petname, fingerprint: cert.fingerprint(), cert, rev, }) } /// Returns the certificate with the pregenerated hard revocation. #[allow(dead_code)] pub fn hard_revoke(&self) -> Cert { self.cert.clone().insert_packets(self.rev.clone()) .expect("ok") } } pub enum TempDir { TempDir(tempfile::TempDir), PathBuf(PathBuf), } impl TempDir { fn new() -> Result { Ok(TempDir::TempDir(tempfile::TempDir::new()?)) } fn path(&self) -> &Path { match self { TempDir::TempDir(d) => d.path(), TempDir::PathBuf(p) => p.as_path(), } } fn persist(&mut self) { let d = std::mem::replace(self, TempDir::PathBuf(PathBuf::new())); match d { TempDir::TempDir(d) => *self = TempDir::PathBuf(d.into_path()), TempDir::PathBuf(p) => *self = TempDir::PathBuf(p), } } } pub struct Environment { pub wd: TempDir, pub willow: Identity, pub willow_release: Identity, pub buffy: Identity, pub xander: Identity, pub riley: Identity, } impl Environment { pub fn new() -> Result { let e = Environment { wd: TempDir::new()?, willow: Identity::new("willow@scoobies.example", "Willow Rosenberg Code Signing")?, willow_release: Identity::new("willow@scoobies.example", "Willow Rosenberg Release Signing")?, buffy: Identity::new("buffy@scoobies.example", "Buffy Summers")?, xander: Identity::new("xander@scoobies.example", "Xander Harris")?, riley: Identity::new("riley@scoobies.example", "Riley Finn")?, }; std::fs::create_dir(e.gnupg_state())?; std::fs::create_dir(e.git_state())?; std::fs::create_dir(e.certd_state())?; std::fs::create_dir(e.xdg_cache_home())?; std::fs::create_dir(e.scratch_state())?; e.import(&e.willow.cert)?; e.import(&e.willow_release.cert)?; e.import(&e.buffy.cert)?; e.import(&e.xander.cert)?; e.import(&e.riley.cert)?; e.git(&["init", &e.git_state().display().to_string()])?; e.git(&["config", "--local", "user.email", "you@example.org"])?; e.git(&["config", "--local", "user.name", "Your Name"])?; // git's default is to not sign. But, the user might have // overridden this in their ~/.gitconfig, and be using an old // version of git (<2.32). In that case, GIT_CONFIG_GLOBAL // won't suppress this setting. Setting it unconditionally in // the local configuration file is a sufficient workaround. e.git(&["config", "--local", "user.signingkey", "0xDEADBEEF"])?; e.git(&["config", "--local", "commit.gpgsign", "false"])?; Ok(e) } /// Persists the directory so that it can be examined after this run. #[allow(dead_code)] pub fn persist(&mut self) { self.wd.persist(); eprintln!("Persisting temporary directory: {}", self.wd.path().display()); } /// Returns the environment and the root commit. #[allow(dead_code)] pub fn scooby_gang_bootstrap() -> Result<(Environment, String)> { let e = Environment::new()?; // Willow has a code-signing key. e.sq_git(&[ "policy", "authorize", e.willow.petname, &e.willow.fingerprint.to_string(), "--sign-commit" ])?; // Additionally, Willow also has a release key on her security // token. e.sq_git(&[ "policy", "authorize", e.willow_release.petname, &e.willow_release.fingerprint.to_string(), "--sign-commit", "--sign-tag", "--sign-archive", "--add-user", "--retire-user", "--audit", ])?; e.git(&["add", "openpgp-policy.toml"])?; e.git(&[ "commit", "-m", "Initial commit.", &format!("-S{}", e.willow_release.fingerprint), ])?; let root = e.git_current_commit()?; Ok((e, root)) } pub fn gnupg_state(&self) -> PathBuf { self.wd.path().join("gnupg") } pub fn git_state(&self) -> PathBuf { self.wd.path().join("git") } pub fn certd_state(&self) -> PathBuf { self.wd.path().join("certd") } pub fn xdg_cache_home(&self) -> PathBuf { self.wd.path().join("xdg_cache_home") } #[allow(dead_code)] pub fn scratch_state(&self) -> PathBuf { self.wd.path().join("scratch") } pub fn import(&self, cert: &Cert) -> Result<()> { let mut certd = CertD::open(self.certd_state())?; certd.update(std::borrow::Cow::Owned(cert.clone().into()))?; let mut c = Command::new("gpg"); c.arg("--status-fd=2"); c.arg("--import").stdin(Stdio::piped()); let mut child = self.spawn(c)?; // Write in a separate thread to avoid deadlocks. let mut stdin = child.stdin.take().expect("failed to get stdin"); let cert = cert.clone(); let thread_handle = std::thread::spawn(move || { cert.as_tsk().serialize(&mut stdin) }); let output = child.wait_with_output()?; thread_handle.join().unwrap()?; if output.status.success() { Ok(()) } else { Err(anyhow!("gpg --import failed\n\nstdout:\n{}\n\n stderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr))) } } pub fn git>(&self, args: &[A]) -> Result<(Vec, Vec)> { eprint!("$ git"); let mut c = Command::new("git"); for a in args { eprint!(" {}", a.as_ref()); c.arg(a.as_ref()); } eprintln!(); self.run(c) } // A convenience function to optionally modify and commit a few // files. // // Returns the new commit id. #[allow(dead_code)] pub fn git_commit(&self, files: &[(&str, Option<&[u8]>)], commit_msg: &str, signer: Option<&Identity>) -> Result { let p = self.git_state(); for (filename, content) in files.iter() { if let Some(content) = content { std::fs::write(p.join(filename), content).unwrap(); } self.git(&["add", filename])?; } let mut git_args = vec!["commit", "-m", commit_msg]; let signer_; if let Some(signer) = signer { signer_ = format!("-S{}", signer.fingerprint); git_args.push(&signer_); } self.git(&git_args)?; Ok(self.git_current_commit()?) } pub fn git_current_commit(&self) -> Result { Ok(String::from_utf8(self.git(&["rev-parse", "HEAD"])?.0)? .trim().to_string()) } pub fn sq_git_path() -> Result { use std::sync::Once; static BUILD: Once = Once::new(); BUILD.call_once(|| { let o = Command::new("cargo") .arg("build").arg("--quiet") .arg("--bin").arg("sq-git") .output() .expect("running cargo failed"); if ! o.status.success() { panic!("build failed:\n\nstdout:\n{}\n\n stderr:\n{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr)); } }); Ok(if let Ok(target) = std::env::var("CARGO_TARGET_DIR") { PathBuf::from(target).canonicalize()? } else { std::env::current_dir()?.join("target") }.join("debug/sq-git")) } pub fn sq_git>(&self, args: &[A]) -> Result { eprint!("$ sq-git"); let mut c = Command::new(Self::sq_git_path()?); // We are a machine, request machine-readable output. c.arg("--output-format=json"); for a in args { eprint!(" {}", sh_quote(a)); c.arg(a.as_ref()); } eprintln!(); let output = self.spawn(c)?.wait_with_output()?; if output.status.success() { Ok(output) } else { Err(CliError { output }.into()) } } pub fn spawn(&self, mut c: Command) -> Result { Ok(c.current_dir(self.git_state()) .env_clear() // Filter out all git-related environment variables. .envs(std::env::vars() .filter(|(k, _)| ! k.starts_with("GIT_")) .collect::>()) .env("SQ_CERT_STORE", self.certd_state()) .env("GNUPGHOME", self.gnupg_state()) .env("GIT_CONFIG_GLOBAL", "/dev/null") .env("GIT_CONFIG_NOSYSTEM", "1") .env("XDG_CACHE_HOME", self.xdg_cache_home()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?) } pub fn run(&self, c: Command) -> Result<(Vec, Vec)> { let output = self.spawn(c)?.wait_with_output()?; if output.status.success() { Ok((output.stdout, output.stderr)) } else { Err(anyhow!("command failed\n\nstdout:\n{}\n\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr))) } } } /// Errors for this crate. #[derive(thiserror::Error, Debug)] #[error("command failed\n\nstdout:\n{}\n\nstderr:\n{}", String::from_utf8_lossy(&self.output.stdout), String::from_utf8_lossy(&self.output.stderr))] pub struct CliError { output: std::process::Output, } fn sh_quote<'s, S: AsRef + 's>(s: S) -> String { let s = s.as_ref(); if s.contains(char::is_whitespace) { format!("{:?}", s) } else { s.to_string() } } sequoia-git-0.1.0/tests/git-update-hook.rs000064400000000000000000000102551046102023000165650ustar 00000000000000use std::fs; use std::io::Write; use std::os::unix::fs::PermissionsExt; mod common; use common::Environment; fn create_environment() -> anyhow::Result<(Environment, String)> { let (e, root) = Environment::scooby_gang_bootstrap()?; // Create a bare repository at our scratch location. let scratch = e.scratch_state().display().to_string(); e.git(&["init", "--bare", &scratch])?; // Set us as update hook. let update_hook = e.scratch_state().join("hooks").join("update"); let mut update = fs::File::create(&update_hook)?; writeln!(update, "#!/bin/sh")?; writeln!(update)?; writeln!(update, "{} update-hook --trust-root={} \"$@\"", Environment::sq_git_path()?.display(), root)?; // Make file executable. let metadata = update.metadata()?; let mut permissions = metadata.permissions(); permissions.set_mode(0o755); update.set_permissions(permissions)?; // Add as origin. e.git(&["remote", "add", "origin", &scratch])?; Ok((e, root)) } #[test] fn update_hook() -> anyhow::Result<()> { let (e, _root) = create_environment()?; let p = e.git_state(); // Bookmark. e.git(&["checkout", "-b", "test-base"])?; // Willow's code-signing key can change the source code, as she // has the sign-commit right. e.git(&["checkout", "-b", "test-willow"])?; fs::write(p.join("a"), "Aller Anfang ist schwer.")?; e.git(&["add", "a"])?; e.git(&[ "commit", "-m", "First change.", &format!("-S{}", e.willow.fingerprint), ])?; e.git(&["push", "origin", "test-willow"])?; // Reset. e.git(&["checkout", "test-base"])?; e.git(&["clean", "-fdx"])?; // Her release key also has that right, because she needs it in // order to give it to new users. e.git(&["checkout", "-b", "test-willow-release"])?; fs::write(p.join("a"), "Aller Anfang ist schwer. -- Schiller")?; e.git(&["add", "a"])?; e.git(&[ "commit", "-m", "Someone is not quite correct on the internet.", &format!("-S{}", e.willow_release.fingerprint), ])?; e.git(&["push", "origin", "test-willow-release"])?; // Reset. e.git(&["checkout", "test-base"])?; e.git(&["clean", "-fdx"])?; // Buffy's cert was not yet added, so she may not sign commits. e.git(&["checkout", "-b", "test-buffy"])?; fs::write(p.join("a"), "Aller Anfang ist schwer, unless you are super strong!1")?; e.git(&["add", "a"])?; e.git(&[ "commit", "-m", "Well, actually...", &format!("-S{}", e.buffy.fingerprint), ])?; if let Ok((_, stderr)) = e.git(&["push", "origin", "test-buffy"]) { eprintln!("stderr: {}", String::from_utf8_lossy(&stderr)); } assert!(e.git(&["push", "origin", "test-buffy"]).is_err()); Ok(()) } #[test] fn rebase() -> anyhow::Result<()> { let (e, _root) = create_environment()?; let p = e.git_state(); // Bookmark. e.git(&["checkout", "-b", "test-base"])?; // There are two threads of development. Let's start the first // one. e.git(&["checkout", "-b", "feature-one"])?; fs::write(p.join("a"), "Aller Anfang ist schwer.")?; e.git(&["add", "a"])?; e.git(&[ "commit", "-m", "First change of the first feature.", &format!("-S{}", e.willow.fingerprint), ])?; e.git(&["push", "origin", "feature-one"])?; // Reset. e.git(&["checkout", "test-base"])?; e.git(&["clean", "-fdx"])?; // There are two threads of development. Let's start the second // one. e.git(&["checkout", "-b", "feature-two"])?; fs::write(p.join("b"), "And now for something completely different.")?; e.git(&["add", "b"])?; e.git(&[ "commit", "-m", "First change of the second feature.", &format!("-S{}", e.willow.fingerprint), ])?; e.git(&["push", "origin", "feature-two"])?; // Now we rebase feature-two on top of feature-one and push it to // update the remote feature-two branch. e.git(&[ "rebase", "feature-one", &format!("-S{}", e.willow.fingerprint), ])?; e.git(&["push", "origin", "--force", "feature-two"])?; Ok(()) } sequoia-git-0.1.0/tests/policy-authorize.rs000064400000000000000000000113111046102023000170650ustar 00000000000000use std::collections::BTreeSet; mod common; use common::Environment; fn create_environment() -> anyhow::Result<(Environment, String)> { Environment::scooby_gang_bootstrap() } // The keys for the different authorizations in the JSON file. const SIGN_COMMIT: &str = "sign_commit"; const SIGN_TAG: &str = "sign_tag"; const SIGN_ARCHIVE: &str = "sign_archive"; const ADD_USER: &str = "add_user"; const RETIRE_USER: &str = "retire_user"; const AUDIT: &str = "audit"; const CAPS: &[&str] = &[ SIGN_COMMIT, SIGN_TAG, SIGN_ARCHIVE, ADD_USER, RETIRE_USER, AUDIT ]; fn check(e: &Environment, args: &[&str], expected_caps: &[&str]) { let petname = e.buffy.petname; let fpr = e.buffy.fingerprint.to_string(); let openpgp_policy_toml = e.git_state().join("openpgp-policy.toml"); if let Err(err) = std::fs::remove_file(&openpgp_policy_toml) { if std::io::ErrorKind::NotFound != err.kind() { panic!("Removing {}", openpgp_policy_toml.display()); } } let mut sq_git: Vec<&str> = [ "policy", "authorize", &petname, &fpr, ].to_vec(); sq_git.extend(args); eprintln!("Running: sq-git {}", sq_git.join(" ")); e.sq_git(&sq_git).unwrap(); let output = e.sq_git(&[ "policy", "describe", "--output-format", "json" ]).unwrap(); eprintln!("Output:\n{}", String::from_utf8_lossy(&output.stdout)); let status: serde_json::Value = serde_json::from_slice(&output.stdout) .expect("\"sq policy describe\" emits valid json output"); // eprintln!("JSON:\n{:?}", status); let auths = &status["authorization"]; // eprintln!("JSON[\"authorization\"]:\n{:?}", auths); let user = &auths[petname]; eprintln!("JSON[\"authorization\"][\"{}\"]:\n{:?}", petname, user); let caps = BTreeSet::from_iter(CAPS.iter()); let expected_caps_present = BTreeSet::from_iter(expected_caps.iter()); for cap in expected_caps_present.iter() { eprintln!("Checking that {} is true", cap); match &user[cap] { serde_json::Value::Bool(true) => (), v => { panic!("expected {} to be true, but it is: {:?}", cap, v); } } } let expected_caps_missing = caps.difference(&expected_caps_present); for cap in expected_caps_missing { eprintln!("Checking that {} is not set or false", cap); match &user[cap] { serde_json::Value::Null => (), serde_json::Value::Bool(false) => (), v => { panic!("expected {} to be false, but it is: {:?}", cap, v); } } } } #[test] fn check_flags() -> anyhow::Result<()> { let (e, _root) = create_environment()?; // One at a time. check(&e, &["--sign-commit"], &[ SIGN_COMMIT ]); check(&e, &["--sign-archive"], &[ SIGN_ARCHIVE ]); check(&e, &["--sign-tag"], &[ SIGN_TAG ]); check(&e, &["--add-user"], &[ ADD_USER ]); check(&e, &["--retire-user"], &[ RETIRE_USER ]); check(&e, &["--audit"], &[ AUDIT ]); // Mix and match. check(&e, &["--sign-commit", "--sign-archive"], &[ SIGN_COMMIT, SIGN_ARCHIVE ]); check(&e, &["--sign-tag", "--add-user", "--retire-user"], &[ SIGN_TAG, ADD_USER, RETIRE_USER ]); // Add some negatives. The last positive or negative wins. check(&e, &["--no-sign-commit", "--sign-archive"], &[ SIGN_ARCHIVE ]); check(&e, &["--sign-commit", "--no-sign-commit", "--sign-archive"], &[ SIGN_ARCHIVE ]); check(&e, &["--no-sign-commit", "--sign-commit", "--sign-archive"], &[ SIGN_COMMIT, SIGN_ARCHIVE ]); // The meta-capabilities. check(&e, &["--committer"], &[ SIGN_COMMIT ]); check(&e, &["--release-manager"], &[ SIGN_COMMIT, SIGN_TAG, SIGN_ARCHIVE ]); check(&e, &["--project-maintainer"], &[ SIGN_COMMIT, SIGN_TAG, SIGN_ARCHIVE, ADD_USER, RETIRE_USER, AUDIT ]); // Union. check(&e, &["--project-maintainer", "--committer"], &[ SIGN_COMMIT, SIGN_TAG, SIGN_ARCHIVE, ADD_USER, RETIRE_USER, AUDIT ]); check(&e, &["--project-maintainer", "--no-sign-archive"], &[ SIGN_COMMIT, SIGN_TAG, ADD_USER, RETIRE_USER, AUDIT ]); // A meta-capability does not trump a negative capability. check(&e, &["--no-sign-archive", "--project-maintainer"], &[ SIGN_COMMIT, SIGN_TAG, ADD_USER, RETIRE_USER, AUDIT ]); Ok(()) } sequoia-git-0.1.0/tests/policy.rs000064400000000000000000001161231046102023000150640ustar 00000000000000use std::fs; use sequoia_openpgp as openpgp; use openpgp::serialize::Serialize; mod common; use common::Environment; fn create_environment() -> anyhow::Result<(Environment, String)> { Environment::scooby_gang_bootstrap() } #[test] fn sign_commit() -> anyhow::Result<()> { let (e, root) = create_environment()?; let p = e.git_state(); // Bookmark. e.git(&["checkout", "-b", "test-base"])?; // Willow's code-signing key can change the source code, as she // has the sign-commit right. e.git(&["checkout", "-b", "test-willow"])?; fs::write(p.join("a"), "Aller Anfang ist schwer.")?; e.git(&["add", "a"])?; e.git(&[ "commit", "-m", "First change.", &format!("-S{}", e.willow.fingerprint), ])?; e.sq_git(&["log", "--trust-root", &root])?; // Reset. e.git(&["checkout", "test-base"])?; e.git(&["clean", "-fdx"])?; // Her release key also has that right, because she needs it in // order to give it to new users. e.git(&["checkout", "-b", "test-willow-release"])?; fs::write(p.join("a"), "Aller Anfang ist schwer. -- Schiller")?; e.git(&["add", "a"])?; e.git(&[ "commit", "-m", "Someone is not quite correct on the internet.", &format!("-S{}", e.willow_release.fingerprint), ])?; e.sq_git(&["log", "--trust-root", &root])?; // Reset. e.git(&["checkout", "test-base"])?; e.git(&["clean", "-fdx"])?; // Buffy's cert was not yet added, so she may not sign commits. e.git(&["checkout", "-b", "test-buffy"])?; fs::write(p.join("a"), "Aller Anfang ist schwer, unless you are super strong!1")?; e.git(&["add", "a"])?; e.git(&[ "commit", "-m", "Well, actually...", &format!("-S{}", e.buffy.fingerprint), ])?; let commit_2 = e.git_current_commit()?; assert!(e.sq_git(&["log", "--trust-root", &root]).is_err()); let commit_3 = e.git_commit(&[("workwork.txt", Some(b"Hiho, hiho"))], "Off to work we go...", Some(&e.willow_release)).unwrap(); // commit 2 is still bad. assert!(e.sq_git(&["log", "--trust-root", &root]).is_err()); // But if we use the bad commit as the trust root, it should work, // because we don't check that the trust root is authenticated by // its parent, and commit 2 authenticates commit 3. assert!(e.sq_git(&["log", "--trust-root", &commit_2]).is_ok()); // Let's check the range commit_2..commit_3, but use the root as // our trust root instead of commit_2. This should again fail. assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..{}", commit_2, commit_3)]).is_err()); // commit_2..commit_2 is empty. But, we should still check for a // path from the trust root to commit_2. assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..{}", commit_2, commit_2)]).is_err()); // commit_3..commit_3 is empty. But, we should still check for a // path from the commit_2 to commit_3. assert!(e.sq_git(&["log", "--trust-root", &commit_2, &format!("{}..{}", commit_3, commit_3)]).is_ok()); Ok(()) } #[test] fn add_user() -> anyhow::Result<()> { let (e, root) = create_environment()?; // Bookmark. e.git(&["checkout", "-b", "test-base"])?; // Willow's code-signing key can change the source code, but she // can not add users. e.git(&["checkout", "-b", "test-willow"])?; // Try to add Buffy. e.sq_git(&[ "policy", "authorize", e.buffy.petname, &e.buffy.fingerprint.to_string(), "--sign-commit" ])?; e.git(&["add", "openpgp-policy.toml"])?; e.git(&[ "commit", "-m", "Add Buffy.", &format!("-S{}", e.willow.fingerprint), ])?; assert!(e.sq_git(&["log", "--trust-root", &root]).is_err()); // Reset. e.git(&["checkout", "test-base"])?; e.git(&["clean", "-fdx"])?; // However, her release key does have that right. e.git(&["checkout", "-b", "test-willow-release"])?; // Try to add Buffy. e.sq_git(&[ "policy", "authorize", e.buffy.petname, &e.buffy.fingerprint.to_string(), "--sign-commit" ])?; e.git(&["add", "openpgp-policy.toml"])?; e.git(&[ "commit", "-m", "Add Buffy.", &format!("-S{}", e.willow_release.fingerprint), ])?; e.sq_git(&["log", "--trust-root", &root])?; // Reset. e.git(&["checkout", "test-base"])?; e.git(&["clean", "-fdx"])?; // Xander's cert was not yet added, so he definitely may not add // users either. e.git(&["checkout", "-b", "test-xander"])?; // Try to add Buffy. e.sq_git(&[ "policy", "authorize", e.buffy.petname, &e.buffy.fingerprint.to_string(), "--sign-commit" ])?; e.git(&["add", "openpgp-policy.toml"])?; e.git(&[ "commit", "-m", "Add Buffy.", &format!("-S{}", e.xander.fingerprint), ])?; assert!(e.sq_git(&["log", "--trust-root", &root]).is_err()); Ok(()) } #[test] fn retire_user() -> anyhow::Result<()> { let (e, root) = create_environment()?; // Add Buffy. e.sq_git(&[ "policy", "authorize", e.buffy.petname, &e.buffy.fingerprint.to_string(), "--sign-commit" ])?; e.git(&["add", "openpgp-policy.toml"])?; e.git(&[ "commit", "-m", "Add Buffy.", &format!("-S{}", e.willow_release.fingerprint), ])?; e.sq_git(&["log", "--trust-root", &root])?; // Bookmark. e.git(&["checkout", "-b", "test-base"])?; // Willow's code signing key may not retire Buffy. e.git(&["checkout", "-b", "test-willow"])?; // Try to retire Buffy. e.sq_git(&[ "policy", "authorize", e.buffy.petname, &e.buffy.fingerprint.to_string(), "--no-sign-commit", ])?; e.git(&["add", "openpgp-policy.toml"])?; e.git(&[ "commit", "-m", "Add Buffy.", &format!("-S{}", e.willow.fingerprint), ])?; assert!(e.sq_git(&["log", "--trust-root", &root]).is_err()); // Reset. e.git(&["checkout", "test-base"])?; e.git(&["clean", "-fdx"])?; // However, her release key does have that right. e.git(&["checkout", "-b", "test-willow-release"])?; // Try to retire Buffy. e.sq_git(&[ "policy", "authorize", e.buffy.petname, &e.buffy.fingerprint.to_string(), "--no-sign-commit", ])?; e.git(&["add", "openpgp-policy.toml"])?; e.git(&[ "commit", "-m", "Add Buffy.", &format!("-S{}", e.willow_release.fingerprint), ])?; e.sq_git(&["log", "--trust-root", &root])?; // Reset. e.git(&["checkout", "test-base"])?; e.git(&["clean", "-fdx"])?; // Xander's cert was not yet added, so he definitely may not // retire users either. e.git(&["checkout", "-b", "test-xander"])?; // Try to retire Buffy. e.sq_git(&[ "policy", "authorize", e.buffy.petname, &e.buffy.fingerprint.to_string(), "--no-sign-commit", ])?; e.git(&["add", "openpgp-policy.toml"])?; e.git(&[ "commit", "-m", "Add Buffy.", &format!("-S{}", e.xander.fingerprint), ])?; assert!(e.sq_git(&["log", "--trust-root", &root]).is_err()); Ok(()) } #[test] fn audit() -> anyhow::Result<()> { // Introduce some bad commits, and try to recover. // // When we use in-band policies, then only a certificate with the // audit capability can goodlist commits, and can it can only // recover from hard revocations. // // When we use an external policy (using --policy-file), we can't // see who added a goodlist entry. But that doesn't matter // because the policy is fully trusted. An external policy can // recover from any type of veritifcation failure include the // complete absence of a signature. let (e, root) = create_environment()?; let p = e.git_state(); // Add a commit from Willow, which is allowed. This makes sure // the trust root's policy is not applied to the first dodgy commit. fs::write(p.join("superpowers"), "Willow can break encryption.")?; e.git(&["add", "superpowers"])?; e.git(&[ "commit", "-m", "Willow data commit.", &format!("-S{}", e.willow.fingerprint), ])?; assert!(e.sq_git(&["log", "--trust-root", &root]).is_ok()); // Add a commit signed by Xander, who is not authorized to do // that. fs::write(p.join("a"), "Aller Anfang ist schwer, I'll go fetch the hammer!")?; e.git(&["add", "a"])?; e.git(&[ "commit", "-m", "No problem, we'll get you up and running in no time.", &format!("-S{}", e.xander.fingerprint), ])?; assert!(e.sq_git(&["log", "--trust-root", &root]).is_err()); assert!(e.sq_git(&["log", "--trust-root", &root, "--policy-file", "openpgp-policy.toml"]).is_err()); let bad_commit = e.git_current_commit()?; // Bookmark. e.git(&["checkout", "-b", "test-base"])?; // Now we try to recover by good-listing the bad commit. // Willow's code signing key may not goodlist commits. e.git(&["checkout", "-b", "test-willow"])?; // Try to goodlist the commit. e.sq_git(&["policy", "goodlist", &bad_commit])?; e.git(&["add", "openpgp-policy.toml"])?; e.git(&[ "commit", "-m", "Goodlist the bad commit.", &format!("-S{}", e.willow.fingerprint), ])?; assert!(e.sq_git(&["log", "--trust-root", &root]).is_err()); // When specified via an external policy, this is enough. assert!(e.sq_git(&["log", "--trust-root", &root, "--policy-file", "openpgp-policy.toml"]).is_ok()); // But, if we look further back, the commit is now goodlisted, but // the signer that got the commit goodlisted does not have the // audit right. assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); // When specified via an external policy, this is enough. assert!(e.sq_git(&["log", "--trust-root", &root, "--policy-file", "openpgp-policy.toml", &format!("{}..", root)]).is_ok()); // Reset. e.git(&["checkout", "test-base"])?; e.git(&["clean", "-fdx"])?; // Willow's release key may goodlist commits. e.git(&["checkout", "-b", "test-willow-release"])?; // Try to goodlist the commit. e.sq_git(&["policy", "goodlist", &bad_commit])?; e.git(&["add", "openpgp-policy.toml"])?; e.git(&[ "commit", "-m", "Goodlist the bad commit.", &format!("-S{}", e.willow_release.fingerprint), ])?; assert!(e.sq_git(&["log", "--trust-root", &root]).is_err()); // When specified via an external policy, this is enough. assert!(e.sq_git(&["log", "--trust-root", &root, "--policy-file", "openpgp-policy.toml"]).is_ok()); // But, if we look further back, the commit is now goodlisted, but // it is still considered bad, because in-band goodlisting can // only be used to recover from signing keys that have been hard // revoked. assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); // Goodlisting this commit from an external policy is enough. e.sq_git(&["log", "--trust-root", &root, "--policy-file", "openpgp-policy.toml", &format!("{}..", root)])?; // Reset. e.git(&["checkout", "test-base"])?; e.git(&["clean", "-fdx"])?; // Xander's cert was not yet added, so he definitely may not // goodlist his own commit. e.git(&["checkout", "-b", "test-xander"])?; // Try to goodlist the commit. e.sq_git(&["policy", "goodlist", &bad_commit])?; e.git(&["add", "openpgp-policy.toml"])?; e.git(&[ "commit", "-m", "Goodlist the bad commit.", &format!("-S{}", e.xander.fingerprint), ])?; assert!(e.sq_git(&["log", "--trust-root", &root]).is_err()); assert!(e.sq_git(&["log", "--trust-root", &root, "--policy-file", "openpgp-policy.toml"]).is_err()); // But, if we look further back, the commit is now goodlisted, but // the signer that got the commit goodlisted does not have the // audit right. assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); // Goodlisting this commit from an external policy is enough. assert!(e.sq_git(&["log", "--trust-root", &root, "--policy-file", "openpgp-policy.toml", &format!("{}..", root)]).is_err()); Ok(()) } #[test] #[allow(unused_variables)] fn goodlist_1() -> anyhow::Result<()> { let (e, root) = create_environment()?; // G <- target // | // F <- Good list B // | // E // | // D <- Add revocation for Riley // | // C // | // B <- Signature by Riley // | // A <- Trust root. // Add Riley as a committer. e.sq_git(&[ "policy", "authorize", e.riley.petname, &e.riley.fingerprint.to_string(), "--sign-commit", ])?; let commit_a = e.git_commit(&[("openpgp-policy.toml", None)], "A: willow authorizes riley", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); let commit_b = e.git_commit(&[("riley.txt", Some(b"riley was here"))], "B: riley signs a commit", Some(&e.riley)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); let commit_c = e.git_commit(&[("workwork.txt", Some(b"1"))], "C: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); // Willow adds a hard revocation for Riley, but does not goodlist // his commit. let riley_revocation_pgp = e.scratch_state().join("riley-revocation.pgp"); let mut f = std::fs::File::create(&riley_revocation_pgp).unwrap(); e.riley.hard_revoke().serialize(&mut f).unwrap(); drop(f); e.sq_git(&[ "policy", "authorize", "--cert-file", &riley_revocation_pgp.to_str().unwrap(), e.riley.petname, ])?; let commit_d = e.git_commit(&[("openpgp-policy.toml", None)], "D: willow imports hard revocation for riley", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); let commit_e = e.git_commit(&[("workwork.txt", Some(b"e"))], "E: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); e.sq_git(&[ "policy", "goodlist", &commit_b, ])?; let commit_f = e.git_commit(&[("openpgp-policy.toml", None)], "F: willow good lists riley's commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); let commit_g = e.git_commit(&[("workwork.txt", Some(b"g"))], "G: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); Ok(()) } #[test] #[allow(unused_variables)] fn goodlist_2() -> anyhow::Result<()> { let (e, root) = create_environment()?; // K <- target // / \ // | I <- Good list B // | | // | H // | | // J G // | | // | F // | | // | E // \ / // D <- Add revocation for Riley // | // C // | // B <- Signature by Riley // | // A <- Trust root. // // By inspection, we see that there is an authenticated path from // A to K via I, because I goodlists B. That path is longer than // the path via J. If we do a breath-first walk from K to A, then // we'll visit the nodes in the following order: K, J, I, D, H, C, // G, B... We'll reject B, because the good list hasn't // propagated to B. And the goodlist will never propagate to B, // because we'd have to visit D, C, and B a second time. // // We avoid this problem by doing a topographical walk. That is, // we don't visit D until we've visit all of its children (J and // E). Then, all good lists have propagated to D and when we // visit B, B is on the goodlist. // Add Riley as a committer. e.sq_git(&[ "policy", "authorize", e.riley.petname, &e.riley.fingerprint.to_string(), "--sign-commit", ])?; let commit_a = e.git_commit(&[("openpgp-policy.toml", None)], "A: willow authorizes riley", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); let commit_b = e.git_commit(&[("riley.txt", Some(b"riley was here"))], "B: riley signs a commit", Some(&e.riley)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); let commit_c = e.git_commit(&[("workwork.txt", Some(b"1"))], "C: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); // Willow adds a hard revocation for Riley, but does not goodlist // his commit. let riley_revocation_pgp = e.scratch_state().join("riley-revocation.pgp"); let mut f = std::fs::File::create(&riley_revocation_pgp).unwrap(); e.riley.hard_revoke().serialize(&mut f).unwrap(); drop(f); e.sq_git(&[ "policy", "authorize", "--cert-file", &riley_revocation_pgp.to_str().unwrap(), e.riley.petname, ])?; let commit_d = e.git_commit(&[("openpgp-policy.toml", None)], "D: willow imports hard revocation for riley", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); let commit_e = e.git_commit(&[("workwork.txt", Some(b"e"))], "E: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); let commit_f = e.git_commit(&[("workwork.txt", Some(b"f"))], "F: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); let commit_g = e.git_commit(&[("workwork.txt", Some(b"g"))], "G: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); let commit_h = e.git_commit(&[("workwork.txt", Some(b"h"))], "H: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); e.sq_git(&[ "policy", "goodlist", &commit_b, ])?; let commit_i = e.git_commit(&[("openpgp-policy.toml", None)], "I: willow good lists riley's commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); // Reset to d. e.git(&["checkout", &commit_d])?; e.git(&["clean", "-fdx"])?; let commit_j = e.git_commit(&[("workwork.md", Some(b"1"))], "J: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); // Merge I and J. e.git(&[ "merge", "-m", "K: Merge I and J", &format!("-S{}", e.willow_release.fingerprint), &commit_j, &commit_i])?; let commit_k = e.git_current_commit()?; assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); Ok(()) } #[test] #[allow(unused_variables)] fn goodlist_3() -> anyhow::Result<()> { let (e, root) = create_environment()?; // F <- target // / \ // | D // | | // E | <- Add revocation for Riley // | | // | C // \ / // B <- Signature by Riley // | // A <- Trust root. // // // We shouldn't be able to verify F, because even though A - B - C // - D - F taken alone is an authentic path, we should notice the // revocation certificate added in E and reject B. // Add Riley as a committer. e.sq_git(&[ "policy", "authorize", e.riley.petname, &e.riley.fingerprint.to_string(), "--sign-commit", ])?; let commit_a = e.git_commit(&[("openpgp-policy.toml", None)], "A: willow authorizes riley", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); let commit_b = e.git_commit(&[("riley.txt", Some(b"riley was here"))], "B: riley signs a commit", Some(&e.riley)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); let commit_c = e.git_commit(&[("workwork.txt", Some(b"c"))], "C: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); let commit_d = e.git_commit(&[("workwork.txt", Some(b"d"))], "D: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); // Reset to b. e.git(&["checkout", &commit_b])?; e.git(&["clean", "-fdx"])?; // Willow adds a hard revocation for Riley, but does not goodlist // his commit. let riley_revocation_pgp = e.scratch_state().join("riley-revocation.pgp"); let mut f = std::fs::File::create(&riley_revocation_pgp).unwrap(); e.riley.hard_revoke().serialize(&mut f).unwrap(); drop(f); e.sq_git(&[ "policy", "authorize", "--cert-file", &riley_revocation_pgp.to_str().unwrap(), e.riley.petname, ])?; let commit_e = e.git_commit(&[("openpgp-policy.toml", None)], "E: willow imports hard revocation for riley", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..{}", root, commit_d)]).is_ok()); // Merge D and E. e.git(&[ "merge", "-m", "F: Merge D and E", &format!("-S{}", e.willow_release.fingerprint), &commit_d, &commit_e])?; let commit_k = e.git_current_commit()?; assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); Ok(()) } #[test] #[allow(unused_variables)] fn goodlist_4() -> anyhow::Result<()> { let (e, root) = create_environment()?; // G <- target // / \ // F | <- Good list C // | | // | E <- Good list B // \ / // D <- Add revocation for Riley. // | // C <- Signature by Riley // | // B <- Signature by Riley // | // A <- Trust root. // // // Along, neither A-B-C-D-E-G or A-B-C-D-F-G is valid. But taken // together, when visiting C, there is an authenticated suffix // that goodlists C, and when visiting B, there is also an // authenticates suffix that authenticates B. So, G is // authenticated! // Add Riley as a committer. e.sq_git(&[ "policy", "authorize", e.riley.petname, &e.riley.fingerprint.to_string(), "--sign-commit", ])?; let commit_a = e.git_commit(&[("openpgp-policy.toml", None)], "A: willow authorizes riley", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); let commit_b = e.git_commit(&[("riley.txt", Some(b"riley was here"))], "B: riley signs a commit", Some(&e.riley)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); let commit_c = e.git_commit(&[("riley.txt", Some(b"riley was here, again"))], "C: riley signs a commit", Some(&e.riley)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); // Willow adds a hard revocation for Riley, but does not goodlist // his commit. let riley_revocation_pgp = e.scratch_state().join("riley-revocation.pgp"); let mut f = std::fs::File::create(&riley_revocation_pgp).unwrap(); e.riley.hard_revoke().serialize(&mut f).unwrap(); drop(f); e.sq_git(&[ "policy", "authorize", "--cert-file", &riley_revocation_pgp.to_str().unwrap(), e.riley.petname, ])?; let commit_d = e.git_commit(&[("openpgp-policy.toml", None)], "D: willow imports hard revocation for riley", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); e.sq_git(&[ "policy", "goodlist", &commit_b, ])?; let commit_e = e.git_commit(&[("openpgp-policy.toml", None)], "E: willow good lists riley's commit (B)", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); // Reset to d. e.git(&["checkout", &commit_d])?; e.git(&["clean", "-fdx"])?; e.sq_git(&[ "policy", "goodlist", &commit_c, ])?; let commit_f = e.git_commit(&[("openpgp-policy.toml", None)], "F: willow good lists riley's commit (C)", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); // Merge E and F. // // This is a bit complicated, because the two good list entries // are going to conflict. Create a merge commit (but don't commit // it) using the content of F. Then manually merge in E (which // goodlisted B). e.git(&[ "merge", "-m", "G: Merge E and F", "-s", "ours", "--no-commit", &commit_e, &commit_f])?; e.sq_git(&[ "policy", "goodlist", &commit_b, ])?; let commit_f = e.git_commit(&[("openpgp-policy.toml", None)], "G: Merge E and F", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); Ok(()) } #[test] #[allow(unused_variables)] fn goodlist_5() -> anyhow::Result<()> { let (e, root) = create_environment()?; // G <- target // | // F <- Xander goodlists B, but that's not allowed // | // E // | // D <- Add revocation for Riley // | // C // | // B <- Signature by Riley // | // A <- Trust root. // // Make sure that goodlisting that is not allowed is, in fact, // ignored. // Add Riley as a committer. e.sq_git(&[ "policy", "authorize", e.riley.petname, &e.riley.fingerprint.to_string(), "--sign-commit", ])?; let commit_a = e.git_commit(&[("openpgp-policy.toml", None)], "A: willow authorizes riley", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); let commit_b = e.git_commit(&[("riley.txt", Some(b"riley was here"))], "B: riley signs a commit", Some(&e.riley)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); let commit_c = e.git_commit(&[("workwork.txt", Some(b"1"))], "C: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); // Willow adds a hard revocation for Riley, but does not goodlist // his commit. let riley_revocation_pgp = e.scratch_state().join("riley-revocation.pgp"); let mut f = std::fs::File::create(&riley_revocation_pgp).unwrap(); e.riley.hard_revoke().serialize(&mut f).unwrap(); drop(f); e.sq_git(&[ "policy", "authorize", "--cert-file", &riley_revocation_pgp.to_str().unwrap(), e.riley.petname, ])?; let commit_d = e.git_commit(&[("openpgp-policy.toml", None)], "D: willow imports hard revocation for riley", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); let commit_e = e.git_commit(&[("workwork.txt", Some(b"e"))], "E: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); e.sq_git(&[ "policy", "goodlist", &commit_b, ])?; let commit_f = e.git_commit(&[("openpgp-policy.toml", None)], "F: xander good lists riley's commit", Some(&e.xander)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); let commit_g = e.git_commit(&[("workwork.txt", Some(b"g"))], "G: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); Ok(()) } #[test] #[allow(unused_variables)] fn via() -> anyhow::Result<()> { let (e, root) = create_environment()?; // F <- target // / \ // E D <- unauthorized // | | // | C // \ / // B // | // A // // When we do: sq-git log --trust-root A E..F, this means we want // a valid path from A to F via E. Since E is unauthorized, this // should fail. let commit_a = e.git_commit(&[("workwork.txt", Some(b"1"))], "A: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); let commit_b = e.git_commit(&[("workwork.txt", Some(b"2"))], "B: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); let commit_c = e.git_commit(&[("workwork.txt", Some(b"3"))], "C: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); let commit_d = e.git_commit(&[("workwork.txt", Some(b"4"))], "D: willow signs a commit", Some(&e.willow_release)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); // Reset to b. e.git(&["checkout", &commit_b])?; e.git(&["clean", "-fdx"])?; // Unauthorized. let commit_e = e.git_commit(&[("busybusy.txt", Some(b"1"))], "E: xander signs a commit", Some(&e.xander)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_err()); // Merge D and E. e.git(&[ "merge", "-m", "F: Merge D and E", &format!("-S{}", e.willow_release.fingerprint), &commit_d, &commit_e])?; let commit_f = e.git_current_commit()?; assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..", root)]).is_ok()); // Make sure that we can go via D. assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..{}", commit_d, commit_f)]).is_ok()); // But not via E. assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..{}", commit_e, commit_f)]).is_err()); // We can't authenticate E via F, as F comes later. assert!(e.sq_git(&["log", "--trust-root", &root, &format!("{}..{}", commit_f, commit_e)]).is_err()); Ok(()) } #[test] fn external_policy_authenticates_trust_root() -> anyhow::Result<()> { // Normally the trust root is implicitly trusted. When we use an // external policy, we also use it to check the trust root. Make // sure that works. let (e, root) = create_environment()?; // Xander is not allowed to sign a commit. let commit_a = e.git_commit(&[("workwork.txt", Some(b"1"))], "A: xander signs a commit", Some(&e.xander)).unwrap(); // Willow is. let _commit_b = e.git_commit(&[("workwork.txt", Some(b"2"))], "B: willow signs a commit", Some(&e.willow)).unwrap(); // A is bad: Xander is not allowed to make a commit. assert!(e.sq_git(&["log", "--trust-root", &root]).is_err()); // When A is the trust root, it is implicitly trusted, and the // rest of the commits are good. assert!(e.sq_git(&["log", "--trust-root", &commit_a]).is_ok()); // But when we use an external policy, we also use the policy to // check A. So, it is bad again. assert!(e.sq_git(&["log", "--trust-root", &commit_a, "--policy-file", "openpgp-policy.toml"]).is_err()); // Authorize Xander. e.sq_git(&[ "policy", "authorize", e.xander.petname, &e.xander.fingerprint.to_string(), "--sign-commit", ])?; // When we use an external policy that says that Xander is // authorized, we're good again. assert!(e.sq_git(&["log", "--trust-root", &commit_a, "--policy-file", "openpgp-policy.toml"]).is_ok()); Ok(()) } #[test] fn symbolic_names() -> anyhow::Result<()> { // Make sure that symbolic names resolve. let (e, root) = create_environment()?; // Willow signs a commit. e.git(&["checkout", "-b", "commit-a"])?; let commit_a = e.git_commit(&[("workwork.txt", Some(b"1"))], "A: willow signs a commit", Some(&e.willow)).unwrap(); e.git(&["checkout", "-b", "commit-b"])?; let commit_b = e.git_commit(&[("workwork.txt", Some(b"2"))], "B: willow signs a commit", Some(&e.willow)).unwrap(); e.git(&["checkout", "-b", "commit-c"])?; let commit_c = e.git_commit(&[("workwork.txt", Some(b"3"))], "C: willow signs a commit", Some(&e.willow)).unwrap(); assert!(e.sq_git(&["log", "--trust-root", &root]).is_ok()); assert!(e.sq_git(&["log", "--trust-root", &commit_a]).is_ok()); assert!(e.sq_git(&["log", "--trust-root", "commit-a"]).is_ok()); // Name doesn't exist... assert!(e.sq_git(&["log", "--trust-root", "commit_a"]).is_err()); // Symbolic names in different places. assert!(e.sq_git(&["log", "--trust-root", "commit-a", &commit_b]).is_ok()); assert!(e.sq_git(&["log", "--trust-root", "commit-a", &format!("{}..{}", commit_b, commit_c)]).is_ok()); // Mix of hashes and symbolic names. assert!(e.sq_git(&["log", "--trust-root", "commit-a", &format!("commit-b..{}", commit_c)]).is_ok()); assert!(e.sq_git(&["log", "--trust-root", "commit-a", &format!("{}..commit-c", commit_b)]).is_ok()); assert!(e.sq_git(&["log", "--trust-root", "commit-a", "commit-b..commit-c"]).is_ok()); Ok(()) } sequoia-git-0.1.0/tests/refs.rs000064400000000000000000000116151046102023000145240ustar 00000000000000mod common; use common::Environment; fn create_environment() -> anyhow::Result<(Environment, String)> { Environment::scooby_gang_bootstrap() } #[test] #[allow(unused)] fn git_refs() -> anyhow::Result<()> { // The commits: // // commit-2-1 commit-3-1 // | good | good // commit-2-0 commit-3-0 // bad \ / good // commit-2 // | good // commit-1 // | good // commit-0 let (e, commit_0) = create_environment()?; let p = e.git_state(); e.git(&["branch", "commit-0"])?; let commit_1 = e.git_commit(&[("a", Some(b"1"))], "1", Some(&e.willow)).unwrap(); e.git(&["branch", "commit-1"])?; assert!(e.sq_git(&["log", "--trust-root", &commit_0]).is_ok()); let commit_2 = e.git_commit(&[("a", Some(b"2"))], "2", Some(&e.willow)).unwrap(); e.git(&["branch", "commit-2"])?; assert!(e.sq_git(&["log", "--trust-root", &commit_0]).is_ok()); // The bad commit. let commit_2_0 = e.git_commit(&[("a", Some(b"2-0"))], "2-0", Some(&e.xander)).unwrap(); e.git(&["branch", "commit-2-0"])?; assert!(e.sq_git(&["log", "--trust-root", &commit_0]).is_err()); let commit_2_1 = e.git_commit(&[("a", Some(b"2-1"))], "2-1", Some(&e.willow)).unwrap(); e.git(&["branch", "commit-2-1"])?; assert!(e.sq_git(&["log", "--trust-root", &commit_0]).is_err()); // Move back to commit-2. e.git(&["checkout", "commit-2"])?; e.git(&["clean", "-fdx"])?; assert!(e.sq_git(&["log", "--trust-root", &commit_0]).is_ok()); let commit_3_0 = e.git_commit(&[("a", Some(b"3-0"))], "3-0", Some(&e.willow)).unwrap(); e.git(&["branch", "commit-3-0"])?; assert!(e.sq_git(&["log", "--trust-root", &commit_0]).is_ok()); let commit_3_1 = e.git_commit(&[("a", Some(b"3-1"))], "3-1", Some(&e.willow)).unwrap(); e.git(&["branch", "commit-3-1"])?; assert!(e.sq_git(&["log", "--trust-root", &commit_0]).is_ok()); // Debugging. let (stdout, _stderr) = e.git( &["log", "--pretty=oneline", "--graph", "commit-2-1", "commit-3-1"]) .unwrap(); eprintln!("{}", String::from_utf8_lossy(&stdout)); assert!(e.git(&["tag", "v1", "commit-1"]).is_ok()); assert!(e.git(&["tag", "v2", "commit-2"]).is_ok()); assert!(e.git(&["tag", "v2.1", "commit-2-1"]).is_ok()); let paths = &[ // Trust root / commit to check / ok / ok with trust root's parent. (&commit_1[..], "commit-1", &commit_2[..], "commit-2", true, true), (&commit_1[..], "commit-1", &commit_3_1[..], "commit-3-0", true, true), (&commit_1[..], "commit-1", &commit_3_1[..], "commit-3-1", true, true), (&commit_1[..], "commit-1", &commit_2_1[..], "commit-2-1", false, false), (&commit_2_0[..], "commit-2-0", &commit_2_1[..], "commit-2-1", true, false), // Use tags instead of branches. (&commit_1[..], "v1", &commit_2[..], "v2", true, true), (&commit_1[..], "v1", &commit_2_1[..], "v2.1", false, false), ]; for (trust_root_hash, trust_root, commit_hash, commit, good, p_good) in paths.iter() { eprintln!("Testing: {} ({}) .. {} ({}) ({}, {})", trust_root_hash, trust_root, commit_hash, commit, good, p_good); // Commit id. assert_eq!( e.sq_git(&["log", "--trust-root", trust_root_hash, commit_hash]) .is_ok(), *good); // Short commit id. assert_eq!( e.sq_git(&["log", "--trust-root", &trust_root_hash[0..7], &commit_hash[..]]) .is_ok(), *good); // Using branches. assert_eq!( e.sq_git(&["log", "--trust-root", trust_root, commit]) .is_ok(), *good); // Trust root's parent. assert_eq!( e.sq_git(&["log", "--trust-root", &format!("{}^", trust_root_hash), commit_hash]) .is_ok(), *p_good); // Trust root's parent, with branches assert_eq!( e.sq_git(&["log", "--trust-root", &format!("{}^", trust_root), commit]) .is_ok(), *p_good); // Make sure we can use the configuration file to set a trust // root. assert!(e.git(&["config", "sequoia.trust-root", trust_root_hash]).is_ok()); assert_eq!( e.sq_git(&["log", commit_hash]) .is_ok(), *good); assert!(e.git(&["config", "sequoia.trust-root", trust_root]).is_ok()); assert_eq!( e.sq_git(&["log", commit_hash]) .is_ok(), *good); } Ok(()) }