├── .art └── settings.toml ├── .envrc ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci-nix.yml │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── HACKING.md ├── LICENSE-APACHE ├── LICENSE-MIT ├── LICENSE-MPL2 ├── README.md ├── appveyor.yml ├── cargo-crev ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-MIT ├── README.md ├── rustfmt.toml ├── src │ ├── crates_io.rs │ ├── deps.rs │ ├── deps │ │ ├── print_term.rs │ │ └── scan.rs │ ├── doc │ │ ├── advisories.md │ │ ├── cargo_specific.md │ │ ├── compiling.md │ │ ├── getting_started.md │ │ ├── glossary.md │ │ ├── mod.rs │ │ ├── organizations.md │ │ ├── tips_and_tricks.md │ │ └── trust.md │ ├── dyn_proof.rs │ ├── edit.rs │ ├── info.rs │ ├── known_cargo_owners_defaults.txt │ ├── lib.rs │ ├── main.rs │ ├── opts.rs │ ├── prelude.rs │ ├── repo.rs │ ├── review.rs │ ├── shared.rs │ ├── term.rs │ ├── tokei.rs │ └── wot.rs └── tests │ └── clitest.rs ├── ci ├── build-deb ├── cargo-out-dir ├── docker │ ├── README.md │ ├── arm-unknown-linux-gnueabihf │ │ ├── Dockerfile │ │ └── build │ ├── i686-unknown-linux-gnu │ │ ├── Dockerfile │ │ └── build │ ├── mips64-unknown-linux-gnuabi64 │ │ ├── Dockerfile │ │ └── build │ └── x86_64-unknown-linux-musl │ │ ├── Dockerfile │ │ └── build ├── macos-install-packages ├── prep_deploy.sh ├── sha256-releases ├── ubuntu-install-packages └── utils.sh ├── crev-common ├── Cargo.toml ├── README.md ├── rustfmt.toml └── src │ ├── blake2b256.rs │ ├── fs.rs │ ├── lib.rs │ ├── rand.rs │ └── serde.rs ├── crev-data ├── Cargo.toml ├── LICENSE-MIT ├── README.md ├── rustfmt.toml └── src │ ├── digest.rs │ ├── id.rs │ ├── level.rs │ ├── lib.rs │ ├── proof │ ├── content.rs │ ├── mod.rs │ ├── package_info.rs │ ├── review │ │ ├── code.rs │ │ ├── mod.rs │ │ └── package.rs │ ├── revision.rs │ └── trust.rs │ ├── tests.rs │ ├── url.rs │ └── util │ └── mod.rs ├── crev-lib ├── .gitignore ├── Cargo.toml ├── LICENSE-MIT ├── README.md ├── rc │ └── doc │ │ ├── README.md │ │ ├── editing-code-review.md │ │ ├── editing-package-review.md │ │ └── editing-trust.md ├── rustfmt.toml └── src │ ├── activity.rs │ ├── id.rs │ ├── lib.rs │ ├── local.rs │ ├── proof.rs │ ├── repo │ ├── mod.rs │ └── staging.rs │ ├── staging.rs │ ├── tests.rs │ └── util │ ├── git.rs │ └── mod.rs ├── crev-wot ├── Cargo.toml ├── LICENSE-MIT ├── README.md └── src │ ├── lib.rs │ ├── tests.rs │ ├── tests │ └── issues.rs │ └── trust_set.rs ├── default.nix ├── design └── purpose.md ├── flake.lock ├── flake.nix ├── misc └── git-hooks │ ├── commit-template.txt │ └── pre-commit ├── release.toml ├── rustfmt.toml └── shell.nix /.art/settings.toml: -------------------------------------------------------------------------------- 1 | # ---- Artifact Project Settings ---- 2 | 3 | # Paths containing artifact markdown/toml files 4 | artifact_paths = ["design"] 5 | 6 | # Paths to exclude when loading artifacts 7 | exclude_artifact_paths = [] 8 | 9 | # Paths containing code that has artifact links 10 | code_paths = [] 11 | 12 | # Paths to exclude when searching through code 13 | exclude_code_paths = [] 14 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thank you for using `crev`! 2 | 3 | If you want to ask a question, try crev's gitter channel first: https://gitter.im/dpc/crev . You might get a faster answer. 4 | 5 | If you want to report an issue, please go through the following checklist: 6 | 7 | * If a command is failing try running it with `RUST_BACKTRACE=1` 8 | * Paste the exact command and error and/or outputs (along with the backtrace) 9 | * Please let us know: 10 | * Which version are you using (eg. `cargo crev --version`) 11 | * How did you install `crev` (git?, cargo?, your distribution?) 12 | * What OS/platform are you running on 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Checklist: 2 | 3 | * [ ] `cargo +nightly fmt --all` 4 | * [ ] Modify `CHANGELOG.md` if applicable 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: github-actions 8 | directory: "/" 9 | schedule: 10 | interval: daily 11 | -------------------------------------------------------------------------------- /.github/workflows/ci-nix.yml: -------------------------------------------------------------------------------- 1 | name: "CI (nix)" 2 | 3 | env: 4 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: "sparse" 5 | 6 | # Controls when the workflow will run 7 | on: 8 | # Triggers the workflow on push or pull request events but only for the "main" branch 9 | push: 10 | branches: [ "main", "master", "devel" ] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | jobs: 16 | build: 17 | name: "Build" 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 30 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: cachix/install-nix-action@v31 23 | with: 24 | nix_path: nixpkgs=channel:nixos-unstable 25 | - uses: cachix/cachix-action@v16 26 | with: 27 | name: cargo-crev 28 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 29 | 30 | - name: Build workspace 31 | run: nix build -L --extra-experimental-features nix-command --extra-experimental-features flakes .#workspaceBuild 32 | 33 | # - name: Clippy workspace 34 | # run: nix build -L --extra-experimental-features nix-command --extra-experimental-features flakes .#workspaceClippy 35 | 36 | - name: Run cargo doc 37 | run: nix build -L --extra-experimental-features nix-command --extra-experimental-features flakes .#workspaceDoc 38 | 39 | - name: Test workspace 40 | run: nix build -L --extra-experimental-features nix-command --extra-experimental-features flakes .#workspaceTest 41 | 42 | containers: 43 | name: "Containers" 44 | runs-on: ubuntu-latest 45 | timeout-minutes: 30 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: cachix/install-nix-action@v31 49 | with: 50 | nix_path: nixpkgs=channel:nixos-unstable 51 | - uses: cachix/cachix-action@v16 52 | with: 53 | name: cargo-crev 54 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 55 | 56 | - name: Build cargo-crev container 57 | run: | 58 | nix build -L --extra-experimental-features nix-command --extra-experimental-features flakes .#container.cargo-crev 59 | echo "cargo-crev_container_tag=$(docker load < result | awk '{ print $3 }')" >> $GITHUB_ENV 60 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | env: 4 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: "sparse" 5 | 6 | on: 7 | pull_request: 8 | push: 9 | branches: 10 | - master 11 | schedule: 12 | - cron: '00 01 * * *' 13 | jobs: 14 | test: 15 | name: test 16 | env: 17 | # For some builds, we use cross to test on 32-bit and big-endian 18 | # systems. 19 | CARGO: cargo 20 | # When CARGO is set to CROSS, this is set to `--target matrix.target`. 21 | TARGET_FLAGS: 22 | # When CARGO is set to CROSS, TARGET_DIR includes matrix.target. 23 | TARGET_DIR: ./target 24 | # Emit backtraces on panics. 25 | RUST_BACKTRACE: 1 26 | runs-on: ${{ matrix.os }} 27 | strategy: 28 | matrix: 29 | build: 30 | - stable 31 | - beta 32 | # Our release builds are generated by a nightly compiler to take 33 | # advantage of the latest optimizations/compile time improvements. So 34 | # we test all of them here. (We don't do mips releases, but test on 35 | # mips for big-endian coverage.) 36 | - nightly 37 | - nightly-musl 38 | - nightly-32 39 | # FIXME: fails on `cargo` crate build 40 | # - nightly-mips 41 | - nightly-arm 42 | - macos 43 | # FIXME: openssl 44 | # - win-msvc 45 | # - win-gnu 46 | include: 47 | - build: stable 48 | os: ubuntu-18.04 49 | rust: stable 50 | - build: beta 51 | os: ubuntu-latest 52 | rust: beta 53 | - build: nightly 54 | os: ubuntu-latest 55 | rust: nightly 56 | - build: nightly-musl 57 | os: ubuntu-latest 58 | rust: nightly 59 | target: x86_64-unknown-linux-musl 60 | - build: nightly-32 61 | os: ubuntu-18.04 62 | rust: nightly 63 | target: i686-unknown-linux-gnu 64 | # - build: nightly-mips 65 | # os: ubuntu-18.04 66 | # rust: nightly 67 | # target: mips64-unknown-linux-gnuabi64 68 | - build: nightly-arm 69 | os: ubuntu-latest 70 | rust: nightly 71 | # For stripping release binaries: 72 | # docker run --rm -v $PWD/target:/target:Z \ 73 | # rustembedded/cross:arm-unknown-linux-gnueabihf \ 74 | # arm-linux-gnueabihf-strip \ 75 | # /target/arm-unknown-linux-gnueabihf/debug/rg 76 | target: arm-unknown-linux-gnueabihf 77 | - build: macos 78 | os: macos-latest 79 | rust: nightly 80 | # FIXME: openssl 81 | # - build: win-msvc 82 | # os: windows-2019 83 | # rust: nightly 84 | # - build: win-gnu 85 | # os: windows-2019 86 | # rust: nightly-x86_64-gnu 87 | steps: 88 | - name: Checkout repository 89 | uses: actions/checkout@v4 90 | 91 | - name: Install packages (Ubuntu) 92 | if: matrix.os == 'ubuntu-18.04' 93 | run: | 94 | ci/ubuntu-install-packages 95 | 96 | - name: Install packages (macOS) 97 | if: matrix.os == 'macos-latest' 98 | run: | 99 | ci/macos-install-packages 100 | 101 | - name: Install Rust 102 | uses: actions-rs/toolchain@v1 103 | with: 104 | toolchain: ${{ matrix.rust }} 105 | profile: minimal 106 | override: true 107 | 108 | - name: Use Cross 109 | if: matrix.target != '' 110 | run: | 111 | cargo install cross 112 | echo "CARGO=cross" >> $GITHUB_ENV 113 | echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV 114 | echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV 115 | 116 | - name: Show command used for Cargo 117 | run: | 118 | echo "cargo command is: ${{ env.CARGO }}" 119 | echo "target flag is: ${{ env.TARGET_FLAGS }}" 120 | 121 | - name: Build cargo-crev and all crates 122 | run: ${{ env.CARGO }} build --verbose --workspace ${{ env.TARGET_FLAGS }} 123 | 124 | - name: Run tests (with cross) 125 | # These tests should actually work, but they almost double the runtime. 126 | # Every integration test spins up qemu to run 'rg', and when PCRE2 is 127 | # enabled, every integration test is run twice: one with the default 128 | # regex engine and once with PCRE2. 129 | if: matrix.target != '' 130 | run: ${{ env.CARGO }} test --verbose --workspace ${{ env.TARGET_FLAGS }} 131 | 132 | rustfmt: 133 | name: rustfmt 134 | runs-on: ubuntu-18.04 135 | continue-on-error: true 136 | steps: 137 | - name: Checkout repository 138 | uses: actions/checkout@v4 139 | - name: Install Rust 140 | uses: actions-rs/toolchain@v1 141 | with: 142 | toolchain: stable 143 | override: true 144 | profile: minimal 145 | components: rustfmt 146 | - name: Check formatting 147 | run: | 148 | rustfmt --version 149 | cargo fmt --all -- --check 150 | 151 | docs: 152 | name: docs 153 | runs-on: ubuntu-20.04 154 | steps: 155 | - name: Checkout repository 156 | uses: actions/checkout@v4 157 | - name: Install Rust 158 | uses: actions-rs/toolchain@v1 159 | with: 160 | toolchain: stable 161 | profile: minimal 162 | override: true 163 | - name: Check documentation 164 | env: 165 | RUSTDOCFLAGS: -D warnings 166 | run: cargo doc --no-deps --document-private-items --workspace 167 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # The way this works is the following: 2 | # 3 | # The create-release job runs purely to initialize the GitHub release itself 4 | # and to output upload_url for the following job. 5 | # 6 | # The build-release job runs only once create-release is finished. It gets the 7 | # release upload URL from create-release job outputs, then builds the release 8 | # executables for each supported platform and attaches them as release assets 9 | # to the previously created release. 10 | # 11 | # The key here is that we create the release only once. 12 | # 13 | # Reference: 14 | # https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/ 15 | 16 | name: release 17 | on: 18 | push: 19 | # Enable when testing release infrastructure on a branch. 20 | # branches: 21 | # - ag/work 22 | tags: 23 | - "v[0-9]+.[0-9]+.[0-9]+" 24 | jobs: 25 | create-release: 26 | name: create-release 27 | runs-on: ubuntu-latest 28 | # env: 29 | # Set to force version number, e.g., when no tag exists. 30 | # CARGO_CREV_VERSION: TEST-0.0.0 31 | outputs: 32 | upload_url: ${{ steps.release.outputs.upload_url }} 33 | cargo_crev_version: ${{ env.CARGO_CREV_VERSION }} 34 | steps: 35 | - name: Get the release version from the tag 36 | shell: bash 37 | if: env.CARGO_CREV_VERSION == '' 38 | run: | 39 | # Apparently, this is the right way to get a tag name. Really? 40 | # 41 | # See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 42 | echo "CARGO_CREV_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV 43 | echo "version is: ${{ env.CARGO_CREV_VERSION }}" 44 | - name: Create GitHub release 45 | id: release 46 | uses: actions/create-release@v1 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | with: 50 | tag_name: v${{ env.CARGO_CREV_VERSION }} 51 | release_name: v${{ env.CARGO_CREV_VERSION }} 52 | 53 | build-release: 54 | name: build-release 55 | needs: ['create-release'] 56 | runs-on: ${{ matrix.os }} 57 | env: 58 | # For some builds, we use cross to test on 32-bit and big-endian 59 | # systems. 60 | CARGO: cargo 61 | # When CARGO is set to CROSS, this is set to `--target matrix.target`. 62 | TARGET_FLAGS: "" 63 | # When CARGO is set to CROSS, TARGET_DIR includes matrix.target. 64 | TARGET_DIR: ./target 65 | # Emit backtraces on panics. 66 | RUST_BACKTRACE: 1 67 | strategy: 68 | matrix: 69 | # build: [linux, linux-arm, macos, win-msvc, win-gnu, win32-msvc] 70 | build: [linux, linux-arm, macos] 71 | include: 72 | - build: linux 73 | os: ubuntu-18.04 74 | rust: nightly 75 | target: x86_64-unknown-linux-musl 76 | - build: linux-arm 77 | os: ubuntu-18.04 78 | rust: nightly 79 | target: arm-unknown-linux-gnueabihf 80 | - build: macos 81 | os: macos-latest 82 | rust: nightly 83 | target: x86_64-apple-darwin 84 | # FIXME: openssl 85 | # - build: win-msvc 86 | # os: windows-2019 87 | # rust: nightly 88 | # target: x86_64-pc-windows-msvc 89 | # - build: win-gnu 90 | # os: windows-2019 91 | # rust: nightly-x86_64-gnu 92 | # target: x86_64-pc-windows-gnu 93 | # - build: win32-msvc 94 | # os: windows-2019 95 | # rust: nightly 96 | # target: i686-pc-windows-msvc 97 | 98 | steps: 99 | - name: Checkout repository 100 | uses: actions/checkout@v4 101 | with: 102 | fetch-depth: 1 103 | 104 | - name: Install packages (Ubuntu) 105 | if: matrix.os == 'ubuntu-18.04' 106 | run: | 107 | ci/ubuntu-install-packages 108 | 109 | - name: Install packages (macOS) 110 | if: matrix.os == 'macos-latest' 111 | run: | 112 | ci/macos-install-packages 113 | 114 | - name: Install Rust 115 | uses: actions-rs/toolchain@v1 116 | with: 117 | toolchain: ${{ matrix.rust }} 118 | profile: minimal 119 | override: true 120 | target: ${{ matrix.target }} 121 | 122 | - name: Use Cross 123 | shell: bash 124 | run: | 125 | cargo install cross 126 | echo "CARGO=cross" >> $GITHUB_ENV 127 | echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV 128 | echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV 129 | 130 | - name: Show command used for Cargo 131 | run: | 132 | echo "cargo command is: ${{ env.CARGO }}" 133 | echo "target flag is: ${{ env.TARGET_FLAGS }}" 134 | echo "target dir is: ${{ env.TARGET_DIR }}" 135 | 136 | - name: Build release binary 137 | run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }} 138 | 139 | - name: Strip release binary (linux and macos) 140 | if: matrix.build == 'linux' || matrix.build == 'macos' 141 | run: strip "target/${{ matrix.target }}/release/cargo-crev" 142 | 143 | - name: Strip release binary (arm) 144 | if: matrix.build == 'linux-arm' 145 | run: | 146 | docker run --rm -v \ 147 | "$PWD/target:/target:Z" \ 148 | rustembedded/cross:arm-unknown-linux-gnueabihf \ 149 | arm-linux-gnueabihf-strip \ 150 | /target/arm-unknown-linux-gnueabihf/release/cargo-crev 151 | 152 | - name: Build archive 153 | shell: bash 154 | run: | 155 | outdir="$(ci/cargo-out-dir "${{ env.TARGET_DIR }}")" 156 | staging="cargo-crev-v${{ needs.create-release.outputs.cargo_crev_version }}-${{ matrix.target }}" 157 | mkdir -p "$staging"/{complete,doc} 158 | 159 | cp {README.md,LICENSE-APACHE,LICENSE-MIT,LICENSE-MPL2} "$staging/" 160 | cp cargo-crev/CHANGELOG.md "$staging/doc/" 161 | 162 | if [ "${{ matrix.os }}" = "windows-2019" ]; then 163 | cp "target/${{ matrix.target }}/release/cargo-crev.exe" "$staging/" 164 | 7z a "$staging.zip" "$staging" 165 | echo "ASSET=$staging.zip" >> $GITHUB_ENV 166 | else 167 | cp "target/${{ matrix.target }}/release/cargo-crev" "$staging/" 168 | tar czf "$staging.tar.gz" "$staging" 169 | echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV 170 | fi 171 | 172 | - name: Upload release archive 173 | uses: actions/upload-release-asset@v1.0.2 174 | env: 175 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 176 | with: 177 | upload_url: ${{ needs.create-release.outputs.upload_url }} 178 | asset_path: ${{ env.ASSET }} 179 | asset_name: ${{ env.ASSET }} 180 | asset_content_type: application/octet-stream 181 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.direnv 3 | target 4 | **/*.rs.bk 5 | 6 | # Intellij project files 7 | *.iml 8 | .idea 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com/ for usage and config 2 | repos: 3 | - repo: local 4 | hooks: 5 | - id: fmt 6 | name: cargo fmt 7 | description: Format files with cargo fmt. 8 | entry: cargo fmt -- 9 | language: system 10 | types: [rust] 11 | stages: [commit] 12 | args: [] 13 | 14 | - id: test 15 | name: cargo test 16 | description: Run tests with cargo test. 17 | entry: cargo test -- 18 | language: rust 19 | types: [rust] 20 | stages: [push] 21 | args: [] 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | cargo-crev/CHANGELOG.md -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "cargo-crev", 5 | "crev-common", 6 | "crev-data", 7 | "crev-wot", 8 | "crev-lib", 9 | ] 10 | exclude = ["crevette"] 11 | 12 | [workspace.package] 13 | authors = ["Dawid Ciężarkiewicz ", "Kornel Lesiński "] 14 | edition = "2021" 15 | license = "MPL-2.0 OR MIT OR Apache-2.0" 16 | repository = "https://github.com/crev-dev/cargo-crev" 17 | rust-version = "1.77" 18 | version = "0.26.4" 19 | 20 | [workspace.dependencies] 21 | crev-common = { path = "crev-common", version = "0.26.1"} 22 | crev-data = { path = "crev-data", version = "0.26.1"} 23 | crev-lib = { path = "crev-lib", version = "0.26.3"} 24 | crev-wot = { path = "crev-wot", version = "0.26" } 25 | chrono = { version = "0.4.28", default-features = false, features = ["std", "clock"] } 26 | blake2 = "0.10.6" 27 | default = "0.1.2" 28 | cargo = "0.86" 29 | git2 = ">= 0.19, < 0.21" 30 | itertools = "0.14" 31 | log = "0.4.20" 32 | rayon = "1.7.0" 33 | resiter = "0.5" 34 | semver = { version = "1.0.18", features = [ "serde" ] } 35 | serde = { version = "1.0.188", features = ["derive"] } 36 | serde_json = "1.0.105" 37 | serde_yaml = "0.9.25" 38 | thiserror = "2.0" 39 | -------------------------------------------------------------------------------- /HACKING.md: -------------------------------------------------------------------------------- 1 | # Hacking 2 | 3 | ## Structure 4 | 5 | `Crev` is split into layered crates (lowest layer to highest): 6 | 7 | - `crev-common` is a common utility code 8 | - `crev-data` contains core data types, crypto, and serialization code. 9 | - `crev-lib` implements basic concepts (think `libgit2`) 10 | - binary crates - the actual utilities that users will call 11 | - `cargo-crev` - frontend integrated with Cargo for Rust 12 | - auxiliary tools: 13 | - `crevette` - Crev to cargo-vet interoperability 14 | - `recursive-digest` - library implementing a recursive digest over a 15 | directory content 16 | - `rblake2sum` - a binary on top of `recursive-digest` 17 | 18 | For core crates, the rule is that any given crate can only depend on the lower 19 | layer. 20 | 21 | ## Continuous Integration 22 | 23 | To streamline development, this repository uses continuous integration (CI) to 24 | check that tests pass and that the code is correctly formatted. 25 | 26 | These checks can be automatically executed locally using [Git 27 | hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks). They will be 28 | executed before running Git commands (such as commit). Git hooks for this 29 | repository can be trivially installed using the 30 | [pre-commit](https://pre-commit.com/) Python package: 31 | 32 | ``` shell 33 | pip install pre-commit 34 | pre-commit install -t pre-commit -t pre-push 35 | ``` 36 | 37 | Hooks for this project are defined in `./.pre-commit-config.yaml`. Pre-commit 38 | allows a developer to chose which hooks to install. 39 | 40 | ## Misc 41 | 42 | Other than that there's not that much structure yet, and everything is still 43 | fluid and not necessarily properly done. 44 | 45 | Seek help on [crev gitter channel](https://gitter.im/dpc/crev) before you start 46 | hacking on the code. 47 | 48 | The immediate goal is to get `cargo-crev` binary to be usable. 49 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 The Rust Project Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cargo-crev/README.md -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | os: Visual Studio 2015 2 | 3 | cache: 4 | - 'C:\Users\appveyor\.cargo' 5 | - target 6 | 7 | matrix: 8 | allow_failures: 9 | - channel: nightly 10 | 11 | install: 12 | - appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe 13 | - rustup-init.exe -y --default-host %target% --default-toolchain %channel% 14 | - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin 15 | - rustc -V 16 | - cargo -V 17 | 18 | build_script: 19 | - cmd: cargo build %RELEASE% --verbose 20 | 21 | test_script: 22 | - cmd: cargo test --all %RELEASE% --verbose -- --ignored 23 | 24 | branches: 25 | only: 26 | - master 27 | - /v\d+\.\d+\.\d+/ 28 | environment: 29 | matrix: 30 | # Build a release build on master to make sure it builds. 31 | - channel: stable 32 | target: x86_64-pc-windows-msvc 33 | RELEASE: --release 34 | DEPLOY: true 35 | 36 | - channel: nightly 37 | target: x86_64-pc-windows-msvc 38 | 39 | before_deploy: 40 | - ps: | 41 | $NAME = "cargo-crev-${env:APPVEYOR_REPO_TAG_NAME}-${env:TARGET}" 42 | New-Item -Path $NAME -ItemType directory 43 | Copy-Item target/release/cargo-crev.exe "${NAME}/" 44 | Copy-Item LICENSE-MIT "${NAME}/" 45 | Copy-Item LICENSE-MPL2 "${NAME}/" 46 | Copy-Item LICENSE-APACHE "${NAME}/" 47 | Copy-Item cargo-crev/README.md "${NAME}/" 48 | 7z a -ttar "${NAME}.tar" "${NAME}" 49 | 7z a "${NAME}.tar.gz" "${NAME}.tar" 50 | (Get-FileHash "${NAME}.tar.gz").Hash | Out-File "${NAME}.tar.gz.sha256" -NoNewline 51 | Push-AppveyorArtifact "${NAME}.tar.gz" -DeploymentName windep 52 | Push-AppveyorArtifact "${NAME}.tar.gz.sha256" -DeploymentName windep 53 | 54 | deploy: 55 | - provider: GitHub 56 | artifact: windep 57 | auth_token: 58 | secure: x1b6Wm5JImKLmkZZSGxNvxTiRqTpk5Fi/+ttBL+CHOPg1ChRXbbhmwLwdqXzmlXi 59 | description: '' 60 | on: 61 | APPVEYOR_REPO_TAG: true 62 | DEPLOY: true 63 | force_update: true 64 | -------------------------------------------------------------------------------- /cargo-crev/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-crev" 3 | categories = ["development-tools::cargo-plugins"] 4 | default-run = "cargo-crev" 5 | description = "Distibuted Code REView system for verifying security and quality of Cargo dependencies" 6 | documentation = "https://docs.rs/cargo-crev" 7 | homepage = "https://github.com/crev-dev/cargo-crev" 8 | keywords = ["code", "review", "supply-chain", "security", "distributed"] 9 | readme = "README.md" 10 | include = ["src/**", "Cargo.toml", "README.md", "LICENSE-MIT"] 11 | authors.workspace = true 12 | edition.workspace = true 13 | license.workspace = true 14 | repository.workspace = true 15 | rust-version.workspace = true 16 | version.workspace = true 17 | 18 | [[bin]] 19 | name = "cargo-crev" 20 | path = "src/main.rs" 21 | 22 | [lib] 23 | name = "cargo_crev" 24 | path = "src/lib.rs" 25 | 26 | [dependencies] 27 | crev-common.workspace = true 28 | crev-data.workspace = true 29 | crev-wot.workspace = true 30 | crev-lib.workspace = true 31 | anyhow = "1.0.93" 32 | atty = "0.2.14" 33 | curl-sys = { version = "0.4", features = ["force-system-lib-on-osx"] } 34 | cargo.workspace = true 35 | cargo-platform = "0.2.0" 36 | crates_io_api = "0.11.0" 37 | crossbeam = "0.8.2" 38 | chrono.workspace = true 39 | env_logger = { version = "0.11.3", default-features = false, features = ["auto-color", "humantime"] } 40 | fnv = "1.0.7" 41 | geiger = { version = "0.5.0", optional = true } 42 | itertools.workspace = true 43 | lazy_static = "1.4.0" 44 | petgraph = "0.7" 45 | rayon = "1.7.0" 46 | resiter = "0.5.0" 47 | rpassword = "7.2.0" 48 | serde.workspace = true 49 | serde_json.workspace = true 50 | serde_yaml.workspace = true 51 | structopt = "0.3.26" 52 | time = "0.3.28" 53 | tokei = "13.0.0-alpha.5" 54 | walkdir = "2.3.3" 55 | openssl-sys = "0.9.92" 56 | git2.workspace = true 57 | tempfile = "3.8.0" 58 | rprompt = "2.0.2" 59 | thiserror.workspace = true 60 | log.workspace = true 61 | term = "1.0" 62 | syn-inline-mod = "0.6.0" 63 | quote = "1.0.33" 64 | 65 | [features] 66 | default = ["openssl-sys/vendored"] 67 | geiger = ["dep:geiger"] 68 | 69 | documentation = [] 70 | 71 | [package.metadata.docs.rs] 72 | all-features = true 73 | 74 | 75 | [package.metadata.release] 76 | pre-release-replacements = [ 77 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, 78 | {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, 79 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, 80 | {file="CHANGELOG.md", search="", replace="\n## [Unreleased](https://github.com/crev-dev/cargo-crev/compare/{{tag_name}}...HEAD) - ReleaseDate\n\n", exactly=1}, 81 | ] 82 | shared-version=true 83 | -------------------------------------------------------------------------------- /cargo-crev/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /cargo-crev/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | community discussion 4 | 5 | 6 | Github Actions CI Build Status 7 | 8 | 9 | crates.io 10 | 11 |
12 | 13 |

14 | 15 | 16 |

17 | jesus, that's a lot of dependencies 18 |
19 | image credit 20 |

