diff --git a/.clippy.toml b/.clippy.toml new file mode 100644 index 0000000..2d600ad --- /dev/null +++ b/.clippy.toml @@ -0,0 +1,11 @@ +disallowed-methods = [ + { path = "std::slice::from_raw_parts", reason = "see null_safe_slice" } +] +disallowed-macros = [ + { path = "std::dbg" } +] +allow-mixed-uninlined-format-args = false +allow-unwrap-in-tests = true +allow-dbg-in-tests = true +avoid-breaking-exported-api = false # We have one consumer and can afford to break the API. +pass-by-value-size-limit = 32 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..a7c1e1d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @KershawChang @martinthomson @larseggert @mxinden diff --git a/.github/actionlint-matcher.json b/.github/actionlint-matcher.json new file mode 100644 index 0000000..4613e16 --- /dev/null +++ b/.github/actionlint-matcher.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "actionlint", + "pattern": [ + { + "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$", + "file": 1, + "line": 2, + "column": 3, + "message": 4, + "code": 5 + } + ] + } + ] +} diff --git a/.github/actionlint.yml b/.github/actionlint.yml new file mode 100644 index 0000000..df153a3 --- /dev/null +++ b/.github/actionlint.yml @@ -0,0 +1,6 @@ +# Configuration related to self-hosted runner. +self-hosted-runner: + # Labels of self-hosted runner in array of strings. + labels: + - moonshot + - moonshot-exp diff --git a/.github/actions/check-android/action.yml b/.github/actions/check-android/action.yml new file mode 100644 index 0000000..129874f --- /dev/null +++ b/.github/actions/check-android/action.yml @@ -0,0 +1,112 @@ +name: 'CI VM' +description: 'Run main CI steps in VMs for VM-only platforms.' + +inputs: + target: + description: 'Rust target to build for.' + required: true + working-directory: + description: 'Working directory.' + default: '.' + ndk-version: + description: 'NDK version to install.' + # https://searchfox.org/mozilla-central/search?q=NDK_VERSION =&path=python/mozboot/mozboot/android.py + default: '27.2.12479018' + api-level: + description: 'Android API level to use.' + # https://searchfox.org/mozilla-central/search?q=\bapi_level=&path=taskcluster/scripts/misc/build-llvm-common.sh®exp=true + # However, NSS requires an API >= 23 for a few symbols. + default: '23' + minimum-nss-version: + description: 'If NSS is required, the minimum version required.' + default: '' + codecov-token: + description: 'Codecov token, if Codecov upload is desired.' + default: '' + github-token: + description: 'A Github PAT' + required: true + +runs: + using: composite + steps: + - uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 + with: + distribution: zulu + java-version: 23 + + - uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2 + with: + accept-android-sdk-licenses: true + log-accepted-android-sdk-licenses: false + + - shell: bash + env: + NDK_VERSION: ${{ inputs.ndk-version }} + WD: ${{ inputs.working-directory }} + run: cd "$WD" && sdkmanager --install "ndk;$NDK_VERSION" + + - uses: ./.github/actions/rust + with: + version: stable + targets: ${{ inputs.target }} + tools: cargo-ndk@^4 + token: ${{ inputs.github-token }} + + - uses: ./.github/actions/nss + if: ${{ inputs.minimum-nss-version != '' }} + with: + minimum-version: ${{ inputs.minimum-nss-version }} + target: ${{ inputs.target }} + + - shell: bash + env: + TARGET: ${{ startsWith(inputs.target, 'arm') && 'arm64-v8a' || inputs.target }} + API_LEVEL: ${{ inputs.api-level }} + WD: ${{ inputs.working-directory }} + run: cd "$WD" && cargo ndk --platform "$API_LEVEL" --target "$TARGET" test --no-run + + - shell: bash + env: + TARGET: ${{ inputs.target }} + API_LEVEL: ${{ inputs.api-level }} + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + cat <<'EOF' > /tmp/rust-android-run-tests-on-emulator.sh + #!/bin/bash + set -ex + adb wait-for-device + while [ -z "$(adb shell getprop sys.boot_completed | tr -d '\r')" ]; do sleep 1; done + any_failures=0 + TMP=/data/local/tmp + [ -e "$WD/test-fixture/db" ] && adb push "test-fixture/db" "$TMP/" + [ "$LD_LIBRARY_PATH" ] && adb push "$LD_LIBRARY_PATH" "$TMP/" + for test in $(find $WD/target/$TARGET/debug/deps/ -type f -executable ! -name "*.so" -name "*-*"); do + adb push "$test" "$TMP/" + adb shell chmod +x "$TMP/$(basename "$test")" + # See https://unix.stackexchange.com/a/451140/409256 + adb shell "CARGO_TERM_COLOR=always RUST_BACKTRACE=1 LD_LIBRARY_PATH=$TMP/lib NSS_DB_PATH=$TMP/db API_LEVEL=$API_LEVEL $TMP/$(basename "$test") || echo _FAIL_" 2>&1 | tee output + grep _FAIL_ output > /dev/null && any_failures=1 + done + exit $any_failures + EOF + chmod a+x /tmp/rust-android-run-tests-on-emulator.sh + + - uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # v2.34.0 + with: + api-level: ${{ inputs.api-level }} + arch: ${{ startsWith(inputs.target, 'x86_64') && 'x86_64' || (startsWith(inputs.target, 'i686') && 'x86' || (startsWith(inputs.target, 'aarch64') && 'arm64-v8a')) }} + ndk: ${{ inputs.ndk-version }} + emulator-boot-timeout: 120 + disk-size: 2G + script: /tmp/rust-android-run-tests-on-emulator.sh + + - if: ${{ inputs.codecov-token != '' }} + uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0 + with: + files: lcov.info + fail_ci_if_error: false + token: ${{ inputs.codecov-token }} + verbose: true diff --git a/.github/actions/check-vm/action.yml b/.github/actions/check-vm/action.yml new file mode 100644 index 0000000..142bff9 --- /dev/null +++ b/.github/actions/check-vm/action.yml @@ -0,0 +1,146 @@ +name: 'CI VM' +description: 'Run main CI steps in VMs for VM-only platforms.' + +inputs: + working-directory: + description: 'Working directory.' + default: '.' + platform: + description: 'Platform to run the checks on.' + default: '' + codecov-token: + description: 'Codecov token, if Codecov upload is desired.' + default: '' + +runs: + using: composite + steps: + - shell: bash + id: prep + env: + WD: ${{ inputs.working-directory }} + PLATFORM: ${{ inputs.platform }} + WORKSPACE: ${{ inputs.working-directory == '.' && '--workspace' || '' }} + run: | + cat < prepare.sh + # This executes as root + set -ex + pwd + case "$PLATFORM" in + freebsd) pkg install -y curl llvm nss pkgconf + ;; + openbsd) # TODO: Is there a way to not pin the version of llvm? -z to pkg_add does not work. + pkg_add rust rust-clippy rust-rustfmt llvm-19.1.7p3 nss pkgconf # rustup does not support OpenBSD at all + ;; + netbsd) /usr/sbin/pkg_add pkgin && pkgin -y install curl clang nss pkgconf + ;; + solaris) pkg install clang-libs nss pkg-config + ;; + *) echo "Unsupported OS: $PLATFORM" + exit 1 + ;; + esac + EOF + { + echo 'prepare<> "$GITHUB_OUTPUT" + + cat < run.sh + # This executes as user + set -ex + cd "$WD" + pwd + case "$PLATFORM" in + freebsd) sh rustup.sh --default-toolchain stable --profile minimal --component clippy,llvm-tools,rustfmt -y + . "\$HOME/.cargo/env" + ;; + openbsd) export LIBCLANG_PATH=/usr/local/llvm19/lib + export LLVM_COV=/usr/local/llvm19/bin/llvm-cov + export LLVM_PROFDATA=/usr/local/llvm19/bin/llvm-profdata + [ "$WORKSPACE" ] && EXCLUDE="--exclude fuzz" # Fuzzing not supported on OpenBSD + ;; + netbsd) sh rustup.sh --default-toolchain stable --profile minimal --component clippy,llvm-tools,rustfmt -y + . "\$HOME/.cargo/env" + # FIXME: Why do we need to set this on NetBSD? + export LD_LIBRARY_PATH=/usr/pkg/lib/nss:/usr/pkg/lib/nspr + [ "$WORKSPACE" ] && EXCLUDE="--exclude fuzz" # Fuzzing not supported on NetBSD + ;; + solaris) curl --output rust.sh -s https://raw.githubusercontent.com/psumbera/solaris-rust/refs/heads/main/sh.rust-web-install + chmod a+x rust.sh + ls -lt + source ./rust.sh || true # This does not exit with zero on success + export LIBCLANG_PATH="/usr/lib/amd64" + [ "$WORKSPACE" ] && EXCLUDE="--exclude fuzz" # Fuzzing not supported on Solaris + ;; + esac + cargo version + cargo check --locked --all-targets $WORKSPACE \$EXCLUDE + case "$PLATFORM" in + openbsd) # clippy fails on OpenBSD, because libfuzzer-sys is not supported. + ;; + *) cargo clippy -- -D warnings + ;; + esac + cargo fmt --all -- --check + case "$PLATFORM" in + freebsd) cargo install cargo-llvm-cov --locked + cargo llvm-cov test --locked --no-fail-fast --lcov --output-path lcov.info + ;; + *) # FIXME: No profiler support on other platforms, error is: cannot find crate for profiler_builtins + cargo test --locked --no-fail-fast # We do this instead for now + ;; + esac + cargo test --locked --no-fail-fast --release + rm -rf target # Do not sync this back to host + EOF + { + echo 'run<> "$GITHUB_OUTPUT" + + curl -o "$WD/rustup.sh" --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs + echo "envs=CARGO_TERM_COLOR RUST_BACKTRACE RUST_LOG GITHUB_ACTIONS RUST_TEST_TIME_UNIT RUST_TEST_TIME_INTEGRATION RUST_TEST_TIME_DOCTEST WD" >> "$GITHUB_OUTPUT" + + - if: ${{ inputs.platform == 'freebsd' }} + uses: vmactions/freebsd-vm@966989c456d41351f095a421f60e71342d3bce41 # v1.2.1 + with: + usesh: true + envs: ${{ steps.prep.outputs.envs }} + prepare: ${{ steps.prep.outputs.prepare }} + run: ${{ steps.prep.outputs.run }} + + - if: ${{ inputs.platform == 'openbsd' }} + uses: vmactions/openbsd-vm@0d65352eee1508bab7cb12d130536d3a556be487 # v1.1.8 + with: + usesh: true + envs: ${{ steps.prep.outputs.envs }} + prepare: ${{ steps.prep.outputs.prepare }} + run: ${{ steps.prep.outputs.run }} + + - if: ${{ inputs.platform == 'netbsd' }} + uses: vmactions/netbsd-vm@d0228be27fbaba19418cc1b332609a895cf16561 # v1.1.9 + with: + usesh: true + envs: ${{ steps.prep.outputs.envs }} + prepare: ${{ steps.prep.outputs.prepare }} + run: ${{ steps.prep.outputs.run }} + + - if: ${{ inputs.platform == 'solaris' }} + uses: vmactions/solaris-vm@58cbd70c6e051860f9b8f65908cc582938fbbdba # v1.1.5 + with: + release: "11.4-gcc" + usesh: true + envs: ${{ steps.prep.outputs.envs }} + prepare: ${{ steps.prep.outputs.prepare }} + run: ${{ steps.prep.outputs.run }} + + - if: ${{ inputs.codecov-token != '' }} + uses: codecov/codecov-action@fdcc8476540edceab3de004e990f80d881c6cc00 # v5.5.0 + with: + files: lcov.info + fail_ci_if_error: false + token: ${{ inputs.codecov-token }} + verbose: true diff --git a/.github/actions/nss/action.yml b/.github/actions/nss/action.yml new file mode 100644 index 0000000..bd80e00 --- /dev/null +++ b/.github/actions/nss/action.yml @@ -0,0 +1,262 @@ +name: Install NSS +description: Install NSS + +inputs: + type: + description: "When building, whether to do a debug or release build of NSS" + default: "Release" + minimum-version: + description: "Minimum required version of NSS" + required: true + target: + description: "Target for cross-compilation" + default: "" + +runs: + using: composite + steps: + - name: Install system NSS (Linux) + shell: bash + if: ${{ runner.os == 'Linux' && runner.environment == 'github-hosted' && inputs.target == '' }} + env: + DEBIAN_FRONTEND: noninteractive + run: | + [ "$APT_UPDATED" ] || sudo apt-get update && echo "APT_UPDATED=1" >> "$GITHUB_ENV" + sudo apt-get install -y --no-install-recommends libnss3-dev pkg-config + + - name: Install system NSS (MacOS) + shell: bash + if: ${{ runner.os == 'MacOS' && runner.environment == 'github-hosted' && inputs.target == '' }} + run: | + [ "$BREW_UPDATED" ] || brew update && echo "BREW_UPDATED=1" >> "$GITHUB_ENV" + brew install nss + + - name: Check system NSS version + id: system_nss + env: + MIN_VERSION: ${{ inputs.minimum-version }} + shell: bash + if: inputs.target == '' + run: | + if ! command -v pkg-config &> /dev/null; then + echo "pkg-config: not found" + exit 0 + fi + if ! pkg-config --exists nss; then + echo "pkg-config: NSS not found" + exit 0 + fi + NSS_VERSION="$(pkg-config --modversion nss)" + if [ "$?" -ne 0 ]; then + echo "pkg-config: failed to determine NSS version" + exit 0 + fi + NSS_MAJOR=$(echo "$NSS_VERSION" | cut -d. -f1) + NSS_MINOR=$(echo "$NSS_VERSION" | cut -d. -f2) + REQ_NSS_MAJOR=$(echo "$MIN_VERSION" | cut -d. -f1) + REQ_NSS_MINOR=$(echo "$MIN_VERSION" | cut -d. -f2) + if [[ "$NSS_MAJOR" -lt "$REQ_NSS_MAJOR" || "$NSS_MAJOR" -eq "$REQ_NSS_MAJOR" && "$NSS_MINOR" -lt "$REQ_NSS_MINOR" ]]; then + echo "System NSS is too old: $NSS_VERSION" + exit 0 + fi + echo "System NSS is suitable: $NSS_VERSION" + echo "suitable=1" >> "$GITHUB_OUTPUT" + + - name: Use sccache + # Apparently the action can't be installed twice in the same workflow, so check if + # it's already installed by checking if the SCCACHE_ENABLED environment variable is set + # (which every "use" of this action needs to therefore set) + # + # Also, only enable sscache on our self-hosted runner, because the GitHub cache limit + # is too small for this to be effective there. + if: ${{ env.SCCACHE_ENABLED != '1' && !steps.system_nss.outputs.suitable && runner.environment != 'github-hosted' }} + uses: mozilla-actions/sccache-action@2e7f9ec7921547d4b46598398ca573513895d0bd # v0.0.4 + + - name: Enable sscache + if: ${{ !steps.system_nss.outputs.suitable && runner.environment != 'github-hosted' }} + env: + RUNNER_ENVIRONMENT: ${{ runner.environment }} + RUNNER_OS: ${{ runner.os }} + shell: bash + run: | + echo "SCCACHE_ENABLED=1" >> "$GITHUB_ENV" + if [ "$RUNNER_OS" != "Windows" ]; then + # TODO: Figure out how to make this work on Windows + echo "SCCACHE_CC=sccache cc" >> "$GITHUB_ENV" + echo "SCCACHE_CXX=sccache c++" >> "$GITHUB_ENV" + fi + echo "CMAKE_C_COMPILER_LAUNCHER=sccache" >> "$GITHUB_ENV" + echo "CMAKE_CXX_COMPILER_LAUNCHER=sccache" >> "$GITHUB_ENV" + if [ "$RUNNER_ENVIRONMENT" == "github-hosted" ]; then + echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" + fi + + - name: Checkout NSS + if: ${{ !steps.system_nss.outputs.suitable }} + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + repository: nss-dev/nss + path: nss + persist-credentials: false + + - name: Retrieve NSPR + id: nspr + if: ${{ !steps.system_nss.outputs.suitable }} + shell: bash + env: + NSPR_VERSION: 4.37 # This changes so rarely that we can hardcode it. + run: | + curl -L https://ftp.mozilla.org/pub/nspr/releases/v$NSPR_VERSION/src/nspr-$NSPR_VERSION.tar.gz | + tar xz --strip-components=1 + echo "version=$NSPR_VERSION" >> "$GITHUB_OUTPUT" + + - name: Store NSS version + id: nss + if: ${{ !steps.system_nss.outputs.suitable }} + shell: bash + run: | + NSS_HEAD=$(git -C nss rev-parse HEAD) + echo "version=$NSS_HEAD" >> "$GITHUB_OUTPUT" + + - name: Cache NSS + id: cache + if: ${{ !steps.system_nss.outputs.suitable && runner.environment == 'github-hosted' }} + uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + with: + path: dist + key: nss-${{ inputs.target && inputs.target || runner.os }}-${{ runner.arch }}-${{ inputs.type }}-${{ steps.nss.outputs.version }}-${{ steps.nspr.outputs.version }} + + - name: Check if build is needed + id: check_build + if: ${{ !steps.system_nss.outputs.suitable }} + env: + CACHE_HIT: ${{ steps.cache.outputs.cache-hit }} + RUNNER_ENVIRONMENT: ${{ runner.environment }} + shell: bash + run: | + if [ "$RUNNER_ENVIRONMENT" != "github-hosted" ] || [ ! "$CACHE_HIT" ]; then + echo "Building NSS from source" + echo "build_nss=1" >> "$GITHUB_OUTPUT" + else + echo "Using cached prebuilt NSS" + fi + + - name: Install build dependencies (Linux) + shell: bash + if: ${{ runner.os == 'Linux' && steps.check_build.outputs.build_nss && runner.environment == 'github-hosted' }} + env: + DEBIAN_FRONTEND: noninteractive + run: sudo apt-get install -y --no-install-recommends gyp ninja-build + + - name: Install build dependencies (MacOS) + shell: bash + if: ${{ runner.os == 'MacOS' && steps.check_build.outputs.build_nss }} + run: | + brew install ninja + echo "gyp-next>=0.18.1" > req.txt + python3 -m pip install --break-system-packages -r req.txt + + - name: Install build dependencies (Windows) + shell: bash + if: ${{ runner.os == 'Windows' && steps.check_build.outputs.build_nss }} + run: | + # shellcheck disable=SC2028 + { + echo C:/msys64/usr/bin + echo C:/msys64/mingw64/bin + } >> "$GITHUB_PATH" + /c/msys64/usr/bin/pacman -S --noconfirm python3-pip nsinstall + echo "gyp-next>=0.18.1" > req.txt + python3 -m pip install -r req.txt + + - name: Set up MSVC (Windows) + if: ${{ runner.os == 'Windows' && steps.check_build.outputs.build_nss }} + uses: ilammy/msvc-dev-cmd@v1 # zizmor: ignore[unpinned-uses] + # TODO: Would like to pin this, but the Mozilla org allowlist requires "ilammy/msvc-dev-cmd@v1*" + # uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 + + - name: Set up build environment (Windows) + shell: bash + if: ${{ runner.os == 'Windows' && steps.check_build.outputs.build_nss }} + run: | + { + echo "GYP_MSVS_OVERRIDE_PATH=$VSINSTALLDIR" + echo "GYP_MSVS_VERSION=2022" + echo "BASH=$SHELL" + } >> "$GITHUB_ENV" + # See https://github.com/ilammy/msvc-dev-cmd#name-conflicts-with-shell-bash + rm /usr/bin/link.exe || true + + - name: Set up environment + shell: bash + if: ${{ !steps.system_nss.outputs.suitable }} + env: + NSS_TARGET: ${{ inputs.type }} + NSS_TYPE: ${{ inputs.type }} + NSS_DIR: ${{ github.workspace }}/nss + RUNNER_OS: ${{ runner.os }} + WORKSPACE: ${{ github.workspace }} + run: | # zizmor: ignore[github-env] We need to write to GITHUB_PATH on Windows. + NSS_OUT="$WORKSPACE/dist/$NSS_TARGET" + { + echo "LD_LIBRARY_PATH=$NSS_OUT/lib" + echo "DYLD_FALLBACK_LIBRARY_PATH=$NSS_OUT/lib" + echo "NSS_TARGET=$NSS_TARGET" + echo "NSS_DIR=$NSS_DIR" + echo "NSS_PREBUILT=1" + } >> "$GITHUB_ENV" + if [ "$RUNNER_OS" == "Windows" ]; then + echo "$NSS_OUT/lib" >> "$GITHUB_PATH" + fi + + - name: Build + shell: bash + if: ${{ steps.check_build.outputs.build_nss }} + env: + TARGET_PLATFORM: ${{ inputs.target }} + RUNNER_OS: ${{ runner.os }} + run: | + if [ "$NSS_TARGET" != "Debug" ]; then + # We want to do an optimized build for accurate CPU profiling, but + # we also want debug symbols and frame pointers for that, which the normal optimized NSS + # build process doesn't provide. + OPT="-o" + [ "$RUNNER_OS" != "Windows" ] && export CFLAGS="-ggdb3 -fno-omit-frame-pointer" + fi + if [[ $TARGET_PLATFORM == *-android* ]]; then + for file in build-nss-android.sh build-android-common.sh; do + curl -o "$file" -sSf "https://raw.githubusercontent.com/mozilla/application-services/refs/tags/v137.0/libs/$file" + chmod +x "$file" + done + # See https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md#android + ANDROID_NDK_VERSION=$(basename "$ANDROID_NDK" | cut -d. -f1) + # See https://github.com/mozilla/application-services/blob/46cacda811da094653dc8e93158956f4cd57e87a/libs/build-all.sh#L89-L102 + # It figures that NSPR would require monkey-patching to build on Android. + sed -i'' 's/if test -z "$android_ndk" ; then/$as_echo "#define ANDROID 1" >>confdefs.h\n ;;\nunreachable)\n if test -z "$android_ndk" ; then/g' nspr/configure + ./build-nss-android.sh "$(pwd)" "/tmp/dist" "$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64" "$TARGET_PLATFORM" "$ANDROID_NDK_VERSION" + # Manually move the temporary build directory to the final location, which is what neqo-crypto expects. + find /tmp/tmp.* > tmp + CERTUTIL="$(grep certutil tmp)" + TARGET_DIR="$(dirname $(dirname $CERTUTIL))" + mkdir -p "dist/$NSS_TARGET" + cp -vaL "$TARGET_DIR"/* "dist/$NSS_TARGET/" + NSPR_H="$(grep nspr.h tmp)" + INCLUDE_DIR="$(dirname $NSPR_H)" + mkdir -p "dist/$NSS_TARGET/include/nspr" + cp -vaL "$INCLUDE_DIR"/* "dist/$NSS_TARGET/include/nspr" + CHACHA="$(grep chacha20poly1305.h tmp)" + PRIVATE_DIR="$(dirname $(dirname $CHACHA))" + mkdir -p "dist/private" + cp -vaL "$PRIVATE_DIR"/* "dist/private/" + UTILRENAME="$(grep utilrename.h tmp)" + PUBLIC_DIR="$(dirname $(dirname $(dirname $UTILRENAME)))" + mkdir -p "dist/public" + cp -vaL "$PUBLIC_DIR"/* "dist/" + LIBNSPR4="$(grep lib/libnspr4.a tmp)" + LIB_DIR="$(dirname $LIBNSPR4)" + mkdir -p "dist/$NSS_TARGET/lib" + cp -vaL "$LIB_DIR"/* "dist/$NSS_TARGET/lib" + else + [ "$SCCACHE_CC" ] && [ "$SCCACHE_CXX" ] && export CC="$SCCACHE_CC" CXX="$SCCACHE_CXX" + $NSS_DIR/build.sh -g -Ddisable_tests=1 -Ddisable_dbm=1 -Ddisable_libpkix=1 -Ddisable_ckbi=1 -Ddisable_fips=1 $OPT --static + fi diff --git a/.github/actions/pr-comment-data-export/action.yml b/.github/actions/pr-comment-data-export/action.yml new file mode 100644 index 0000000..9c27c79 --- /dev/null +++ b/.github/actions/pr-comment-data-export/action.yml @@ -0,0 +1,41 @@ +name: 'Export data for PR comment' +description: 'Exports the neccessary data to post a PR comment securely.' + +# This action might be running off of a fork and would thus not have write +# permissions on the origin repository. In order to allow a separate +# priviledged action to post a comment on a pull request, upload the +# necessary metadata. + +inputs: + name: + description: 'A unique name for the artifact used for exporting.' + required: true + contents: + description: 'A filename with a comment (in Markdown) to be added to the PR.' + required: true + log-md: + description: 'A Markdown string to append to the PR comment.' + required: false + +runs: + using: composite + steps: + - if: ${{ github.event_name == 'pull_request' }} + shell: bash + env: + CONTENTS: ${{ inputs.contents }} + NAME: ${{ inputs.name }} + LOG_MD: ${{ inputs.log-md }} + EVENT_NUMBER: ${{ github.event.number }} + run: | + mkdir comment-data + cp "$CONTENTS" comment-data/contents + echo "$NAME" > comment-data/name + echo "$EVENT_NUMBER" > comment-data/pr-number + echo "$LOG_MD" > comment-data/log-md + - if: ${{ github.event_name == 'pull_request' }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ${{ inputs.name }} + path: comment-data + retention-days: 1 diff --git a/.github/actions/pr-comment/action.yml b/.github/actions/pr-comment/action.yml new file mode 100644 index 0000000..ac828ba --- /dev/null +++ b/.github/actions/pr-comment/action.yml @@ -0,0 +1,40 @@ +name: 'Comment on PR' +description: 'Post a PR comment securely.' + +inputs: + name: + description: 'Artifact name to import comment data from.' + required: true + mode: + description: 'Mode of operation (upsert/recreate/delete).' + default: 'recreate' + token: + description: 'A Github PAT' + required: true + +runs: + using: composite + steps: + - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + with: + run-id: ${{ github.event.workflow_run.id }} + name: ${{ inputs.name }} + github-token: ${{ inputs.token }} + + - id: pr-number + shell: bash + run: echo "number=$(cat pr-number)" >> "$GITHUB_OUTPUT" + + - shell: bash + run: | + { + echo + cat log-md + } >> contents || true + + - uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 + with: + filePath: contents + mode: ${{ inputs.mode }} + pr_number: ${{ steps.pr-number.outputs.number }} + comment_tag: ${{ inputs.name }}-comment diff --git a/.github/actions/rust/action.yml b/.github/actions/rust/action.yml new file mode 100644 index 0000000..3f6922f --- /dev/null +++ b/.github/actions/rust/action.yml @@ -0,0 +1,105 @@ +name: Install Rust +description: Install Rust and tools + +inputs: + version: + description: 'Rust toolchain version to install' + default: 'stable' + components: + description: 'Rust components to install' + default: '' + tools: + description: 'Additional Rust tools to install' + default: '' + token: + description: 'A Github PAT' + required: true + targets: + description: Comma-separated list of target triples to install for this toolchain + required: false + workspaces: + description: Newline-separated list of workspaces + required: false + +runs: + using: composite + steps: + - name: Install Rust + # TODO: Manually upate this, Dependabot will skip it because no tags associated with SHA: + uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # zizmor: ignore[stale-action-refs] + with: + toolchain: ${{ inputs.version }} + components: ${{ inputs.components }} + targets: ${{ inputs.targets }} + + - name: Use sccache + # Apparently the action can't be installed twice in the same workflow, so check if + # it's already installed by checking if the SCCACHE_ENABLED environment variable is set + # (which every "use" of this action needs to therefore set) + # + # Also, only enable sscache on our self-hosted runner, because the GitHub cache limit + # is too small for this to be effective there. + if: ${{ env.SCCACHE_ENABLED != '1' && runner.environment != 'github-hosted' }} + uses: mozilla-actions/sccache-action@2e7f9ec7921547d4b46598398ca573513895d0bd # v0.0.4 + + # See https://corrode.dev/blog/tips-for-faster-ci-builds/ + - name: Set up build environment + shell: bash + env: + RUNNER_OS: ${{ runner.os }} + run: | + { + echo "CARGO_PROFILE_RELEASE_LTO=true" + echo "CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1" + } >> "$GITHUB_ENV" + + - name: Enable sscache + if: ${{ runner.environment != 'github-hosted' }} + env: + RUNNER_ENVIRONMENT: ${{ runner.environment }} + shell: bash + run: | + echo "SCCACHE_ENABLED=1" >> "$GITHUB_ENV" + echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" + if [ "$RUNNER_ENVIRONMENT" == "github-hosted" ]; then + echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" + fi + + - name: Enable Rust cache + uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7 + with: + cache-bin: ${{ runner.environment != 'github-hosted' }} + cache-all-crates: true + key: ${{ inputs.targets != '' && format('{0}-', inputs.targets) || '' }}${{ runner.os }} + workspaces: ${{ inputs.workspaces }} + # Only create the cache when we push to `main` (also via a merge group). + save-if: ${{ github.ref == 'refs/heads/main' || github.event.merge_group.base_ref == 'refs/heads/main' }} + + - name: Set up MSVC (Windows) + if: ${{ runner.os == 'Windows' }} + uses: ilammy/msvc-dev-cmd@v1 # zizmor: ignore[unpinned-uses] + # TODO: Would like to pin this, but the Mozilla org allowlist requires "ilammy/msvc-dev-cmd@v1*" + # uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 + + # See https://github.com/ilammy/msvc-dev-cmd#name-conflicts-with-shell-bash + - name: Set up build environment (Windows) + shell: bash + if: ${{ runner.os == 'Windows' }} + run: rm /usr/bin/link.exe || true + + - name: Install Rust tools + shell: bash + if: ${{ inputs.tools != '' }} + env: + GITHUB_TOKEN: ${{ inputs.token }} + TOOLS: ${{ inputs.tools }} + run: | + for tool in $(echo $TOOLS | tr -d ","); do + if [ "$tool" == "samply" ]; then + # TODO: Install released version once `--presymbolicate` (https://github.com/mstange/samply/pull/634) is released. + cargo install --git https://github.com/mstange/samply --rev f6ff5dedc73ab84a8ef45231f41cd3e721fb5cd4 samply + else + # FIXME: See https://github.com/Swatinem/rust-cache/issues/204 for why `--force`. + cargo install --locked --force "$tool" + fi + done diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d9941de --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + # We don't really use Dependabot for Rust dependencies, since we are tracking Gecko, + # but we do want to be notified of security vulnerabilities. + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + # Disable all non-security updates. + # + open-pull-requests-limit: 0 + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/semantic.yml b/.github/semantic.yml new file mode 100644 index 0000000..be3439f --- /dev/null +++ b/.github/semantic.yml @@ -0,0 +1,3 @@ +enabled: true +titleOnly: true +targetUrl: "https://www.conventionalcommits.org/en/v1.0.0/#summary" diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml new file mode 100644 index 0000000..5cb2ba0 --- /dev/null +++ b/.github/workflows/actionlint.yml @@ -0,0 +1,60 @@ +name: Lint GitHub Actions workflows +on: + push: + branches: ["main"] + paths: [".github/**"] + pull_request: + branches: ["main"] + paths: [".github/**"] + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + actionlint: + name: actionlint 🛠️ + runs-on: ubuntu-24.04 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: Download actionlint + id: get_actionlint + run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) + + - name: Check workflow files + env: + ACTIONLINT: ${{ steps.get_actionlint.outputs.executable }} + run: | + echo "::add-matcher::.github/actionlint-matcher.json" + $ACTIONLINT -color + + zizmor: + name: zizmor 🌈 + runs-on: ubuntu-24.04 + permissions: + security-events: write + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1 + + - run: uvx zizmor --persona auditor --format sarif . > results.sarif + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: github/codeql-action/upload-sarif@192325c86100d080feab897ff886c34abd4c83a3 # v3.29.5 + with: + sarif_file: results.sarif + category: zizmor diff --git a/.github/workflows/check-mtu.yml b/.github/workflows/check-mtu.yml new file mode 100644 index 0000000..359fd2a --- /dev/null +++ b/.github/workflows/check-mtu.yml @@ -0,0 +1,51 @@ +name: CI MTU +on: + workflow_dispatch: + pull_request: + branches: ["main"] + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + check-android: + name: Check Android + if: github.actor != 'dependabot[bot]' + runs-on: ubuntu-24.04 + strategy: + matrix: + target: ['x86_64-linux-android', 'i686-linux-android'] # 'aarch64-linux-android' not currently working + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - uses: ./.github/actions/check-android + with: + target: ${{ matrix.target }} + working-directory: mtu + github-token: ${{ secrets.GITHUB_TOKEN }} + codecov-token: ${{ secrets.CODECOV_TOKEN }} + + check-vm: + name: Run checks for VM-only platforms + if: github.actor != 'dependabot[bot]' + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + os: [ freebsd, openbsd, netbsd, solaris ] + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - uses: ./.github/actions/check-vm + with: + working-directory: mtu + platform: ${{ matrix.os }} + codecov-token: ${{ secrets.CODECOV_TOKEN }} + diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..ce98ce0 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,218 @@ +name: CI +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + merge_group: + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + RUST_TEST_TIME_UNIT: 10,30 + RUST_TEST_TIME_INTEGRATION: 10,30 + RUST_TEST_TIME_DOCTEST: 10,30 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + toolchains: + name: Determine toolchains + runs-on: ubuntu-24.04 + outputs: + toolchains: ${{ steps.toolchains.outputs.toolchains }} + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + sparse-checkout: Cargo.toml + persist-credentials: false + + - id: toolchains + run: | + msrv="$(grep rust-version Cargo.toml | tr -d '"' | cut -f3 -d\ )" + echo "toolchains=[\"$msrv\", \"stable\", \"nightly\"]" >> "$GITHUB_OUTPUT" + + check: + name: Run checks + needs: toolchains + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15, windows-2025] + rust-toolchain: ${{ fromJSON(needs.toolchains.outputs.toolchains) }} + type: [debug] + # Include some dynamically-linked release builds, to check that that works on all platforms. + include: + - os: ubuntu-24.04 + rust-toolchain: stable + type: release + - os: macos-15 + rust-toolchain: stable + type: release + - os: windows-2025 + rust-toolchain: stable + type: release + # TODO: Remove once Neqo's MSRV is increased to 1.82.0. NSS build + # fails on Windows with Rust 1.81.0. Firefox does not use Rust 1.81.0 + # (except for testing, see + # https://bugzilla.mozilla.org/show_bug.cgi?id=1968057#c1). Firefox + # uses Rust 1.82.0. Thus this is not worth fixing. Let's explicitly + # use Rust 1.82.0 here for now. + - os: windows-2025 + rust-toolchain: 1.82.0 + type: debug + # Also do some debug builds on the oldest OS versions. + # FIXME: NSS compile fails? + # - os: ubuntu-22.04 + # rust-toolchain: stable + # type: debug + - os: macos-13 + rust-toolchain: stable + type: debug + - os: windows-2022 + rust-toolchain: stable + type: debug + exclude: + # TODO: Remove once Neqo's MSRV is increased to 1.82.0. NSS build + # fails on Windows with Rust 1.81.0. Firefox does not use Rust 1.81.0 + # (except for testing, see + # https://bugzilla.mozilla.org/show_bug.cgi?id=1968057#c1). Firefox + # uses Rust 1.82.0. Thus this is not worth fixing. Let's explicitly + # use Rust 1.82.0 here for now. + - os: windows-2025 + rust-toolchain: 1.81.0 + type: debug + env: + BUILD_TYPE: ${{ matrix.type == 'release' && '--release' || '' }} + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - uses: ./.github/actions/rust + with: + version: ${{ matrix.rust-toolchain }} + components: ${{ matrix.rust-toolchain == 'stable' && 'llvm-tools' || '' }} + tools: ${{ matrix.rust-toolchain == 'stable' && 'cargo-llvm-cov' || '' }} + token: ${{ secrets.GITHUB_TOKEN }} + + - id: nss-version + run: echo "minimum=$(cat min_version.txt)" >> "$GITHUB_OUTPUT" + + - uses: ./.github/actions/nss + with: + minimum-version: ${{ steps.nss-version.outputs.minimum }} + + - name: Check + run: | + # shellcheck disable=SC2086 + cargo check $BUILD_TYPE --locked --all-targets + + - name: Run tests and determine coverage + env: + RUST_LOG: trace + RUST_BACKTRACE: 1 + RUST_TEST_TIME_UNIT: 10,30 + RUST_TEST_TIME_INTEGRATION: 10,30 + RUST_TEST_TIME_DOCTEST: 10,30 + TOOLCHAIN: ${{ matrix.rust-toolchain }} + run: | + DUMP_SIMULATION_SEEDS="$(pwd)/simulation-seeds" + export DUMP_SIMULATION_SEEDS + # shellcheck disable=SC2086 + if [ "$TOOLCHAIN" == "stable" ]; then + cargo llvm-cov test $BUILD_TYPE --locked --include-ffi --codecov --output-path codecov.json + else + cargo test $BUILD_TYPE --locked + fi + + - name: CodeCov Windows workaround + if: matrix.os == 'windows-2025' && matrix.type == 'debug' && matrix.rust-toolchain == 'stable' + run: | + # FIXME: Without this, the codecov/codecov-action fails. No idea why it's looking under C:/msys64 now, it shouldn't. + mkdir -p C:/msys64/home/runneradmin/ + touch C:/msys64/home/runneradmin/.gitconfig + + - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + with: + files: codecov.json + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + if: matrix.type == 'debug' && matrix.rust-toolchain == 'stable' + + - name: Save simulation seeds artifact + if: ${{ always() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: simulation-seeds-${{ matrix.os }}-${{ matrix.rust-toolchain }}-${{ matrix.type }} + path: simulation-seeds + compression-level: 9 + + check-cargo-lock: + name: Ensure `Cargo.lock` contains all required dependencies + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - uses: ./.github/actions/rust + with: + version: stable + tools: cargo-hack + token: ${{ secrets.GITHUB_TOKEN }} + - run: | + cargo update -w --locked + cargo hack update -w --locked + + check-android: + name: Check Android + if: github.actor != 'dependabot[bot]' + runs-on: ubuntu-24.04 + strategy: + matrix: + target: ['x86_64-linux-android', 'i686-linux-android'] # 'aarch64-linux-android' not currently working + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - id: nss-version + run: echo "minimum=$(cat min_version.txt)" >> "$GITHUB_OUTPUT" + - uses: ./.github/actions/check-android + with: + target: ${{ matrix.target }} + minimum-nss-version: ${{ steps.nss-version.outputs.minimum }} + github-token: ${{ secrets.GITHUB_TOKEN }} + codecov-token: ${{ secrets.CODECOV_TOKEN }} + + check-vm: + name: Run checks for VM-only platforms + if: github.actor != 'dependabot[bot]' + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + os: [ freebsd, openbsd, netbsd ] # NSS package on 'solaris' is too old. + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - uses: ./.github/actions/check-vm + with: + platform: ${{ matrix.os }} + codecov-token: ${{ secrets.CODECOV_TOKEN }} + diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml new file mode 100644 index 0000000..7c8e311 --- /dev/null +++ b/.github/workflows/clippy.yml @@ -0,0 +1,54 @@ +name: Clippy +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + merge_group: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + clippy: + name: cargo clippy + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-15, windows-2025] + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash + + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - uses: ./.github/actions/rust + with: + components: clippy + tools: cargo-hack + token: ${{ secrets.GITHUB_TOKEN }} + + - id: nss-version + run: echo "minimum=$(cat min_version.txt)" >> "$GITHUB_OUTPUT" + + - uses: ./.github/actions/nss + with: + minimum-version: ${{ steps.nss-version.outputs.minimum }} + + # Use cargo-hack to run clippy on each crate individually with its + # respective default features only. Can reveal warnings otherwise + # hidden given that a plain cargo clippy combines all features of the + # workspace. See e.g. https://github.com/mozilla/neqo/pull/1695. + - run: cargo hack clippy --all-targets --benches --feature-powerset --exclude-features gecko -- -D warnings + - run: cargo doc --workspace --no-deps --document-private-items + env: + RUSTDOCFLAGS: "--deny rustdoc::broken_intra_doc_links --deny warnings" diff --git a/.github/workflows/deny.yml b/.github/workflows/deny.yml new file mode 100644 index 0000000..71f0612 --- /dev/null +++ b/.github/workflows/deny.yml @@ -0,0 +1,35 @@ +name: Deny +on: + workflow_dispatch: + pull_request: + branches: ["main"] + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + deny: + name: cargo deny + runs-on: ubuntu-24.04 + strategy: + matrix: + checks: + - advisories + - bans licenses sources + + # Prevent sudden announcement of a new advisory from failing ci: + continue-on-error: ${{ matrix.checks == 'advisories' }} + + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - uses: EmbarkStudios/cargo-deny-action@f2ba7abc2abebaf185c833c3961145a3c275caad # v2.0.13 + with: + command: check ${{ matrix.checks }} diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..40d12af --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,34 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, +# PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +name: 'Dependency Review' +on: + pull_request: + branches: ["main"] + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + dependency-review: + name: Dependency review + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1 + with: + base-ref: ${{ github.event.pull_request.base.sha || 'main' }} + head-ref: ${{ github.event.pull_request.head.sha || github.ref }} diff --git a/.github/workflows/fuzz-bench.yml b/.github/workflows/fuzz-bench.yml new file mode 100644 index 0000000..5b92fce --- /dev/null +++ b/.github/workflows/fuzz-bench.yml @@ -0,0 +1,53 @@ +name: Fuzz & Bench +on: + workflow_dispatch: + pull_request: + branches: ["main"] + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + fuzz-bench: + name: Check that the fuzz and bench targets work + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-15] # FIXME: ubuntu-24.04-arm has issues + check: [fuzz, bench] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - uses: ./.github/actions/rust + with: + version: nightly + tools: ${{ matrix.check == 'fuzz' && 'cargo-fuzz' || ''}} + token: ${{ secrets.GITHUB_TOKEN }} + + - id: nss-version + run: echo "minimum=$(cat min_version.txt)" >> "$GITHUB_OUTPUT" + + - uses: ./.github/actions/nss + with: + minimum-version: ${{ steps.nss-version.outputs.minimum }} + + - if: ${{ matrix.check == 'fuzz' }} + env: + UBUNTU: ${{ startsWith(matrix.os, 'ubuntu') }} + run: | + cargo fuzz build --dev + for fuzzer in $(cargo fuzz list); do + cargo fuzz run --dev --sanitizer none "$fuzzer" -- -runs=1 + done + + - if: ${{ matrix.check == 'bench' }} + run: cargo bench --features bench --profile dev -- --profile-time 1 diff --git a/.github/workflows/machete.yml b/.github/workflows/machete.yml new file mode 100644 index 0000000..4236a2f --- /dev/null +++ b/.github/workflows/machete.yml @@ -0,0 +1,34 @@ +name: Machete +on: + workflow_dispatch: + pull_request: + branches: ["main"] + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + machete: + name: Check for unused dependencies + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: Install Rust + uses: ./.github/actions/rust + with: + tools: cargo-hack, cargo-machete + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Check for unused dependencies + run: | + # --with-metadata has false positives, see https://github.com/bnjbvr/cargo-machete/issues/127 + cargo machete --with-metadata + cargo hack --workspace --no-manifest-path machete --with-metadata diff --git a/.github/workflows/mutants.yml b/.github/workflows/mutants.yml new file mode 100644 index 0000000..0d2e6c6 --- /dev/null +++ b/.github/workflows/mutants.yml @@ -0,0 +1,72 @@ +name: Find mutants +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + mutants: + name: Find mutants + if: ${{ github.event_name == 'pull_request' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - id: nss-version + run: echo "minimum=$(cat min_version.txt)" >> "$GITHUB_OUTPUT" + + - uses: ./.github/actions/nss + with: + minimum-version: ${{ steps.nss-version.outputs.minimum }} + + - uses: ./.github/actions/rust + with: + tools: cargo-mutants + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Find incremental mutants + id: mutants + env: + BASE_REF: ${{ github.base_ref }} + if: ${{ github.event_name == 'pull_request' }} + run: | + git diff "origin/$BASE_REF".. > pr.diff + set -o pipefail + cargo mutants --no-shuffle -j 2 -vV --in-diff pr.diff | tee results.txt || true + echo 'title=Incremental Mutants' >> "$GITHUB_OUTPUT" + + - name: Find mutants + if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} + run: | + set -o pipefail + cargo mutants -vV --in-place | tee results.txt || true + echo 'title=All Mutants' >> "$GITHUB_OUTPUT" + + - name: Post step summary + env: + TITLE: ${{ steps.mutants.outputs.title }} + run: | + { + echo "### $TITLE" + echo "See https://mutants.rs/using-results.html for more information." + echo '```' + sed 's/\x1b\[[0-9;]*[mGKHF]//g' results.txt || true + echo '```' + } > "$GITHUB_STEP_SUMMARY" + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: mutants.out + path: mutants.out diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml new file mode 100644 index 0000000..048d910 --- /dev/null +++ b/.github/workflows/pr-comment.yml @@ -0,0 +1,35 @@ +# Post test results as pull request comment. +# +# This is done as a separate workflow as it requires write permissions. The +# tests itself might run off of a fork, i.e., an untrusted environment and should +# thus not be granted write permissions. + +name: PR Comment + +on: + workflow_run: + workflows: ["QNS", "cargo bench", "Performance comparison", "Firefox"] + types: + - completed # zizmor: ignore[dangerous-triggers] + +permissions: + contents: read + +jobs: + comment: + name: Comment on PR + permissions: + pull-requests: write + runs-on: ubuntu-24.04 + if: | + github.event.workflow_run.event == 'pull_request' && + (github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure') + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - uses: ./.github/actions/pr-comment + with: + name: ${{ github.event.workflow_run.name }} + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/readme.yml b/.github/workflows/readme.yml new file mode 100644 index 0000000..973fce1 --- /dev/null +++ b/.github/workflows/readme.yml @@ -0,0 +1,33 @@ +name: Check README.md +on: + workflow_dispatch: + pull_request: + branches: ["main"] + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + readme: + name: Check README.md + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - uses: ./.github/actions/rust + with: + tools: cargo-readme + token: ${{ secrets.GITHUB_TOKEN }} + + - run: | + cd mtu # Only works for this crate currently. + # TODO: cargo-readme doesn't support workspaces: https://github.com/webern/cargo-readme/issues/81 + # Leaving this here so we remember to fix it later. + # cargo readme -o /tmp/README.md + # diff -u README.md /tmp/README.md diff --git a/.github/workflows/rustfmt.yml b/.github/workflows/rustfmt.yml new file mode 100644 index 0000000..066ff26 --- /dev/null +++ b/.github/workflows/rustfmt.yml @@ -0,0 +1,31 @@ +name: Format +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + merge_group: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + format: + name: Format + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - uses: ./.github/actions/rust + with: + version: nightly + components: rustfmt + token: ${{ secrets.GITHUB_TOKEN }} + - run: cargo fmt --all -- --check diff --git a/.github/workflows/sanitize.yml b/.github/workflows/sanitize.yml new file mode 100644 index 0000000..08eb15a --- /dev/null +++ b/.github/workflows/sanitize.yml @@ -0,0 +1,94 @@ +name: Sanitize +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + merge_group: + workflow_dispatch: +env: + DUMP_SIMULATION_SEEDS: /tmp/simulation-seeds + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + sanitize: + name: Sanitize + if: github.actor != 'dependabot[bot]' + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-15] # No Windows support for sanitizers. + sanitizer: [address, thread, leak] # TODO: memory + exclude: + # Memory and leak sanitizers are not supported on macOS. + - os: macos-15 + sanitizer: leak + # - os: macos-15 + # sanitizer: memory + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - uses: ./.github/actions/rust + with: + version: nightly + components: rust-src + tools: cargo-hack + token: ${{ secrets.GITHUB_TOKEN }} + + - id: nss-version + run: echo "minimum=$(cat min_version.txt)" >> "$GITHUB_OUTPUT" + + - uses: ./.github/actions/nss + with: + minimum-version: ${{ steps.nss-version.outputs.minimum }} + + - name: Run tests with sanitizers + env: + RUST_LOG: trace + RUSTDOCFLAGS: "-Z sanitizer=${{ matrix.sanitizer }} -Cunsafe-allow-abi-mismatch=sanitizer" + ASAN_OPTIONS: detect_leaks=1:detect_stack_use_after_return=1 + RUST_BACKTRACE: 1 + OS: ${{ matrix.os }} + SANITIZER: ${{ matrix.sanitizer }} + run: | + # Append to RUSTFLAGS, which may already be set by the Rust action. + export RUSTFLAGS="-Z sanitizer=$SANITIZER $RUSTFLAGS" + if [ "$OS" = "ubuntu-24.04" ]; then + sudo apt-get install -y --no-install-recommends llvm + TARGET="x86_64-unknown-linux-gnu" + elif [ "$OS" = "macos-15" ]; then + # llvm-symbolizer (as part of llvm) is installed by default on macOS runners + TARGET="aarch64-apple-darwin" + # Suppress non-neqo leaks on macOS. TODO: Check occasionally if these are still needed. + { + echo "leak:dyld4::RuntimeState" + echo "leak:fetchInitializingClassList" + echo "leak:std::rt::lang_start_internal" + } > suppressions.txt + PWD=$(pwd) + export LSAN_OPTIONS="suppressions=$PWD/suppressions.txt" + fi + cargo test --locked -Z build-std --target "$TARGET" + cargo hack --workspace test --locked -Z build-std --target "$TARGET" + + - name: Save simulation seeds artifact + if: ${{ env.DUMP_SIMULATION_SEEDS }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: simulation-seeds-${{ matrix.os }}-sanitizer-${{ matrix.sanitizer }} + path: ${{ env.DUMP_SIMULATION_SEEDS }} + compression-level: 9 diff --git a/.github/workflows/semver.yml b/.github/workflows/semver.yml new file mode 100644 index 0000000..de6fef3 --- /dev/null +++ b/.github/workflows/semver.yml @@ -0,0 +1,34 @@ +name: Check semver +on: + workflow_dispatch: + pull_request: + branches: ["main"] + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + semver: + name: Check semver + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + fetch-depth: 0 # We need the full history to compare against `main` for semver changes. + - uses: ./.github/actions/rust + with: + tools: cargo-semver-checks + token: ${{ secrets.GITHUB_TOKEN }} + - id: nss-version + run: echo "minimum=$(cat min_version.txt)" >> "$GITHUB_OUTPUT" + - uses: ./.github/actions/nss + with: + minimum-version: ${{ steps.nss-version.outputs.minimum }} + + - run: cargo semver-checks --default-features --baseline-rev origin/main --workspace diff --git a/Cargo.lock b/Cargo.lock index a759f59..136ca7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,7 +173,7 @@ dependencies = [ ] [[package]] -name = "nss-gk-api" +name = "nss-rs" version = "0.3.0" dependencies = [ "bindgen", @@ -337,7 +337,7 @@ name = "test-fixture" version = "0.1.0" dependencies = [ "log", - "nss-gk-api", + "nss-rs", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4a238d7..4448074 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,15 @@ [package] -name = "nss-gk-api" +name = "nss-rs" version = "0.3.0" authors = ["Martin Thomson ", "Andy Leiserson ", "John M. Schanck ", "Benjamin Beurdouche ", "Anna Weine "] +categories = ["network-programming", "web-programming"] +keywords = ["nss", "crypto", "mozilla", "firefox"] edition = "2021" -rust-version = "1.82.0" +rust-version = "1.81.0" build = "build.rs" license = "MIT/Apache-2.0" description = "Gecko API for NSS" -repository = "https://github.com/mozilla/nss-gk-api" +repository = "https://github.com/mozilla/nss-rs" [dependencies] enum-map = { version = "2.7", default-features = false } @@ -33,6 +35,93 @@ test-fixture = {path = "test-fixture"} [package.metadata.cargo-machete] ignored = ["bindgen", "semver", "serde", "serde_derive", "toml"] +[lints.rust] +absolute_paths_not_starting_with_crate = "warn" +# TODO: Re-activate with MSRV 1.82.0. See +# https://github.com/mozilla/neqo/pull/2661 for details. +# ambiguous_negative_literals = "warn" +explicit_outlives_requirements = "warn" +macro_use_extern_crate = "warn" +missing_abi = "warn" +non_ascii_idents = "warn" +# TODO: Re-activate with MSRV 1.82.0. See +# https://github.com/mozilla/neqo/pull/2661 for details. +# redundant_imports = "warn" +redundant_lifetimes = "warn" +trivial_numeric_casts = "warn" +unit_bindings = "warn" +unused_import_braces = "warn" +unused_lifetimes = "warn" +unused_macro_rules = "warn" +unused_qualifications = "warn" + +[lints.clippy] +cargo = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +allow_attributes = "warn" +allow_attributes_without_reason = "allow" +cfg_not_test = "warn" +clone_on_ref_ptr = "warn" +create_dir = "warn" +dbg_macro = "warn" +empty_drop = "warn" +empty_enum_variants_with_brackets = "warn" +field_scoped_visibility_modifiers = "warn" +filetype_is_file = "warn" +float_cmp_const = "warn" +fn_to_numeric_cast_any = "warn" +get_unwrap = "warn" +if_then_some_else_none = "warn" +# TODO: Re-activate with MSRV 1.82.0. See +# https://github.com/mozilla/neqo/pull/2661 for details. +# impl_trait_in_params = "warn" +infinite_loop = "warn" +iter_over_hash_type = "warn" +large_include_file = "warn" +let_underscore_must_use = "warn" +let_underscore_untyped = "warn" +literal_string_with_formatting_args = "allow" # FIXME: Re-enable "warn" when MSRV is > 1.87. See https://github.com/rust-lang/rust-clippy/pull/13953#issuecomment-2676336899 +lossy_float_literal = "warn" +map_with_unused_argument_over_ranges = "warn" +mem_forget = "warn" +missing_asserts_for_indexing = "warn" +mixed_read_write_in_expression = "warn" +module_name_repetitions = "warn" +multiple_crate_versions = "allow" +multiple_inherent_impl = "warn" +mutex_atomic = "warn" +mutex_integer = "warn" +needless_raw_strings = "warn" +non_ascii_literal = "warn" +non_zero_suggestions = "warn" +partial_pub_fields = "warn" +pathbuf_init_then_push = "warn" +precedence_bits = "warn" +pub_without_shorthand = "warn" +rc_buffer = "warn" +rc_mutex = "warn" +redundant_test_prefix = "warn" +redundant_type_annotations = "warn" +ref_patterns = "warn" +renamed_function_params = "warn" +rest_pat_in_fully_bound_structs = "warn" +return_and_then = "warn" +self_named_module_files = "warn" +semicolon_inside_block = "warn" +string_lit_chars_any = "warn" +suspicious_xor_used_as_pow = "warn" +try_err = "warn" +unnecessary_safety_comment = "warn" +unnecessary_safety_doc = "warn" +unnecessary_self_imports = "warn" +unneeded_field_pattern = "warn" +unused_result_ok = "warn" +unused_trait_names = "warn" +unwrap_in_result = "warn" +unwrap_used = "warn" +verbose_file_reads = "warn" + [features] bench = [] deny-warnings = [] diff --git a/build.rs b/build.rs index b62c394..32149d8 100644 --- a/build.rs +++ b/build.rs @@ -79,7 +79,7 @@ fn setup_clang() { PathBuf::from(dir.trim()) } else { eprintln!("warning: Building without a gecko setup is not likely to work."); - eprintln!(" A working libclang is needed to build nss-gk-api."); + eprintln!(" A working libclang is needed to build nss-rs."); eprintln!(" Either LIBCLANG_PATH or MOZBUILD_STATE_PATH needs to be set."); eprintln!(); eprintln!(" We recommend checking out https://github.com/mozilla/gecko-dev"); @@ -366,7 +366,7 @@ fn pkg_config() -> Result, Box> { assert!( version_req.matches(&modversion_for_cmp), - "neqo has NSS version requirement {version_req}, found {modversion}", + "nss-rs has NSS version requirement {version_req}, found {modversion}", ); let cfg = Command::new("pkg-config") diff --git a/src/aead.rs b/src/aead.rs index bd0cd06..4c60ef4 100644 --- a/src/aead.rs +++ b/src/aead.rs @@ -12,7 +12,7 @@ use std::{ use crate::{ constants::{Cipher, Version}, - err::Res, + err::{sec::SEC_ERROR_BAD_DATA, Error, Res}, experimental_api, p11::{ self, Context, PK11SymKey, PK11_AEADOp, PK11_CreateContextBySymKey, CKA_DECRYPT, @@ -22,9 +22,69 @@ use crate::{ prtypes::{PRUint16, PRUint64, PRUint8}, scoped_ptr, secstatus_to_res, ssl::SSLAeadContext, - Error, SECItemBorrowed, SymKey, + SECItemBorrowed, SymKey, }; +/// Trait for AEAD (Authenticated Encryption with Associated Data) operations. +/// +/// This trait provides a common interface for both real and null AEAD implementations, +/// eliminating code duplication and allowing for consistent usage patterns. +pub trait AeadTrait { + /// Create a new AEAD instance. + /// + /// # Errors + /// + /// Returns `Error` when the underlying crypto operations fail. + fn new(version: Version, cipher: Cipher, secret: &SymKey, prefix: &str) -> Res + where + Self: Sized; + + /// Get the expansion size (authentication tag length) for this AEAD. + fn expansion(&self) -> usize; + + /// Encrypt plaintext with associated data. + /// + /// # Errors + /// + /// Returns `Error` when encryption fails. + fn encrypt<'a>( + &self, + count: u64, + aad: &[u8], + input: &[u8], + output: &'a mut [u8], + ) -> Res<&'a [u8]>; + + /// Encrypt plaintext in place with associated data. + /// + /// # Errors + /// + /// Returns `Error` when encryption fails. + fn encrypt_in_place<'a>(&self, count: u64, aad: &[u8], data: &'a mut [u8]) + -> Res<&'a mut [u8]>; + + /// Decrypt ciphertext with associated data. + /// + /// # Errors + /// + /// Returns `Error` when decryption or authentication fails. + fn decrypt<'a>( + &self, + count: u64, + aad: &[u8], + input: &[u8], + output: &'a mut [u8], + ) -> Res<&'a [u8]>; + + /// Decrypt ciphertext in place with associated data. + /// + /// # Errors + /// + /// Returns `Error` when decryption or authentication fails. + fn decrypt_in_place<'a>(&self, count: u64, aad: &[u8], data: &'a mut [u8]) + -> Res<&'a mut [u8]>; +} + experimental_api!(SSL_MakeAead( version: PRUint16, cipher: PRUint16, @@ -63,21 +123,6 @@ pub struct RealAead { } impl RealAead { - /// Create a new AEAD based on the indicated TLS version and cipher suite. - /// - /// # Errors - /// - /// Returns `Error` when the supporting NSS functions fail. - pub fn new(version: Version, cipher: Cipher, secret: &SymKey, prefix: &str) -> Res { - let s: *mut PK11SymKey = **secret; - unsafe { Self::from_raw(version, cipher, s, prefix) } - } - - #[must_use] - pub const fn expansion() -> usize { - 16 - } - unsafe fn from_raw( version: Version, cipher: Cipher, @@ -98,16 +143,19 @@ impl RealAead { ctx: AeadContext::from_ptr(ctx)?, }) } +} - /// Encrypt a plaintext. - /// - /// The space provided in `output` needs to be larger than `input` by - /// the value provided in `Aead::expansion`. - /// - /// # Errors - /// - /// If the input can't be protected or any input is too large for NSS. - pub fn encrypt<'a>( +impl AeadTrait for RealAead { + fn new(version: Version, cipher: Cipher, secret: &SymKey, prefix: &str) -> Res { + let s: *mut PK11SymKey = **secret; + unsafe { Self::from_raw(version, cipher, s, prefix) } + } + + fn expansion(&self) -> usize { + 16 + } + + fn encrypt<'a>( &self, count: u64, aad: &[u8], @@ -131,24 +179,16 @@ impl RealAead { Ok(&output[..l.try_into()?]) } - /// Encrypt `data` consisting of `aad` and plaintext `data` in place. - /// - /// The last `Aead::expansion` of `data` is overwritten by the AEAD tag by this function. - /// Therefore, a buffer should be provided that is that much larger than the plaintext. - /// - /// # Panics - /// - /// If `data` is shorter than `::expansion()`. - /// - /// # Errors - /// - /// If the input can't be protected or any input is too large for NSS. - pub fn encrypt_in_place<'a>( + fn encrypt_in_place<'a>( &self, count: u64, aad: &[u8], data: &'a mut [u8], ) -> Res<&'a mut [u8]> { + if data.len() < self.expansion() { + return Err(Error::from(SEC_ERROR_BAD_DATA)); + } + let mut l: c_uint = 0; unsafe { SSL_AeadEncrypt( @@ -157,21 +197,17 @@ impl RealAead { aad.as_ptr(), c_uint::try_from(aad.len())?, data.as_ptr(), - c_uint::try_from(data.len() - Self::expansion())?, - data.as_ptr(), + c_uint::try_from(data.len() - self.expansion())?, + data.as_mut_ptr(), &mut l, c_uint::try_from(data.len())?, ) }?; - Ok(&mut data[..l.try_into()?]) + debug_assert_eq!(usize::try_from(l)?, data.len()); + Ok(data) } - /// Decrypt a ciphertext. - /// - /// # Errors - /// - /// If the input isn't authenticated or any input is too large for NSS. - pub fn decrypt<'a>( + fn decrypt<'a>( &self, count: u64, aad: &[u8], @@ -198,14 +234,7 @@ impl RealAead { Ok(&output[..l.try_into()?]) } - /// Decrypt a ciphertext in place. - /// Returns a subslice of `data` (without the last `::expansion()` bytes), - /// that has been decrypted in place. - /// - /// # Errors - /// - /// If the input isn't authenticated or any input is too large for NSS. - pub fn decrypt_in_place<'a>( + fn decrypt_in_place<'a>( &self, count: u64, aad: &[u8], @@ -223,12 +252,12 @@ impl RealAead { c_uint::try_from(aad.len())?, data.as_ptr(), c_uint::try_from(data.len())?, - data.as_ptr(), + data.as_mut_ptr(), &mut l, c_uint::try_from(data.len())?, ) }?; - debug_assert_eq!(usize::try_from(l)?, data.len() - Self::expansion()); + debug_assert_eq!(usize::try_from(l)?, data.len() - self.expansion()); Ok(&mut data[..l.try_into()?]) } } diff --git a/src/aead_null.rs b/src/aead_null.rs index ea1e3c2..855cf5e 100644 --- a/src/aead_null.rs +++ b/src/aead_null.rs @@ -4,11 +4,10 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -#![expect(clippy::missing_errors_doc, reason = "OK here.")] - use std::fmt; use crate::{ + aead::AeadTrait, constants::{Cipher, Version}, err::{sec::SEC_ERROR_BAD_DATA, Error, Res}, p11::SymKey, @@ -19,30 +18,39 @@ pub const AEAD_NULL_TAG: &[u8] = &[0x0a; 16]; pub struct AeadNull {} impl AeadNull { - #[expect( - clippy::unnecessary_wraps, - reason = "Need to replicate the API of aead::RealAead." - )] - pub const fn new( - _version: Version, - _cipher: Cipher, - _secret: &SymKey, - _prefix: &str, - ) -> Res { + fn decrypt_check(&self, _count: u64, _aad: &[u8], input: &[u8]) -> Res { + if input.len() < self.expansion() { + return Err(Error::from(SEC_ERROR_BAD_DATA)); + } + + let len_encrypted = input + .len() + .checked_sub(self.expansion()) + .ok_or_else(|| Error::from(SEC_ERROR_BAD_DATA))?; + // Check that: + // 1) expansion is all zeros and + // 2) if the encrypted data is also supplied that at least some values are no zero + // (otherwise padding will be interpreted as a valid packet) + if &input[len_encrypted..] == AEAD_NULL_TAG + && (len_encrypted == 0 || input[..len_encrypted].iter().any(|x| *x != 0x0)) + { + Ok(len_encrypted) + } else { + Err(Error::from(SEC_ERROR_BAD_DATA)) + } + } +} + +impl AeadTrait for AeadNull { + fn new(_version: Version, _cipher: Cipher, _secret: &SymKey, _prefix: &str) -> Res { Ok(Self {}) } - #[must_use] - pub const fn expansion() -> usize { + fn expansion(&self) -> usize { AEAD_NULL_TAG.len() } - #[expect( - clippy::unnecessary_wraps, - clippy::unused_self, - reason = "Need to replicate the API of aead::RealAead." - )] - pub fn encrypt<'a>( + fn encrypt<'a>( &self, _count: u64, _aad: &[u8], @@ -51,53 +59,22 @@ impl AeadNull { ) -> Res<&'a [u8]> { let l = input.len(); output[..l].copy_from_slice(input); - output[l..l + Self::expansion()].copy_from_slice(AEAD_NULL_TAG); - Ok(&output[..l + Self::expansion()]) + output[l..l + self.expansion()].copy_from_slice(AEAD_NULL_TAG); + Ok(&output[..l + self.expansion()]) } - #[expect( - clippy::unnecessary_wraps, - clippy::unused_self, - reason = "Need to replicate the API of aead::RealAead." - )] - pub fn encrypt_in_place<'a>( + fn encrypt_in_place<'a>( &self, _count: u64, _aad: &[u8], data: &'a mut [u8], ) -> Res<&'a mut [u8]> { - let pos = data.len() - Self::expansion(); + let pos = data.len() - self.expansion(); data[pos..].copy_from_slice(AEAD_NULL_TAG); Ok(data) } - #[expect( - clippy::unused_self, - reason = "Need to replicate the API of aead::RealAead." - )] - fn decrypt_check(&self, _count: u64, _aad: &[u8], input: &[u8]) -> Res { - if input.len() < Self::expansion() { - return Err(Error::from(SEC_ERROR_BAD_DATA)); - } - - let len_encrypted = input - .len() - .checked_sub(Self::expansion()) - .ok_or_else(|| Error::from(SEC_ERROR_BAD_DATA))?; - // Check that: - // 1) expansion is all zeros and - // 2) if the encrypted data is also supplied that at least some values are no zero - // (otherwise padding will be interpreted as a valid packet) - if &input[len_encrypted..] == AEAD_NULL_TAG - && (len_encrypted == 0 || input[..len_encrypted].iter().any(|x| *x != 0x0)) - { - Ok(len_encrypted) - } else { - Err(Error::from(SEC_ERROR_BAD_DATA)) - } - } - - pub fn decrypt<'a>( + fn decrypt<'a>( &self, count: u64, aad: &[u8], @@ -110,7 +87,7 @@ impl AeadNull { }) } - pub fn decrypt_in_place<'a>( + fn decrypt_in_place<'a>( &self, count: u64, aad: &[u8], diff --git a/src/agent.rs b/src/agent.rs index 18e0e0f..7983029 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -1470,6 +1470,7 @@ impl From for Agent { } #[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] mod tests { use crate::ResumptionToken; diff --git a/src/err.rs b/src/err.rs index 64c6b42..61ac7ea 100644 --- a/src/err.rs +++ b/src/err.rs @@ -217,6 +217,7 @@ pub fn secstatus_to_res(code: SECStatus) -> Res<()> { } #[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] mod tests { use test_fixture::fixture_init; diff --git a/src/lib.rs b/src/lib.rs index 8387c79..3002267 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ #![allow(clippy::missing_panics_doc)] #![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_safety_doc)] +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] #[cfg(feature = "disable-encryption")] pub mod aead_null; @@ -65,6 +66,7 @@ pub use self::aead::RealAead; #[cfg(feature = "disable-encryption")] pub use self::aead_null::AeadNull as Aead; pub use self::{ + aead::AeadTrait, agent::{ Agent, AllowZeroRtt, Client, HandshakeState, Record, RecordList, ResumptionToken, SecretAgent, SecretAgentInfo, SecretAgentPreInfo, Server, ZeroRttCheckResult, diff --git a/src/min_version.rs b/src/min_version.rs index edf58d8..cf558b7 100644 --- a/src/min_version.rs +++ b/src/min_version.rs @@ -4,6 +4,6 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -/// The minimum version of NSS that is required by this version of neqo. +/// The minimum version of NSS that is required by this version of nss-rs. /// Note that the string may contain whitespace at the beginning and/or end. pub const MINIMUM_NSS_VERSION: &str = include_str!("../min_version.txt"); diff --git a/src/p11.rs b/src/p11.rs index 2dea452..105cc19 100644 --- a/src/p11.rs +++ b/src/p11.rs @@ -308,6 +308,7 @@ pub fn random() -> [u8; N] { impl_into_result!(SECOidData); #[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] mod test { use test_fixture::fixture_init; diff --git a/src/result.rs b/src/result.rs index a1f07de..8757f67 100644 --- a/src/result.rs +++ b/src/result.rs @@ -54,6 +54,7 @@ fn result_helper(rv: SECStatus, allow_blocked: bool) -> Res { } #[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] mod tests { use super::{result, result_or_blocked}; use crate::{ diff --git a/src/selfencrypt.rs b/src/selfencrypt.rs index f518da9..81be3d8 100644 --- a/src/selfencrypt.rs +++ b/src/selfencrypt.rs @@ -9,6 +9,7 @@ use std::{fmt::Write as _, io::Write as _, mem}; use log::{info, trace}; use crate::{ + aead::AeadTrait as _, constants::{Cipher, Version}, err::{Error, Res}, hkdf, @@ -93,7 +94,7 @@ impl SelfEncrypt { // AAD covers the entire header, plus the value of the AAD parameter that is provided. let salt = random::<{ Self::SALT_LENGTH }>(); let cipher = self.make_aead(&self.key, &salt)?; - let encoded_len = 2 + salt.len() + plaintext.len() + Aead::expansion(); + let encoded_len = 2 + salt.len() + plaintext.len() + cipher.expansion(); let mut enc = Vec::::with_capacity(encoded_len); enc.write_all(&[Self::VERSION]) @@ -142,10 +143,10 @@ impl SelfEncrypt { /// when the keys have been rotated; or when NSS fails. #[expect(clippy::similar_names, reason = "aad is similar to aead.")] pub fn open(&self, aad: &[u8], ciphertext: &[u8]) -> Res> { - if ciphertext[0] != Self::VERSION { + if *ciphertext.first().ok_or(Error::SelfEncrypt)? != Self::VERSION { return Err(Error::SelfEncrypt); } - let Some(key) = self.select_key(ciphertext[1]) else { + let Some(key) = self.select_key(*ciphertext.get(1).ok_or(Error::SelfEncrypt)?) else { return Err(Error::SelfEncrypt); }; let offset = 2 + Self::SALT_LENGTH; diff --git a/src/sig.rs b/src/sig.rs index ac29b6f..5a1d519 100644 --- a/src/sig.rs +++ b/src/sig.rs @@ -20,9 +20,8 @@ // CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // use crate::Result; -use nss::{ec::Curve, ec::PublicKey}; - -use nss_gk_api::HashAlgorithm; +use nss::ec::{Curve, PublicKey}; +use nss_rs::HashAlgorithm; /// A signature verification algorithm. pub struct SignatureAlgorithm { @@ -70,9 +69,10 @@ impl<'a> Signature<'a> { #[cfg(test)] mod tests { - use super::*; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; + use super::*; + #[test] fn test_ecdsa_p384_sha384_verify() { // Test generated with JS DOM's WebCrypto. diff --git a/src/ssl.rs b/src/ssl.rs index b34f4d0..6804a25 100644 --- a/src/ssl.rs +++ b/src/ssl.rs @@ -137,6 +137,7 @@ experimental_api!(SSL_SetCertificateCompressionAlgorithm( )); #[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] mod tests { use super::{SSL_GetNumImplementedCiphers, SSL_NumImplementedCiphers}; diff --git a/src/time.rs b/src/time.rs index 30f8ebf..c0f3909 100644 --- a/src/time.rs +++ b/src/time.rs @@ -218,6 +218,7 @@ impl Default for TimeHolder { } #[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] mod test { use std::{ convert::{TryFrom as _, TryInto as _}, diff --git a/test-fixture/Cargo.toml b/test-fixture/Cargo.toml index 5774e1f..cb2669e 100644 --- a/test-fixture/Cargo.toml +++ b/test-fixture/Cargo.toml @@ -8,10 +8,10 @@ license = "MIT/Apache-2.0" [dependencies] log = {version = "0.4.0", default-features = false} -nss-gk-api = { path = "../" } +nss-rs = { path = "../" } [features] -bench = ["nss-gk-api/bench"] +bench = ["nss-rs/bench"] disable-random = [] [package.metadata.cargo-machete] diff --git a/test-fixture/src/lib.rs b/test-fixture/src/lib.rs index ac6d601..f1ae154 100644 --- a/test-fixture/src/lib.rs +++ b/test-fixture/src/lib.rs @@ -11,7 +11,7 @@ use std::{ time::{Duration, Instant}, }; -use nss_gk_api::{init_db, AntiReplay}; +use nss_rs::{init_db, AntiReplay}; /// The path for the database used in tests. /// diff --git a/tests/aead.rs b/tests/aead.rs index 3c20106..560fd3d 100644 --- a/tests/aead.rs +++ b/tests/aead.rs @@ -7,9 +7,9 @@ #![warn(clippy::pedantic)] #![cfg(not(feature = "disable-encryption"))] -use nss_gk_api::{ +use nss_rs::{ constants::{Cipher, TLS_AES_128_GCM_SHA256, TLS_VERSION_1_3}, - hkdf, Aead, + hkdf, Aead, AeadTrait as _, }; use test_fixture::fixture_init; @@ -120,3 +120,14 @@ fn aead_encrypt_decrypt() { let res = aead.decrypt(1, &scratch[..], ciphertext, plaintext_buf); assert!(res.is_err()); } + +#[test] +fn aead_encrypt_in_place_too_small_buffer() { + let aead = make_aead(TLS_AES_128_GCM_SHA256); + + // Create a buffer that's smaller than the expansion size + let mut small_buffer = vec![0u8; aead.expansion() - 1]; + + let result = aead.encrypt_in_place(1, AAD, &mut small_buffer); + assert!(result.is_err()); +} diff --git a/tests/agent.rs b/tests/agent.rs index a695e9d..652861e 100644 --- a/tests/agent.rs +++ b/tests/agent.rs @@ -3,7 +3,7 @@ use std::ffi::CStr; -use nss_gk_api::{ +use nss_rs::{ agent::CertificateCompressor, generate_ech_keys, AuthenticationStatus, Client, Error, HandshakeState, Res, SecretAgentPreInfo, Server, ZeroRttCheckResult, ZeroRttChecker, TLS_AES_128_GCM_SHA256, TLS_CHACHA20_POLY1305_SHA256, TLS_GRP_EC_SECP256R1, TLS_GRP_EC_X25519, diff --git a/tests/ext.rs b/tests/ext.rs index 738dff6..c389603 100644 --- a/tests/ext.rs +++ b/tests/ext.rs @@ -7,7 +7,7 @@ use std::{cell::RefCell, rc::Rc}; use handshake::forward_records; -use nss_gk_api::{ +use nss_rs::{ constants::{HandshakeMessage, TLS_HS_CLIENT_HELLO, TLS_HS_ENCRYPTED_EXTENSIONS}, ext::{ExtensionHandler, ExtensionHandlerResult, ExtensionWriterResult}, generate_ech_keys, AuthenticationStatus, Client, Error, HandshakeState, Server, diff --git a/tests/handshake.rs b/tests/handshake.rs index 130bc49..5e65f1b 100644 --- a/tests/handshake.rs +++ b/tests/handshake.rs @@ -9,7 +9,7 @@ use std::{mem, time::Instant}; use log::info; -use nss_gk_api::{ +use nss_rs::{ AntiReplay, AuthenticationStatus, Client, HandshakeState, RecordList, Res, ResumptionToken, SecretAgent, Server, ZeroRttCheckResult, ZeroRttChecker, }; diff --git a/tests/hkdf.rs b/tests/hkdf.rs index bfae875..ec16e72 100644 --- a/tests/hkdf.rs +++ b/tests/hkdf.rs @@ -4,7 +4,7 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -use nss_gk_api::{ +use nss_rs::{ constants::{ Cipher, TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_VERSION_1_3, diff --git a/tests/hp.rs b/tests/hp.rs index af72795..59eb64a 100644 --- a/tests/hp.rs +++ b/tests/hp.rs @@ -4,7 +4,7 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -use nss_gk_api::{ +use nss_rs::{ constants::{ Cipher, TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_VERSION_1_3, diff --git a/tests/init.rs b/tests/init.rs index a520b00..d459534 100644 --- a/tests/init.rs +++ b/tests/init.rs @@ -4,18 +4,18 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -// This uses external interfaces to neqo_crypto rather than being a module +// This uses external interfaces to nss-rs rather than being a module // inside of lib.rs. Because all other code uses the test_fixture module, // they will be calling into the public version of init_db(). Calling into // the version exposed to an inner module in lib.rs would result in calling // a different version of init_db. That causes explosions as they get // different versions of the Once instance they use and they initialize NSS // twice, probably likely in parallel. That doesn't work out well. -use nss_gk_api::assert_initialized; +use nss_rs::assert_initialized; #[cfg(nss_nodb)] -use nss_gk_api::init; +use nss_rs::init; #[cfg(not(nss_nodb))] -use nss_gk_api::init_db; +use nss_rs::init_db; // Pull in the NSS internals so that we can ask NSS if it thinks that // it is properly initialized. @@ -25,14 +25,14 @@ use nss_gk_api::init_db; reason = "Code is bindgen-generated." )] mod nss { - use nss_gk_api::nss_prelude::*; + use nss_rs::nss_prelude::*; include!(concat!(env!("OUT_DIR"), "/nss_init.rs")); } #[cfg(nss_nodb)] #[test] fn init_nodb() { - neqo_crypto::init().unwrap(); + nss_rs::init().unwrap(); assert_initialized(); unsafe { assert_ne!(nss::NSS_IsInitialized(), 0); diff --git a/tests/selfencrypt.rs b/tests/selfencrypt.rs index f61fa77..bc18f94 100644 --- a/tests/selfencrypt.rs +++ b/tests/selfencrypt.rs @@ -7,7 +7,7 @@ #![cfg(not(feature = "disable-encryption"))] #![cfg(test)] -use nss_gk_api::{ +use nss_rs::{ constants::{TLS_AES_128_GCM_SHA256, TLS_VERSION_1_3}, init, selfencrypt::SelfEncrypt,