21 | 22 | 23 | # cargo-crev 24 | 25 | > A cryptographically verifiable **c**ode **rev**iew system for the cargo (Rust) 26 | > package manager. 27 | 28 | ## Introduction 29 | 30 | [Crev](https://github.com/crev-dev/crev/) is a language and ecosystem agnostic, 31 | distributed **c**ode **rev**iew system. 32 | 33 | `cargo-crev` is an implementation of Crev as a command line tool integrated with 34 | `cargo`. This tool helps Rust users evaluate the quality and trustworthiness of 35 | their package dependencies. 36 | 37 | ## Features 38 | 39 | `cargo-crev` can already: 40 | 41 | - warn you about untrustworthy crates and security vulnerabilities, 42 | - display useful metrics about your dependencies, 43 | - help you identify dependency-bloat, 44 | - allow you to review most suspicious dependencies and publish your findings, 45 | - use reviews produced by other users, 46 | - increase trustworthiness of your own code, 47 | - build a web of trust of other reputable users to help verify the code you use, 48 | 49 | and many other things with many more to come. 50 | 51 | ## Getting started 52 | 53 | Static binaries are available from the [releases 54 | page](https://github.com/crev-dev/cargo-crev/releases). 55 | 56 | Follow the [`cargo-crev` - Getting Started 57 | Guide](https://github.com/crev-dev/cargo-crev/blob/main/cargo-crev/src/doc/getting_started.md) 58 | (more documentation available on [docs.rs](https://docs.rs/cargo-crev)). 59 | 60 | `cargo-crev` is a work in progress, but it should be usable at all times. 61 | Use [discussions](https://github.com/crev-dev/cargo-crev/discussions) 62 | to get help, more information and report feedback. Thank you\! 63 | 64 | ## Raise awareness 65 | 66 | If you're supportive of the cause, we would appreciate helping to raise 67 | awareness of the project. Consider putting the below note in the README of your 68 | Rust 69 | projects: 70 | 71 | It is recommended to always use [cargo-crev](https://github.com/crev-dev/cargo-crev) 72 | to verify the trustworthiness of each of your dependencies, including this one. 73 | 74 | Thank you\! 75 | 76 | ## Changelog 77 | 78 | Changelog can be found here: 79 | 80 | -------------------------------------------------------------------------------- /cargo-crev/rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | # imports_granularity="Crate" 3 | 4 | -------------------------------------------------------------------------------- /cargo-crev/src/crates_io.rs: -------------------------------------------------------------------------------- 1 | use crate::{deps::DownloadsStats, prelude::*}; 2 | use serde::{de::DeserializeOwned, Serialize}; 3 | use std::{ 4 | fs, 5 | io::Read, 6 | path::{Path, PathBuf}, 7 | time::Duration, 8 | }; 9 | 10 | pub struct Client { 11 | client: crates_io_api::SyncClient, 12 | cache_dir: PathBuf, 13 | } 14 | 15 | fn is_fresh(path: &Path) -> Result { 16 | let metadata = fs::metadata(path)?; 17 | let created = metadata.created().or_else(|_e| metadata.modified())?; 18 | let now = std::time::SystemTime::now(); 19 | Ok(((now - Duration::from_secs(60 * 60 * 72)) < created) && (created < now)) 20 | } 21 | 22 | trait Cacheable: Sized { 23 | fn get_cache_path(base: &Path, name: &str, version: &str) -> PathBuf; 24 | fn fetch(client: &crates_io_api::SyncClient, crate_: &str, _version: &str) -> Result; 25 | } 26 | 27 | impl Cacheable for crates_io_api::CrateResponse { 28 | fn get_cache_path(base: &Path, name: &str, _version: &str) -> PathBuf { 29 | base.join("crate").join(format!("{name}.json")) 30 | } 31 | fn fetch(client: &crates_io_api::SyncClient, crate_: &str, _version: &str) -> Result { 32 | Ok(client.get_crate(crate_)?) 33 | } 34 | } 35 | 36 | impl Cacheable for crates_io_api::Owners { 37 | fn get_cache_path(base: &Path, name: &str, _version: &str) -> PathBuf { 38 | base.join("owners").join(format!("{name}.json")) 39 | } 40 | fn fetch(client: &crates_io_api::SyncClient, crate_: &str, _version: &str) -> Result { 41 | Ok(crates_io_api::Owners { 42 | users: client.crate_owners(crate_)?, 43 | }) 44 | } 45 | } 46 | 47 | fn get_downloads_stats(resp: &crates_io_api::CrateResponse, version: &Version) -> DownloadsStats { 48 | DownloadsStats { 49 | version: resp 50 | .versions 51 | .iter() 52 | .find(|v| v.num == version.to_string()) 53 | .map_or(0, |v| v.downloads), 54 | total: resp.crate_data.downloads, 55 | recent: resp.crate_data.recent_downloads.unwrap_or(0), 56 | } 57 | } 58 | 59 | impl Client { 60 | pub fn new(local: &crev_lib::Local) -> Result { 61 | let cache_dir = local.cache_root().join("crates_io"); 62 | fs::create_dir_all(&cache_dir)?; 63 | Ok(Self { 64 | client: crates_io_api::SyncClient::new( 65 | "cargo-crev", 66 | std::time::Duration::from_millis(1000), 67 | )?, 68 | cache_dir, 69 | }) 70 | } 71 | 72 | fn load_cache(&self, path: &Path) -> Result { 73 | let mut file = std::fs::File::open(path)?; 74 | let mut content = String::new(); 75 | file.read_to_string(&mut content)?; 76 | 77 | Ok(content) 78 | } 79 | 80 | fn get_from_cache( 81 | &self, 82 | name: &str, 83 | version: &str, 84 | ) -> Result> { 85 | let path = T::get_cache_path(&self.cache_dir, name, version); 86 | if path.exists() { 87 | let content = self.load_cache(&path)?; 88 | let v = serde_json::from_str::(&content)?; 89 | Ok(Some((v, is_fresh(&path)?))) 90 | } else { 91 | Ok(None) 92 | } 93 | } 94 | 95 | fn store_in_cache(&self, path: &Path, resp: &T) -> Result<()> { 96 | crev_common::store_to_file_with(path, |file| serde_json::to_writer(file, &resp))??; 97 | Ok(()) 98 | } 99 | 100 | fn fetch(&self, crate_: &str, version: &str) -> Result { 101 | let resp = T::fetch(&self.client, crate_, version)?; 102 | self.store_in_cache(&T::get_cache_path(&self.cache_dir, crate_, version), &resp)?; 103 | Ok(resp) 104 | } 105 | 106 | fn get( 107 | &self, 108 | crate_: &str, 109 | version: &str, 110 | ) -> Result { 111 | let cached: Option<(T, bool)> = self.get_from_cache(crate_, version)?; 112 | 113 | match cached { 114 | Some((resp, true)) => Ok(resp), 115 | Some((resp, false)) => match self.fetch(crate_, version) { 116 | Ok(new_resp) => Ok(new_resp), 117 | Err(_e) => Ok(resp), 118 | }, 119 | None => self.fetch(crate_, version), 120 | } 121 | } 122 | 123 | pub fn get_downloads_count(&self, crate_: &str, version: &Version) -> Result { 124 | Ok(get_downloads_stats( 125 | &self.get::(crate_, &version.to_string())?, 126 | version, 127 | )) 128 | } 129 | 130 | pub fn get_owners(&self, crate_: &str) -> Result> { 131 | let owners = self.get::(crate_, "")?; 132 | Ok(owners.users.into_iter().map(|u| u.login).collect()) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /cargo-crev/src/deps/print_term.rs: -------------------------------------------------------------------------------- 1 | // Functions related to writing dependencies in the standard 2 | // terminal (not in the context of a real terminal application) 3 | 4 | use super::*; 5 | use crate::term::{self, Term}; 6 | use std::{io, io::Write, write, writeln}; 7 | 8 | const CRATE_VERIFY_CRATE_COLUMN_TITLE: &str = "crate"; 9 | const CRATE_VERIFY_VERSION_COLUMN_TITLE: &str = "version"; 10 | 11 | #[derive(Copy, Clone, Debug)] 12 | pub struct VerifyOutputColumnWidths { 13 | pub name: usize, 14 | pub version: usize, 15 | } 16 | 17 | impl VerifyOutputColumnWidths { 18 | pub fn from_pkgsids<'a>(pkgs_ids: impl Iterator) -> Self { 19 | let (name, version) = pkgs_ids.fold( 20 | ( 21 | CRATE_VERIFY_CRATE_COLUMN_TITLE.len(), 22 | CRATE_VERIFY_VERSION_COLUMN_TITLE.len(), 23 | ), 24 | |(name, version), pkgid| { 25 | ( 26 | name.max(pkgid.name().len()), 27 | version.max(pkgid.version().to_string().len()), 28 | ) 29 | }, 30 | ); 31 | 32 | Self { name, version } 33 | } 34 | } 35 | 36 | pub fn print_header( 37 | _term: &mut Term, 38 | columns: &CrateVerifyColumns, 39 | column_widths: VerifyOutputColumnWidths, 40 | ) -> Result<()> { 41 | write!(io::stdout(), "{:>6} ", "status")?; 42 | 43 | if columns.show_reviews() { 44 | write!(io::stdout(), "{:>7} ", "reviews")?; 45 | } 46 | 47 | if columns.show_issues() { 48 | write!(io::stdout(), "{:>6} ", "issues")?; 49 | } 50 | 51 | if columns.show_owners() { 52 | write!(io::stdout(), "{:>5} ", "owner")?; 53 | } 54 | 55 | if columns.show_downloads() { 56 | write!(io::stdout(), "{:>14} ", "downloads")?; 57 | } 58 | 59 | if columns.show_loc() { 60 | write!(io::stdout(), "{:>6} ", "loc")?; 61 | } 62 | 63 | if columns.show_leftpad_index() { 64 | write!(io::stdout(), "{:>5} ", "lpidx")?; 65 | } 66 | 67 | if columns.show_geiger() { 68 | write!(io::stdout(), "{:>6} ", "geiger")?; 69 | } 70 | 71 | if columns.show_flags() { 72 | write!(io::stdout(), "{:>4} ", "flgs")?; 73 | } 74 | 75 | let name_column_width = column_widths.name; 76 | let version_column_width = column_widths.version; 77 | write!( 78 | io::stdout(), 79 | "{: Result<()> { 103 | if cdep.accumulative.is_local_source_code { 104 | term.print(format_args!("{:6} ", "local"), None)?; 105 | } else if !cdep.accumulative.has_trusted_ids 106 | && cdep.accumulative.trust == VerificationStatus::Insufficient 107 | { 108 | term.print(format_args!("{:6} ", "N/A"), None)?; 109 | } else { 110 | term.print( 111 | format_args!("{:6} ", cdep.accumulative.trust), 112 | term::verification_status_color(cdep.accumulative.trust), 113 | )?; 114 | } 115 | 116 | if columns.show_reviews() { 117 | write!( 118 | io::stdout(), 119 | "{:3} {:3} ", 120 | cdep.version_reviews.count, 121 | cdep.version_reviews.total 122 | )?; 123 | } 124 | 125 | if columns.show_issues() { 126 | term.print( 127 | format_args!("{:2} ", cdep.accumulative.trusted_issues.count), 128 | if cdep.accumulative.trusted_issues.count > 0 { 129 | Some(::term::color::RED) 130 | } else { 131 | None 132 | }, 133 | )?; 134 | term.print( 135 | format_args!("{:3} ", cdep.accumulative.trusted_issues.total), 136 | if cdep.accumulative.trusted_issues.total > 0 { 137 | Some(::term::color::YELLOW) 138 | } else { 139 | None 140 | }, 141 | )?; 142 | } 143 | 144 | if columns.show_owners() { 145 | if recursive_mode { 146 | term.print( 147 | format_args!( 148 | "{:>2} {:>2} ", 149 | cdep.accumulative.owner_set.to_total_owners(), 150 | cdep.accumulative.owner_set.to_total_distinct_groups() 151 | ), 152 | None, 153 | )?; 154 | } else if let Some(known_owners) = &cdep.known_owners { 155 | term.print( 156 | format_args!("{:>2} ", known_owners.count), 157 | term::known_owners_count_color(known_owners.count), 158 | )?; 159 | term.print(format_args!("{:>2} ", known_owners.total), None)?; 160 | } else { 161 | term.print( 162 | format_args!("{:>2} ", "?"), 163 | term::known_owners_count_color(0), 164 | )?; 165 | term.print(format_args!("{:>2} ", "?"), None)?; 166 | } 167 | } 168 | 169 | if columns.show_downloads() { 170 | if let Some(downloads) = &cdep.downloads { 171 | term.print( 172 | format_args!("{:>5}K ", downloads.version / 1000), 173 | if downloads.version < 2000 { 174 | Some(::term::color::YELLOW) 175 | } else { 176 | None 177 | }, 178 | )?; 179 | term.print( 180 | format_args!("{:>6}K ", downloads.total / 1000), 181 | if downloads.total < 20000 { 182 | Some(::term::color::YELLOW) 183 | } else { 184 | None 185 | }, 186 | )?; 187 | } else { 188 | term.print(format_args!("{:>8} {:>9} ", "?", "?"), None)?; 189 | } 190 | } 191 | 192 | if columns.show_loc() { 193 | match cdep.accumulative.loc { 194 | Some(loc) => write!(io::stdout(), "{loc:>6} ")?, 195 | None => write!(io::stdout(), "{:>6} ", "err")?, 196 | } 197 | } 198 | 199 | if columns.show_leftpad_index() { 200 | write!( 201 | io::stdout(), 202 | "{:>5} ", 203 | (cdep.leftpad_idx as f64).sqrt().round() as usize 204 | )?; 205 | } 206 | 207 | Ok(()) 208 | } 209 | 210 | fn write_stats_crate_id( 211 | stats: &CrateStats, 212 | _term: &mut Term, 213 | column_widths: VerifyOutputColumnWidths, 214 | ) -> Result<()> { 215 | let name_column_width = column_widths.name; 216 | let version_column_width = column_widths.version; 217 | write!( 218 | io::stdout(), 219 | "{:name_column_width$} {: Result<()> { 238 | let details = stats.details(); 239 | 240 | write_details(details, term, columns, recursive_mode)?; 241 | if columns.show_geiger() { 242 | match details.accumulative.geiger_count { 243 | Some(geiger_count) => write!(io::stdout(), "{geiger_count:>6} ")?, 244 | None => write!(io::stdout(), "{:>6} ", "err")?, 245 | } 246 | } 247 | 248 | if columns.show_flags() { 249 | if stats.has_custom_build() { 250 | term.print(format_args!("CB"), ::term::color::YELLOW)?; 251 | } else { 252 | write!(io::stdout(), "__")?; 253 | } 254 | 255 | if stats.is_unmaintained() { 256 | write!(io::stdout(), "UM")?; 257 | } else { 258 | write!(io::stdout(), "__")?; 259 | } 260 | write!(io::stdout(), " ")?; 261 | } 262 | 263 | write_stats_crate_id(stats, term, column_widths)?; 264 | 265 | if columns.show_latest_trusted() { 266 | write!( 267 | io::stdout(), 268 | "{:<12}", 269 | latest_trusted_version_string(stats.info.id.version(), &details.latest_trusted_version) 270 | )?; 271 | } 272 | 273 | if columns.show_digest() { 274 | write!( 275 | io::stdout(), 276 | "{}", 277 | details 278 | .digest 279 | .as_ref() 280 | .map_or_else(|| "-".into(), |d| d.to_string()) 281 | )?; 282 | } 283 | 284 | writeln!(io::stdout())?; 285 | Ok(()) 286 | } 287 | -------------------------------------------------------------------------------- /cargo-crev/src/doc/advisories.md: -------------------------------------------------------------------------------- 1 | # Reporting Advisories (and Issues) 2 | 3 | `crev` (and so `cargo-crev`) comes with support for issue/advisory reporting. 4 | 5 | Both issues and advisories are an optional section of a *package review proof*. 6 | For this reason they are always associated with a specific crate version, which 7 | makes them work in a bit peculiar way 8 | 9 | ## Issues 10 | 11 | `cargo crev report` can be used to report an issue in a given release of a 12 | crate. 13 | 14 | Each issue is marked with an ID. It can be any string. Identifiers like 15 | `CVE-xxxx-xxxx`, `RUSTSEC-xxxx-xxxx` are recommended, but when not available an 16 | URL can be used instead. 17 | 18 | Issues associated with a crate version, are only stating that this particular 19 | release is affected. The do not imply that this is necessarily the first or only 20 | version being affected. 21 | 22 | Also, issues are treated as an open from the first version reported. `crev` will 23 | consider all the later versions to be affected as well, until a corresponding 24 | *advisory* is found with a matching `id`. 25 | 26 | **It is generally better to report *advisories* instead of issues**. Issues are 27 | most useful when the fixed release is not yet available, so it's impossible to 28 | create an advisory associated with a version that does not yet exist. 29 | 30 | ## Advisories 31 | 32 | `cargo crev advise` can be used to create *package review proof* including an 33 | advisory. 34 | 35 | Advisory should be reported on the first version fixing a problem for a given 36 | range of previously affected versions. 37 | 38 | For simplicity a single `range` field is used to specify the range of affected 39 | versions. 40 | 41 | For example: If a problem affects all releases from 1.4.0 and the fix was 42 | released in the version 1.4.5, a `range: minor` since the whole minor release 43 | was affected (all versions matching 1.4.x, before release containing the 44 | advisory). 45 | 46 | This simplifies specifying the range, but is not always precise. Had the issue 47 | been first introduced in version 1.4.1, the version 1.4.0 would be incorrectly 48 | affected as well. This is however rare and overshooting is not a problem. 49 | 50 | In some cases the same advisory might need to be created for multiple versions. 51 | I.e. when the patched versions was provided for multiple affected major 52 | versions. 53 | -------------------------------------------------------------------------------- /cargo-crev/src/doc/cargo_specific.md: -------------------------------------------------------------------------------- 1 | # Cargo specific features 2 | 3 | `crev` is a language and ecosystem agnostic system for reviewing code. While 4 | being quite generic it does not forbid or prevent integrating with particular 5 | features and data available in each ecosystem. Quite the opposite - part of the 6 | vision of `crev` is to build well integrated ecosystem-specific tools. 7 | `cargo-crev` is exactly such a tool for Rust language and `cargo` package 8 | manager. 9 | 10 | For this reason `cargo-crev` implements multiple features to help `cargo` users. 11 | 12 | ## Known Owners 13 | 14 | While in a perfect world everyone would just review the code they are using 15 | and/or rely on other reputable reviewers, this will be a difficult target until 16 | a critical mass of adoption is reached. 17 | 18 | To address this problem, `cargo-crev` allows reasoning about trustworthinnes of 19 | crates by the reputation of their authors. 20 | 21 | Every crev identity can create and maintain a "known owners" list. Use `cargo 22 | crev config edit known` command to edit it. Each line is crates.io username or 23 | group name that will be considered somewhat trustworthy. 24 | 25 | During dependency verification a `--skip-known-owners` argument can be used to 26 | skip crates that have at least one known owner. 27 | 28 | It's important to consider the security implications. crates.io or the personal 29 | accounts of reputable crate authors could get compromised. And just because the 30 | crate owner is on a list of authors does not mean other co-authors are 31 | necessarily trustworthy. 32 | 33 | So this feature is definitely a compromise. But it is very useful for filtering 34 | out dependencies that are most probably OK, and can be reviewed after code from 35 | less reputable sources is reviewed first. 36 | 37 | ## Download counters 38 | 39 | `cargo crev crate verify` will display download counts for both specific crate 40 | version and total crate downloads, as a quick estimate of crate popularity. 41 | Crates and versions with particularly low download count at higher risk of 42 | introducing serious bugs or malicious code. 43 | 44 | ## Geiger count 45 | 46 | [`geiger`](https://crates.io/crates/geiger) is a binary and a library 47 | calculating number of `unsafe` lines of code. `cargo-crev` uses it to display 48 | the *geiger count* for each dependency. `unsafe` code can introduce memory 49 | safety issues, and non-zero geiger count is a good reason to prioritze reviewing 50 | the code. 51 | 52 | ## Lines of code 53 | 54 | `cargo-crev` uses [`tokei`](https://crates.io/crates/tokei) to calculate the 55 | total number of Rust code each dependency introduces. Small crates are a good 56 | candidate for immediate review (because it will be quick). Bigger ones can often 57 | be replaced with smaller alternatives. 58 | -------------------------------------------------------------------------------- /cargo-crev/src/doc/compiling.md: -------------------------------------------------------------------------------- 1 | # Building from source 2 | 3 | ## Latest Rust required 4 | 5 | `cargo-crev` requires the latest stable version of Rust. If you have installed a 6 | Rust package from a slow-moving Linux distro, it's probably outdated and won't 7 | work. If you get compilation errors and warnings about unstable features, it 8 | means *your Rust version is too old*. Run: 9 | 10 | ``` bash 11 | rustup update 12 | rustup default stable 13 | ``` 14 | 15 | If you don't have `rustup`, uninstall any Rust or Cargo package you may have, 16 | and [install Rust via `rustup`](https://rustup.rs/). 17 | 18 | ## OpenSSL dependency 19 | 20 | Currently `cargo-crev` requires a non-Rust dependency to compile, as OpenSSL is 21 | required for TLS support. 22 | 23 | Though OpenSSL is popular and readily available, it's virtually impossible to 24 | cover installing it on all the available operating systems. We list some 25 | examples below. They should have matching commands and similar package names in 26 | the Unix-like OS of your choice. 27 | 28 | In case of problems, don't hesitate to ask for help. 29 | 30 | ### Debian and Ubuntu 31 | 32 | The following should work on Debian and Debian based distributions such as 33 | Ubuntu: 34 | 35 | ``` bash 36 | sudo apt-get install openssl libssl-dev 37 | ``` 38 | 39 | ### Arch Linux 40 | 41 | On Arch and Arch based distributions such as Manjaro make sure the latest 42 | OpenSSL is installed: 43 | 44 | ``` bash 45 | sudo pacman -Syu openssl 46 | ``` 47 | 48 | ### RedHat 49 | 50 | On RedHat and its derivates Fedora and CentOS the following should work: 51 | 52 | ``` bash 53 | sudo yum install openssl openssl-devel 54 | ``` 55 | 56 | ### SuSE 57 | 58 | On SuSE Linux the following should work: 59 | 60 | ``` bash 61 | sudo zypper install openssl libopenssl-devel 62 | ``` 63 | 64 | ## Compiling 65 | 66 | To compile and install latest `cargo-crev` release use `cargo`: 67 | 68 | ``` bash 69 | cargo install cargo-crev 70 | ``` 71 | 72 | In case you'd like to try latest features from the main branch, try: 73 | 74 | ``` bash 75 | cargo install --git https://github.com/crev-dev/cargo-crev/ cargo-crev 76 | ``` 77 | 78 | ### Compiling from a local git checkout 79 | 80 | ``` bash 81 | cargo build --release -p cargo-crev 82 | ``` 83 | 84 | It will build `target/release/cargo-crev` executable. 85 | 86 | ## Support 87 | 88 | If you have any trouble compiling, [ask in our gitter 89 | channel](https://gitter.im/dpc/crev) or use [pre-built 90 | binaries](https://github.com/crev-dev/cargo-crev/releases). 91 | -------------------------------------------------------------------------------- /cargo-crev/src/doc/glossary.md: -------------------------------------------------------------------------------- 1 | # Glossary 2 | 3 | The following is glossary of terms commonly used in `crev` and `cargo-crev` 4 | 5 | ### *Advisory* 6 | 7 | An optional part of a *Review*, announcing a significant problem fixed in that 8 | package version, advising other users to upgrade. 9 | 10 | Notably *Advisories* implicitly denote existence of such issue in all previous 11 | versions in a certain *VersionRange*. 12 | 13 | ### *CrevId* 14 | 15 | Default (and currently the only supported) identity type in `crev`. A 16 | self-generated identity used to sign *proofs*. 17 | 18 | Under the hood it's just a keypair, with the public key used as a public ID, and 19 | the secret key stored locally and encrypted with a passphrase. 20 | 21 | ### *Issue* 22 | 23 | An optional part of a *Review*, announcing a significant problem present in a 24 | given package version. 25 | 26 | Very similar to *Advisory*. It's generally better to prefer *Advisories*, 27 | except for cases in which the problem does not have a solution available yet. 28 | 29 | ### *Level* 30 | 31 | Level is commonly used in `crev` to qualify things. It can typically have one 32 | of 4 values: 33 | 34 | - high 35 | - medium 36 | - low 37 | - none 38 | 39 | ### *Proof* 40 | 41 | A YAML document describing attesting a certain fact. Currently supported *proof* 42 | types are: 43 | 44 | - *Trust Proof* - attesting that the author of the proof considers another 45 | identity as trustworthy 46 | - *Package Review Proof* - describing the results of code review of a specific 47 | package/library 48 | 49 | More proof types can be introduced in the future. 50 | 51 | ### *Proof Repository* 52 | 53 | A `git` repository used to store *Proofs*. A *Proof Repository* can be local, or 54 | public, just like `git` repositories are. 55 | 56 | ### *Review* 57 | 58 | Short for *Package Review Proof*. A document describing results of source code 59 | review of a particular package. 60 | 61 | ### *Trust Set* 62 | 63 | A result of traversing a *Web of Trust* from from a given root identity to 64 | calculate a set of all the other identities that are directly or transitiviely 65 | consider trustworthy. 66 | 67 | See [Trust documentation module](../trust/index.html) for introduction. 68 | 69 | ### *Web of Trust* (*WoT*) 70 | 71 | A graph composed of identities as nodes, and trust relations between them as 72 | edges. 73 | 74 | Build on the fly from all the available *Trust Proofs* and used to calculate a 75 | *Trust Set* for a given identity. 76 | 77 | See [Trust documentation module](../trust/index.html) for introduction. 78 | 79 | ### *Version Range* 80 | 81 | In `crev`, a method of simplified version range specification. 82 | 83 | Can take the following values: 84 | 85 | - `all` - all versions 86 | - `major` - major release (eg. 2.x.y) 87 | - `minor` - major release (eg. 2.1.x) 88 | 89 | An *Advisory* reported in a *Review* of version 1.2.3, would affect all versions 90 | between: 91 | 92 | - `0.0.0` and `1.2.3` if reported with `all` *Version Range* value, 93 | - `1.0.0` and `1.2.3` if reported with `major` *Version Range* value, 94 | - `1.2.0` and `1.2.3` if reported with `minor` *Version Range* value 95 | -------------------------------------------------------------------------------- /cargo-crev/src/doc/mod.rs: -------------------------------------------------------------------------------- 1 | /// # User documentation 2 | /// 3 | /// New users are advised to start by reading the [Getting Started Guide](`self::user::getting_started`) 4 | /// and [Glossary](`self::user::glossary`) modules. 5 | /// 6 | /// Please be aware that all user documentation is 7 | /// a continuous work in progress, and can be incorrect 8 | /// or stale. 9 | /// 10 | /// Writing high quality documentation is 11 | /// a lot of work. Please help us! If you spot any 12 | /// mistakes or ways to make improvements: 13 | /// 14 | /// 1. Open 15 | /// [user documentation source code directory](https://github.com/crev-dev/cargo-crev/tree/main/cargo-crev/src/doc), 16 | /// 2. Open the affected file, 17 | /// 3. Use *Edit this file* function, 18 | /// 4. Modify the text, 19 | /// 4. Click *Propose file change* button. 20 | /// 21 | /// See the list of modules for the list of documented topics. 22 | pub mod user { 23 | #[doc = include_str!("getting_started.md")] 24 | pub mod getting_started {} 25 | 26 | #[doc = include_str!("glossary.md")] 27 | pub mod glossary {} 28 | 29 | #[doc = include_str!("organizations.md")] 30 | pub mod organizations {} 31 | 32 | #[doc = include_str!("advisories.md")] 33 | pub mod advisories {} 34 | 35 | #[doc = include_str!("trust.md")] 36 | pub mod trust {} 37 | 38 | #[doc = include_str!("cargo_specific.md")] 39 | pub mod cargo_specific {} 40 | 41 | #[doc = include_str!("tips_and_tricks.md")] 42 | pub mod tips_and_tricks {} 43 | } 44 | -------------------------------------------------------------------------------- /cargo-crev/src/doc/organizations.md: -------------------------------------------------------------------------------- 1 | # Using in organizations and businesses 2 | 3 | `cargo-crev` is being developed with an explicit goal of supporting teams, 4 | groups, businesses and even very large organizations. It is our belief that 5 | companies developing software using Open Source code, have an important need to 6 | ensure integrity and trustworthiness of Open Source code they include. 7 | 8 | Because of that belief, we encourage groups interested in using `crev` to voice 9 | their opinions and needs, so we can address them as fast as possible. We're 10 | hopping that by making `cargo-crev` useful for them, we can quickly bootstrap a 11 | vibrant ecosystem of code review. 12 | 13 | This document describes the general vision of how to utilize `cargo-crev` in a 14 | software organization. A general knowledge of `cargo-crev` might be helpful, so 15 | make sure to read [Getting Started Guide](../getting_started/index.html) first. 16 | 17 | ## Overview 18 | 19 | Tho utilize `cargo-crev` in your organization follow the following steps: 20 | 21 | First, a Web of Trust for the organization has to be built. A WoT starts and is 22 | defined by a root identity, so this step means securely creating a root 23 | identity, and deciding on how to build and maintain over time its WoT. 24 | 25 | Then, a method of storing and circulating *proofs* generated by the members of 26 | the organization has to be established. This typically means preparing one or 27 | more git repositories. 28 | 29 | At this point members can start reviewing dependencies and creating review 30 | proofs. 31 | 32 | The final step is to enforce code review policies automatically - for example in 33 | the CI pipeline. 34 | 35 | ## Root identity 36 | 37 | It's important to take security into consideration when creating the root 38 | identity. Because of that it's recommended that it is created on a trusted 39 | computer, by a trusted member(s) of the organization (eg. a security officer). 40 | Care must be taken to properly store and secure the private key of this 41 | identity. 42 | 43 | The purpose of this ID would is to serve , and it is typically going to be used 44 | only passively. 45 | 46 | Root identity is usually used passively as a root of trust for the whole 47 | organization. It will only be actively used to sign new *trust proofs*: adding 48 | (or removing) other identities used inside the organization. 49 | 50 | The identities used by the individual contributors can be self-created, and 51 | managed by their respective owners. After creation they can be submitted for 52 | inclusion into the WoT of the organization. 53 | 54 | In case of a very large organization, a hierarchy of identities can be 55 | established with intermediate identities managed by particular groups and teams. 56 | This can help distribute the process of signing identities, and help keep the 57 | root identity offline and secure. 58 | 59 | ## Proof management 60 | 61 | A shared git repository can be used for all identities within the organization. 62 | Alternatively each individual can use their own git repository to publish their 63 | reviews. 64 | 65 | Depending on the code review policy, proofs created by individual contributors 66 | can be internal, or publicly available. Sharing code reviews with wider 67 | community can be a proof of a conscious approach to security, and a form of 68 | giving back to the community. 69 | 70 | ## Enforcing code review policy 71 | 72 | `cargo-crev` implements dependency trust verification from the perspective of an 73 | arbitrary identity. 74 | 75 | ``` text 76 | cargo crev crate verify --for-id 77 | ``` 78 | 79 | is the basic way to verify review status of all the packages from the 80 | perspective of the `rootID`. This can be used locally by every developer. It can 81 | also be set up as a part of a CI pipeline - the returned exit code will signal 82 | the verification status. 83 | 84 | Other configuration options can define the exact details required for passing 85 | the verification: number of reviews required, minimum thoroughness, etc. 86 | 87 | ## Help us help you 88 | 89 | We're very interested in improving `crev` project to be more suitable for 90 | organizations. Please create a github issue, or contact us on the gitter channel 91 | to discuss your needs. 92 | -------------------------------------------------------------------------------- /cargo-crev/src/doc/tips_and_tricks.md: -------------------------------------------------------------------------------- 1 | # `cargo-crev` tips & tricks 2 | 3 | ### Find the most heavy dependencies 4 | 5 | ``` text,ignore 6 | $ cargo crev crate verify --recursive 7 | status reviews downloads owner issues lines geiger flgs crate version latest_t 8 | pass 1 1 932599 973037 1 1 0/0 40 3 fuchsia-cprng 0.1.1 = 9 | pass 1 1 3299093 5112798 1 1 0/0 137 0 version_check 0.1.5 = 10 | 11 | (...) 12 | 13 | warn 0 0 1668529 2824521 42 20 2/2 414818 5240 CB tokio-udp 0.1.3 14 | warn 0 0 635827 3488232 44 19 3/3 439497 7088 CB tokio 0.1.22 15 | warn 0 3 308272 5924679 47 20 3/3 494108 8155 CB hyper 0.12.33 ↓0.10.2 16 | warn 0 0 849186 2003601 62 26 3/3 550963 17683 CB hyper-tls 0.3.2 17 | warn 0 0 141213 2088970 86 38 3/3 903083 20094 CB reqwest 0.9.20 18 | warn 0 1 825 6205 90 40 3/3 916613 20106 CB crates_io_api 0.5.1 ↓0.3.0 19 | warn 0 1 7417 355870 85 42 0/0 813073 18826 CB cargo 0.38.0 ↓0.20.0 20 | ``` 21 | 22 | Explanation: 23 | 24 | `--recursive` will make the dependency scanner track the stats recursively 25 | (including all the dependencies). 26 | 27 | As you can see `cargo` library brings 831 thousands lines of code with it, 18 28 | thousands of which are `unsafe`. This code has 85 total owners on crates.io, 29 | within 42 distinct groups of ownership. 30 | 31 | See `cargo crev crate verify --help` more details. This future is still under 32 | active development. 33 | 34 | ### Find the top contributors 35 | 36 | ``` text,ignore 37 | > cargo crev crate mvp 38 | 26 FYlr8YoYGVvDwHQxqEIs89reKKDy-oWisoO0qXXEfHE https://github.com/dpc/crev-proofs 39 | 19 X98FCpyv5I7z-xv4u-xMWLsFgb_Y0cG7p5xNFHSjbLA https://github.com/kornelski/crev-proofs 40 | 17 6OZqHXqyUAF57grEY7IVMjRljdd9dgDxiNtr1NF1BdY https://github.com/MaulingMonkey/crev-proofs 41 | 12 lr2ldir9XdBsKQkW3YGpRIO2pxhtSucdzf3M5ivfv4A https://git.sr.ht/~icefox/crev-proofs 42 | 11 Qf4cHJBEoho61fd5zoeweyrFCIZ7Pb5X5ggc5iw4B50 https://github.com/kornelski/crev-proofs 43 | 9 ZOm7om6WZyEf3SBmDC69BXs8sc1VPniYx7Nfz2Du6hM https://gitlab.com/KonradBorowski/crev-proofs 44 | 6 VylyTuk8CMGqIxgHixWaqfiUn3xZyzOA1wFrQ0sR1As https://github.com/BurntSushi/crev-proofs 45 | 3 ZGgmIacywCRKLa33k7W04VFcK-glDkcBXKG4oF7t--4 https://github.com/kpcyrd/crev-proofs 46 | 2 ZCBwWlOeJyU79adJqX9-irGH5wrmuYxUPXeSrFKuayg https://github.com/Lokathor/crev-proofs 47 | 1 aD4K0g6AcSKUDp3VPF7u4hM94zEkqjWeRQwmabLBcV0 https://github.com/Mark-Simulacrum/crev-proofs 48 | 1 FBkykBV6YaqAaGoUXyvd-XkEqDYxQNM7EUnZ2nuy-XQ https://github.com/Canop/crev-proofs 49 | 1 pt_he2sLPg2w2u4YN7lj-6Gvu25R8aN6ZCcuQFzxC1g https://gitlab.com/phgsng/crev-proofs 50 | 1 YWfa4SGgcW87fIT88uCkkrsRgIbWiGOOYmBbA1AtnKA https://github.com/oherrala/crev-proofs 51 | ``` 52 | 53 | Explanation: 54 | 55 | `cargo crev crate mvp` counts number of dependencies reviewed by each trusted 56 | ID. These are the people you doing most heavy work for you. 57 | 58 | You can add `--trust none` argument to discover people that did review some of 59 | your dependencies, yet you still don't have them in your WoT. 60 | 61 | ### Deal with too many dependencies displayed at once 62 | 63 | `cargo crev verify` can be given flags and arguments to narrow down the crate 64 | selection. 65 | 66 | ``` text,ignore 67 | $ cargo crev crate verify structopt 68 | status reviews downloads owner issues lines geiger flgs crate version latest_t 69 | pass 1 2 883719 6669131 1/1 0/0 141 10 atty 0.2.13 = 70 | pass 1 1 1400432 17086407 3/3 0/0 875 0 CB bitflags 1.1.0 = 71 | none 0 0 1593839 5174664 0/1 0/0 900 0 textwrap 0.11.0 72 | none 0 2 256588 1041610 0/1 0/0 2110 0 structopt 0.2.18 ↓0.2.16 73 | none 0 2 269250 1055834 0/1 0/0 930 0 structopt-derive 0.2.18 ↓0.2.16 74 | none 0 0 473513 6201912 3/6 0/0 419 0 unicode-width 0.1.6 75 | none 0 0 3718852 5903147 0/1 0/0 737 23 ansi_term 0.11.0 76 | none 0 0 1030341 13059608 1/1 0/0 904 0 quote 0.6.13 77 | none 0 0 1949014 9339336 2/2 0/0 3541 0 CB proc-macro2 0.4.30 78 | pass 1 1 1490421 2020940 1/1 0/0 308 0 heck 0.3.1 = 79 | pass 1 1 6226050 11896441 3/6 0/0 514 0 unicode-xid 0.1.0 = 80 | none 0 0 3532538 5637039 0/3 0/0 989 0 vec_map 0.8.1 81 | pass 1 1 1474739 7299608 0/1 0/0 677 0 strsim 0.8.0 = 82 | pass 1 1 2158613 2226697 1/1 0/0 13 0 CB winapi-x86_64-pc-windows-gnu 0.4.0 = 83 | none 0 0 2106302 2173081 1/1 0/0 13 0 CB winapi-i686-pc-windows-gnu 0.4.0 84 | none 0 1 1593422 6323276 1/1 0/0 11198 1 clap 2.33.0 ↓2.32.0 85 | none 0 0 955234 3870084 3/6 0/0 6062 0 unicode-segmentation 1.3.0 86 | none 0 0 620721 14604329 1/1 0/0 31985 35 CB syn 0.15.44 87 | pass 1 1 838269 18472077 4/4 0/0 58231 37 CB libc 0.2.62 = 88 | pass 1 2 1177413 10994523 1/1 0/0 160451 197 CB winapi 0.3.7 ↑0.3.8 89 | ``` 90 | 91 | An optional argument to verify only a given crate and its dependencies. 92 | 93 | ``` text,ignore 94 | > cargo crev crate verify structopt --skip-indirect 95 | status reviews downloads owner issues lines geiger flgs crate version latest_t 96 | none 0 2 256588 1041610 0/1 0/0 2110 0 structopt 0.2.18 ↓0.2.16 97 | none 0 2 269250 1055834 0/1 0/0 930 0 structopt-derive 0.2.18 ↓0.2.16 98 | none 0 1 1593422 6323276 1/1 0/0 11198 1 clap 2.33.0 ↓2.32.0 99 | ``` 100 | 101 | `--skip-indirect` displays only a direct dependencies. 102 | 103 | Check the `cargo crev crate verify --help` output for more helpful flags. 104 | 105 | ### Use `cargo crev` to recommend dependencies 106 | 107 | ``` text,ignore 108 | > cargo crev crate search logging 109 | 3 rand 110 | 2 log 111 | 1 env_logger 112 | 1 slog-term 113 | 1 directories 114 | 1 slog-async 115 | 1 ct-logs 116 | 1 wild 117 | 1 imagequant-sys 118 | 0 unicode-segmentation 119 | ``` 120 | 121 | `cargo crev crate search ` will query crates.io for crate matching a given 122 | query, and then sort them from the most trustworthy. 123 | 124 | This features is still new and is planed to be expanded and improved. 125 | -------------------------------------------------------------------------------- /cargo-crev/src/doc/trust.md: -------------------------------------------------------------------------------- 1 | # Trust and Web of Trust 2 | 3 | The goal of this document is to help users understand trust in `crev` (and 4 | `cargo-crev`). 5 | 6 | ## Web of Trust 7 | 8 | ### Trust proofs 9 | 10 | Any identity can generate and sign a *trust proof* to express direct trust in 11 | another identity. 12 | 13 | Example. 14 | 15 | ``` text 16 | -----BEGIN CREV TRUST ----- 17 | version: -1 18 | date: "2019-04-28T22:05:05.147481998-07:00" 19 | from: 20 | id-type: crev 21 | id: FYlr8YoYGVvDwHQxqEIs89reKKDy-oWisoO0qXXEfHE 22 | url: "https://github.com/dpc/crev-proofs" 23 | ids: 24 | - id-type: crev 25 | id: YWfa4SGgcW87fIT88uCkkrsRgIbWiGOOYmBbA1AtnKA 26 | url: "https://github.com/oherrala/crev-proofs" 27 | trust: low 28 | -----BEGIN CREV TRUST SIGNATURE----- 29 | 02BF0i1K0O7uR8T5UHzymqTo65P9R7JDuvfowZuHb3ubW8kd2-Fbl4jSv0n08ZdSU9P_E2YLWvEJrVQDYfjVCg 30 | -----END CREV TRUST----- 31 | ``` 32 | 33 | Identity `FYlr8YoYGVvDwHQxqEIs89reKKDy-oWisoO0qXXEfHE` trusts identity 34 | `YWfa4SGgcW87fIT88uCkkrsRgIbWiGOOYmBbA1AtnKA`. Notably, *trust proofs* include 35 | trust level information (`trust` field). 36 | 37 | `cargo-crev` builds WoT from from all the available *trust proofs*, and 38 | calculates a personal *trust set* from it. 39 | 40 | When calculating the *trust set*, `crev` recursively traverses the graph from 41 | the given root identity. This makes *trust* transitive. 42 | 43 | The root identity is typically the current identity of the user, but can be 44 | specified arbitrarily with the `--for-id` argument. 45 | 46 | ### *effective trust level* 47 | 48 | While traversing the graph `crev` keeps track of an *effective trust level* of 49 | each trusted identity. In simple terms: if R is the root identity, and R trust X 50 | with a low *trust level*, and X trusts Y with high *trust level*, R will have a 51 | low *effective trust level* for Y, because *effective trust level* for Y can't 52 | exceed the *effective trust level* in X. 53 | 54 | More precisely: *effective trust level* of R for Y is equal to: 55 | 56 | - maximum of: 57 | - direct *trust level* of R for Y (if available), or 58 | - for any already trusted identity Xi that also trusts Y, the maximum value of 59 | the lowest of: 60 | - direct trust level of Xi for Y 61 | - the *effective trust level* of R for Y 62 | 63 | Or in other words: for R to have a given *effective trust* for Y, there has to 64 | exist at least on path from R and Y, where every previous node directly trusts 65 | the next one at the level at least as high. 66 | 67 | That's because any *effective trust level* can only be as high as the highest 68 | *effective trust level* 69 | 70 | ### Depth of the WoT 71 | 72 | While traversing the trust graph to calculate the WoT, `cargo-crev` keeps track 73 | of the distance from the root ID. The exact details how far from it it will 74 | reach can be controlled by the following command line options: 75 | 76 | ``` text 77 | --depth 78 | --high-cost 79 | --medium-cost 80 | --low-cost 81 | ``` 82 | 83 | This allows flexible control over transitive trust. For example: 84 | 85 | ``` text 86 | --high-cost 1 --medium-cost 1 --low-cost 1 --depth 1 87 | ``` 88 | 89 | would effectively make `cargo-crev` use only directly trusted identities. 90 | 91 | ### Filtering reviews 92 | 93 | In addition to control over how the WoT is calculated, it is possible to filter 94 | package reviews used by other criteria. 95 | 96 | `--trust` options allows verification of packages only by reviews created by 97 | identities of a given trust level (or higher). 98 | 99 | The following options: 100 | 101 | ``` text 102 | --thoroughness 103 | --understanding 104 | ``` 105 | 106 | control filtering of the reviews by their qualities. 107 | 108 | Finally: 109 | 110 | ``` text 111 | --redundancy Number of reviews required [default: 1] 112 | ``` 113 | 114 | control how many trusted reviews is required to consider each package as 115 | trustworthy. 116 | -------------------------------------------------------------------------------- /cargo-crev/src/dyn_proof.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use crev_data::{ 3 | proof::{self, CommonOps, ContentExt}, 4 | PublicId, UnlockedId, 5 | }; 6 | 7 | pub fn parse_dyn_content(proof: &proof::Proof) -> Result> { 8 | Ok(match proof.kind() { 9 | proof::CodeReview::KIND => Box::new(proof.parse_content::()?), 10 | proof::PackageReview::KIND => Box::new(proof.parse_content::()?), 11 | proof::Trust::KIND => Box::new(proof.parse_content::()?), 12 | kind => bail!("Unsupported proof kind: {}", kind), 13 | }) 14 | } 15 | 16 | pub trait DynContent { 17 | fn set_date(&mut self, date: &proof::Date); 18 | fn set_author(&mut self, id: &PublicId); 19 | fn sign_by(&self, id: &UnlockedId) -> Result; 20 | } 21 | 22 | impl DynContent for proof::review::Code { 23 | fn set_date(&mut self, date: &proof::Date) { 24 | self.common.date = *date; 25 | } 26 | fn set_author(&mut self, id: &PublicId) { 27 | self.common.from = id.clone(); 28 | } 29 | fn sign_by(&self, id: &UnlockedId) -> Result { 30 | Ok(ContentExt::sign_by(self, id)?) 31 | } 32 | } 33 | impl DynContent for proof::review::Package { 34 | fn set_date(&mut self, date: &proof::Date) { 35 | self.common.date = *date; 36 | } 37 | fn set_author(&mut self, id: &PublicId) { 38 | self.common.from = id.clone(); 39 | } 40 | fn sign_by(&self, id: &UnlockedId) -> Result { 41 | Ok(ContentExt::sign_by(self, id)?) 42 | } 43 | } 44 | impl DynContent for proof::trust::Trust { 45 | fn set_date(&mut self, date: &proof::Date) { 46 | self.common.date = *date; 47 | } 48 | fn set_author(&mut self, id: &PublicId) { 49 | self.common.from = id.clone(); 50 | } 51 | fn sign_by(&self, id: &UnlockedId) -> Result { 52 | Ok(ContentExt::sign_by(self, id)?) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cargo-crev/src/edit.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use crev_common::{run_with_shell_cmd, CancelledError}; 3 | use crev_data::{proof, proof::content::ContentExt}; 4 | use crev_lib::{local::Local, util::get_documentation_for}; 5 | use std::{ 6 | env, ffi, 7 | fmt::Write, 8 | path::{Path, PathBuf}, 9 | }; 10 | 11 | fn get_git_default_editor() -> Result { 12 | let cfg = git2::Config::open_default()?; 13 | Ok(cfg.get_string("core.editor")?) 14 | } 15 | 16 | fn get_editor_to_use() -> Result { 17 | Ok(if let Some(v) = env::var_os("VISUAL") { 18 | v 19 | } else if let Some(v) = env::var_os("EDITOR") { 20 | v 21 | } else if let Ok(v) = get_git_default_editor() { 22 | v.into() 23 | } else { 24 | "vi".into() 25 | }) 26 | } 27 | 28 | /// Returns the edited string, and bool indicating if the file was ever written to/ (saved). 29 | fn edit_text_iteractively_raw(text: &str) -> Result<(String, bool)> { 30 | let dir = tempfile::tempdir()?; 31 | let file_path = dir.path().join("crev.review.yaml"); 32 | std::fs::write(&file_path, text)?; 33 | 34 | let starting_ts = std::fs::metadata(&file_path)? 35 | .modified() 36 | .unwrap_or_else(|_| std::time::SystemTime::now()); 37 | 38 | edit_file(&file_path)?; 39 | 40 | let modified_ts = std::fs::metadata(&file_path)? 41 | .modified() 42 | .unwrap_or_else(|_| std::time::SystemTime::now()); 43 | 44 | Ok(( 45 | std::fs::read_to_string(&file_path)?, 46 | starting_ts != modified_ts, 47 | )) 48 | } 49 | 50 | pub fn edit_text_iteractively(text: &str) -> Result { 51 | Ok(edit_text_iteractively_raw(text)?.0) 52 | } 53 | 54 | pub fn edit_text_iteractively_until_written_to(text: &str) -> Result { 55 | loop { 56 | let (text, modified) = edit_text_iteractively_raw(text)?; 57 | if !modified { 58 | eprintln!( 59 | "File not written to. Make sure to save it at least once to confirm the data." 60 | ); 61 | let reply = rprompt::prompt_reply_from_bufread( 62 | &mut std::io::stdin().lock(), 63 | &mut std::io::stderr(), 64 | "Commit anyway? (y/N/q) ", 65 | )?; 66 | 67 | match reply.as_str() { 68 | "y" | "Y" => return Ok(text), 69 | "q" | "Q" => return Err(CancelledError::ByUser.into()), 70 | "n" | "N" | "" | _ => continue, 71 | } 72 | } 73 | return Ok(text); 74 | } 75 | } 76 | 77 | pub fn edit_file(path: &Path) -> Result<()> { 78 | let editor = get_editor_to_use()?; 79 | 80 | let status = run_with_shell_cmd(&editor, Some(path))?; 81 | 82 | if !status.success() { 83 | bail!( 84 | "Can't launch editor {}: {}", 85 | editor.to_str().unwrap_or("?"), 86 | status 87 | ); 88 | } 89 | Ok(()) 90 | } 91 | 92 | pub fn edit_proof_content_iteractively( 93 | content: &C, 94 | previous_date: Option<&proof::Date>, 95 | base_version: Option<&crev_data::Version>, 96 | extra_leading_comment: Option<&str>, 97 | extra_follow_content_fn: impl FnOnce(&mut String) -> Result<()>, 98 | ) -> Result { 99 | let mut text = String::new(); 100 | if let Some(date) = previous_date { 101 | writeln!( 102 | &mut text, 103 | "# Overwriting existing proof created on {}", 104 | date.to_rfc3339() 105 | )?; 106 | } 107 | let draft = content.to_draft(); 108 | 109 | writeln!(&mut text, "# {}", draft.title())?; 110 | if let Some(extra_comment) = extra_leading_comment { 111 | writeln!(&mut text, "# {extra_comment}")?; 112 | } 113 | if let Some(base_version) = base_version { 114 | writeln!(&mut text, "# Diff base version: {base_version}")?; 115 | } 116 | text.write_str(draft.body())?; 117 | (extra_follow_content_fn)(&mut text)?; 118 | text.write_str("\n\n")?; 119 | for line in get_documentation_for(content).lines() { 120 | writeln!(&mut text, "# {line}")?; 121 | } 122 | loop { 123 | text = edit_text_iteractively_until_written_to(&text)?; 124 | match content.apply_draft(&text) { 125 | Err(e) => { 126 | eprintln!("There was an error parsing content: {e}"); 127 | crev_common::try_again_or_cancel()?; 128 | } 129 | Ok(content) => { 130 | if let Err(e) = content.ensure_serializes_to_valid_proof() { 131 | eprintln!("There was an error validating serialized proof: {e}"); 132 | crev_common::try_again_or_cancel()?; 133 | } else { 134 | return Ok(content); 135 | } 136 | } 137 | } 138 | } 139 | } 140 | 141 | /// interactively edit currnent user's yaml config file 142 | pub fn edit_user_config(local: &Local) -> Result<()> { 143 | let config = local.load_user_config()?; 144 | let mut text = serde_yaml::to_string(&config)?; 145 | loop { 146 | text = edit_text_iteractively(&text)?; 147 | match serde_yaml::from_str(&text) { 148 | Err(e) => { 149 | eprintln!("There was an error parsing content: {e}"); 150 | crev_common::try_again_or_cancel()?; 151 | } 152 | Ok(edited_config) => { 153 | return Ok(local.store_user_config(&edited_config)?); 154 | } 155 | } 156 | } 157 | } 158 | 159 | /// interactively edit readme file of the current user's proof repo 160 | pub fn edit_readme(local: &Local) -> Result<()> { 161 | edit_file(&local.get_proofs_dir_path()?.join("README.md"))?; 162 | local.proof_dir_git_add_path(&PathBuf::from("README.md"))?; 163 | Ok(()) 164 | } 165 | -------------------------------------------------------------------------------- /cargo-crev/src/info.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | deps::{ 3 | scan::{self, RequiredDetails}, 4 | AccumulativeCrateDetails, 5 | }, 6 | opts::{CrateSelector, CrateVerify, CrateVerifyCommon, WotOpts}, 7 | Repo, 8 | }; 9 | use anyhow::{bail, Result}; 10 | use crev_data::proof; 11 | use serde::{Deserialize, Serialize}; 12 | use std::{collections::HashSet, io}; 13 | 14 | #[derive(Clone, Debug, Serialize, Deserialize)] 15 | #[serde(rename_all = "kebab-case")] 16 | pub struct Details { 17 | pub verified: bool, 18 | pub loc: Option, 19 | pub geiger_count: Option, 20 | pub has_custom_build: bool, 21 | pub unmaintained: bool, 22 | } 23 | 24 | impl From for Details { 25 | fn from(details: AccumulativeCrateDetails) -> Self { 26 | Details { 27 | verified: details.verified, 28 | loc: details.loc, 29 | geiger_count: details.geiger_count, 30 | has_custom_build: details.has_custom_build, 31 | unmaintained: details.is_unmaintained, 32 | } 33 | } 34 | } 35 | 36 | #[derive(Clone, Debug, Serialize, Deserialize)] 37 | #[serde(rename_all = "kebab-case")] 38 | pub struct CrateInfoDepOutput { 39 | pub details: Details, 40 | pub recursive_details: Details, 41 | pub dependencies: Vec, 42 | pub rev_dependencies: Vec, 43 | } 44 | 45 | #[derive(Clone, Debug, Serialize, Deserialize)] 46 | #[serde(rename_all = "kebab-case")] 47 | pub struct CrateInfoOutput { 48 | pub package: proof::PackageVersionId, 49 | #[serde(flatten)] 50 | pub deps: Option, 51 | pub alternatives: HashSet, 52 | // pub flags: proof::Flags, 53 | } 54 | 55 | pub fn get_crate_deps_info( 56 | pkg_id: cargo::core::PackageId, 57 | common_opts: CrateVerifyCommon, 58 | required_details: &RequiredDetails, 59 | ) -> Result { 60 | let args = CrateVerify { 61 | common: common_opts, 62 | ..Default::default() 63 | }; 64 | let scanner = scan::Scanner::new(CrateSelector::default(), &args)?; 65 | let mut events = scanner.run(required_details); 66 | 67 | let stats = events 68 | .find(|stats| stats.info.id == pkg_id) 69 | .expect("result"); 70 | 71 | Ok(CrateInfoDepOutput { 72 | details: stats.details().accumulative_own.clone().into(), 73 | recursive_details: stats.details().accumulative_recursive.clone().into(), 74 | dependencies: stats.details().dependencies.clone(), 75 | rev_dependencies: stats.details().rev_dependencies.clone(), 76 | }) 77 | } 78 | 79 | pub fn get_crate_info( 80 | root_crate: CrateSelector, 81 | common_opts: CrateVerifyCommon, 82 | wot_opts: WotOpts, 83 | ) -> Result { 84 | if root_crate.name.is_none() { 85 | bail!("Crate selector required"); 86 | } 87 | 88 | let local = crev_lib::Local::auto_create_or_open()?; 89 | let db = local.load_db()?; 90 | let trust_set = local.trust_set_for_id( 91 | wot_opts.for_id.as_deref(), 92 | &wot_opts.trust_params.clone().into(), 93 | &db, 94 | )?; 95 | 96 | let repo = Repo::auto_open_cwd(common_opts.cargo_opts.clone())?; 97 | let pkg_id = repo.find_pkgid_by_crate_selector(&root_crate)?; 98 | let crev_pkg_id = crate::cargo_pkg_id_to_crev_pkg_id(&pkg_id); 99 | Ok(CrateInfoOutput { 100 | package: crev_pkg_id.clone(), 101 | deps: if root_crate.unrelated { 102 | None 103 | } else { 104 | Some(get_crate_deps_info( 105 | pkg_id, 106 | common_opts, 107 | &RequiredDetails::none(), 108 | )?) 109 | }, 110 | alternatives: db 111 | .get_pkg_alternatives(&crev_pkg_id.id) 112 | .iter() 113 | .filter(|(author, _)| trust_set.is_trusted(author)) 114 | .map(|(_, id)| id) 115 | .cloned() 116 | .collect(), 117 | // flags: db 118 | // .get_pkg_flags(&crev_pkg_id.id) 119 | // .filter(|(author, _)| trust_set.contains_trusted(author)) 120 | // .map(|(_, flags)| flags) 121 | // .fold(proof::Flags::default(), |acc, flags| acc + flags.clone()), 122 | }) 123 | } 124 | 125 | pub fn print_crate_info( 126 | root_crate: CrateSelector, 127 | args: CrateVerifyCommon, 128 | wot_opts: WotOpts, 129 | ) -> Result<()> { 130 | let info = get_crate_info(root_crate, args, wot_opts)?; 131 | serde_yaml::to_writer(io::stdout(), &info)?; 132 | println!(); 133 | 134 | Ok(()) 135 | } 136 | -------------------------------------------------------------------------------- /cargo-crev/src/known_cargo_owners_defaults.txt: -------------------------------------------------------------------------------- 1 | # This file helps identify dependencies owned by known 2 | # and reputable authors. 3 | # 4 | # List usernames of crates.io users line by line to 5 | # make them count as "known owners" in `crev verify` 6 | # output. 7 | # 8 | # Remember that `crev` has no way to verify information 9 | # returned by crates.io, or if the crate was in fact 10 | # published by their real owners. This feature is only 11 | # meant to help identify crates that should be reviewed 12 | # and verified first, and might be removed in the future. 13 | 14 | aaronepower 15 | alexcrichton 16 | amanieu 17 | bluss 18 | brendanzab 19 | burntsushi 20 | BurntSushi 21 | bvssvni 22 | byron 23 | carllerche 24 | cramertj 25 | ctz 26 | cuviper 27 | dhardy 28 | dtolnay 29 | erickt 30 | fitzgen 31 | gankro 32 | Geal 33 | gnzlbg 34 | guillaumegomez 35 | hauleth 36 | huonw 37 | ivanukhov 38 | jackpot51 39 | japaric 40 | kbknapp 41 | killercup 42 | kimundi 43 | kwantam 44 | lifthrasiir 45 | matthewkmayer 46 | matthiasbeyer 47 | mitsuhiko 48 | mvdnes 49 | newpavlov 50 | nikomatsakis 51 | nox 52 | pitdicker 53 | pnkfelix 54 | reem 55 | retep998 56 | seanmonstar 57 | sergiobenitez 58 | sfackler 59 | simonsapin 60 | softprops 61 | stebalien 62 | steveklabnik 63 | stjepang 64 | strake 65 | tailhook 66 | tomaka 67 | tyoverby 68 | vks 69 | withoutboats 70 | 71 | github:rust-lang:libs 72 | github:rust-lang-nursery:log-owners 73 | github:rust-random:maintainers 74 | github:rust-lang-nursery:libs 75 | -------------------------------------------------------------------------------- /cargo-crev/src/lib.rs: -------------------------------------------------------------------------------- 1 | // this is a dummy library just to make docs.rs render what I need 2 | #![allow(clippy::module_name_repetitions)] 3 | #![allow(clippy::redundant_closure_for_method_calls)] 4 | #![cfg_attr( 5 | feature = "documentation", 6 | doc = "See the [user documentation module](./doc/user/index.html) and in particular the [Getting Started Guide](./doc/user/getting_started/index.html)." 7 | )] 8 | 9 | #[cfg(feature = "documentation")] 10 | /// Documentation 11 | pub mod doc; 12 | -------------------------------------------------------------------------------- /cargo-crev/src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use anyhow::{bail, format_err, Result}; 2 | pub use crev_data::Version; 3 | -------------------------------------------------------------------------------- /cargo-crev/src/term.rs: -------------------------------------------------------------------------------- 1 | use crev_lib::VerificationStatus; 2 | use std::{ 3 | env, 4 | fmt::Arguments, 5 | io::{self, Write}, 6 | }; 7 | use term::{ 8 | self, 9 | color::{self, Color}, 10 | StderrTerminal, StdoutTerminal, 11 | }; 12 | 13 | pub fn verification_status_color(s: VerificationStatus) -> Option { 14 | use VerificationStatus::*; 15 | match s { 16 | Verified | Local => Some(term::color::GREEN), 17 | Insufficient => None, 18 | Negative => Some(term::color::YELLOW), 19 | } 20 | } 21 | 22 | pub fn known_owners_count_color(count: u64) -> Option { 23 | if count > 0 { 24 | Some(color::GREEN) 25 | } else { 26 | None 27 | } 28 | } 29 | 30 | /// Helper to control (possibly) colored output 31 | pub struct Term { 32 | pub stdout_is_tty: bool, 33 | stderr_is_tty: bool, 34 | stdin_is_tty: bool, 35 | stdout: Option>, 36 | #[allow(unused)] 37 | stderr: Option>, 38 | } 39 | 40 | fn output_to( 41 | args: std::fmt::Arguments<'_>, 42 | color: Option, 43 | term: &mut dyn term::Terminal, 44 | is_tty: bool, 45 | ) -> io::Result<()> 46 | where 47 | O: Write, 48 | { 49 | let use_color = is_tty && term.supports_color(); 50 | if use_color { 51 | if let Some(color) = color { 52 | term.fg(color)?; 53 | } 54 | } 55 | term.get_mut().write_fmt(args)?; 56 | 57 | if use_color && color.is_some() { 58 | term.reset()?; 59 | } 60 | 61 | Ok(()) 62 | } 63 | 64 | impl Term { 65 | pub fn new() -> Term { 66 | Term { 67 | stdout: term::stdout(), 68 | stderr: term::stderr(), 69 | stdin_is_tty: atty::is(atty::Stream::Stdin), 70 | stdout_is_tty: atty::is(atty::Stream::Stdout), 71 | stderr_is_tty: atty::is(atty::Stream::Stderr), 72 | } 73 | } 74 | 75 | pub fn print(&mut self, fmt: Arguments<'_>, color: C) -> io::Result<()> 76 | where 77 | C: Into>, 78 | { 79 | let color = color.into(); 80 | 81 | if let Some(ref mut term) = self.stdout { 82 | output_to( 83 | fmt, 84 | color, 85 | (&mut **term) as &mut dyn term::Terminal, 86 | self.stdout_is_tty, 87 | )?; 88 | } 89 | Ok(()) 90 | } 91 | 92 | pub fn eprint(&mut self, fmt: Arguments<'_>, color: C) -> io::Result<()> 93 | where 94 | C: Into>, 95 | { 96 | let color = color.into(); 97 | 98 | if let Some(ref mut term) = self.stderr { 99 | output_to( 100 | fmt, 101 | color, 102 | (&mut **term) as &mut dyn term::Terminal, 103 | self.stdout_is_tty, 104 | )?; 105 | } 106 | Ok(()) 107 | } 108 | 109 | pub fn eprintln(&mut self, fmt: Arguments<'_>, color: C) -> io::Result<()> 110 | where 111 | C: Into>, 112 | { 113 | let color = color.into(); 114 | self.print(fmt, color)?; 115 | self.print(format_args!("\n"), color)?; 116 | 117 | Ok(()) 118 | } 119 | 120 | pub(crate) fn is_interactive(&self) -> bool { 121 | self.stderr_is_tty && self.stdout_is_tty 122 | } 123 | 124 | pub(crate) fn is_input_interactive(&self) -> bool { 125 | self.stdin_is_tty 126 | } 127 | } 128 | 129 | pub fn read_passphrase() -> io::Result { 130 | if let Ok(pass) = env::var("CREV_PASSPHRASE") { 131 | eprintln!("Using passphrase set in CREV_PASSPHRASE"); 132 | return Ok(pass); 133 | } else if let Some(cmd) = env::var_os("CREV_PASSPHRASE_CMD") { 134 | return Ok( 135 | String::from_utf8_lossy(&crev_common::run_with_shell_cmd_capture_stdout(&cmd, None)?) 136 | .trim() 137 | .to_owned(), 138 | ); 139 | } 140 | eprint!("Enter passphrase to unlock: "); 141 | rpassword::read_password() 142 | } 143 | 144 | pub fn read_new_passphrase() -> io::Result { 145 | if let Ok(pass) = env::var("CREV_PASSPHRASE") { 146 | eprintln!("Using passphrase set in CREV_PASSPHRASE"); 147 | return Ok(pass); 148 | } 149 | loop { 150 | eprint!("Enter new passphrase: "); 151 | let p1 = rpassword::read_password()?; 152 | eprint!("Enter new passphrase again: "); 153 | let p2 = rpassword::read_password()?; 154 | if p1 == p2 { 155 | return Ok(p1); 156 | } 157 | eprintln!("\nPassphrases don't match, try again."); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /cargo-crev/src/tokei.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use std::path::Path; 3 | use tokei::{Config, LanguageType, Languages}; 4 | 5 | pub fn get_rust_line_count(path: &Path) -> Result { 6 | let excluded = &["tests/", "examples/"]; 7 | let config = Config { 8 | treat_doc_strings_as_comments: Some(true), 9 | no_ignore_vcs: Some(true), 10 | hidden: Some(true), 11 | ..Default::default() 12 | }; 13 | let mut languages = Languages::new(); 14 | languages.get_statistics(&[path], excluded, &config); 15 | let rust = languages 16 | .get(&LanguageType::Rust) 17 | .ok_or_else(|| format_err!("Rust should work"))?; 18 | Ok(rust.code) 19 | } 20 | -------------------------------------------------------------------------------- /cargo-crev/src/wot.rs: -------------------------------------------------------------------------------- 1 | use std::{io, io::Write as _}; 2 | 3 | use crate::{opts::WotOpts, term, url_to_status_str}; 4 | use ::term::color::{BLUE, GREEN, RED, YELLOW}; 5 | use anyhow::Result; 6 | use crev_wot::trust_set::TraverseLogItem::{Edge, Node}; 7 | use itertools::Itertools; 8 | 9 | pub fn print_log(wot_opts: WotOpts) -> Result<()> { 10 | let mut term = term::Term::new(); 11 | let local = crev_lib::Local::auto_create_or_open()?; 12 | let db = local.load_db()?; 13 | let trust_set = local.trust_set_for_id( 14 | wot_opts.for_id.as_deref(), 15 | &wot_opts.trust_params.clone().into(), 16 | &db, 17 | )?; 18 | 19 | if term.is_interactive() { 20 | writeln!( 21 | io::stderr(), 22 | "{:^43} {:>6} {:>4}", 23 | "TRUST-FROM-ID", 24 | "TRUST", 25 | "DIST" 26 | )?; 27 | writeln!(io::stderr(), "\\_ status URL",)?; 28 | writeln!( 29 | io::stderr(), 30 | " {:^43} {:>6} {:>6} +{:<3} notes", 31 | "TRUST-TO-ID", 32 | "D.TRST", 33 | "E.TRST", 34 | "DIS" 35 | )?; 36 | writeln!(io::stderr(), " \\_ status URL",)?; 37 | } 38 | for log_item in trust_set.traverse_log { 39 | match log_item { 40 | Node(node) => { 41 | let (status, url) = url_to_status_str(&db.lookup_url(&node.id)); 42 | 43 | term.print(format_args!("{}", &node.id), GREEN)?; 44 | 45 | writeln!( 46 | io::stdout(), 47 | " {:>6} {:>4}", 48 | node.effective_trust, 49 | node.total_distance, 50 | )?; 51 | writeln!(io::stdout(), "\\_ {status} {url}")?; 52 | } 53 | Edge(edge) => { 54 | let (status, url) = url_to_status_str(&db.lookup_url(&edge.to)); 55 | 56 | write!(io::stdout(), " ")?; 57 | 58 | term.print(format_args!("{}", &edge.to), BLUE)?; 59 | 60 | write!( 61 | io::stdout(), 62 | " {:>6} {:>6} {:>4} ", 63 | edge.direct_trust, 64 | edge.effective_trust, 65 | edge.relative_distance 66 | .map_or_else(|| "inf".into(), |d| format!("+{d}")), 67 | )?; 68 | if edge.no_change { 69 | term.print(format_args!("no change"), YELLOW)?; 70 | } else { 71 | term.print(format_args!("queued"), GREEN)?; 72 | } 73 | if edge.ignored_distrusted { 74 | write!(io::stdout(), "; ")?; 75 | term.print(format_args!("distrusted"), RED)?; 76 | } 77 | if edge.ignored_overridden { 78 | write!(io::stdout(), "; ")?; 79 | term.print(format_args!("overridden"), YELLOW)?; 80 | write!(io::stdout(), " by {}", edge.overridden_by.iter().join(", "))?; 81 | } 82 | if edge.ignored_too_far { 83 | write!(io::stdout(), "; ")?; 84 | term.print(format_args!("too far"), YELLOW)?; 85 | } 86 | if edge.ignored_trust_too_low { 87 | write!(io::stdout(), "; ")?; 88 | term.print(format_args!("trust too low"), YELLOW)?; 89 | } 90 | 91 | writeln!(io::stdout())?; 92 | 93 | writeln!(io::stdout(), " \\_ {status} {url}")?; 94 | } 95 | } 96 | } 97 | 98 | Ok(()) 99 | } 100 | -------------------------------------------------------------------------------- /cargo-crev/tests/clitest.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::*, io::Write, path::*, process::*}; 2 | 3 | #[test] 4 | #[ignore] 5 | // TODO: rewrite to be a standalone binary 6 | fn creates_new_id_implicitly() { 7 | let c = Cli::new(); 8 | let empty_id = c.run(&["id", "query", "own"], ""); 9 | assert!(!empty_id.status.success(), "{empty_id:?}"); 10 | let trust = c.run( 11 | &[ 12 | "id", 13 | "trust", 14 | "--level=medium", 15 | "FYlr8YoYGVvDwHQxqEIs89reKKDy-oWisoO0qXXEfHE", 16 | ], 17 | "", 18 | ); 19 | assert!(trust.status.success(), "{trust:?}"); 20 | assert!(c.run(&["id", "query", "own"], "").status.success()); 21 | } 22 | 23 | struct Cli { 24 | home: tempfile::TempDir, 25 | exe: PathBuf, 26 | } 27 | 28 | impl Cli { 29 | pub fn new() -> Self { 30 | Self { 31 | exe: PathBuf::from(env!("CARGO_BIN_EXE_cargo-crev")), 32 | home: tempfile::tempdir().unwrap(), 33 | } 34 | } 35 | 36 | #[track_caller] 37 | pub fn run(&self, args: &[impl AsRef], stdin_data: impl Into) -> Output { 38 | let mut child = Command::new(&self.exe) 39 | .env("CARGO_CREV_ROOT_DIR_OVERRIDE", self.home.path()) 40 | .env("EDITOR", "cat") 41 | .env("VISUAL", "cat") 42 | .stdin(Stdio::piped()) 43 | .stdout(Stdio::piped()) 44 | .stderr(Stdio::piped()) 45 | .arg("crev") 46 | .args(args) 47 | .spawn() 48 | .unwrap_or_else(|_| panic!("Failed to run {}", self.exe.display())); 49 | 50 | let stdin_data = stdin_data.into(); 51 | let mut stdin = child.stdin.take().unwrap(); 52 | std::thread::spawn(move || { 53 | stdin.write_all(stdin_data.as_bytes()).unwrap(); 54 | }); 55 | child.wait_with_output().expect("child process lost") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ci/build-deb: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | D="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 5 | 6 | # This script builds a binary dpkg for Debian based distros. It does not 7 | # currently run in CI, and is instead run manually and the resulting dpkg is 8 | # uploaded to GitHub via the web UI. 9 | # 10 | # Note that this requires 'cargo deb', which can be installed with 11 | # 'cargo install cargo-deb'. 12 | # 13 | # This should be run from the root of the cargo-crev repo. 14 | 15 | if ! command -V cargo-deb > /dev/null 2>&1; then 16 | echo "cargo-deb command missing" >&2 17 | exit 1 18 | fi 19 | 20 | if ! command -V asciidoctor > /dev/null 2>&1; then 21 | echo "asciidoctor command missing" >&2 22 | exit 1 23 | fi 24 | 25 | # 'cargo deb' does not seem to provide a way to specify an asset that is 26 | # created at build time, such as cargo-crev's man page. To work around this, 27 | # we force a debug build, copy out the man page (and shell completions) 28 | # produced from that build, put it into a predictable location and then build 29 | # the deb, which knows where to look. 30 | cargo build 31 | 32 | DEPLOY_DIR=deployment/deb 33 | OUT_DIR="$("$D"/cargo-out-dir target/debug/)" 34 | mkdir -p "$DEPLOY_DIR" 35 | 36 | # Copy man page and shell completions. 37 | cp "$OUT_DIR"/{cargo-crev.1,cargo-crev.bash,cargo-crev.fish} "$DEPLOY_DIR/" 38 | cp complete/_cargo-crev "$DEPLOY_DIR/" 39 | 40 | # Since we're distributing the dpkg, we don't know whether the user will have 41 | # PCRE2 installed, so just do a static build. 42 | PCRE2_SYS_STATIC=1 cargo deb --target x86_64-unknown-linux-musl 43 | -------------------------------------------------------------------------------- /ci/cargo-out-dir: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Finds Cargo's `OUT_DIR` directory from the most recent build. 4 | # 5 | # This requires one parameter corresponding to the target directory 6 | # to search for the build output. 7 | 8 | if [ $# != 1 ]; then 9 | echo "Usage: $(basename "$0") " >&2 10 | exit 2 11 | fi 12 | 13 | # This works by finding the most recent stamp file, which is produced by 14 | # every cargo-crev build. 15 | target_dir="$1" 16 | find "$target_dir" -name cargo-crev-stamp -print0 \ 17 | | xargs -0 ls -t \ 18 | | head -n1 \ 19 | | xargs dirname 20 | -------------------------------------------------------------------------------- /ci/docker/README.md: -------------------------------------------------------------------------------- 1 | These are Docker images used for cross compilation in CI builds (or locally) 2 | via the [Cross](https://github.com/rust-embedded/cross) tool. 3 | 4 | The Cross tool actually provides its own Docker images, and all Docker images 5 | in this directory are derived from one of them. We provide our own in order 6 | to customize the environment. For example, we need to install some things like 7 | `asciidoctor` in order to generate man pages. We also install compression tools 8 | like `xz` so that tests for the `-z/--search-zip` flag are run. 9 | 10 | If you make a change to a Docker image, then you can re-build it. `cd` into the 11 | directory containing the `Dockerfile` and run: 12 | 13 | $ cd x86_64-unknown-linux-musl 14 | $ ./build 15 | 16 | At this point, subsequent uses of `cross` will now use your built image since 17 | Docker prefers local images over remote images. In order to make these changes 18 | stick, they need to be pushed to Docker Hub: 19 | 20 | $ docker push burntsushi/cross:x86_64-unknown-linux-musl 21 | 22 | Of course, only I (BurntSushi) can push to that location. To make `cross` use 23 | a different location, then edit `Cross.toml` in the root of this repo to use 24 | a different image name for the desired target. 25 | -------------------------------------------------------------------------------- /ci/docker/arm-unknown-linux-gnueabihf/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rustembedded/cross:arm-unknown-linux-gnueabihf 2 | 3 | COPY stage/ubuntu-install-packages / 4 | RUN /ubuntu-install-packages 5 | -------------------------------------------------------------------------------- /ci/docker/arm-unknown-linux-gnueabihf/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p stage 4 | cp ../../ubuntu-install-packages ./stage/ 5 | docker build -t burntsushi/cross:arm-unknown-linux-gnueabihf . 6 | -------------------------------------------------------------------------------- /ci/docker/i686-unknown-linux-gnu/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rustembedded/cross:i686-unknown-linux-gnu 2 | 3 | COPY stage/ubuntu-install-packages / 4 | RUN /ubuntu-install-packages 5 | -------------------------------------------------------------------------------- /ci/docker/i686-unknown-linux-gnu/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p stage 4 | cp ../../ubuntu-install-packages ./stage/ 5 | docker build -t burntsushi/cross:i686-unknown-linux-gnu . 6 | -------------------------------------------------------------------------------- /ci/docker/mips64-unknown-linux-gnuabi64/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rustembedded/cross:mips64-unknown-linux-gnuabi64 2 | 3 | COPY stage/ubuntu-install-packages / 4 | RUN /ubuntu-install-packages 5 | -------------------------------------------------------------------------------- /ci/docker/mips64-unknown-linux-gnuabi64/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p stage 4 | cp ../../ubuntu-install-packages ./stage/ 5 | docker build -t burntsushi/cross:mips64-unknown-linux-gnuabi64 . 6 | -------------------------------------------------------------------------------- /ci/docker/x86_64-unknown-linux-musl/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rustembedded/cross:x86_64-unknown-linux-musl 2 | 3 | COPY stage/ubuntu-install-packages / 4 | RUN /ubuntu-install-packages 5 | -------------------------------------------------------------------------------- /ci/docker/x86_64-unknown-linux-musl/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p stage 4 | cp ../../ubuntu-install-packages ./stage/ 5 | docker build -t burntsushi/cross:x86_64-unknown-linux-musl . 6 | -------------------------------------------------------------------------------- /ci/macos-install-packages: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | brew install asciidoctor 4 | -------------------------------------------------------------------------------- /ci/prep_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | export CRATE_NAME=cargo-crev 6 | name="$CRATE_NAME-$TRAVIS_TAG-$TARGET" 7 | mkdir -p "$name" 8 | cp "target/$TARGET/release/$CRATE_NAME" "$name/" 9 | cp cargo-crev/README.md LICENSE* "$name/" 10 | tar czvf "$name.tar.gz" "$name" 11 | 12 | # Get the sha-256 checksum w/o filename and newline 13 | echo -n "$(shasum -ba 256 "$name.tar.gz" | cut -d " " -f 1)" > "$name.tar.gz.sha256" 14 | 15 | -------------------------------------------------------------------------------- /ci/sha256-releases: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ $# != 1 ]; then 6 | echo "Usage: $(basename $0) version" >&2 7 | exit 1 8 | fi 9 | version="$1" 10 | 11 | # Linux and Darwin builds. 12 | for arch in i686 x86_64; do 13 | for target in apple-darwin unknown-linux-musl; do 14 | url="https://github.com/crev-dev/cargo-crev/releases/download/$version/cargo-crev-$version-$arch-$target.tar.gz" 15 | sha=$(curl -sfSL "$url" | sha256sum) 16 | echo "$version-$arch-$target $sha" 17 | done 18 | done 19 | 20 | # Source. 21 | for ext in zip tar.gz; do 22 | url="https://github.com/crev-dev/cargo-crev/archive/$version.$ext" 23 | sha=$(curl -sfSL "$url" | sha256sum) 24 | echo "source.$ext $sha" 25 | done 26 | -------------------------------------------------------------------------------- /ci/ubuntu-install-packages: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sudo apt-get update 4 | sudo apt-get install -y --no-install-recommends \ 5 | asciidoctor \ 6 | zsh xz-utils liblz4-tool musl-tools 7 | -------------------------------------------------------------------------------- /ci/utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Various utility functions used through CI. 4 | 5 | # Finds Cargo's `OUT_DIR` directory from the most recent build. 6 | # 7 | # This requires one parameter corresponding to the target directory 8 | # to search for the build output. 9 | cargo_out_dir() { 10 | # This works by finding the most recent stamp file, which is produced by 11 | # every cargo-crev build. 12 | target_dir="$1" 13 | find "$target_dir" -name cargo-crev-stamp -print0 \ 14 | | xargs -0 ls -t \ 15 | | head -n1 \ 16 | | xargs dirname 17 | } 18 | 19 | host() { 20 | case "$TRAVIS_OS_NAME" in 21 | linux) 22 | echo x86_64-unknown-linux-gnu 23 | ;; 24 | osx) 25 | echo x86_64-apple-darwin 26 | ;; 27 | esac 28 | } 29 | 30 | architecture() { 31 | case "$TARGET" in 32 | x86_64-*) 33 | echo amd64 34 | ;; 35 | i686-*|i586-*|i386-*) 36 | echo i386 37 | ;; 38 | arm*-unknown-linux-gnueabihf) 39 | echo armhf 40 | ;; 41 | *) 42 | die "architecture: unexpected target $TARGET" 43 | ;; 44 | esac 45 | } 46 | 47 | gcc_prefix() { 48 | case "$(architecture)" in 49 | armhf) 50 | echo arm-linux-gnueabihf- 51 | ;; 52 | *) 53 | return 54 | ;; 55 | esac 56 | } 57 | 58 | is_musl() { 59 | case "$TARGET" in 60 | *-musl) return 0 ;; 61 | *) return 1 ;; 62 | esac 63 | } 64 | 65 | is_x86() { 66 | case "$(architecture)" in 67 | amd64|i386) return 0 ;; 68 | *) return 1 ;; 69 | esac 70 | } 71 | 72 | is_x86_64() { 73 | case "$(architecture)" in 74 | amd64) return 0 ;; 75 | *) return 1 ;; 76 | esac 77 | } 78 | 79 | is_arm() { 80 | case "$(architecture)" in 81 | armhf) return 0 ;; 82 | *) return 1 ;; 83 | esac 84 | } 85 | 86 | is_linux() { 87 | case "$TRAVIS_OS_NAME" in 88 | linux) return 0 ;; 89 | *) return 1 ;; 90 | esac 91 | } 92 | 93 | is_osx() { 94 | case "$TRAVIS_OS_NAME" in 95 | osx) return 0 ;; 96 | *) return 1 ;; 97 | esac 98 | } 99 | 100 | builder() { 101 | if is_musl && is_x86_64; then 102 | cargo install cross 103 | echo "cross" 104 | else 105 | echo "cargo" 106 | fi 107 | } 108 | -------------------------------------------------------------------------------- /crev-common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crev-common" 3 | description = "Scalable, social, Code REView system that we desperately need - common code" 4 | documentation = "https://docs.rs/crev-lib" 5 | keywords = ["code", "review", "trust", "distributed"] 6 | readme = "README.md" 7 | include = ["src/**", "Cargo.toml", "README.md", "LICENSE-MIT"] 8 | authors.workspace = true 9 | edition.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | rust-version.workspace = true 13 | version.workspace = true 14 | 15 | [dependencies] 16 | base64 = "0.22" 17 | blake2.workspace = true 18 | chrono.workspace = true 19 | hex = "0.4.3" 20 | rprompt = "2.0.2" 21 | serde.workspace = true 22 | serde_yaml.workspace = true 23 | shell-escape = "0.1.5" 24 | thiserror.workspace = true 25 | rand = "0.8.5" 26 | 27 | [package.metadata.release] 28 | shared-version = true 29 | -------------------------------------------------------------------------------- /crev-common/README.md: -------------------------------------------------------------------------------- 1 | # `crev-common` 2 | 3 | Common utility functionality for other `crev` libraries. 4 | -------------------------------------------------------------------------------- /crev-common/rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | # imports_granularity="Crate" 3 | -------------------------------------------------------------------------------- /crev-common/src/blake2b256.rs: -------------------------------------------------------------------------------- 1 | use blake2::digest::consts::U32; 2 | use blake2::Blake2b; 3 | 4 | pub type Blake2b256 = Blake2b; 5 | -------------------------------------------------------------------------------- /crev-common/src/fs.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | /// Move dir content from `from` dir to `to` dir 7 | pub fn move_dir_content(from: &Path, to: &Path) -> std::io::Result<()> { 8 | fs::create_dir_all(to)?; 9 | 10 | for entry in fs::read_dir(from)? { 11 | let entry = entry?; 12 | let path = entry.path(); 13 | let path = path 14 | .strip_prefix(from) 15 | .expect("Strip prefix should have worked"); 16 | fs::rename(from.join(path), to.join(path))?; 17 | } 18 | 19 | Ok(()) 20 | } 21 | 22 | #[must_use] 23 | pub fn append_to_path(path: PathBuf, ext: &str) -> PathBuf { 24 | let mut path = path.into_os_string(); 25 | path.push(ext); 26 | path.into() 27 | } 28 | -------------------------------------------------------------------------------- /crev-common/src/rand.rs: -------------------------------------------------------------------------------- 1 | use rand::{self, Rng}; 2 | 3 | #[must_use] 4 | pub fn random_vec(len: usize) -> Vec { 5 | rand::thread_rng() 6 | .sample_iter(&rand::distributions::Standard) 7 | .take(len) 8 | .collect() 9 | } 10 | -------------------------------------------------------------------------------- /crev-common/src/serde.rs: -------------------------------------------------------------------------------- 1 | // Oh dear, this module is called serde, and is in the root 2 | // so we have to import serde crate here 3 | use serde; 4 | 5 | use self::serde::Deserialize; 6 | use chrono::{self, offset::FixedOffset, prelude::*}; 7 | use hex::{self, FromHex, FromHexError}; 8 | use std::{fmt, io}; 9 | 10 | // {{{ Serde serialization 11 | pub trait MyTryFromBytes: Sized { 12 | type Err: 'static + Sized + ::std::error::Error; 13 | fn try_from(_: &[u8]) -> Result; 14 | } 15 | 16 | pub fn from_base64<'d, T, D>(deserializer: D) -> Result 17 | where 18 | D: serde::Deserializer<'d>, 19 | T: MyTryFromBytes, 20 | { 21 | use self::serde::de::Error; 22 | String::deserialize(deserializer) 23 | .and_then(|string| { 24 | crate::base64_decode(&string).map_err(|err| Error::custom(err.to_string())) 25 | }) 26 | .and_then(|ref bytes| { 27 | ::try_from(bytes) 28 | .map_err(|err| Error::custom(format!("{}", &err as &dyn (::std::error::Error)))) 29 | }) 30 | } 31 | 32 | pub fn as_base64(key: &T, serializer: S) -> Result 33 | where 34 | T: AsRef<[u8]>, 35 | S: serde::Serializer, 36 | { 37 | serializer.serialize_str(&crate::base64_encode(key.as_ref())) 38 | } 39 | 40 | pub fn from_hex<'d, T, D>(deserializer: D) -> Result 41 | where 42 | D: serde::Deserializer<'d>, 43 | T: MyTryFromBytes, 44 | { 45 | use self::serde::de::Error; 46 | String::deserialize(deserializer) 47 | .and_then(|string| { 48 | FromHex::from_hex(string.as_str()) 49 | .map_err(|err: FromHexError| Error::custom(err.to_string())) 50 | }) 51 | .and_then(|bytes: Vec| { 52 | ::try_from(&bytes) 53 | .map_err(|err| Error::custom(format!("{}", &err as &dyn (::std::error::Error)))) 54 | }) 55 | } 56 | 57 | pub fn as_hex(key: &T, serializer: S) -> Result 58 | where 59 | T: AsRef<[u8]>, 60 | S: serde::Serializer, 61 | { 62 | serializer.serialize_str(&hex::encode(key)) 63 | } 64 | 65 | pub fn from_rfc3339_fixed<'d, D>(deserializer: D) -> Result, D::Error> 66 | where 67 | D: serde::Deserializer<'d>, 68 | { 69 | use self::serde::de::Error; 70 | String::deserialize(deserializer) 71 | .and_then(|string| { 72 | DateTime::::parse_from_rfc3339(&string) 73 | .map_err(|err| Error::custom(err.to_string())) 74 | }) 75 | .map(|dt| dt.with_timezone(&dt.timezone())) 76 | } 77 | 78 | pub fn as_rfc3339_fixed( 79 | key: &chrono::DateTime, 80 | serializer: S, 81 | ) -> Result 82 | where 83 | S: serde::Serializer, 84 | { 85 | serializer.serialize_str(&key.to_rfc3339()) 86 | } 87 | 88 | impl MyTryFromBytes for Vec { 89 | type Err = io::Error; 90 | fn try_from(slice: &[u8]) -> Result { 91 | Ok(Vec::from(slice)) 92 | } 93 | } 94 | // }}} 95 | 96 | /// Write out a value as YAML without a `---` prefix 97 | /// 98 | /// This is how a lot of stuff in `Crev` is serialized 99 | pub fn write_as_headerless_yaml( 100 | t: &T, 101 | f: &mut dyn fmt::Write, 102 | ) -> fmt::Result { 103 | let s = serde_yaml::to_string(t).map_err(|_| fmt::Error)?; 104 | f.write_str(&s) 105 | } 106 | -------------------------------------------------------------------------------- /crev-data/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crev-data" 3 | description = "Scalable, social, Code REView system that we desperately need - data types library" 4 | documentation = "https://docs.rs/crev-lib" 5 | keywords = ["code", "peer", "review", "verification", "dependencies"] 6 | readme = "README.md" 7 | include = ["src/**", "Cargo.toml", "README.md", "LICENSE-MIT"] 8 | authors.workspace = true 9 | edition.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | rust-version.workspace = true 13 | version.workspace = true 14 | 15 | [dependencies] 16 | crev-common.workspace = true 17 | 18 | chrono.workspace = true 19 | derive_builder = "0.20.0" 20 | ed25519-dalek = { version = "2.1", features = ["rand_core"] } 21 | typed-builder = "0.20" 22 | rand = "0.8.5" 23 | semver.workspace = true 24 | serde.workspace = true 25 | serde_yaml.workspace = true 26 | thiserror.workspace = true 27 | 28 | [package.metadata.release] 29 | shared-version=true 30 | -------------------------------------------------------------------------------- /crev-data/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /crev-data/README.md: -------------------------------------------------------------------------------- 1 | # `crev-data` 2 | 3 | Data types used by `crev` - mostly regarding serialization formats and 4 | crypto-bits. 5 | -------------------------------------------------------------------------------- /crev-data/rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | # imports_granularity="Crate" 3 | -------------------------------------------------------------------------------- /crev-data/src/digest.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | /// This is a cryptographic hash of everything in a crate, which reliably identifies its exact source code 4 | #[derive(Eq, PartialEq, Debug, Clone)] 5 | pub struct Digest([u8; 32]); 6 | 7 | impl From<[u8; 32]> for Digest { 8 | fn from(arr: [u8; 32]) -> Self { 9 | Self(arr) 10 | } 11 | } 12 | 13 | impl Digest { 14 | #[must_use] 15 | pub fn as_slice(&self) -> &[u8] { 16 | &self.0 17 | } 18 | 19 | #[must_use] 20 | pub fn from_bytes(bytes: &[u8]) -> Option { 21 | if bytes.len() == 32 { 22 | let mut out = [0; 32]; 23 | out.copy_from_slice(bytes); 24 | Some(Self(out)) 25 | } else { 26 | None 27 | } 28 | } 29 | 30 | #[must_use] 31 | pub fn into_vec(self) -> Vec { 32 | self.as_slice().to_vec() 33 | } 34 | } 35 | 36 | impl fmt::Display for Digest { 37 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 38 | f.write_str(&crev_common::base64_encode(&self.0)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crev-data/src/id.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | proof::{self, ContentExt, OverrideItem}, 3 | Url, 4 | }; 5 | use crev_common::{ 6 | self, 7 | serde::{as_base64, from_base64}, 8 | }; 9 | use derive_builder::Builder; 10 | use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey}; 11 | use rand::rngs::OsRng; 12 | use serde::{Deserialize, Serialize}; 13 | use std::{convert::TryFrom, fmt}; 14 | 15 | #[derive(Clone, Debug, Serialize, Deserialize)] 16 | pub enum IdType { 17 | #[serde(rename = "crev")] 18 | Crev, 19 | } 20 | 21 | #[derive(Debug, thiserror::Error)] 22 | pub enum IdError { 23 | #[error("wrong length of crev id, expected 32 bytes, got {}", _0)] 24 | WrongIdLength(usize), 25 | #[error("Invalid CrevId: {}", _0)] 26 | InvalidCrevId(Box), 27 | #[error("Invalid signature: {}", _0)] 28 | InvalidSignature(Box), 29 | #[error("Invalid public key: {}", _0)] 30 | InvalidPublicKey(Box), 31 | #[error("Invalid secret key: {}", _0)] 32 | InvalidSecretKey(Box), 33 | } 34 | 35 | impl fmt::Display for IdType { 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | use IdType::Crev; 38 | f.write_str(match self { 39 | Crev => "crev", 40 | }) 41 | } 42 | } 43 | 44 | /// An Id supported by `crev` system 45 | /// 46 | /// Right now it's only native `CrevID`, but in future at least GPG 47 | /// should be supported. 48 | #[derive(Clone, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)] 49 | #[serde(tag = "id-type")] 50 | pub enum Id { 51 | #[serde(rename = "crev")] 52 | Crev { 53 | #[serde(serialize_with = "as_base64", deserialize_with = "from_base64")] 54 | id: Vec, 55 | }, 56 | } 57 | 58 | impl fmt::Debug for Id { 59 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 60 | match self { 61 | Id::Crev { id } => f.write_str(&crev_common::base64_encode(id)), 62 | } 63 | } 64 | } 65 | 66 | impl fmt::Display for Id { 67 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 68 | match self { 69 | Id::Crev { id } => f.write_str(&crev_common::base64_encode(id)), 70 | } 71 | } 72 | } 73 | 74 | impl Id { 75 | pub fn new_crev(bytes: Vec) -> Result { 76 | if bytes.len() != 32 { 77 | return Err(IdError::WrongIdLength(bytes.len())); 78 | } 79 | Ok(Id::Crev { id: bytes }) 80 | } 81 | 82 | pub fn crevid_from_str(s: &str) -> Result { 83 | let bytes = crev_common::base64_decode(s) 84 | .map_err(|e| IdError::InvalidCrevId(e.to_string().into()))?; 85 | Self::new_crev(bytes) 86 | } 87 | 88 | pub fn verify_signature(&self, content: &[u8], sig_str: &str) -> Result<(), IdError> { 89 | match self { 90 | Id::Crev { id } => { 91 | let pubkey = VerifyingKey::from_bytes(id.as_slice().try_into().map_err(|_| IdError::WrongIdLength(id.len()))?) 92 | .map_err(|e| IdError::InvalidPublicKey(e.to_string().into()))?; 93 | 94 | let sig_bytes = crev_common::base64_decode(sig_str) 95 | .map_err(|e| IdError::InvalidSignature(e.to_string().into()))?; 96 | let signature = ed25519_dalek::Signature::try_from(sig_bytes.as_slice()) 97 | .map_err(|e| IdError::InvalidSignature(e.to_string().into()))?; 98 | pubkey 99 | .verify(content, &signature) 100 | .map_err(|e| IdError::InvalidSignature(e.to_string().into()))?; 101 | } 102 | } 103 | 104 | Ok(()) 105 | } 106 | 107 | #[must_use] 108 | pub fn to_bytes(&self) -> Vec { 109 | match self { 110 | Id::Crev { id } => id.clone(), 111 | } 112 | } 113 | } 114 | 115 | /// A unique ID accompanied by publicly identifying data. 116 | #[derive(Clone, Debug, Builder, Serialize, Deserialize, PartialEq, Eq, Hash)] 117 | pub struct PublicId { 118 | #[serde(flatten)] 119 | pub id: Id, 120 | #[serde(flatten)] 121 | pub url: Option, 122 | } 123 | 124 | impl PublicId { 125 | #[must_use] 126 | pub fn new(id: Id, url: Url) -> Self { 127 | Self { id, url: Some(url) } 128 | } 129 | 130 | #[must_use] 131 | pub fn new_id_only(id: Id) -> Self { 132 | Self { id, url: None } 133 | } 134 | 135 | pub fn new_from_pubkey(v: Vec, url: Option) -> Result { 136 | Ok(Self { 137 | id: Id::new_crev(v)?, 138 | url, 139 | }) 140 | } 141 | 142 | pub fn new_crevid_from_base64(s: &str, url: Url) -> Result { 143 | let v = crev_common::base64_decode(s) 144 | .map_err(|e| IdError::InvalidCrevId(e.to_string().into()))?; 145 | Ok(Self { 146 | id: Id::new_crev(v)?, 147 | url: Some(url), 148 | }) 149 | } 150 | 151 | pub fn create_trust_proof<'a>( 152 | &self, 153 | ids: impl IntoIterator, 154 | trust_level: proof::trust::TrustLevel, 155 | override_: Vec, 156 | ) -> crate::Result { 157 | proof::TrustBuilder::default() 158 | .from(self.clone()) 159 | .trust(trust_level) 160 | .ids(ids.into_iter().cloned().collect()) 161 | .override_(override_) 162 | .build() 163 | .map_err(|e| crate::Error::BuildingProof(e.to_string().into())) 164 | } 165 | 166 | pub fn create_package_review_proof( 167 | &self, 168 | package: proof::PackageInfo, 169 | review: proof::review::Review, 170 | override_: Vec, 171 | comment: String, 172 | ) -> crate::Result { 173 | proof::review::PackageBuilder::default() 174 | .from(self.clone()) 175 | .package(package) 176 | .review(review) 177 | .override_(override_) 178 | .comment(comment) 179 | .build() 180 | .map_err(|e| crate::Error::BuildingProof(e.to_string().into())) 181 | } 182 | 183 | #[must_use] 184 | pub fn url_display(&self) -> &str { 185 | match &self.url { 186 | Some(url) => &url.url, 187 | None => "(no url)", 188 | } 189 | } 190 | } 191 | 192 | /// A `PublicId` with the corresponding secret key 193 | #[derive(Debug)] 194 | pub struct UnlockedId { 195 | pub id: PublicId, 196 | pub keypair: Keypair, 197 | } 198 | 199 | #[derive(Debug)] 200 | pub struct Keypair { 201 | pub secret: SigningKey, 202 | pub public: VerifyingKey, 203 | } 204 | 205 | impl AsRef for UnlockedId { 206 | fn as_ref(&self) -> &Id { 207 | &self.id.id 208 | } 209 | } 210 | 211 | impl AsRef for UnlockedId { 212 | fn as_ref(&self) -> &PublicId { 213 | &self.id 214 | } 215 | } 216 | 217 | impl UnlockedId { 218 | #[allow(clippy::new_ret_no_self)] 219 | pub fn new(url: Option, sec_key: &[u8]) -> Result { 220 | let sec_key = SigningKey::from_bytes(sec_key.try_into().map_err(|_| IdError::WrongIdLength(sec_key.len()))?); 221 | let calculated_pub_key = sec_key.verifying_key(); 222 | 223 | Ok(Self { 224 | id: crate::PublicId::new_from_pubkey(calculated_pub_key.as_bytes().to_vec(), url)?, 225 | keypair: Keypair { 226 | secret: sec_key, 227 | public: calculated_pub_key, 228 | }, 229 | }) 230 | } 231 | 232 | #[must_use] 233 | pub fn sign(&self, msg: &[u8]) -> Vec { 234 | self.keypair.secret.sign(msg).to_bytes().to_vec() 235 | } 236 | 237 | #[must_use] 238 | pub fn type_as_string(&self) -> String { 239 | "crev".into() 240 | } 241 | 242 | #[must_use] 243 | pub fn as_public_id(&self) -> &PublicId { 244 | &self.id 245 | } 246 | 247 | #[must_use] 248 | pub fn url(&self) -> Option<&Url> { 249 | self.id.url.as_ref() 250 | } 251 | 252 | #[must_use] 253 | pub fn generate_for_git_url(url: &str) -> Self { 254 | Self::generate(Some(Url::new_git(url.to_owned()))) 255 | } 256 | 257 | pub fn generate(url: Option) -> Self { 258 | let secret = ed25519_dalek::SigningKey::generate(&mut OsRng); 259 | let public = secret.verifying_key(); 260 | Self { 261 | id: PublicId::new_from_pubkey(public.as_bytes().to_vec(), url) 262 | .expect("should be valid keypair"), 263 | keypair: Keypair { secret, public }, 264 | } 265 | } 266 | 267 | pub fn create_signed_trust_proof<'a>( 268 | &self, 269 | ids: impl IntoIterator, 270 | trust_level: proof::trust::TrustLevel, 271 | override_: Vec, 272 | ) -> crate::Result { 273 | self.id 274 | .create_trust_proof(ids, trust_level, override_)? 275 | .sign_by(self) 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /crev-data/src/level.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::fmt; 3 | 4 | #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)] 5 | #[serde(rename_all = "lowercase")] 6 | pub enum Level { 7 | None, 8 | Low, 9 | #[default] 10 | Medium, 11 | High, 12 | } 13 | 14 | impl fmt::Display for Level { 15 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 16 | use Level::*; 17 | f.write_str(match self { 18 | None => "none", 19 | Low => "low", 20 | Medium => "medium", 21 | High => "high", 22 | }) 23 | } 24 | } 25 | 26 | #[derive(thiserror::Error, Debug)] 27 | #[error("Can't convert string to Level")] 28 | pub struct FromStrErr; 29 | 30 | impl std::str::FromStr for Level { 31 | type Err = FromStrErr; 32 | 33 | fn from_str(s: &str) -> std::result::Result { 34 | Ok(match s { 35 | "none" => Level::None, 36 | "low" => Level::Low, 37 | "medium" => Level::Medium, 38 | "high" => Level::High, 39 | _ => return Err(FromStrErr), 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crev-data/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate contains only code handling data types 2 | //! used by `crev`, without getting into details 3 | //! how actually `crev` works (where and how it manages data). 4 | #![allow(clippy::default_trait_access)] 5 | #![allow(clippy::items_after_statements)] 6 | #![allow(clippy::missing_errors_doc)] 7 | #![allow(clippy::missing_panics_doc)] 8 | #![allow(clippy::module_name_repetitions)] 9 | 10 | pub mod digest; 11 | pub mod id; 12 | pub mod level; 13 | pub mod proof; 14 | pub mod url; 15 | #[macro_use] 16 | pub mod util; 17 | use crate::{id::IdError, proof::content::ValidationError}; 18 | pub use semver::Version; 19 | 20 | pub use crate::{ 21 | digest::Digest, 22 | id::{Id, PublicId, UnlockedId}, 23 | level::Level, 24 | proof::{ 25 | review, 26 | review::{Rating, Review}, 27 | trust::TrustLevel, 28 | }, 29 | url::Url, 30 | }; 31 | 32 | /// It's just a string. See [`SOURCE_CRATES_IO`] 33 | pub type RegistrySource<'a> = &'a str; 34 | 35 | /// Constant for `source` arguments, indicating 36 | pub const SOURCE_CRATES_IO: RegistrySource<'static> = "https://crates.io"; 37 | 38 | #[cfg(test)] 39 | mod tests; 40 | 41 | type Result = std::result::Result; 42 | 43 | #[derive(thiserror::Error, Debug)] 44 | pub enum Error { 45 | #[error("`kind` field missing")] 46 | KindFieldMissing, 47 | 48 | #[error("Unexpected `kind` value in a legacy format")] 49 | UnexpectedKindValueInALegacyFormat, 50 | 51 | #[error("Parsing error when looking for start of code review proof")] 52 | ParsingErrorWhenLookingForStartOfCodeReviewProof, 53 | 54 | #[error("Parsing error: type name mismatch in the signature")] 55 | ParsingErrorTypeNameMismatchInTheSignature, 56 | #[error("Parsing error: type name mismatch in the footer")] 57 | ParsingErrorTypeNameMismatchInTheFooter, 58 | #[error("Signature too long")] 59 | SignatureTooLong, 60 | #[error("Unexpected EOF while parsing")] 61 | UnexpectedEOFWhileParsing, 62 | #[error("Proof body too long")] 63 | ProofBodyTooLong, 64 | 65 | #[error(transparent)] 66 | Validation(#[from] ValidationError), 67 | 68 | #[error("YAML formatting: {}", _0)] 69 | YAMLFormat(Box), 70 | 71 | #[error(transparent)] 72 | Id(#[from] IdError), 73 | 74 | #[error(transparent)] 75 | Parse(#[from] ParseError), 76 | 77 | #[error("Unknown level: {}", _0)] 78 | UnknownLevel(Box), 79 | 80 | #[error("I/O: {}", _0)] 81 | IO(#[from] std::io::Error), 82 | 83 | #[error("Error building proof: {}", _0)] 84 | BuildingProof(Box), 85 | 86 | #[error("Error building review: {}", _0)] 87 | BuildingReview(Box), 88 | 89 | #[error("Serialized to {} proofs", _0)] 90 | SerializedTooManyProofs(usize), 91 | } 92 | 93 | #[derive(Debug, thiserror::Error)] 94 | pub enum ParseError { 95 | #[error("Draft parse error: {}", _0)] 96 | Draft(#[source] serde_yaml::Error), 97 | 98 | #[error("Proof parse error: {}", _0)] 99 | Proof(#[source] serde_yaml::Error), 100 | } 101 | -------------------------------------------------------------------------------- /crev-data/src/proof/content.rs: -------------------------------------------------------------------------------- 1 | use crate::{proof, proof::Proof, Error, ParseError, Result}; 2 | use chrono::{self, prelude::*}; 3 | use crev_common::{ 4 | self, 5 | serde::{as_base64, as_rfc3339_fixed, from_base64, from_rfc3339_fixed}, 6 | }; 7 | use derive_builder::Builder; 8 | use serde::{self, Deserialize, Serialize}; 9 | use std::{fmt, io}; 10 | 11 | pub type Date = chrono::DateTime; 12 | 13 | /// Common operations on types containing `Common` 14 | pub trait CommonOps { 15 | // until we support legacy, we have to stick to `Common` here 16 | fn common(&self) -> &Common; 17 | 18 | /// Is it crate review or something else? 19 | fn kind(&self) -> &str { 20 | self.common() 21 | .kind 22 | .as_ref() 23 | .expect("Common types are expected to always have the `kind` field backfilled") 24 | } 25 | 26 | /// Who wrote and signed this review/trust 27 | fn from(&self) -> &crate::PublicId { 28 | &self.common().from 29 | } 30 | 31 | /// When it has been written according to its creator 32 | fn date(&self) -> &chrono::DateTime { 33 | &self.common().date 34 | } 35 | 36 | /// When it has been written according to its creator 37 | fn date_utc(&self) -> chrono::DateTime { 38 | self.date().with_timezone(&Utc) 39 | } 40 | 41 | /// ID of the person who signed it 42 | fn author_id(&self) -> &crate::Id { 43 | &self.common().from.id 44 | } 45 | 46 | /// Displayable version of ID of the person who signed it 47 | fn author_public_id(&self) -> &crate::PublicId { 48 | &self.common().from 49 | } 50 | 51 | /// Easy check of `kind()` 52 | fn ensure_kind_is(&self, kind: &str) -> ValidationResult<()> { 53 | let expected = self.kind(); 54 | if expected != kind { 55 | return Err(ValidationError::InvalidKind(Box::new(( 56 | expected.to_string(), 57 | kind.to_string(), 58 | )))); 59 | } 60 | Ok(()) 61 | } 62 | } 63 | 64 | /// Reference to original proof when reissuing 65 | #[derive(Clone, Debug, Serialize, Deserialize)] 66 | pub struct OriginalReference { 67 | /// original proof digest (blake2b256) 68 | #[serde(serialize_with = "as_base64", deserialize_with = "from_base64")] 69 | pub proof: Vec, 70 | /// Any text 71 | #[serde(skip_serializing_if = "String::is_empty", default = "Default::default")] 72 | pub comment: String, 73 | } 74 | 75 | /// A `Common` part of every `Content` format 76 | #[derive(Clone, Builder, Debug, Serialize, Deserialize)] 77 | pub struct Common { 78 | /// Type of this review/trust/whatever file 79 | pub kind: Option, 80 | /// A version, to allow future backward-incompatible extensions 81 | /// and changes. 82 | pub version: i64, 83 | #[builder(default = "crev_common::now()")] 84 | #[serde( 85 | serialize_with = "as_rfc3339_fixed", 86 | deserialize_with = "from_rfc3339_fixed" 87 | )] 88 | /// Timestamp of proof creation 89 | pub date: chrono::DateTime, 90 | /// Author of the proof 91 | pub from: crate::PublicId, 92 | /// Reference to original proof when reissuing 93 | #[serde(skip_serializing_if = "Option::is_none", default = "Option::default")] 94 | pub original: Option, 95 | } 96 | 97 | impl CommonOps for Common { 98 | fn common(&self) -> &Common { 99 | self 100 | } 101 | } 102 | 103 | pub trait WithReview { 104 | fn review(&self) -> &super::Review; 105 | } 106 | 107 | #[derive(Debug, thiserror::Error)] 108 | pub enum ValidationError { 109 | #[error("Invalid kind: {}, expected: {}", _0.0, _0.1)] 110 | InvalidKind(Box<(String, String)>), 111 | 112 | /// Alternative source can't be empty 113 | #[error("Alternative source can't be empty")] 114 | AlternativeSourceCanNotBeEmpty, 115 | 116 | /// Alternative name can't be empty 117 | #[error("Alternative name can't be empty")] 118 | AlternativeNameCanNotBeEmpty, 119 | 120 | /// Issues with an empty `id` field are not allowed 121 | #[error("Issues with an empty `id` field are not allowed")] 122 | IssuesWithAnEmptyIDFieldAreNotAllowed, 123 | 124 | /// Advisories with no `id`s are not allowed 125 | #[error("Advisories with no `id`s are not allowed")] 126 | AdvisoriesWithNoIDSAreNotAllowed, 127 | 128 | /// Advisories with an empty `id` field are not allowed 129 | #[error("Advisories with an empty `id` field are not allowed")] 130 | AdvisoriesWithAnEmptyIDFieldAreNotAllowed, 131 | } 132 | 133 | pub type ValidationResult = std::result::Result; 134 | 135 | /// Proof Content 136 | /// 137 | /// `Content` is a standardized format of a crev proof body 138 | /// (part that is being signed over). 139 | /// 140 | /// It is open-ended, and different software 141 | /// can implement their own formats. 142 | pub trait Content: CommonOps { 143 | fn validate_data(&self) -> ValidationResult<()> { 144 | // typically just OK 145 | Ok(()) 146 | } 147 | 148 | fn serialize_to(&self, fmt: &mut dyn std::fmt::Write) -> fmt::Result; 149 | } 150 | 151 | pub trait ContentDeserialize: Content + Sized { 152 | fn deserialize_from(io: IO) -> std::result::Result 153 | where 154 | IO: io::Read; 155 | } 156 | 157 | impl ContentDeserialize for T 158 | where 159 | T: serde::de::DeserializeOwned + Content + Sized, 160 | { 161 | fn deserialize_from(io: IO) -> std::result::Result 162 | where 163 | IO: io::Read, 164 | { 165 | let s: Self = serde_yaml::from_reader(io).map_err(ParseError::Proof)?; 166 | 167 | s.validate_data()?; 168 | 169 | Ok(s) 170 | } 171 | } 172 | 173 | /// A Proof Content `Draft` 174 | /// 175 | /// A simplified version of content, used 176 | /// for user interaction - editing the parts 177 | /// that are not necessary for the user to see. 178 | pub struct Draft { 179 | pub(crate) title: String, 180 | pub(crate) body: String, 181 | } 182 | 183 | impl Draft { 184 | #[must_use] 185 | pub fn title(&self) -> &str { 186 | &self.title 187 | } 188 | 189 | #[must_use] 190 | pub fn body(&self) -> &str { 191 | &self.body 192 | } 193 | } 194 | 195 | /// A content with draft support 196 | /// 197 | /// Draft is a compact, human 198 | pub trait ContentWithDraft: Content { 199 | fn to_draft(&self) -> Draft; 200 | 201 | fn apply_draft(&self, body: &str) -> Result 202 | where 203 | Self: Sized; 204 | } 205 | 206 | pub trait ContentExt: Content { 207 | fn serialize(&self) -> Result { 208 | let mut body = String::new(); 209 | self.serialize_to(&mut body) 210 | .map_err(|e| crate::Error::YAMLFormat(e.to_string().into()))?; 211 | Ok(body) 212 | } 213 | 214 | fn sign_by(&self, id: &crate::id::UnlockedId) -> Result { 215 | let body = self.serialize()?; 216 | let signature = id.sign(body.as_bytes()); 217 | Ok(Proof { 218 | digest: crev_common::blake2b256sum(body.as_bytes()), 219 | body, 220 | signature: crev_common::base64_encode(&signature), 221 | common_content: self.common().clone(), 222 | }) 223 | } 224 | 225 | /// Ensure the proof generated from this `Content` is going to deserialize 226 | fn ensure_serializes_to_valid_proof(&self) -> Result<()> { 227 | let body = self.serialize()?; 228 | let signature = "somefakesignature"; 229 | let proof = proof::Proof { 230 | digest: crev_common::blake2b256sum(body.as_bytes()), 231 | body, 232 | signature: crev_common::base64_encode(&signature), 233 | common_content: self.common().clone(), 234 | }; 235 | let parsed = proof::Proof::parse_from(std::io::Cursor::new(proof.to_string().as_bytes()))?; 236 | 237 | if parsed.len() != 1 { 238 | return Err(Error::SerializedTooManyProofs(parsed.len())); 239 | } 240 | 241 | Ok(()) 242 | } 243 | } 244 | 245 | impl ContentExt for T where T: Content {} 246 | -------------------------------------------------------------------------------- /crev-data/src/proof/package_info.rs: -------------------------------------------------------------------------------- 1 | use crate::proof; 2 | 3 | use crev_common::serde::{as_base64, from_base64}; 4 | use derive_builder::Builder; 5 | pub use semver::Version; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Clone, Debug, Builder, Serialize, Deserialize, PartialEq, Hash, Eq)] 9 | pub struct PackageId { 10 | pub source: String, 11 | pub name: String, 12 | } 13 | 14 | #[derive(Clone, Debug, Builder, Serialize, Deserialize, PartialEq, Hash, Eq)] 15 | pub struct PackageVersionId { 16 | #[serde(flatten)] 17 | pub id: PackageId, 18 | pub version: Version, 19 | } 20 | 21 | impl PackageVersionId { 22 | #[must_use] 23 | pub fn new(source: String, name: String, version: Version) -> Self { 24 | Self { 25 | id: PackageId { source, name }, 26 | version, 27 | } 28 | } 29 | } 30 | 31 | #[derive(Clone, Debug, Builder, Serialize, Deserialize, PartialEq)] 32 | pub struct PackageInfo { 33 | #[serde(flatten)] 34 | pub id: PackageVersionId, 35 | 36 | #[serde(skip_serializing_if = "proof::equals_default", default)] 37 | pub revision: String, 38 | #[serde( 39 | rename = "revision-type", 40 | skip_serializing_if = "proof::equals_default_revision_type", 41 | default = "proof::default_revision_type" 42 | )] 43 | pub revision_type: String, 44 | 45 | #[serde(serialize_with = "as_base64", deserialize_with = "from_base64")] 46 | pub digest: Vec, 47 | #[serde( 48 | skip_serializing_if = "proof::equals_default_digest_type", 49 | default = "proof::default_digest_type" 50 | )] 51 | pub digest_type: String, 52 | } 53 | -------------------------------------------------------------------------------- /crev-data/src/proof/review/code.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | proof, proof::content::ValidationResult, serde_content_serialize, serde_draft_serialize, 3 | ParseError, Result, 4 | }; 5 | use crev_common::{ 6 | self, 7 | serde::{as_base64, from_base64}, 8 | }; 9 | use derive_builder::Builder; 10 | use proof::{CommonOps, Content}; 11 | use serde::{Deserialize, Serialize}; 12 | use std::{self, default::Default, fmt, path::PathBuf}; 13 | 14 | const CURRENT_CODE_REVIEW_PROOF_SERIALIZATION_VERSION: i64 = -1; 15 | 16 | fn cur_version() -> i64 { 17 | CURRENT_CODE_REVIEW_PROOF_SERIALIZATION_VERSION 18 | } 19 | 20 | #[derive(Clone, Debug, Serialize, Deserialize)] 21 | pub struct File { 22 | pub path: PathBuf, 23 | #[serde(serialize_with = "as_base64", deserialize_with = "from_base64")] 24 | pub digest: Vec, 25 | #[serde(rename = "digest-type")] 26 | #[serde( 27 | skip_serializing_if = "proof::equals_default_digest_type", 28 | default = "proof::default_digest_type" 29 | )] 30 | pub digest_type: String, 31 | } 32 | 33 | /// Body of a Code Review Proof 34 | #[derive(Clone, Builder, Debug, Serialize, Deserialize)] 35 | // TODO: validate setters(no newlines, etc) 36 | // TODO: https://github.com/colin-kiegel/rust-derive-builder/issues/136 37 | pub struct Code { 38 | #[serde(flatten)] 39 | pub common: proof::Common, 40 | #[serde(rename = "package")] 41 | pub package: proof::PackageInfo, 42 | #[serde(flatten)] 43 | #[builder(default = "Default::default()")] 44 | pub review: super::Review, 45 | #[serde(skip_serializing_if = "String::is_empty", default = "Default::default")] 46 | #[builder(default = "Default::default()")] 47 | pub comment: String, 48 | #[serde( 49 | skip_serializing_if = "std::vec::Vec::is_empty", 50 | default = "std::vec::Vec::new" 51 | )] 52 | #[builder(default = "Default::default()")] 53 | pub files: Vec, 54 | } 55 | 56 | impl Code { 57 | pub const KIND: &'static str = "code review"; 58 | } 59 | 60 | impl CodeBuilder { 61 | pub fn from>(&mut self, value: VALUE) -> &mut Self { 62 | if let Some(ref mut common) = self.common { 63 | common.from = value.into(); 64 | } else { 65 | self.common = Some(proof::Common { 66 | kind: Some(Code::KIND.into()), 67 | version: cur_version(), 68 | date: crev_common::now(), 69 | from: value.into(), 70 | original: None, 71 | }); 72 | } 73 | self 74 | } 75 | } 76 | 77 | impl proof::CommonOps for Code { 78 | fn common(&self) -> &proof::Common { 79 | &self.common 80 | } 81 | 82 | fn kind(&self) -> &str { 83 | // Backfill the `kind` if it is empty (legacy format) 84 | self.common.kind.as_deref().unwrap_or(Self::KIND) 85 | } 86 | } 87 | 88 | impl fmt::Display for Code { 89 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 90 | self.serialize_to(f).map_err(|_| fmt::Error) 91 | } 92 | } 93 | 94 | /// Like `Code` but serializes for interactive editing 95 | #[derive(Clone, Debug, Serialize, Deserialize)] 96 | pub struct Draft { 97 | review: super::Review, 98 | #[serde(default = "Default::default")] 99 | comment: String, 100 | } 101 | 102 | impl Draft { 103 | pub fn parse(s: &str) -> Result { 104 | Ok(serde_yaml::from_str(s).map_err(ParseError::Draft)?) 105 | } 106 | } 107 | 108 | impl From for Draft { 109 | fn from(code: Code) -> Self { 110 | Draft { 111 | review: code.review, 112 | comment: code.comment, 113 | } 114 | } 115 | } 116 | 117 | impl proof::WithReview for Code { 118 | fn review(&self) -> &super::Review { 119 | &self.review 120 | } 121 | } 122 | 123 | impl proof::content::Content for Code { 124 | fn validate_data(&self) -> ValidationResult<()> { 125 | self.ensure_kind_is(Code::KIND)?; 126 | Ok(()) 127 | } 128 | 129 | fn serialize_to(&self, fmt: &mut dyn std::fmt::Write) -> fmt::Result { 130 | serde_content_serialize!(self, fmt); 131 | Ok(()) 132 | } 133 | } 134 | 135 | impl proof::ContentWithDraft for Code { 136 | fn to_draft(&self) -> proof::Draft { 137 | proof::Draft { 138 | title: format!( 139 | "Code Review of {} files of {} {}", 140 | self.files.len(), 141 | self.package.id.id.name, 142 | self.package.id.version 143 | ), 144 | body: Draft::from(self.clone()).to_string(), 145 | } 146 | } 147 | 148 | fn apply_draft(&self, s: &str) -> Result { 149 | let draft = Draft::parse(s)?; 150 | 151 | let mut copy = self.clone(); 152 | copy.review = draft.review; 153 | copy.comment = draft.comment; 154 | 155 | copy.validate_data()?; 156 | Ok(copy) 157 | } 158 | } 159 | 160 | impl fmt::Display for Draft { 161 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 162 | serde_draft_serialize!(self, fmt); 163 | Ok(()) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /crev-data/src/proof/review/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::level::Level; 2 | pub use code::*; 3 | use derive_builder::Builder; 4 | pub use package::Draft; 5 | pub use package::*; 6 | use serde::{Deserialize, Serialize}; 7 | use std::default::Default; 8 | 9 | pub mod code; 10 | pub mod package; 11 | 12 | #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialOrd, Ord, PartialEq, Eq, Default)] 13 | #[serde(rename_all = "lowercase")] 14 | pub enum Rating { 15 | #[serde(alias = "dangerous")] // for backward compat with some previous versions 16 | Negative, 17 | #[default] 18 | Neutral, 19 | Positive, 20 | Strong, 21 | } 22 | 23 | /// Information about review result 24 | #[derive(Clone, Debug, Serialize, Deserialize, Builder, PartialEq, Eq)] 25 | pub struct Review { 26 | #[builder(default = "Default::default()")] 27 | pub thoroughness: Level, 28 | #[builder(default = "Default::default()")] 29 | pub understanding: Level, 30 | #[builder(default = "Default::default()")] 31 | pub rating: Rating, 32 | } 33 | 34 | impl Default for Review { 35 | fn default() -> Self { 36 | Review::new_none() 37 | } 38 | } 39 | 40 | impl Review { 41 | #[must_use] 42 | pub fn new_positive() -> Self { 43 | Review { 44 | thoroughness: Level::Low, 45 | understanding: Level::Medium, 46 | rating: Rating::Positive, 47 | } 48 | } 49 | 50 | #[must_use] 51 | pub fn new_negative() -> Self { 52 | Review { 53 | thoroughness: Level::Low, 54 | understanding: Level::Medium, 55 | rating: Rating::Negative, 56 | } 57 | } 58 | #[must_use] 59 | pub fn new_none() -> Self { 60 | Review { 61 | thoroughness: Level::None, 62 | understanding: Level::None, 63 | rating: Rating::Neutral, 64 | } 65 | } 66 | 67 | #[must_use] 68 | pub fn is_none(&self) -> bool { 69 | *self == Self::new_none() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crev-data/src/proof/revision.rs: -------------------------------------------------------------------------------- 1 | use crate::proof; 2 | use derive_builder::Builder; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Clone, Debug, Builder, Serialize, Deserialize)] 6 | pub struct Revision { 7 | pub revision: String, 8 | #[serde( 9 | rename = "revision-type", 10 | skip_serializing_if = "proof::equals_default_revision_type", 11 | default = "proof::default_revision_type" 12 | )] 13 | #[builder(default = "\"git\".into()")] 14 | pub revision_type: String, 15 | } 16 | -------------------------------------------------------------------------------- /crev-data/src/proof/trust.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | proof::{self, content::ValidationResult, CommonOps, Content}, 3 | serde_content_serialize, serde_draft_serialize, Error, Level, ParseError, Result, 4 | }; 5 | 6 | use derive_builder::Builder; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use std::fmt; 10 | 11 | use super::{OverrideItem, OverrideItemDraft}; 12 | 13 | const CURRENT_TRUST_PROOF_SERIALIZATION_VERSION: i64 = -1; 14 | 15 | fn cur_version() -> i64 { 16 | CURRENT_TRUST_PROOF_SERIALIZATION_VERSION 17 | } 18 | 19 | #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialOrd, Ord, PartialEq, Eq, Default)] 20 | #[serde(rename_all = "lowercase")] 21 | pub enum TrustLevel { 22 | Distrust, 23 | None, 24 | Low, 25 | #[default] 26 | Medium, 27 | High, 28 | } 29 | 30 | impl fmt::Display for TrustLevel { 31 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 32 | use TrustLevel::*; 33 | f.pad(match self { 34 | Distrust => "distrust", 35 | None => "none", 36 | Low => "low", 37 | Medium => "medium", 38 | High => "high", 39 | }) 40 | } 41 | } 42 | 43 | #[derive(thiserror::Error, Debug)] 44 | #[error("Can't convert string to TrustLevel. Possible values are: \"none\" or \"untrust\", \"low\", \"medium\", \"high\" and \"distrust\".")] 45 | pub struct FromStrErr; 46 | 47 | impl std::str::FromStr for TrustLevel { 48 | type Err = FromStrErr; 49 | 50 | fn from_str(s: &str) -> std::result::Result { 51 | Ok(match s { 52 | "none" | "untrust" => TrustLevel::None, 53 | "low" => TrustLevel::Low, 54 | "medium" => TrustLevel::Medium, 55 | "high" => TrustLevel::High, 56 | "distrust" => TrustLevel::Distrust, 57 | _ => return Err(FromStrErr), 58 | }) 59 | } 60 | } 61 | 62 | impl std::convert::From for TrustLevel { 63 | fn from(l: Level) -> Self { 64 | match l { 65 | Level::None => TrustLevel::None, 66 | Level::Low => TrustLevel::Low, 67 | Level::Medium => TrustLevel::Medium, 68 | Level::High => TrustLevel::High, 69 | } 70 | } 71 | } 72 | 73 | impl TrustLevel { 74 | #[allow(unused)] 75 | fn from_str(s: &str) -> Result { 76 | Ok(match s { 77 | "distrust" => TrustLevel::Distrust, 78 | "none" => TrustLevel::None, 79 | "low" => TrustLevel::Low, 80 | "medium" => TrustLevel::Medium, 81 | "high" => TrustLevel::High, 82 | _ => return Err(Error::UnknownLevel(s.into())), 83 | }) 84 | } 85 | } 86 | 87 | /// Body of a Trust Proof 88 | #[derive(Clone, Debug, Builder, Serialize, Deserialize)] 89 | pub struct Trust { 90 | #[serde(flatten)] 91 | pub common: proof::Common, 92 | pub ids: Vec, 93 | #[builder(default = "Default::default()")] 94 | pub trust: TrustLevel, 95 | #[serde(skip_serializing_if = "String::is_empty", default = "Default::default")] 96 | #[builder(default = "Default::default()")] 97 | pub comment: String, 98 | #[serde( 99 | default = "Default::default", 100 | skip_serializing_if = "Vec::is_empty", 101 | rename = "override" 102 | )] 103 | #[builder(default = "Default::default()")] 104 | pub override_: Vec, 105 | } 106 | 107 | impl TrustBuilder { 108 | pub fn from>(&mut self, value: VALUE) -> &mut Self { 109 | if let Some(ref mut common) = self.common { 110 | common.from = value.into(); 111 | } else { 112 | self.common = Some(proof::Common { 113 | kind: Some(Trust::KIND.into()), 114 | version: cur_version(), 115 | date: crev_common::now(), 116 | from: value.into(), 117 | original: None, 118 | }); 119 | } 120 | self 121 | } 122 | } 123 | 124 | impl fmt::Display for Trust { 125 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 126 | self.serialize_to(f).map_err(|_| fmt::Error) 127 | } 128 | } 129 | 130 | impl proof::CommonOps for Trust { 131 | fn common(&self) -> &proof::Common { 132 | &self.common 133 | } 134 | 135 | fn kind(&self) -> &str { 136 | // Backfill the `kind` if it is empty (legacy format) 137 | self.common.kind.as_deref().unwrap_or(Self::KIND) 138 | } 139 | } 140 | 141 | impl Trust { 142 | pub const KIND: &'static str = "trust"; 143 | 144 | pub fn touch_date(&mut self) { 145 | self.common.date = crev_common::now(); 146 | } 147 | } 148 | 149 | /// Like `Trust` but serializes for interactive editing 150 | #[derive(Clone, Debug, Serialize, Deserialize)] 151 | pub struct Draft { 152 | pub trust: TrustLevel, 153 | #[serde(default = "Default::default", skip_serializing_if = "String::is_empty")] 154 | comment: String, 155 | #[serde( 156 | default = "Default::default", 157 | skip_serializing_if = "Vec::is_empty", 158 | rename = "override" 159 | )] 160 | override_: Vec, 161 | } 162 | 163 | impl From for Draft { 164 | fn from(trust: Trust) -> Self { 165 | Draft { 166 | trust: trust.trust, 167 | comment: trust.comment, 168 | override_: trust.override_.into_iter().map(Into::into).collect(), 169 | } 170 | } 171 | } 172 | 173 | impl fmt::Display for Draft { 174 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 175 | serde_draft_serialize!(self, fmt); 176 | Ok(()) 177 | } 178 | } 179 | 180 | impl proof::Content for Trust { 181 | fn serialize_to(&self, fmt: &mut dyn std::fmt::Write) -> fmt::Result { 182 | serde_content_serialize!(self, fmt); 183 | Ok(()) 184 | } 185 | 186 | fn validate_data(&self) -> ValidationResult<()> { 187 | self.ensure_kind_is(Self::KIND)?; 188 | Ok(()) 189 | } 190 | } 191 | 192 | impl Trust { 193 | fn draft_title(&self) -> String { 194 | match self.ids.len() { 195 | 0 => "Trust for noone?!".into(), 196 | 1 => format!("Trust for {} {}", self.ids[0].id, self.ids[0].url_display()), 197 | n => format!( 198 | "Trust for {} {} and {} other", 199 | self.ids[0].id, 200 | self.ids[0].url_display(), 201 | n - 1 202 | ), 203 | } 204 | } 205 | } 206 | 207 | impl proof::ContentWithDraft for Trust { 208 | fn to_draft(&self) -> proof::Draft { 209 | proof::Draft { 210 | title: self.draft_title(), 211 | body: Draft::from(self.clone()).to_string(), 212 | } 213 | } 214 | 215 | fn apply_draft(&self, s: &str) -> Result { 216 | let draft = Draft::parse(s)?; 217 | 218 | let mut copy = self.clone(); 219 | copy.trust = draft.trust; 220 | copy.comment = draft.comment; 221 | copy.override_ = draft.override_.into_iter().map(Into::into).collect(); 222 | 223 | copy.validate_data()?; 224 | Ok(copy) 225 | } 226 | } 227 | 228 | impl Draft { 229 | pub fn parse(s: &str) -> std::result::Result { 230 | serde_yaml::from_str(s).map_err(ParseError::Draft) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /crev-data/src/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | id::UnlockedId, 3 | proof::{self, Content, ContentExt, ContentWithDraft, Proof}, 4 | Error, Result, Url, 5 | }; 6 | use semver::Version; 7 | use std::{default::Default, path::PathBuf}; 8 | 9 | #[test] 10 | pub fn signed_parse() -> Result<()> { 11 | let s = r#" 12 | -----BEGIN CREV PACKAGE REVIEW----- 13 | version: -1 14 | date: "2018-12-18T23:10:21.111854021-08:00" 15 | from: 16 | id-type: crev 17 | id: FYlr8YoYGVvDwHQxqEIs89reKKDy-oWisoO0qXXEfHE 18 | url: "https://github.com/dpc/crev-proofs" 19 | package: 20 | source: "https://crates.io" 21 | name: log 22 | version: 0.4.6 23 | digest: BhDmOOjfESqs8i3z9qsQANH8A39eKklgQKuVtrwN-Tw 24 | review: 25 | thoroughness: low 26 | understanding: medium 27 | rating: positive 28 | -----BEGIN CREV PACKAGE REVIEW SIGNATURE----- 29 | 4R2WjtU-avpBznmJYAl44H1lOYgETu3RSNhCDcB4GpqhJbSRkd-eqnUuhHgDUs77OlhUf7BSA0dydxaALwx0Dg 30 | -----END CREV PACKAGE REVIEW----- 31 | "#; 32 | 33 | let proofs = Proof::parse_from(s.as_bytes())?; 34 | assert_eq!(proofs.len(), 1); 35 | assert_eq!( 36 | proofs[0].signature(), 37 | "4R2WjtU-avpBznmJYAl44H1lOYgETu3RSNhCDcB4GpqhJbSRkd-eqnUuhHgDUs77OlhUf7BSA0dydxaALwx0Dg" 38 | ); 39 | Ok(()) 40 | } 41 | 42 | #[test] 43 | pub fn signed_parse_multiple() -> Result<()> { 44 | let s = r#" 45 | 46 | -----BEGIN CREV PACKAGE REVIEW----- 47 | version: -1 48 | date: "2018-12-18T23:10:21.111854021-08:00" 49 | from: 50 | id-type: crev 51 | id: FYlr8YoYGVvDwHQxqEIs89reKKDy-oWisoO0qXXEfHE 52 | url: "https://github.com/dpc/crev-proofs" 53 | package: 54 | source: "https://crates.io" 55 | name: log 56 | version: 0.4.6 57 | digest: BhDmOOjfESqs8i3z9qsQANH8A39eKklgQKuVtrwN-Tw 58 | review: 59 | thoroughness: low 60 | understanding: medium 61 | rating: positive 62 | -----BEGIN CREV PACKAGE REVIEW SIGNATURE----- 63 | 4R2WjtU-avpBznmJYAl44H1lOYgETu3RSNhCDcB4GpqhJbSRkd-eqnUuhHgDUs77OlhUf7BSA0dydxaALwx0Dg 64 | -----END CREV PACKAGE REVIEW----- 65 | -----BEGIN CREV PACKAGE REVIEW----- 66 | version: -1 67 | date: "2018-12-18T23:10:21.111854021-08:00" 68 | from: 69 | id-type: crev 70 | id: FYlr8YoYGVvDwHQxqEIs89reKKDy-oWisoO0qXXEfHE 71 | url: "https://github.com/dpc/crev-proofs" 72 | package: 73 | source: "https://crates.io" 74 | name: log 75 | version: 0.4.6 76 | digest: BhDmOOjfESqs8i3z9qsQANH8A39eKklgQKuVtrwN-Tw 77 | review: 78 | thoroughness: low 79 | understanding: medium 80 | rating: positive 81 | -----BEGIN CREV PACKAGE REVIEW SIGNATURE----- 82 | 4R2WjtU-avpBznmJYAl44H1lOYgETu3RSNhCDcB4GpqhJbSRkd-eqnUuhHgDUs77OlhUf7BSA0dydxaALwx0Dg 83 | -----END CREV PACKAGE REVIEW----- 84 | "#; 85 | 86 | let proofs = Proof::parse_from(s.as_bytes())?; 87 | assert_eq!(proofs.len(), 2); 88 | assert_eq!( 89 | proofs[0].signature(), 90 | "4R2WjtU-avpBznmJYAl44H1lOYgETu3RSNhCDcB4GpqhJbSRkd-eqnUuhHgDUs77OlhUf7BSA0dydxaALwx0Dg" 91 | ); 92 | assert_eq!( 93 | proofs[1].signature(), 94 | "4R2WjtU-avpBznmJYAl44H1lOYgETu3RSNhCDcB4GpqhJbSRkd-eqnUuhHgDUs77OlhUf7BSA0dydxaALwx0Dg" 95 | ); 96 | Ok(()) 97 | } 98 | 99 | #[test] 100 | pub fn signed_parse_multiple_newlines() -> Result<()> { 101 | let s = r#" 102 | 103 | -----BEGIN CREV PACKAGE REVIEW----- 104 | version: -1 105 | date: "2018-12-18T23:10:21.111854021-08:00" 106 | from: 107 | id-type: crev 108 | id: FYlr8YoYGVvDwHQxqEIs89reKKDy-oWisoO0qXXEfHE 109 | url: "https://github.com/dpc/crev-proofs" 110 | package: 111 | source: "https://crates.io" 112 | name: log 113 | version: 0.4.6 114 | digest: BhDmOOjfESqs8i3z9qsQANH8A39eKklgQKuVtrwN-Tw 115 | review: 116 | thoroughness: low 117 | understanding: medium 118 | rating: positive 119 | -----BEGIN CREV PACKAGE REVIEW SIGNATURE----- 120 | 4R2WjtU-avpBznmJYAl44H1lOYgETu3RSNhCDcB4GpqhJbSRkd-eqnUuhHgDUs77OlhUf7BSA0dydxaALwx0Dg 121 | -----END CREV PACKAGE REVIEW----- 122 | 123 | 124 | 125 | -----BEGIN CREV PACKAGE REVIEW----- 126 | version: -1 127 | date: "2018-12-18T23:10:21.111854021-08:00" 128 | from: 129 | id-type: crev 130 | id: FYlr8YoYGVvDwHQxqEIs89reKKDy-oWisoO0qXXEfHE 131 | url: "https://github.com/dpc/crev-proofs" 132 | package: 133 | source: "https://crates.io" 134 | name: log 135 | version: 0.4.6 136 | digest: BhDmOOjfESqs8i3z9qsQANH8A39eKklgQKuVtrwN-Tw 137 | review: 138 | thoroughness: low 139 | understanding: medium 140 | rating: positive 141 | -----BEGIN CREV PACKAGE REVIEW SIGNATURE----- 142 | 4R2WjtU-avpBznmJYAl44H1lOYgETu3RSNhCDcB4GpqhJbSRkd-eqnUuhHgDUs77OlhUf7BSA0dydxaALwx0Dg 143 | -----END CREV PACKAGE REVIEW----- 144 | 145 | "#; 146 | 147 | let proofs = Proof::parse_from(s.as_bytes())?; 148 | assert_eq!(proofs.len(), 2); 149 | assert_eq!( 150 | proofs[0].signature(), 151 | "4R2WjtU-avpBznmJYAl44H1lOYgETu3RSNhCDcB4GpqhJbSRkd-eqnUuhHgDUs77OlhUf7BSA0dydxaALwx0Dg" 152 | ); 153 | assert_eq!( 154 | proofs[1].signature(), 155 | "4R2WjtU-avpBznmJYAl44H1lOYgETu3RSNhCDcB4GpqhJbSRkd-eqnUuhHgDUs77OlhUf7BSA0dydxaALwx0Dg" 156 | ); 157 | Ok(()) 158 | } 159 | 160 | pub fn generate_id_and_proof() -> Result<(UnlockedId, Proof)> { 161 | let id = UnlockedId::generate(Some(Url::new_git("https://mypage.com/trust.git"))); 162 | 163 | let package_info = proof::PackageInfo { 164 | id: proof::PackageVersionId::new( 165 | "SOURCE_ID".to_owned(), 166 | "name".into(), 167 | Version::parse("1.0.0").unwrap(), 168 | ), 169 | digest: vec![0, 1, 2, 3], 170 | digest_type: proof::default_digest_type(), 171 | revision: String::new(), 172 | revision_type: proof::default_revision_type(), 173 | }; 174 | let review = proof::review::CodeBuilder::default() 175 | .from(id.id.clone()) 176 | .package(package_info) 177 | .comment("comment".into()) 178 | .files(vec![ 179 | proof::review::code::File { 180 | path: PathBuf::from("foo.x"), 181 | digest: vec![1, 2, 3, 4], 182 | digest_type: "sha256".into(), 183 | }, 184 | proof::review::code::File { 185 | path: PathBuf::from("foo.x"), 186 | digest: vec![1, 2, 3, 4], 187 | digest_type: "sha256".into(), 188 | }, 189 | ]) 190 | .build() 191 | .map_err(|e| Error::BuildingReview(e.to_string().into()))?; 192 | 193 | let proof = review.sign_by(&id)?; 194 | 195 | Ok((id, proof)) 196 | } 197 | 198 | #[test] 199 | pub fn sign_proof_review() -> Result<()> { 200 | let (_id, proof) = generate_id_and_proof()?; 201 | 202 | proof.verify()?; 203 | println!("{proof}"); 204 | 205 | Ok(()) 206 | } 207 | 208 | #[test] 209 | pub fn verify_works() -> Result<()> { 210 | let (_id, proof) = generate_id_and_proof()?; 211 | 212 | let proof = Proof::from_parts(proof.body().to_owned() + "\n", proof.signature().to_owned())?; 213 | 214 | assert!(proof.verify().is_err()); 215 | 216 | Ok(()) 217 | } 218 | 219 | #[test] 220 | pub fn ensure_serializes_to_valid_proof_works() -> Result<()> { 221 | let a = UnlockedId::generate_for_git_url("https://a"); 222 | let digest = vec![0; 32]; 223 | let package = proof::PackageInfo { 224 | id: proof::PackageVersionId::new( 225 | "source".into(), 226 | "name".into(), 227 | Version::parse("1.0.0").unwrap(), 228 | ), 229 | digest, 230 | digest_type: proof::default_digest_type(), 231 | revision: String::new(), 232 | revision_type: proof::default_revision_type(), 233 | }; 234 | 235 | let mut package = a.as_public_id().create_package_review_proof( 236 | package, 237 | Default::default(), 238 | vec![], 239 | "a".into(), 240 | )?; 241 | assert!(package.ensure_serializes_to_valid_proof().is_ok()); 242 | package.comment = "a".repeat(32_000); 243 | assert!(package.ensure_serializes_to_valid_proof().is_err()); 244 | Ok(()) 245 | } 246 | 247 | #[test] 248 | pub fn parse_package_overrides() -> Result<()> { 249 | let s = r#" 250 | version: -1 251 | date: "2018-12-18T23:10:21.111854021-08:00" 252 | from: 253 | id-type: crev 254 | id: FYlr8YoYGVvDwHQxqEIs89reKKDy-oWisoO0qXXEfHE 255 | url: "https://github.com/dpc/crev-proofs" 256 | package: 257 | source: "https://crates.io" 258 | name: log 259 | version: 0.4.6 260 | digest: BhDmOOjfESqs8i3z9qsQANH8A39eKklgQKuVtrwN-Tw 261 | review: 262 | thoroughness: low 263 | understanding: medium 264 | rating: positive 265 | override: 266 | - id-type: crev 267 | id: "-sApEowWcAS9J0R7aO18cghvhLBpuMhyeUuWQq_fits" 268 | url: "https://github.com/foo/bar" 269 | comment: TEST 270 | "#; 271 | 272 | let proof: proof::package::Package = serde_yaml::from_str(s).expect("deserialization failed"); 273 | 274 | proof.validate_data()?; 275 | 276 | let draft = proof.to_draft(); 277 | 278 | assert_eq!(proof.override_.len(), 1); 279 | assert!(draft.body.contains("override:")); 280 | assert!(draft 281 | .body 282 | .contains("-sApEowWcAS9J0R7aO18cghvhLBpuMhyeUuWQq_fits")); 283 | 284 | let new_proof = proof.apply_draft(&draft.body)?; 285 | 286 | assert_eq!(proof.override_.len(), new_proof.override_.len()); 287 | 288 | Ok(()) 289 | } 290 | -------------------------------------------------------------------------------- /crev-data/src/url.rs: -------------------------------------------------------------------------------- 1 | use derive_builder::Builder; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, Builder, Serialize, Deserialize, PartialEq, Eq, Hash)] 5 | pub struct Url { 6 | pub url: String, 7 | #[serde( 8 | rename = "url-type", 9 | skip_serializing_if = "equals_default_url_type", 10 | default = "default_url_type" 11 | )] 12 | pub url_type: String, 13 | } 14 | 15 | impl Url { 16 | pub fn new_git>(url: Stringy) -> Self { 17 | Self { 18 | url: url.into(), 19 | url_type: default_url_type(), 20 | } 21 | } 22 | 23 | #[must_use] 24 | pub fn digest(&self) -> crate::Digest { 25 | let digest = crev_common::blake2b256sum(self.url.to_ascii_lowercase().as_bytes()); 26 | digest.into() 27 | } 28 | } 29 | 30 | pub(crate) fn equals_default_url_type(s: &str) -> bool { 31 | s == default_url_type() 32 | } 33 | 34 | pub(crate) fn default_url_type() -> String { 35 | "git".into() 36 | } 37 | -------------------------------------------------------------------------------- /crev-data/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | use rand::{self, Rng}; 2 | use std::fmt; 3 | 4 | #[must_use] 5 | pub fn random_id_str() -> String { 6 | let mut out = [0u8; 32]; 7 | rand::thread_rng().fill(&mut out[..]); 8 | crev_common::base64_encode(&out) 9 | } 10 | 11 | pub fn write_comment_proof(comment: &str, f: &mut dyn fmt::Write) -> fmt::Result { 12 | if comment.is_empty() { 13 | return Ok(()); 14 | } 15 | writeln!(f, "comment: |-")?; 16 | for line in comment.lines() { 17 | writeln!(f, " {line}")?; 18 | } 19 | Ok(()) 20 | } 21 | 22 | pub fn write_comment_draft(comment: &str, f: &mut dyn fmt::Write) -> fmt::Result { 23 | writeln!(f, "comment: |-")?; 24 | for line in comment.lines() { 25 | writeln!(f, " {line}")?; 26 | } 27 | if comment.is_empty() { 28 | writeln!(f, " ")?; 29 | } 30 | Ok(()) 31 | } 32 | 33 | #[macro_export] 34 | macro_rules! serde_content_serialize { 35 | ($self: ident, $fmt: ident) => { 36 | // Remove comment for manual formatting 37 | let mut clone = $self.clone(); 38 | let mut comment = String::new(); 39 | std::mem::swap(&mut comment, &mut clone.comment); 40 | 41 | if clone.common.kind.is_none() { 42 | clone.common.kind = Some(Self::KIND.into()); 43 | } 44 | 45 | crev_common::serde::write_as_headerless_yaml(&clone, $fmt)?; 46 | $crate::util::write_comment_proof(comment.as_str(), $fmt)?; 47 | }; 48 | } 49 | 50 | #[macro_export] 51 | macro_rules! serde_draft_serialize { 52 | ($self: ident, $fmt: ident) => { 53 | // Remove comment for manual formatting 54 | let mut clone = $self.clone(); 55 | let mut comment = String::new(); 56 | std::mem::swap(&mut comment, &mut clone.comment); 57 | 58 | crev_common::serde::write_as_headerless_yaml(&clone, $fmt)?; 59 | $crate::util::write_comment_draft(comment.as_str(), $fmt)?; 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /crev-lib/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crev-lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crev-lib" 3 | description = "Library interface for programmatic access to crate reviews of cargo-crev" 4 | documentation = "https://docs.rs/crev-lib" 5 | homepage = "https://github.com/crev-dev/cargo-crev" 6 | keywords = ["code", "review", "assessment", "dependency", "assurance"] 7 | categories = ["development-tools", "database"] 8 | readme = "README.md" 9 | authors.workspace = true 10 | edition.workspace = true 11 | license.workspace = true 12 | repository.workspace = true 13 | rust-version.workspace = true 14 | version.workspace = true 15 | 16 | [dependencies] 17 | crev-common.workspace = true 18 | crev-data.workspace = true 19 | crev-wot.workspace = true 20 | 21 | blake2.workspace = true 22 | chrono.workspace = true 23 | crev-recursive-digest = "0.6" 24 | default.workspace = true 25 | directories = { version = "2", package = "directories-next" } 26 | fnv = "1.0.7" 27 | rust-argon2 = "2.0.0" 28 | git2.workspace = true 29 | log = "0.4.20" 30 | resiter.workspace = true 31 | serde.workspace = true 32 | serde_cbor = "0.11.2" 33 | serde_yaml.workspace = true 34 | walkdir = "2.3.3" 35 | thiserror.workspace = true 36 | rayon.workspace = true 37 | aes-siv = "0.7.0" 38 | bstr = "1.6.2" 39 | 40 | [package.metadata.release] 41 | shared-version=true 42 | -------------------------------------------------------------------------------- /crev-lib/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /crev-lib/README.md: -------------------------------------------------------------------------------- 1 | # `crev-lib` 2 | 3 | Bulk of common functionality provided by 4 | [`cargo-crev`](https://lib.rs/cargo-crev). This crate is supposed to be what 5 | `libgit2` is for `git`. 6 | -------------------------------------------------------------------------------- /crev-lib/rc/doc/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Proof Repository 4 | 5 | This git repository is a [Crev Proof 6 | Repository](https://github.com/crev-dev/crev/wiki/Proof-Repository). 7 | 8 | 9 | -------------------------------------------------------------------------------- /crev-lib/rc/doc/editing-code-review.md: -------------------------------------------------------------------------------- 1 | # Creating Code Review Proof 2 | 3 | A Code Review Proof records results of your file-by-file review of a software 4 | project. 5 | 6 | ## Responsibility 7 | 8 | It is important that your review is truthful. At the very least, make sure to 9 | adjust the `thoroughness` and `understanding` correctly. 10 | 11 | Other users might use information you provide, to judge software quality and 12 | trustworthiness. 13 | 14 | Your Proofs are cryptographically signed and will circulate in the ecosystem. 15 | While there is no explicit or implicit legal responsibility attached to using 16 | `crev` system, other people will most probably use it to judge you, your other 17 | work, etc. 18 | 19 | ## Data fields 20 | 21 | - `date` - proof timestamp 22 | - `from` - proof author 23 | - `review` - review details 24 | - `revision` - revision-system checksum at the time of the review 25 | - `digest` - recursive digest of the whole project content 26 | - `thoroughness` - time and effort spent on the review 27 | - `high` - long, deep, focused review - possibly as a part of a formal 28 | security review; "hour or more per file" 29 | - `medium` - a standard, focused code review of a decent depth; "~15 minutes 30 | per file" 31 | - `low` - low intensity review: "~2 minutes per file" 32 | - `none` - no review, or just skimming; "seconds per file"; still useful for 33 | a trusted or reputable project or when when proof is created to warn about 34 | problems 35 | - `understanding` 36 | - `high` - complete understanding 37 | - `medium` - good understanding 38 | - `low` - some parts are unclear 39 | - `none` - lack of understanding 40 | - `rating` 41 | - `strong` - secure and good in all respects, for all applications 42 | - `positive` - secure and ok to use; possibly minor issues 43 | - `neutral` - secure but with flaws 44 | - `negative` - severe flaws and not ok for production usage 45 | - `dangerous` - unsafe to use; severe flaws and/or possibly malicious 46 | - `comment` - human-readable information about this review (e.g. why it was 47 | done, how, and `rating` explanation) 48 | - `files` - list of reviewed files 49 | 50 | ## Further reading 51 | 52 | See 53 | wiki page for more information and Frequently Asked Questions, or join 54 | discussion channel. 55 | -------------------------------------------------------------------------------- /crev-lib/rc/doc/editing-package-review.md: -------------------------------------------------------------------------------- 1 | # Creating Package Review Proof 2 | 3 | A Package Review Proof records results of your review of a version/release of a 4 | software package. 5 | 6 | ## Responsibility 7 | 8 | It is important that your review is truthful. At the very least, make sure to 9 | adjust the `thoroughness` and `understanding` correctly. 10 | 11 | Other users might use information you provide, to judge software quality and 12 | trustworthiness. 13 | 14 | Your Proofs are cryptographically signed and will circulate in the ecosystem. 15 | While there is no explicit or implicit legal responsibility attached to using 16 | `crev` system, other people will most probably use it to judge you, your other 17 | work, etc. 18 | 19 | By creating and publishing proofs, you implicitly agree to other people freely 20 | using them. 21 | 22 | ## Data fields 23 | 24 | - `review` - review of particular version of the crate; all fields set to `none` 25 | mean that no review took place; whole section can be deleted for the same 26 | effect; 27 | - `digest` - recursive digest of the whole project content 28 | - `thoroughness` - time and effort spent on the review 29 | - `high` - long, deep, focused review - possibly as a part of a formal 30 | security review; "hour or more per file" 31 | - `medium` - a standard, focused code review of a decent depth; "~15 minutes 32 | per file" 33 | - `low` - low intensity review: "~2 minutes per file" 34 | - `none` - no review, or just skimming; "seconds per file"; still useful for 35 | a trusted or reputable project or when when proof is created to warn about 36 | problems 37 | - `understanding` 38 | - `high` - complete understanding 39 | - `medium` - good understanding 40 | - `low` - some parts are unclear 41 | - `none` - lack of understanding 42 | - `rating` 43 | - `strong` - secure and good in all respects, for all applications 44 | - `positive` - secure and ok to use; possibly minor issues 45 | - `neutral` - secure but with flaws 46 | - `negative` - severe flaws and not ok for production usage 47 | - `dangerous` - unsafe to use; severe flaws and/or possibly malicious 48 | - `advisories` - advisories mark package versions containing an important fix 49 | (list) 50 | - `ids` - list of IDs identifying the issue being fixed 51 | - `range` - versions are potentially affected 52 | - `all` - all previous versions 53 | - `major` - all previous versions within the same major release version 54 | - `minor` - all previous versions within the same minor release version 55 | - `severity` 56 | - `high` - critical issue (often with security implications) 57 | - `medium` - important 58 | - `low` - low severity 59 | - `issues` - issues report a problem in a release (list) 60 | - `id` - an ID of an issue 61 | - `severity` - same as in the `advisories` section 62 | - `alternatives` - potential alternatives, similar or better; elements of the 63 | list with an empty `name` will be automatically ignored and removed 64 | - `flags` - additional flags 65 | - `unmaintained` - package is not maintained or abandoned; **NOTE**: this flag 66 | applies to the whole package, not only current version, like in most other 67 | data fields 68 | - `comment` - human-readable information about this review (e.g. why it was 69 | done, how, and `rating` explanation) 70 | - `override` - list of Ids from which to override (ignore) reviews for target 71 | package 72 | 73 | # Other information 74 | 75 | More recent proofs overwrite older ones. 76 | 77 | ## Further reading 78 | 79 | See 80 | wiki page for more information and Frequently Asked Questions, or join 81 | discussion channel. 82 | -------------------------------------------------------------------------------- /crev-lib/rc/doc/editing-trust.md: -------------------------------------------------------------------------------- 1 | # Creating Trust Proof 2 | 3 | A Trust Proof records your trust in motivations, abilities and standards 4 | of another entity in the `crev` system. 5 | 6 | All trust levels (except distrust) are positive. The `none` level is 7 | considered a default between two users that don't know each other. 8 | Anything above is "more than nothing". 9 | 10 | ## On Distrust 11 | 12 | Distrust is the only punitive trust level. The purpose of it is to mark 13 | malicious or otherwise harmful entries and warn other users about them. 14 | It should not be used lightly, and always include comment explaining the 15 | reason for trying to exclude them from WoT of others, possibly including 16 | links to discussion about it. 17 | 18 | Before creating distrust proof, it is suggested to try to contact the user 19 | to correct their wrong-doing, and/or get a second opinion about it. 20 | 21 | Example reasons to use `distrust` level: 22 | 23 | * User possibly knowingly creates positive reviews for malicious/broken/poor 24 | quality crates. 25 | * User carelessly inflates their `thoroughness` and `understanding` level 26 | which might mislead others. 27 | * User possibly knowingly holds other offending users in high trust 28 | and refuses to lower/remove them. 29 | * User creates unwaranted distrust proofs. 30 | 31 | Example reasons *NOT* to use distrust level: 32 | 33 | * User creates low `thoroughness` and `understanding` reviews. As long as 34 | self-reported `thoroughness` and `understanding` levels are truthful, 35 | such reviews are still beneficial to the community and it's up to other 36 | users to filter them out with `--thoroughness X` and `--understanding X` 37 | flags if they don't want to use them. 38 | * Users review criteria don't match my higher quality standards. Again, 39 | within reason that does not endanger the community, it is a 40 | reasponsibility of other users to assign lower trust levels to parties 41 | that either can't be trusted to do a good job of reviewing code or judge 42 | quality and standards of other users. Lower trust level of the such party, 43 | and any other party that might transitively introduce them in your WoT 44 | with higher level. Use `--level X` flag to filter out reviews from parties 45 | below certain trust level. 46 | 47 | ## Responsibility 48 | 49 | While `crev` does not directly expose you to any harm from entities you trust, 50 | adding untrustworthy entities into your Web of Trust, might lower your overall 51 | security and/or reputation. 52 | 53 | On the other hand, the more trustworthy entities in your Web of Trust, the 54 | broader the reach of it and more data it can find. 55 | 56 | Your Proofs are cryptographically signed and will circulate in the ecosystem. 57 | While there is no explicit or implicit legal responsibility attached to using 58 | `crev` system, other people will most probably use it to judge you, your other 59 | work, etc. 60 | 61 | By creating and publishing proofs, you implicitly agree to other people freely 62 | using them. 63 | 64 | ## Data fields 65 | 66 | - `trust` - trust level; possible values: 67 | - `high` - "for most practical purposes, I trust this user to do as good or 68 | better work than myself" e.g. "my dayjob ID", "known and reputable expert", 69 | "employee within the same team" 70 | - `medium` - "I trust this user, but not as much as I would trust myself; their 71 | review standards might be OK, but lower standard than mine" 72 | - `low` - "I have some reservations about trusting this entity, but I still 73 | consider their decisions as somewhat valuable" 74 | - `none` - "I don't know or trust this entity"; use to revoke trust (or 75 | distrust) from a previously issued Trust Proof and/or just advertise user's 76 | existence; 77 | - `distrust` - "I think this user is malicious/harmful"; 78 | - `comment` - human-readable information about this trust relationship, (e.g. 79 | who are these entities, why do you trust them) 80 | - `override` - list of Ids from which to override (ignore) trust for target Id(s) 81 | 82 | # Other information 83 | 84 | More recent proofs overwrite older ones. 85 | 86 | ## Further reading 87 | 88 | See 89 | wiki page for more information and Frequently Asked Questions, or join 90 | discussion channel. 91 | -------------------------------------------------------------------------------- /crev-lib/rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | # imports_granularity="Crate" 3 | 4 | -------------------------------------------------------------------------------- /crev-lib/src/activity.rs: -------------------------------------------------------------------------------- 1 | //! Activities track user actions to help verified 2 | //! multi-step flows, and confirm user intention. 3 | //! 4 | //! Eg. when user reviews a package we record details 5 | //! and we can warn them if they attempt to create 6 | //! a proof review which they haven't previously reviewed. 7 | use crev_common::{ 8 | self, 9 | serde::{as_rfc3339_fixed, from_rfc3339_fixed}, 10 | }; 11 | use crev_data::Version; 12 | use serde::{Deserialize, Serialize}; 13 | 14 | pub type Date = chrono::DateTime; 15 | 16 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 17 | pub enum ReviewMode { 18 | Full, 19 | Differential, 20 | } 21 | 22 | impl ReviewMode { 23 | #[must_use] 24 | pub fn is_diff(self) -> bool { 25 | self == ReviewMode::Differential 26 | } 27 | 28 | #[must_use] 29 | pub fn is_full(self) -> bool { 30 | self == ReviewMode::Full 31 | } 32 | 33 | #[must_use] 34 | pub fn from_diff_flag(diff: bool) -> Self { 35 | if diff { 36 | ReviewMode::Differential 37 | } else { 38 | ReviewMode::Full 39 | } 40 | } 41 | } 42 | 43 | /// Which review is the most recent 44 | #[derive(Debug, Clone, Serialize, Deserialize)] 45 | pub struct LatestReviewActivity { 46 | pub source: String, 47 | pub name: String, 48 | pub version: Version, 49 | pub diff_base: Option, 50 | } 51 | 52 | /// Record of an in-progress review 53 | #[derive(Debug, Clone, Serialize, Deserialize)] 54 | pub struct ReviewActivity { 55 | #[serde( 56 | serialize_with = "as_rfc3339_fixed", 57 | deserialize_with = "from_rfc3339_fixed" 58 | )] 59 | pub timestamp: Date, 60 | pub diff_base: Option, 61 | } 62 | 63 | impl ReviewActivity { 64 | #[must_use] 65 | pub fn new(diff_base: Option) -> Self { 66 | Self { 67 | timestamp: crev_common::now(), 68 | diff_base, 69 | } 70 | } 71 | 72 | #[must_use] 73 | pub fn to_review_mode(&self) -> ReviewMode { 74 | if self.diff_base.is_some() { 75 | ReviewMode::Differential 76 | } else { 77 | ReviewMode::Full 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /crev-lib/src/id.rs: -------------------------------------------------------------------------------- 1 | //! `LockedId` is for you, the local crev user. `Id` is for identifying other users. 2 | 3 | use crate::{Error, Result}; 4 | use aes_siv::KeyInit; 5 | use argon2::{self, Config}; 6 | use crev_common::{ 7 | rand::random_vec, 8 | serde::{as_base64, from_base64}, 9 | }; 10 | use crev_data::id::{PublicId, UnlockedId}; 11 | use serde::{Deserialize, Serialize}; 12 | use std::{self, fmt, io::BufReader, path::Path}; 13 | 14 | const CURRENT_LOCKED_ID_SERIALIZATION_VERSION: i64 = -1; 15 | 16 | /// Callback to read the password 17 | pub type PassphraseFn<'a> = &'a dyn Fn() -> std::io::Result; 18 | 19 | /// Stored in your config to know how to hash your passphrase 20 | #[derive(Serialize, Deserialize, Debug, Clone)] 21 | pub struct PassphraseConfig { 22 | version: u32, 23 | variant: String, 24 | iterations: u32, 25 | #[serde(rename = "memory-size")] 26 | memory_size: u32, 27 | lanes: Option, 28 | #[serde(serialize_with = "as_base64", deserialize_with = "from_base64")] 29 | salt: Vec, 30 | } 31 | 32 | /// Serialized, stored on disk 33 | #[derive(Serialize, Deserialize, Debug, Clone)] 34 | pub struct LockedId { 35 | version: i64, 36 | 37 | /// Where your crev-proofs git repo is 38 | #[serde(flatten)] 39 | pub url: Option, 40 | 41 | /// This is used in `PublicId` to identify users 42 | #[serde(serialize_with = "as_base64", deserialize_with = "from_base64")] 43 | #[serde(rename = "public-key")] 44 | pub public_key: Vec, 45 | 46 | /// Needs passphrase 47 | #[serde(serialize_with = "as_base64", deserialize_with = "from_base64")] 48 | #[serde(rename = "sealed-secret-key")] 49 | sealed_secret_key: Vec, 50 | 51 | #[serde(serialize_with = "as_base64", deserialize_with = "from_base64")] 52 | #[serde(rename = "seal-nonce")] 53 | seal_nonce: Vec, 54 | 55 | #[serde(rename = "pass")] 56 | passphrase_config: PassphraseConfig, 57 | } 58 | 59 | impl fmt::Display for LockedId { 60 | /// Somewhat surprisingly, you get full YAML of the file 61 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 62 | f.write_str(&serde_yaml::to_string(self).map_err(|_| fmt::Error)?) 63 | } 64 | } 65 | 66 | /// Parses YAML file 67 | impl std::str::FromStr for LockedId { 68 | type Err = serde_yaml::Error; 69 | 70 | fn from_str(yaml_str: &str) -> std::result::Result { 71 | serde_yaml::from_str::(yaml_str) 72 | } 73 | } 74 | 75 | impl LockedId { 76 | /// Encrypt and throw away the key 77 | pub fn from_unlocked_id(unlocked_id: &UnlockedId, passphrase: &str) -> Result { 78 | let config = if !passphrase.is_empty() { 79 | Config { 80 | variant: argon2::Variant::Argon2id, 81 | version: argon2::Version::Version13, 82 | 83 | hash_length: 64, 84 | mem_cost: 4096, 85 | time_cost: 192, 86 | 87 | lanes: std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1) as u32, 88 | 89 | ad: &[], 90 | secret: &[], 91 | } 92 | } else { 93 | Self::weak_passphrase_config() 94 | }; 95 | 96 | let pwsalt = random_vec(32); 97 | let pwhash = 98 | argon2::hash_raw(passphrase.as_bytes(), &pwsalt, &config).map_err(Error::Passphrase)?; 99 | 100 | let seal_nonce = random_vec(32); 101 | let sealed_secret_key = { 102 | use aes_siv::{aead::generic_array::GenericArray, siv::IV_SIZE}; 103 | 104 | let secret = unlocked_id.keypair.secret.as_bytes(); 105 | let mut siv = aes_siv::siv::Aes256Siv::new(&GenericArray::clone_from_slice(&pwhash)); 106 | let mut buffer = vec![0; IV_SIZE + secret.len()]; 107 | buffer[IV_SIZE..].copy_from_slice(secret); 108 | let tag = siv 109 | .encrypt_in_place_detached([&[] as &[u8], &seal_nonce], &mut buffer[IV_SIZE..]) 110 | .expect("aes-encrypt"); 111 | buffer[..IV_SIZE].copy_from_slice(&tag); 112 | buffer 113 | }; 114 | 115 | Ok(LockedId { 116 | version: CURRENT_LOCKED_ID_SERIALIZATION_VERSION, 117 | public_key: unlocked_id.keypair.public.to_bytes().to_vec(), 118 | sealed_secret_key, 119 | seal_nonce, 120 | url: unlocked_id.url().cloned(), 121 | passphrase_config: PassphraseConfig { 122 | salt: pwsalt, 123 | iterations: config.time_cost, 124 | memory_size: config.mem_cost, 125 | version: 0x13, 126 | lanes: Some(config.lanes), 127 | variant: config.variant.as_lowercase_str().to_string(), 128 | }, 129 | }) 130 | } 131 | 132 | /// Extract only the public identity part from all data. Useful for displaying user's identity. 133 | #[must_use] 134 | pub fn to_public_id(&self) -> PublicId { 135 | PublicId::new_from_pubkey(self.public_key.clone(), self.url.clone()) 136 | .expect("Invalid locked id.") 137 | } 138 | 139 | #[must_use] 140 | pub fn pub_key_as_base64(&self) -> String { 141 | crev_common::base64_encode(&self.public_key) 142 | } 143 | 144 | /// Write the Id to this file, overwriting it 145 | pub fn save_to(&self, path: &Path) -> Result<()> { 146 | let s = self.to_string(); 147 | crev_common::store_str_to_file(path, &s).map_err(|e| Error::FileWrite(e, path.into())) 148 | } 149 | 150 | pub fn read_from_yaml_file(path: &Path) -> Result { 151 | let mut file = BufReader::new( 152 | std::fs::File::open(path) 153 | .map_err(|e| Error::IdLoadError(Box::new((path.into(), e))))?, 154 | ); 155 | 156 | Ok(serde_yaml::from_reader(&mut file)?) 157 | } 158 | 159 | /// Decrypt 160 | pub fn to_unlocked(&self, passphrase: &str) -> Result { 161 | let LockedId { 162 | ref version, 163 | ref url, 164 | ref public_key, 165 | ref sealed_secret_key, 166 | ref seal_nonce, 167 | ref passphrase_config, 168 | } = self; 169 | { 170 | if *version > CURRENT_LOCKED_ID_SERIALIZATION_VERSION { 171 | return Err(Error::UnsupportedVersion(*version)); 172 | } 173 | let mut config = Config { 174 | variant: argon2::Variant::from_str(&passphrase_config.variant)?, 175 | version: argon2::Version::Version13, 176 | 177 | hash_length: 64, 178 | mem_cost: passphrase_config.memory_size, 179 | time_cost: passphrase_config.iterations, 180 | 181 | lanes: std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1) as u32, 182 | 183 | ad: &[], 184 | secret: &[], 185 | }; 186 | 187 | if let Some(lanes) = passphrase_config.lanes { 188 | config.lanes = lanes; 189 | } else { 190 | log::error!( 191 | "`lanes` not configured. Old bug. See: https://github.com/crev-dev/cargo-crev/issues/151" 192 | ); 193 | log::info!("Using `lanes: {}`", config.lanes); 194 | } 195 | 196 | let passphrase_hash = 197 | argon2::hash_raw(passphrase.as_bytes(), &passphrase_config.salt, &config)?; 198 | 199 | let secret_key = { 200 | use aes_siv::{aead::generic_array::GenericArray, siv::IV_SIZE, Tag}; 201 | 202 | let mut siv = 203 | aes_siv::siv::Aes256Siv::new(&GenericArray::clone_from_slice(&passphrase_hash)); 204 | let mut buffer = sealed_secret_key.clone(); 205 | let tag = Tag::clone_from_slice(&buffer[..IV_SIZE]); 206 | siv.decrypt_in_place_detached( 207 | [&[] as &[u8], seal_nonce], 208 | &mut buffer[IV_SIZE..], 209 | &tag, 210 | ) 211 | .map_err(|_| Error::IncorrectPassphrase)?; 212 | buffer.drain(..IV_SIZE); 213 | buffer 214 | }; 215 | 216 | assert!(!secret_key.is_empty()); 217 | 218 | let result = UnlockedId::new(url.clone(), &secret_key)?; 219 | if public_key != &result.keypair.public.to_bytes() { 220 | return Err(Error::PubKeyMismatch); 221 | } 222 | Ok(result) 223 | } 224 | } 225 | 226 | /// Used for temporary/default identity, but obviously not very secure to store 227 | #[must_use] 228 | pub fn has_no_passphrase(&self) -> bool { 229 | self.passphrase_config.iterations == 1 && self.to_unlocked("").is_ok() 230 | } 231 | 232 | /// Config for empty passphrase. User chose no security, so they're getting none. 233 | fn weak_passphrase_config() -> Config<'static> { 234 | Config { 235 | variant: argon2::Variant::Argon2id, 236 | version: argon2::Version::Version13, 237 | 238 | hash_length: 64, 239 | mem_cost: 16, 240 | time_cost: 1, 241 | 242 | lanes: 1, 243 | 244 | ad: &[], 245 | secret: &[], 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /crev-lib/src/proof.rs: -------------------------------------------------------------------------------- 1 | use crate::{ProofStore, TrustLevel}; 2 | use crev_data::proof::{self, CommonOps}; 3 | use std::path::PathBuf; 4 | 5 | fn proof_store_names(proof: &proof::Proof) -> (&str, Option<&str>) { 6 | match proof.kind() { 7 | proof::CodeReview::KIND => ("reviews", Some("code")), 8 | proof::PackageReview::KIND => ("reviews", Some("package")), 9 | proof::Trust::KIND => ("trust", None), 10 | _ => ("other", None), 11 | } 12 | } 13 | 14 | /// The path to use under user store 15 | pub(crate) fn rel_store_path(proof: &proof::Proof, host_salt: &[u8]) -> PathBuf { 16 | let (type_name, type_subname) = proof_store_names(proof); 17 | let date = proof.date_utc().format("%Y-%m").to_string(); 18 | let path = PathBuf::from(proof.author_id().to_string()).join(type_name); 19 | let mut host_full_id = host_salt.to_vec(); 20 | host_full_id.append(&mut proof.author_id().to_bytes()); 21 | let host_plus_id_digest = crev_common::blake2b256sum(&host_full_id); 22 | 23 | path.join(if let Some(type_subname) = type_subname { 24 | format!( 25 | "{}-{}-{}", 26 | date, 27 | type_subname, 28 | // this used to be `[..4]`, but temporarily change it 29 | // to accommodate a new proof format. old clients will 30 | // fail to parse a whole file if it contains a new proof 31 | // format, so this makes sure new proofs are only 32 | // in separate files; this can be reverted back after 33 | // some time 34 | crev_common::base64_encode(&host_plus_id_digest[1..5]) 35 | ) 36 | } else { 37 | format!( 38 | "{}-{}", 39 | date, 40 | crev_common::base64_encode(&host_plus_id_digest[1..5]) 41 | ) 42 | }) 43 | .with_extension("proof.crev") 44 | } 45 | 46 | pub fn store_id_trust_proof( 47 | proof: &crev_data::proof::Proof, 48 | ids: &[crev_data::Id], 49 | trust_level: TrustLevel, 50 | commit: bool, 51 | ) -> crate::Result<()> { 52 | let local = crate::Local::auto_open()?; 53 | local.insert(proof)?; 54 | if commit { 55 | let commit_message = create_id_trust_commit_message(ids, trust_level); 56 | local.proof_dir_commit(&commit_message)?; 57 | } 58 | Ok(()) 59 | } 60 | 61 | fn create_id_trust_commit_message(ids: &[crev_data::Id], trust_level: TrustLevel) -> String { 62 | let string_ids = ids 63 | .iter() 64 | .map(|id| id.to_string()) 65 | .collect::>() 66 | .join(", "); 67 | format!( 68 | "{proof_type} for {ids}", 69 | proof_type = match trust_level { 70 | TrustLevel::None => "Remove trust", 71 | TrustLevel::Distrust => "Set distrust", 72 | TrustLevel::Low | TrustLevel::Medium | TrustLevel::High => "Add trust", 73 | }, 74 | ids = string_ids 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /crev-lib/src/repo/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{local::Local, util, verify_package_digest, Error, Result}; 2 | use crev_data::{proof, Digest}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use std::{ 6 | collections::HashSet, 7 | fs, 8 | path::{Path, PathBuf}, 9 | }; 10 | 11 | pub mod staging; 12 | 13 | #[derive(Serialize, Deserialize, Debug, Clone)] 14 | pub struct PackageConfig { 15 | pub version: u64, 16 | #[serde(rename = "trust-root")] 17 | pub trust_root: String, 18 | } 19 | 20 | const CREV_DOT_NAME: &str = ".crev"; 21 | 22 | #[derive(thiserror::Error, Debug)] 23 | #[error("Package config not-initialized. Use `crev package init` to generate it.")] 24 | pub struct PackageDirNotFound; 25 | 26 | fn find_package_root_dir() -> std::result::Result { 27 | let path = Path::new(".") 28 | .canonicalize() 29 | .map_err(|_| PackageDirNotFound)?; 30 | let mut path = path.as_path(); 31 | loop { 32 | if path.join(CREV_DOT_NAME).is_dir() { 33 | return Ok(path.to_owned()); 34 | } 35 | path = path.parent().ok_or(PackageDirNotFound)?; 36 | } 37 | } 38 | 39 | /// `crev` repository dir inside a package dir 40 | /// 41 | /// This represents the `.crev` directory and all 42 | /// the internals of it. 43 | pub struct Repo { 44 | /// root dir, where `.crev` subdiretory resides 45 | root_dir: PathBuf, 46 | /// lazily loaded `Staging` 47 | staging: Option, 48 | } 49 | 50 | impl Repo { 51 | pub fn init(path: &Path, id_str: String) -> Result { 52 | let repo = Self::new(path)?; 53 | 54 | fs::create_dir_all(repo.dot_crev_path())?; 55 | 56 | let config_path = repo.package_config_path(); 57 | if config_path.exists() { 58 | return Err(Error::PathAlreadyExists(config_path.as_path().into())); 59 | } 60 | util::store_to_file_with(&config_path, move |w| { 61 | serde_yaml::to_writer( 62 | w, 63 | &PackageConfig { 64 | version: 0, 65 | trust_root: id_str.clone(), 66 | }, 67 | ) 68 | })??; 69 | 70 | Ok(repo) 71 | } 72 | 73 | pub fn open(path: &Path) -> Result { 74 | if !path.exists() { 75 | return Err( 76 | std::io::Error::new(std::io::ErrorKind::NotFound, "directory not found").into(), 77 | ); 78 | } 79 | 80 | Self::new(path) 81 | } 82 | 83 | pub fn auto_open() -> Result { 84 | let root_path = find_package_root_dir()?; 85 | Self::open(&root_path) 86 | } 87 | 88 | #[allow(clippy::new_ret_no_self)] 89 | fn new(root_dir: &Path) -> Result { 90 | let root_dir = root_dir.canonicalize()?; 91 | Ok(Self { 92 | root_dir, 93 | staging: None, 94 | }) 95 | } 96 | 97 | fn package_config_path(&self) -> PathBuf { 98 | self.dot_crev_path().join("config.yaml") 99 | } 100 | 101 | pub fn load_package_config(&self) -> Result { 102 | let config = self.try_load_package_config()?; 103 | config.ok_or(Error::PackageConfigNotInitialized) 104 | } 105 | 106 | pub fn try_load_package_config(&self) -> Result> { 107 | let path = self.package_config_path(); 108 | 109 | if !path.exists() { 110 | return Ok(None); 111 | } 112 | let config_str = fs::read_to_string(&path)?; 113 | 114 | Ok(Some(serde_yaml::from_str(&config_str)?)) 115 | } 116 | 117 | #[must_use] 118 | pub fn dot_crev_path(&self) -> PathBuf { 119 | self.root_dir.join(CREV_DOT_NAME) 120 | } 121 | 122 | pub fn staging(&mut self) -> Result<&mut staging::Staging> { 123 | if self.staging.is_none() { 124 | self.staging = Some(staging::Staging::open(&self.root_dir)?); 125 | } 126 | Ok(self.staging.as_mut().unwrap()) 127 | } 128 | 129 | #[must_use] 130 | pub fn get_proof_rel_store_path(&self, _proof: &proof::Proof) -> PathBuf { 131 | unimplemented!(); 132 | } 133 | 134 | pub fn package_verify( 135 | &mut self, 136 | local: &Local, 137 | allow_dirty: bool, 138 | for_id: Option, 139 | params: &crate::TrustDistanceParams, 140 | requirements: &crate::VerificationRequirements, 141 | ) -> Result { 142 | if !allow_dirty && self.is_unclean()? { 143 | return Err(Error::GitRepositoryIsNotInACleanState); 144 | } 145 | 146 | let db = local.load_db()?; 147 | let trust_set = local.trust_set_for_id(for_id.as_deref(), params, &db)?; 148 | let ignore_list = fnv::FnvHashSet::default(); 149 | let digest = crate::get_recursive_digest_for_git_dir(&self.root_dir, &ignore_list)?; 150 | Ok(verify_package_digest( 151 | &digest, 152 | &trust_set, 153 | requirements, 154 | &db, 155 | )) 156 | } 157 | 158 | pub fn package_digest(&mut self, allow_dirty: bool) -> Result { 159 | if !allow_dirty && self.is_unclean()? { 160 | return Err(Error::GitRepositoryIsNotInACleanState); 161 | } 162 | 163 | let ignore_list = HashSet::default(); 164 | crate::get_recursive_digest_for_git_dir(&self.root_dir, &ignore_list) 165 | } 166 | 167 | fn is_unclean(&self) -> Result { 168 | let git_repo = git2::Repository::open(&self.root_dir)?; 169 | if git_repo.state() != git2::RepositoryState::Clean { 170 | return Err(Error::GitRepositoryIsNotInACleanState); 171 | } 172 | let mut status_opts = git2::StatusOptions::new(); 173 | status_opts.include_unmodified(true); 174 | status_opts.include_untracked(false); 175 | let mut unclean_found = false; 176 | for entry in git_repo.statuses(Some(&mut status_opts))?.iter() { 177 | if entry.status() != git2::Status::CURRENT { 178 | unclean_found = true; 179 | } 180 | } 181 | 182 | Ok(unclean_found) 183 | } 184 | 185 | /// Uses `log::info` to "print" 186 | pub fn status(&mut self) -> Result<()> { 187 | let staging = self.staging()?; 188 | for (k, _v) in &staging.entries { 189 | log::info!("{}", k.display()); 190 | } 191 | 192 | Ok(()) 193 | } 194 | 195 | pub fn add(&mut self, file_paths: Vec) -> Result<()> { 196 | let staging = self.staging()?; 197 | for path in file_paths { 198 | staging.insert(&path)?; 199 | } 200 | staging.save()?; 201 | 202 | Ok(()) 203 | } 204 | 205 | pub fn remove(&mut self, file_paths: Vec) -> Result<()> { 206 | let staging = self.staging()?; 207 | for path in file_paths { 208 | staging.remove(&path)?; 209 | } 210 | staging.save()?; 211 | 212 | Ok(()) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /crev-lib/src/repo/staging.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, Result}; 2 | use crev_data::proof; 3 | use serde::{Deserialize, Serialize}; 4 | use std::{ 5 | collections::HashMap, 6 | fs, 7 | io::Write, 8 | path::{Path, PathBuf}, 9 | }; 10 | 11 | #[derive(Serialize, Deserialize, Debug)] 12 | pub struct StagingPathInfo { 13 | blake_hash: [u8; 32], 14 | } 15 | 16 | pub struct Staging { 17 | root_path: PathBuf, 18 | file_path: PathBuf, 19 | pub entries: HashMap, 20 | } 21 | 22 | const STAGING_FILE_NAME: &str = "staging"; 23 | 24 | impl Staging { 25 | pub fn wipe(&mut self) -> Result<()> { 26 | fs::remove_file(&self.file_path)?; 27 | Ok(()) 28 | } 29 | 30 | #[must_use] 31 | pub fn is_empty(&self) -> bool { 32 | self.entries.is_empty() 33 | } 34 | 35 | pub fn save(&mut self) -> Result<()> { 36 | self.write_to_file(&self.file_path) 37 | } 38 | 39 | pub fn open(repo_path: &Path) -> Result { 40 | let path = repo_path.join(super::CREV_DOT_NAME).join(STAGING_FILE_NAME); 41 | if !path.exists() { 42 | return Ok(Self { 43 | root_path: repo_path.to_owned(), 44 | file_path: path, 45 | entries: Default::default(), 46 | }); 47 | } 48 | 49 | let file = fs::File::open(&path)?; 50 | 51 | let path_info: HashMap = serde_cbor::from_reader(&file)?; 52 | 53 | Ok(Self { 54 | root_path: repo_path.to_owned(), 55 | file_path: path, 56 | entries: path_info, 57 | }) 58 | } 59 | 60 | fn write_to_file(&self, path: &Path) -> Result<()> { 61 | let tmp_path = path.with_extension("tmp"); 62 | let mut file = fs::File::create(&tmp_path)?; 63 | serde_cbor::to_writer(&mut file, &self.entries)?; 64 | file.flush()?; 65 | drop(file); 66 | fs::rename(tmp_path, path)?; 67 | Ok(()) 68 | } 69 | 70 | pub fn insert(&mut self, path: &Path) -> Result<()> { 71 | let full_path = path.canonicalize()?; 72 | 73 | let path = full_path 74 | .strip_prefix(&self.root_path) 75 | .map_err(|_| Error::PathNotInStageRootPath)? 76 | .to_owned(); 77 | log::info!("Adding {}", path.display()); 78 | self.entries.insert( 79 | path, 80 | StagingPathInfo { 81 | blake_hash: crev_common::blake2b256sum_file(&full_path)?, 82 | }, 83 | ); 84 | 85 | Ok(()) 86 | } 87 | 88 | pub fn remove(&mut self, path: &Path) -> Result<()> { 89 | let full_path = path.canonicalize()?; 90 | 91 | let path = full_path 92 | .strip_prefix(&self.root_path) 93 | .map_err(|_| Error::PathNotInStageRootPath)? 94 | .to_owned(); 95 | log::info!("Removing {}", path.display()); 96 | 97 | self.entries.remove(&path); 98 | 99 | Ok(()) 100 | } 101 | 102 | #[must_use] 103 | pub fn to_review_files(&self) -> Vec { 104 | self.entries 105 | .iter() 106 | .map(|(k, v)| proof::review::code::File { 107 | path: k.clone(), 108 | digest: v.blake_hash.to_vec(), 109 | digest_type: "blake2b".into(), 110 | }) 111 | .collect() 112 | } 113 | 114 | pub fn enforce_current(&self) -> Result<()> { 115 | for (rel_path, info) in &self.entries { 116 | let path = self.root_path.join(rel_path); 117 | if crev_common::blake2b256sum_file(&path)? != info.blake_hash { 118 | return Err(Error::FileNotCurrent(rel_path.as_path().into())); 119 | } 120 | } 121 | 122 | Ok(()) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /crev-lib/src/staging.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crev-lib/src/tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crev_data::{ 3 | proof::{ContentExt, PackageVersionId}, 4 | Level, UnlockedId, Url, 5 | }; 6 | use crev_wot::{FetchSource, ProofDB}; 7 | use default::default; 8 | use std::{str::FromStr, sync::Arc}; 9 | 10 | // Basic lifetime of an `LockedId`: 11 | // 12 | // * generate 13 | // * lock with a passphrase 14 | // * unlock 15 | // * compare 16 | #[test] 17 | fn lock_and_unlock() -> Result<()> { 18 | let id = UnlockedId::generate_for_git_url("https://example.com/crev-proofs"); 19 | 20 | let id_relocked = id::LockedId::from_unlocked_id(&id, "password")?.to_unlocked("password")?; 21 | assert_eq!(id.id.id, id_relocked.id.id); 22 | 23 | assert!(id::LockedId::from_unlocked_id(&id, "password")? 24 | .to_unlocked("wrongpassword") 25 | .is_err()); 26 | 27 | let id_stored = serde_yaml::to_string(&id::LockedId::from_unlocked_id(&id, "pass")?)?; 28 | let id_restored: UnlockedId = 29 | serde_yaml::from_str::(&id_stored)?.to_unlocked("pass")?; 30 | 31 | println!("{id_stored}"); 32 | 33 | assert_eq!(id.id.id, id_restored.id.id); 34 | Ok(()) 35 | } 36 | 37 | #[test] 38 | fn use_id_generated_by_previous_versions() -> Result<()> { 39 | let yaml = r#" 40 | --- 41 | version: -1 42 | url: "https://github.com/dpc/crev-proofs-test" 43 | public-key: mScrJLNL5NV4DH9mSPsqcvU8wu0P_W6bvXhjViZP4aE 44 | sealed-secret-key: ukQvCTnTX6LmnUaBkoB4IGhIvnMxSNb5T8HoEn6DbFnI1IWzMqsGhkzxVzzc-zDs 45 | seal-nonce: gUu4izYVvDgZjHFGpcunWmNV3nTgmswvSZsCr3lKboQ 46 | pass: 47 | version: 19 48 | variant: argon2id 49 | iterations: 192 50 | memory-size: 4096 51 | lanes: 8 52 | salt: 9jeCQhM2dMZErCErRQ_RmZ08X68xpta1tIhTbCHOTs0 53 | "#; 54 | 55 | let locked = id::LockedId::from_str(yaml)?; 56 | let unlocked = locked.to_unlocked("a")?; 57 | 58 | let _trust_proof = unlocked.create_signed_trust_proof( 59 | vec![unlocked.as_public_id()], 60 | TrustLevel::High, 61 | vec![], 62 | )?; 63 | 64 | Ok(()) 65 | } 66 | 67 | #[test] 68 | fn validate_proof_generated_by_previous_version() -> Result<()> { 69 | let yaml = r#" 70 | -----BEGIN CREV PACKAGE REVIEW----- 71 | version: -1 72 | date: "2019-04-13T00:04:16.625524407-07:00" 73 | from: 74 | id-type: crev 75 | id: mScrJLNL5NV4DH9mSPsqcvU8wu0P_W6bvXhjViZP4aE 76 | url: "https://github.com/dpc/crev-proofs-test" 77 | package: 78 | source: "https://crates.io" 79 | name: hex 80 | version: 0.3.2 81 | digest: 6FtxZesHD7pnSlbpp--CF_MPAnJATZI4ZR-Vdwb6Fes 82 | review: 83 | thoroughness: none 84 | understanding: medium 85 | rating: positive 86 | comment: THIS IS JUST FOR TEST 87 | -----BEGIN CREV PACKAGE REVIEW SIGNATURE----- 88 | NtGu3z1Jtnj6wx8INBrVujcOPz61BiGmJS-UoAOe0XQutatFsEbgAcAo7rBvZz4Q-ccNXIFZtKnXhBDMjVm0Aw 89 | -----END CREV PACKAGE REVIEW----- 90 | "#; 91 | 92 | let proofs = crev_data::proof::Proof::parse_from(yaml.as_bytes())?; 93 | assert_eq!(proofs.len(), 1); 94 | 95 | proofs[0].verify()?; 96 | 97 | Ok(()) 98 | } 99 | 100 | #[test] 101 | fn dont_consider_an_empty_review_as_valid() -> Result<()> { 102 | let url = FetchSource::Url(Arc::new(Url::new_git("https://a"))); 103 | let a = UnlockedId::generate_for_git_url("https://a"); 104 | let digest = [13; 32]; 105 | let package = crev_data::proof::PackageInfo { 106 | id: PackageVersionId::new( 107 | "source".into(), 108 | "name".into(), 109 | Version::parse("1.0.0").unwrap(), 110 | ), 111 | revision: String::new(), 112 | revision_type: crev_data::proof::default_revision_type(), 113 | digest: digest.to_vec(), 114 | digest_type: crev_data::proof::default_digest_type(), 115 | }; 116 | 117 | let review = crev_data::proof::review::Review::new_none(); 118 | 119 | let proof1 = a 120 | .as_public_id() 121 | .create_package_review_proof(package, review, vec![], "a".into())? 122 | .sign_by(&a)?; 123 | 124 | let mut trustdb = ProofDB::new(); 125 | let trust_set = trustdb.calculate_trust_set(&a.id.id, &default()); 126 | trustdb.import_from_iter(vec![proof1].into_iter().map(|x| (x, url.clone()))); 127 | let verification_reqs = VerificationRequirements { 128 | thoroughness: Level::None, 129 | understanding: Level::None, 130 | trust_level: Level::None, 131 | redundancy: 1, 132 | }; 133 | assert!(!verify_package_digest( 134 | &Digest::from(digest), 135 | &trust_set, 136 | &verification_reqs, 137 | &trustdb 138 | ) 139 | .is_verified()); 140 | 141 | Ok(()) 142 | } 143 | -------------------------------------------------------------------------------- /crev-lib/src/util/git.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use git2::{ErrorClass, ErrorCode}; 3 | use log::debug; 4 | use std::path::Path; 5 | 6 | #[derive(PartialEq, Debug, Default)] 7 | pub struct GitUrlComponents { 8 | pub domain: String, 9 | pub username: String, 10 | pub repo: String, 11 | pub suffix: String, 12 | } 13 | 14 | #[must_use] 15 | pub fn parse_git_url_https(http_url: &str) -> Option { 16 | let mut split: Vec<_> = http_url.split('/').collect(); 17 | 18 | while let Some(&"") = split.last() { 19 | split.pop(); 20 | } 21 | if split.len() != 5 { 22 | return None; 23 | } 24 | if split[0] != "https:" && split[0] != "http:" { 25 | return None; 26 | } 27 | let domain = split[2]; 28 | let username = split[3]; 29 | let repo = split[4]; 30 | let suffix = match domain { 31 | "git.sr.ht" => "", 32 | "github.com" | "gitlab.com" => { 33 | if repo.ends_with(".git") { 34 | "" 35 | } else { 36 | ".git" 37 | } 38 | } 39 | _ => return None, 40 | }; 41 | 42 | Some(GitUrlComponents { 43 | domain: domain.to_string(), 44 | username: username.to_string(), 45 | repo: repo.to_string(), 46 | suffix: suffix.to_string(), 47 | }) 48 | } 49 | 50 | #[must_use] 51 | pub fn is_unrecoverable(err: &git2::Error) -> bool { 52 | matches!( 53 | (err.class(), err.code()), 54 | // GitHub's way of saying 404 55 | (ErrorClass::Http, ErrorCode::Auth) | 56 | (ErrorClass::Repository, ErrorCode::NotFound) | 57 | // corrupted loose reference 58 | (ErrorClass::Reference, ErrorCode::GenericError) 59 | ) 60 | } 61 | 62 | pub fn fetch_and_checkout_git_repo(repo: &git2::Repository) -> Result<(), git2::Error> { 63 | let mut fetch_options = default_fetch_options(); 64 | repo.find_remote("origin")? 65 | .fetch::(&[], Some(&mut fetch_options), None)?; 66 | repo.set_head("FETCH_HEAD")?; 67 | let mut opts = git2::build::CheckoutBuilder::new(); 68 | opts.force(); 69 | repo.checkout_head(Some(&mut opts)) 70 | } 71 | 72 | /// Make a git clone with the default fetch options 73 | pub fn clone>( 74 | url: &str, 75 | path: P, 76 | ) -> std::result::Result { 77 | debug!("Cloning {} to {}", url, path.as_ref().display()); 78 | let fetch_options = default_fetch_options(); 79 | git2::build::RepoBuilder::new() 80 | .fetch_options(fetch_options) 81 | .clone(url, path.as_ref()) 82 | } 83 | 84 | /// Get the default fetch options to use when fetching or cloneing 85 | /// 86 | /// Currently this just ensures that git's automatic proxy settings are used. 87 | #[must_use] 88 | pub fn default_fetch_options<'a>() -> git2::FetchOptions<'a> { 89 | // Use automatic proxy configuration for the fetch 90 | let mut proxy_options = git2::ProxyOptions::new(); 91 | proxy_options.auto(); 92 | let mut fetch_options = git2::FetchOptions::new(); 93 | fetch_options.proxy_options(proxy_options); 94 | 95 | fetch_options 96 | } 97 | 98 | #[test] 99 | fn parse_git_url_https_test() { 100 | assert_eq!( 101 | parse_git_url_https("https://github.com/dpc/trust"), 102 | Some(GitUrlComponents { 103 | domain: "github.com".to_string(), 104 | username: "dpc".to_string(), 105 | repo: "trust".to_string(), 106 | suffix: ".git".to_string() 107 | }) 108 | ); 109 | assert_eq!( 110 | parse_git_url_https("https://gitlab.com/hackeraudit/web.git"), 111 | Some(GitUrlComponents { 112 | domain: "gitlab.com".to_string(), 113 | username: "hackeraudit".to_string(), 114 | repo: "web.git".to_string(), 115 | suffix: String::new() 116 | }) 117 | ); 118 | assert_eq!( 119 | parse_git_url_https("https://gitlab.com/hackeraudit/web.git/"), 120 | Some(GitUrlComponents { 121 | domain: "gitlab.com".to_string(), 122 | username: "hackeraudit".to_string(), 123 | repo: "web.git".to_string(), 124 | suffix: String::new() 125 | }) 126 | ); 127 | assert_eq!( 128 | parse_git_url_https("https://gitlab.com/hackeraudit/web.git/////////"), 129 | Some(GitUrlComponents { 130 | domain: "gitlab.com".to_string(), 131 | username: "hackeraudit".to_string(), 132 | repo: "web.git".to_string(), 133 | suffix: String::new() 134 | }) 135 | ); 136 | } 137 | 138 | #[must_use] 139 | pub fn https_to_git_url(http_url: &str) -> Option { 140 | parse_git_url_https(http_url).map(|components| { 141 | format!( 142 | "git@{}:{}/{}{}", 143 | components.domain, components.username, components.repo, components.suffix 144 | ) 145 | }) 146 | } 147 | 148 | #[test] 149 | fn https_to_git_url_test() { 150 | assert_eq!( 151 | https_to_git_url("https://github.com/dpc/trust"), 152 | Some("git@github.com:dpc/trust.git".into()) 153 | ); 154 | assert_eq!( 155 | https_to_git_url("https://gitlab.com/hackeraudit/web.git"), 156 | Some("git@gitlab.com:hackeraudit/web.git".into()) 157 | ); 158 | assert_eq!( 159 | https_to_git_url("https://gitlab.com/hackeraudit/web.git/"), 160 | Some("git@gitlab.com:hackeraudit/web.git".into()) 161 | ); 162 | assert_eq!( 163 | https_to_git_url("https://gitlab.com/hackeraudit/web.git/////////"), 164 | Some("git@gitlab.com:hackeraudit/web.git".into()) 165 | ); 166 | assert_eq!( 167 | https_to_git_url("https://git.sr.ht/~ireas/crev-proofs"), 168 | Some("git@git.sr.ht:~ireas/crev-proofs".into()) 169 | ); 170 | assert_eq!(https_to_git_url("https://example.com/foo/bar"), None); 171 | } 172 | -------------------------------------------------------------------------------- /crev-wot/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crev-wot" 3 | description = "Scalable, social, Code REView system that we desperately need - Web of Trust library" 4 | documentation = "https://docs.rs/cargo-crev" 5 | homepage = "https://github.com/crev-dev/cargo-crev" 6 | keywords = ["web-of-trust", "dependency", "peer", "review", "identity"] 7 | readme = "README.md" 8 | include = ["src/**", "Cargo.toml", "README.md", "LICENSE-MIT"] 9 | authors.workspace = true 10 | edition.workspace = true 11 | license.workspace = true 12 | repository.workspace = true 13 | rust-version.workspace = true 14 | version.workspace = true 15 | 16 | [dependencies] 17 | crev-common.workspace = true 18 | crev-data.workspace = true 19 | 20 | blake2.workspace = true 21 | chrono.workspace = true 22 | default = "0.1.2" 23 | log.workspace = true 24 | serde.workspace = true 25 | serde_yaml.workspace = true 26 | thiserror.workspace = true 27 | itertools.workspace = true 28 | 29 | [package.metadata.release] 30 | shared-version=true 31 | -------------------------------------------------------------------------------- /crev-wot/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /crev-wot/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in 4 | fetchTarball { 5 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 6 | sha256 = lock.nodes.flake-compat.locked.narHash; 7 | } 8 | ) 9 | { src = ./.; } 10 | ).defaultNix 11 | -------------------------------------------------------------------------------- /design/purpose.md: -------------------------------------------------------------------------------- 1 | # REQ-purpose 2 | 3 | The main goal of this project is to solve trust problems of downstream users of 4 | ecosystems like NPM/Cargo/Pip etc. and potentially any organization utilizing 5 | source code written by multiple people. 6 | 7 | No matter how strict the security of such ecosystems are, any downstream users 8 | stay vulnerable to: 9 | 10 | - poor quality of upstream libraries 11 | - maliciousness of the authors of upstream libraries 12 | - compromised accounts 13 | 14 | and while "vetting your dependencies" and upgrading conservatively is 15 | responsibility of the downstream user, in practice it's unrealistic, because it 16 | does not scale. 17 | 18 | This is solved by: 19 | 20 | - Making a cryptographically verifiable code review information become a part of 21 | source code in a way similar to how documentation is a part of source code in 22 | any modern code-bases. (Review Proofs) 23 | - Making personal, technical trust information explicit and cryptographically 24 | verifiable in a similar fashion. 25 | - Establishing common set of formats and artifacts to allow exchanging such 26 | artifacts of code review and personal trust. 27 | - Building tools helping downstream users judge, verify and enforce trust and 28 | safety requirements based on the above. 29 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "crane": { 4 | "inputs": { 5 | "flake-compat": "flake-compat", 6 | "flake-utils": "flake-utils", 7 | "nixpkgs": [ 8 | "nixpkgs" 9 | ] 10 | }, 11 | "locked": { 12 | "lastModified": 1661651047, 13 | "narHash": "sha256-U7O9ej8I1ng0KbfA219LV9h+F4q0lOL1IZlS5fVouY4=", 14 | "owner": "ipetkov", 15 | "repo": "crane", 16 | "rev": "dbda889c05e29d6cf8d2ececa4444813d3b6c67c", 17 | "type": "github" 18 | }, 19 | "original": { 20 | "owner": "ipetkov", 21 | "repo": "crane", 22 | "type": "github" 23 | } 24 | }, 25 | "fenix": { 26 | "inputs": { 27 | "nixpkgs": [ 28 | "nixpkgs" 29 | ], 30 | "rust-analyzer-src": "rust-analyzer-src" 31 | }, 32 | "locked": { 33 | "lastModified": 1697696548, 34 | "narHash": "sha256-653vv/6fwAaLnsm97S+BIAa7OsSlVv9FZIqTzlg4jXQ=", 35 | "owner": "nix-community", 36 | "repo": "fenix", 37 | "rev": "9d8534763043e7761b6872e6210d3a68ea2f296c", 38 | "type": "github" 39 | }, 40 | "original": { 41 | "owner": "nix-community", 42 | "repo": "fenix", 43 | "type": "github" 44 | } 45 | }, 46 | "flake-compat": { 47 | "flake": false, 48 | "locked": { 49 | "lastModified": 1650374568, 50 | "narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=", 51 | "owner": "edolstra", 52 | "repo": "flake-compat", 53 | "rev": "b4a34015c698c7793d592d66adbab377907a2be8", 54 | "type": "github" 55 | }, 56 | "original": { 57 | "owner": "edolstra", 58 | "repo": "flake-compat", 59 | "type": "github" 60 | } 61 | }, 62 | "flake-compat_2": { 63 | "flake": false, 64 | "locked": { 65 | "lastModified": 1650374568, 66 | "narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=", 67 | "owner": "edolstra", 68 | "repo": "flake-compat", 69 | "rev": "b4a34015c698c7793d592d66adbab377907a2be8", 70 | "type": "github" 71 | }, 72 | "original": { 73 | "owner": "edolstra", 74 | "repo": "flake-compat", 75 | "type": "github" 76 | } 77 | }, 78 | "flake-utils": { 79 | "locked": { 80 | "lastModified": 1656928814, 81 | "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=", 82 | "owner": "numtide", 83 | "repo": "flake-utils", 84 | "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249", 85 | "type": "github" 86 | }, 87 | "original": { 88 | "owner": "numtide", 89 | "repo": "flake-utils", 90 | "type": "github" 91 | } 92 | }, 93 | "flake-utils_2": { 94 | "locked": { 95 | "lastModified": 1678901627, 96 | "narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=", 97 | "owner": "numtide", 98 | "repo": "flake-utils", 99 | "rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6", 100 | "type": "github" 101 | }, 102 | "original": { 103 | "owner": "numtide", 104 | "repo": "flake-utils", 105 | "type": "github" 106 | } 107 | }, 108 | "nixpkgs": { 109 | "locked": { 110 | "lastModified": 1730963269, 111 | "narHash": "sha256-rz30HrFYCHiWEBCKHMffHbMdWJ35hEkcRVU0h7ms3x0=", 112 | "owner": "NixOS", 113 | "repo": "nixpkgs", 114 | "rev": "83fb6c028368e465cd19bb127b86f971a5e41ebc", 115 | "type": "github" 116 | }, 117 | "original": { 118 | "owner": "NixOS", 119 | "ref": "nixos-24.05", 120 | "repo": "nixpkgs", 121 | "type": "github" 122 | } 123 | }, 124 | "root": { 125 | "inputs": { 126 | "crane": "crane", 127 | "fenix": "fenix", 128 | "flake-compat": "flake-compat_2", 129 | "flake-utils": "flake-utils_2", 130 | "nixpkgs": "nixpkgs" 131 | } 132 | }, 133 | "rust-analyzer-src": { 134 | "flake": false, 135 | "locked": { 136 | "lastModified": 1697631181, 137 | "narHash": "sha256-W1EWCDHVZTAv1Xp4xirCqaYlHZLIWShVVBk2YQIRcXE=", 138 | "owner": "rust-lang", 139 | "repo": "rust-analyzer", 140 | "rev": "4586a6b26cd5a975a1826c0cfd9004a9bce3d7fd", 141 | "type": "github" 142 | }, 143 | "original": { 144 | "owner": "rust-lang", 145 | "ref": "nightly", 146 | "repo": "rust-analyzer", 147 | "type": "github" 148 | } 149 | } 150 | }, 151 | "root": "root", 152 | "version": 7 153 | } 154 | -------------------------------------------------------------------------------- /misc/git-hooks/commit-template.txt: -------------------------------------------------------------------------------- 1 | 2 | # Explain *why* this change is being made width limit ->| 3 | -------------------------------------------------------------------------------- /misc/git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | git_ls_files="$(git ls-files)" 6 | 7 | >&2 echo "Checking *.nix files..." 8 | # shellcheck disable=SC2046 9 | nixpkgs-fmt --check $(echo "$git_ls_files" | grep -E '.*\.nix$') 10 | 11 | 12 | >&2 echo "Checking Rust projects files..." 13 | # Note: avoid `cargo fmt --all` so we don't need extra stuff in `ci` shell 14 | # so that CI is faster 15 | # shellcheck disable=SC2046 16 | rustfmt --edition 2021 --check $(echo "$git_ls_files" | grep -E '.*\.rs$') 17 | 18 | 19 | >&2 echo "Checking shell script files ..." 20 | for path in $(echo "$git_ls_files" | grep -E '.*\.sh$') ; do 21 | shellcheck --severity=warning $path 22 | done 23 | 24 | errors="" 25 | for path in $(echo "$git_ls_files" | grep -v -E '.*\.ods'); do 26 | # extra branches for clarity 27 | if [ ! -s "$path" ]; then 28 | # echo "$path is empty" 29 | true 30 | elif [ -z "$(tail -c 1 < "$path")" ]; then 31 | # echo "$path ends with a newline or with a null byte" 32 | true 33 | else 34 | echo "$path doesn't end with a newline" 1>&2 35 | errors="true" 36 | fi 37 | done 38 | 39 | if [ -n "$errors" ]; then 40 | exit 1 41 | fi 42 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | # we synchronize all versions, so one tag should be enough 2 | tag-prefix = "" 3 | tag-name = "v{{version}}" 4 | consolidate-commits = true 5 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let 4 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 5 | in 6 | fetchTarball { 7 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 8 | sha256 = lock.nodes.flake-compat.locked.narHash; 9 | } 10 | ) 11 | { 12 | src = ./.; 13 | }).shellNix 14 | --------------------------------------------------------------------------------