diff --git a/.github/workflows/autoconf.yml b/.github/workflows/autoconf.yml deleted file mode 100644 index abad8450..00000000 --- a/.github/workflows/autoconf.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Checks that autoconf has been run if configure.ac was updated -# Assumes that autoconf 2.71 was run, the same as ubuntu 22.04 -name: Autoconf Up To Date -on: - pull_request: - workflow_dispatch: - push: -jobs: - autoconf: - runs-on: 'ubuntu-22.04' - - steps: - - name: deps - run: | - sudo apt-get -y update - sudo apt-get -y install autoconf - - - uses: actions/checkout@v6 - - - name: run autoconf - run: autoconf && autoheader - - - name: check no difference - run: git diff --exit-code diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08b0096c..07424ee2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,14 +6,17 @@ on: pull_request: workflow_dispatch: push: -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: for pass test - run: | - echo "This is a test to check that the workflow is working. If you see this, the workflow is running and you can add more steps to it." + branches: + - main + - master + +# Avoid duplicate push + pull_request runs on PR branches, and cancel +# superseded runs when new commits are pushed. +concurrency: + group: buildtest-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: build: runs-on: ${{ matrix.os || 'ubuntu-24.04' }} strategy: @@ -28,85 +31,93 @@ jobs: # MULTI and NOWRITEV are passed as integers to the build include: - # - name: plain linux - # upload_sizes: 1 - - # - name: multi binary - # multi: 1 - # multilink: 1 - - # - name: multi binary, dropbearmulti argv0 - # multi: 1 - # multiwrapper: 1 - - # - name: client only - # runcheck: 'no' - # make_target: PROGRAMS=dbclient - - # - name: server only - # runcheck: 'no' - # make_target: PROGRAMS=dropbear - - # - name: bundled libtom, 22.04, no writev() - # # test can use an older distro with bundled libtommath - # os: ubuntu-22.04 - # configure_flags: --enable-bundled-libtom --enable-werror - # # NOWRITEV is unrelated, test here to save a job - # nowritev: 1 - # # our tests expect >= python3.7 - # runcheck: 'no' - - # - name: linux clang - # cc: clang + - name: plain linux + upload_sizes: 1 + + - name: multi binary + multi: 1 + multilink: 1 + + - name: multi binary, dropbearmulti argv0 + multi: 1 + multiwrapper: 1 + + - name: client only + runcheck: 'no' + make_target: PROGRAMS=dbclient + + - name: server only + runcheck: 'no' + make_target: PROGRAMS=dropbear + + - name: bundled libtom, 22.04, no writev() + # test can use an older distro with bundled libtommath + os: ubuntu-22.04 + configure_flags: --enable-bundled-libtom --enable-werror + # NOWRITEV is unrelated, test here to save a job + nowritev: 1 + # our tests expect >= python3.7 + runcheck: 'no' + + - name: linux clang + cc: clang # Some platforms only have old compilers, we try to keep # compatibilty. For some reason -std=c89 doesn't enforce # early declarations so we specify it anyway. - # - name: c89 - # extracflags: -std=c89 -Wdeclaration-after-statement - # # enable all options - # nondefault: 1 - # configure_flags: --enable-pam - # # sntrup761.c is not c89 compliant - # localoptions: | - # #define DROPBEAR_SNTRUP761 0 - # #define DROPBEAR_MLKEM768 0 - - # - name: macos 14 - # os: macos-14 - # cc: clang - # # OS X says daemon() and utmp are deprecated. - # # OS X tests for undefined TARGET_OS_EMBEDDED in libc headers - # extracflags: -Wno-deprecated-declarations -Wno-undef - # runcheck: 'no' - # apt: 'no' - # # fails with: - # # .../ranlib: file: libtomcrypt.a(cbc_setiv.o) has no symbols - # ranlib: ranlib -no_warning_for_no_symbols - - # - name: macos 15 - # os: macos-15 - # cc: clang - # # OS X says daemon() and utmp are deprecated. - # # OS X tests for undefined TARGET_OS_EMBEDDED in libc headers - # extracflags: -Wno-deprecated-declarations -Wno-undef - # runcheck: 'no' - # apt: 'no' - # # fails with: - # # .../ranlib: file: libtomcrypt.a(cbc_setiv.o) has no symbols - # ranlib: ranlib -no_warning_for_no_symbols + - name: c89 + extracflags: -std=c89 -Wdeclaration-after-statement + # enable all options + nondefault: 1 + configure_flags: --enable-pam + # sntrup761.c is not c89 compliant + localoptions: | + #define DROPBEAR_SNTRUP761 0 + #define DROPBEAR_MLKEM768 0 + + - name: macos 14 + os: macos-14 + cc: clang + # OS X says daemon() and utmp are deprecated. + # OS X tests for undefined TARGET_OS_EMBEDDED in libc headers + extracflags: -Wno-deprecated-declarations -Wno-undef + runcheck: 'no' + apt: 'no' + # fails with: + # .../ranlib: file: libtomcrypt.a(cbc_setiv.o) has no symbols + ranlib: ranlib -no_warning_for_no_symbols + # macos doesn't have setresgid + localoptions: | + #define DROPBEAR_SVR_DROP_PRIVS 0 + #define DROPBEAR_SVR_LOCALSTREAMFWD 0 + + - name: macos 15 + os: macos-15 + cc: clang + # OS X says daemon() and utmp are deprecated. + # OS X tests for undefined TARGET_OS_EMBEDDED in libc headers + extracflags: -Wno-deprecated-declarations -Wno-undef + runcheck: 'no' + apt: 'no' + # fails with: + # .../ranlib: file: libtomcrypt.a(cbc_setiv.o) has no symbols + ranlib: ranlib -no_warning_for_no_symbols + # macos doesn't have setresgid + localoptions: | + #define DROPBEAR_SVR_DROP_PRIVS 0 + #define DROPBEAR_SVR_LOCALSTREAMFWD 0 # Check that debug code doesn't bitrot - # - name: DEBUG_TRACE - # nondefault: 1 - # configure_flags: --enable-pam - # localoptions: | - # #define DEBUG_TRACE 5 + - name: DEBUG_TRACE + nondefault: 1 + configure_flags: --enable-pam + localoptions: | + #define DEBUG_TRACE 5 # Check off-by-default options don't bitrot - # - name: nondefault options - # nondefault: 1 - # configure_flags: --enable-pam + - name: nondefault options + nondefault: 1 + configure_flags: --enable-pam - name: most options disabled configure_flags: --disable-harden --disable-zlib --disable-openpty --disable-lastlog @@ -145,25 +156,25 @@ jobs: #define DROPBEAR_USE_PASSWORD_ENV 0 #define DROPBEAR_SFTPSERVER 0 - # - name: no sha1 - # runcheck: 'no' - # # disables all sha1 - # localoptions: | - # #define DROPBEAR_SHA1_HMAC 0 - # #define DROPBEAR_RSA_SHA1 0 - # #define DROPBEAR_DH_GROUP14_SHA1 0 - # #define DROPBEAR_ECDSA 0 - # #define DROPBEAR_ED25519 0 - # #define DROPBEAR_SK_KEYS 0 - # #define DROPBEAR_ENABLE_GCM_MODE 1 - # #define DROPBEAR_3DES 1 - # #define DROPBEAR_DH_GROUP16 1 - # #define DROPBEAR_SHA2_512_HMAC 1 - # #define DROPBEAR_CLI_PUBKEY_AUTH 0 - - # - name: pq, no plain x25519 - # localoptions: | - # #define DROPBEAR_CURVE25519 0 + - name: no sha1 + runcheck: 'no' + # disables all sha1 + localoptions: | + #define DROPBEAR_SHA1_HMAC 0 + #define DROPBEAR_RSA_SHA1 0 + #define DROPBEAR_DH_GROUP14_SHA1 0 + #define DROPBEAR_ECDSA 0 + #define DROPBEAR_ED25519 0 + #define DROPBEAR_SK_KEYS 0 + #define DROPBEAR_ENABLE_GCM_MODE 1 + #define DROPBEAR_3DES 1 + #define DROPBEAR_DH_GROUP16 1 + #define DROPBEAR_SHA2_512_HMAC 1 + #define DROPBEAR_CLI_PUBKEY_AUTH 0 + + - name: pq, no plain x25519 + localoptions: | + #define DROPBEAR_CURVE25519 0 # # Fuzzers run standalone. A bit superfluous with cifuzz, but # # good to run the whole corpus to keep it working. @@ -228,7 +239,7 @@ jobs: if: ${{ matrix.nondefault }} run: | # Turn on anything that's off by default. Rough but seems sufficient - grep ' 0$' src/default_options.h | sed 's/0$/1/' >> localoptions.h + grep ' 0$' src/default_options.h | grep -v DROPBEAR_SVR_OTP_PASSWORD | sed 's/0$/1/' >> localoptions.h # PAM clashes with password echo "#define DROPBEAR_SVR_PASSWORD_AUTH 0" >> localoptions.h # 1 second timeout is too short diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 67a5d9d4..ae4ac94f 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -7,25 +7,29 @@ on: push: branches: - master +# Cancel superseded runs on the same ref so pushes don't pile up. +concurrency: + group: cifuzz-${{ github.ref }} + cancel-in-progress: true jobs: Fuzzing: runs-on: ubuntu-latest steps: - - name: Build Fuzzers - id: build - uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master - with: - oss-fuzz-project-name: 'dropbear' - dry-run: false - - name: Run Fuzzers - uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master - with: - oss-fuzz-project-name: 'dropbear' - fuzz-seconds: 1200 - dry-run: false - - name: Upload Crash - uses: actions/upload-artifact@v4 - if: failure() && steps.build.outcome == 'success' - with: - name: artifacts - path: ./out/artifacts + - name: Build Fuzzers + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + oss-fuzz-project-name: "dropbear" + dry-run: false + - name: Run Fuzzers + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + oss-fuzz-project-name: "dropbear" + fuzz-seconds: 300 + dry-run: false + - name: Upload Crash + uses: actions/upload-artifact@v4 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts diff --git a/.github/workflows/outoftree.yml b/.github/workflows/outoftree.yml deleted file mode 100644 index 78020e2f..00000000 --- a/.github/workflows/outoftree.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Can be used locally with https://github.com/nektos/act - -name: Out of tree build -on: - pull_request: - workflow_dispatch: - push: -jobs: - outoftree: - runs-on: 'ubuntu-22.04' - - steps: - - uses: actions/checkout@v6 - - - name: build - run: | - mkdir build - cd build - ../configure --enable-fuzz --enable-bundled-libtom --prefix=$PWD/inst - make -j3 - make -j3 fuzzstandalone - make install - test -x inst/bin/dbclient - test -f inst/share/man/man8/dropbear.8 diff --git a/.github/workflows/release-zig.yml b/.github/workflows/release-zig.yml new file mode 100644 index 00000000..6abbf25c --- /dev/null +++ b/.github/workflows/release-zig.yml @@ -0,0 +1,173 @@ +name: Release (zig build) + +# Builds dropbearmulti with `zig build` and publishes cross-compiled binaries. +# Triggered by tags like `zig/v2025.88` (use `-alpha`/`-beta` suffixes for pre-releases). +on: + push: + tags: + - "zig/v*" + workflow_dispatch: + inputs: + version: + description: "Version to build (e.g. v2025.88), no release is published" + required: false + default: "v0.0.0-dev" + +permissions: + contents: write + +jobs: + build: + name: Build ${{ matrix.label }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + target: x86_64-linux-musl + label: linux_amd64 + - runner: ubuntu-latest + target: aarch64-linux-musl + label: linux_arm64 + - runner: ubuntu-latest + target: arm-linux-musleabihf + label: linux_armv7 + - runner: macos-latest + target: x86_64-macos + label: darwin_amd64 + - runner: macos-latest + target: aarch64-macos + label: darwin_arm64 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + + - name: Cache Zig build artifacts + uses: actions/cache@v4 + with: + path: | + ~/.cache/zig + .zig-cache + key: dropbear-${{ matrix.target }}-${{ hashFiles('build.zig', 'build.zig.zon') }} + restore-keys: | + dropbear-${{ matrix.target }}- + + - name: Parse version and channel + id: version + shell: bash + run: | + if [[ "${GITHUB_REF}" == refs/tags/zig/* ]]; then + version="${GITHUB_REF#refs/tags/zig/}" + else + version="${{ github.event.inputs.version }}" + fi + echo "version=${version}" >> "$GITHUB_OUTPUT" + if [[ "$version" =~ -alpha ]]; then + echo "channel=dev" >> "$GITHUB_OUTPUT" + elif [[ "$version" =~ -beta ]]; then + echo "channel=beta" >> "$GITHUB_OUTPUT" + else + echo "channel=prod" >> "$GITHUB_OUTPUT" + fi + + - name: Build dropbearmulti + shell: bash + run: | + channel="${{ steps.version.outputs.channel }}" + if [[ "$channel" == "prod" ]]; then + optimize="ReleaseSmall" + else + optimize="ReleaseSafe" + fi + echo "Channel: ${channel}, Optimize: ${optimize}, Target: ${{ matrix.target }}" + if [[ "${{ matrix.target }}" == *-macos ]]; then + # Cross-compile both macOS arches on the arm64 runner using + # the universal macOS SDK so system headers like + # resolve (an explicit -Dtarget disables zig's auto SDK + # detection, so we pass --sysroot explicitly). + zig build -Doptimize="${optimize}" -Dtarget="${{ matrix.target }}" --sysroot "$(xcrun --show-sdk-path)" + else + zig build -Doptimize="${optimize}" -Dtarget="${{ matrix.target }}" + fi + + - name: Build sftp-server (Linux only) + if: startsWith(matrix.runner, 'ubuntu') + shell: bash + run: | + ./build-sftp-server.sh "${{ matrix.target }}" zig-out/bin + + - name: Package archive + id: package + shell: bash + run: | + version="${{ steps.version.outputs.version }}" + archive="dropbear_${version}_${{ matrix.label }}.tar.gz" + mkdir -p release-artifacts + # Bundle dropbearmulti plus sftp-server (Linux) if present. + files="dropbearmulti" + [ -f zig-out/bin/sftp-server ] && files="$files sftp-server" + tar -czf "release-artifacts/${archive}" -C zig-out/bin $files + echo "archive=${archive}" >> "$GITHUB_OUTPUT" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: dropbear-${{ matrix.label }} + path: release-artifacts/*.tar.gz + retention-days: 7 + + release: + name: Publish release + needs: build + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/zig/') + steps: + - name: Parse version and channel + id: version + shell: bash + run: | + version="${GITHUB_REF#refs/tags/zig/}" + echo "version=${version}" >> "$GITHUB_OUTPUT" + if [[ "$version" =~ -alpha ]]; then + echo "channel=dev" >> "$GITHUB_OUTPUT" + elif [[ "$version" =~ -beta ]]; then + echo "channel=beta" >> "$GITHUB_OUTPUT" + else + echo "channel=prod" >> "$GITHUB_OUTPUT" + fi + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Generate checksums + shell: bash + run: | + cd dist + sha256sum *.tar.gz > checksums.txt + cat checksums.txt + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.RELEASER_TOKEN || secrets.GITHUB_TOKEN }} + shell: bash + run: | + prerelease="" + if [[ "${{ steps.version.outputs.channel }}" != "prod" ]]; then + prerelease="--prerelease" + fi + gh release create "${{ github.ref_name }}" \ + --repo "${{ github.repository }}" \ + --title "dropbear ${{ steps.version.outputs.version }}" \ + --generate-notes \ + $prerelease \ + dist/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index e6973e81..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Release -on: - push: - tags: - - 'v*.*.*' - -jobs: - release: - name: Release on GitHub - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Get Release Version - run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Build arm binaries - run: | - sh build.sh arm - - - name: Build arm64 binaries - run: | - sh build.sh arm64 - - - name: Archives & Checksum - id: archives - run: | - sh ./package.sh "${{ env.RELEASE_VERSION }}" - - - name: Release - uses: softprops/action-gh-release@v2 - id: create_release - if: github.ref_type == 'tag' - with: - generate_release_notes: true - files: | - ${{ env.RELEASE_VERSION }}-linux-arm.tar.gz - ${{ env.RELEASE_VERSION }}-linux-arm_checksums.txt - ${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz - ${{ env.RELEASE_VERSION }}-linux-arm64_checksums.txt - - - name: Notify Slack - uses: rtCamp/action-slack-notify@v2 - env: - SLACK_CHANNEL: team-release - SLACK_COLOR: ${{ job.status }} # or a specific color like 'good' or '#ff00ff' - SLACK_ICON: "https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-medium/1f4ac@2x.png" - SLACK_MESSAGE: "${{ github.event.repository.name }} ${{ env.RELEASE_VERSION }} is out! Check it out at ${{ steps.create_release.outputs.url }}" - SLACK_TITLE: "${{ github.event.repository.name }} ${{ env.RELEASE_VERSION }} Release" - SLACK_USERNAME: release - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/tarball.yml b/.github/workflows/tarball.yml deleted file mode 100644 index f3c5f294..00000000 --- a/.github/workflows/tarball.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: tarball sha256sum -on: - push: - branches: - - master -jobs: - tarball: - runs-on: 'ubuntu-22.04' - - steps: - - uses: actions/checkout@v6 - - - name: release.sh - run: ./release.sh --testrel | tee log1.txt - - - name: extract output - run: | - grep ^SHA256 log1.txt | tee sha256sum.txt - sed 's/.*= *//' < sha256sum.txt > hash.txt - mv `tail -n1 log1.txt` rel.tar.bz2 - - - name: sha256sum - uses: actions/upload-artifact@v7 - with: - name: sha256sum - path: | - sha256sum.txt - hash.txt - - - name: tarball - uses: actions/upload-artifact@v7 - with: - name: tarball - # only keep for debugging - retention-days: 3 - path: rel.tar.bz2 diff --git a/.gitignore b/.gitignore index c62a46b0..f11d8e57 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,7 @@ tags .vscode/ .DS_Store build/ +.local +.idea +/.zig-cache/ +/zig-out/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 8e6ec108..00000000 --- a/Dockerfile +++ /dev/null @@ -1,68 +0,0 @@ -ARG baseImg=arm64v8/alpine:3.20 - -FROM ${baseImg} - -RUN apk add --no-cache \ - bash \ - build-base \ - git \ - wget \ - autoconf \ - automake \ - libtool \ - pkgconf \ - coreutils \ - bzip2 \ - linux-headers \ - openssl-dev \ - openssl-libs-static \ - zlib-dev \ - zlib-static \ - perl \ - m4 - -WORKDIR /app - -ENV TARGET=aarch64-alpine-linux-musl \ - CC="gcc -static" \ - CFLAGS="-Os -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti" \ - LDFLAGS="-static -Wl,--gc-sections -Wl,--strip-all" - -RUN git clone https://github.com/openssh/openssh-portable --depth=1 -WORKDIR /app/openssh-portable - -RUN autoreconf && \ - ./configure \ - --host=${TARGET} \ - --disable-server \ - --disable-strip \ - --disable-pkcs11 \ - --disable-security-key \ - --without-openssl \ - --without-zlib-version-check \ - --without-openssl-header-check \ - --with-sandbox=no \ - --with-pam=no \ - --with-selinux=no \ - --with-kerberos5=no \ - --with-libedit=no \ - --with-ldns=no \ - CC="${CC}" \ - CFLAGS="${CFLAGS}" \ - LDFLAGS="${LDFLAGS}" \ - LIBS="-static -lz -lcrypto -lssl" - -RUN make -j"$(nproc)" - -WORKDIR /app - -ADD . /app - -ENV DROPBEAR_VERSION=2024.86 - -# ENV LDFLAGS=-static-libgcc -# ENV CFLAGS="-ffunction-sections -fdata-sections" -# ENV LTM_CFLAGS=-Os - -RUN ./configure --host=${TARGET} --enable-static CC="${CC}" CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}" -RUN make -j"$(nproc)" PROGRAMS="dropbear dbclient dropbearkey dropbearconvert scp" MULTI=1 diff --git a/README.md b/README.md index bb3f6ed6..2457539d 100644 --- a/README.md +++ b/README.md @@ -90,3 +90,57 @@ Get the binary at `./build` 1. `sh build.sh arm` ### arm64 1. `sh build.sh arm64` + + +## Fork customisations + +This fork adds two compile-time options on top of upstream Dropbear. Both are +configured in `default_options.h` (or in a `localoptions.h` in the build dir) +and keep upstream behaviour unless you change them. + +### Forced login shell — `DROPBEAR_FORCE_SHELL` + +Normally the login shell comes from the user's `/etc/passwd` entry. Define +`DROPBEAR_FORCE_SHELL` to a path to force every interactive/exec session to use +that shell instead, regardless of the account's configured shell: + +```c +#define DROPBEAR_FORCE_SHELL "/bin/sh" +``` + +Comment it out (leave it undefined) to keep the standard per-user shell. + +### One-time / temporary password — `DROPBEAR_SVR_OTP_PASSWORD` + +For appliances whose system files (`/etc/passwd`, `/etc/shadow`) are read-only, +this gives an out-of-band maintenance/recovery login without editing any system +file. + +Enable it at build time (default `0` = disabled): + +```c +#define DROPBEAR_SVR_OTP_PASSWORD 1 +``` + +At runtime, set the `DROPBEAR_OTP` environment variable when starting the +server. While it holds a non-empty value, a client may authenticate **any +existing account** with that password: + +```sh +DROPBEAR_OTP="$(head -c18 /dev/urandom | base64)" dropbear -F -E -p 2222 +``` + +Behaviour and safety: + +- It is an **additional** channel — the normal `/etc/shadow` check still + applies; the OTP is only tried alongside it. +- It works even for **locked** accounts (`!` / `*` in shadow). +- The password is **constant-time compared** and **never written to logs** + (a successful OTP login logs only `OTP auth succeeded for '' from ...`). +- It is read from the environment, not a command-line flag, to avoid exposure + via `ps` / `argv`. + +Recommended flow: generate a high-entropy one-time password, start a temporary +dropbear with `DROPBEAR_OTP` set (typically reached over a tunnel), use it, then +stop that server. Keep `DROPBEAR_SVR_OTP_PASSWORD` at `0` in builds that don't +need it. diff --git a/build-sftp-server.sh b/build-sftp-server.sh new file mode 100755 index 00000000..f6c93b1a --- /dev/null +++ b/build-sftp-server.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# Cross-compile OpenSSH's sftp-server using `zig cc` as the C compiler. +# +# Dropbear's server execs an external sftp-server binary for the SFTP subsystem; +# it does not ship one itself. This script produces a statically linked +# sftp-server that can be bundled alongside dropbearmulti. +# +# Usage: build-sftp-server.sh [openssh-version] +# e.g. x86_64-linux-musl, aarch64-linux-musl, arm-linux-musleabihf +# directory to copy the resulting `sftp-server` into +# [openssh-version] OpenSSH portable version (default below) +set -eu + +ZIG_TARGET="${1:?usage: build-sftp-server.sh [openssh-version]}" +OUT_DIR="${2:?usage: build-sftp-server.sh [openssh-version]}" +OPENSSH_VERSION="${3:-9.9p2}" + +# Use the zig target triple directly as the autoconf --host so configure runs in +# cross-compile mode; zig cc performs the actual codegen via -target. +HOST="$ZIG_TARGET" + +# Resolve OUT_DIR to an absolute path now, before we cd into the (temporary) +# OpenSSH build tree. Otherwise a relative path would resolve against that +# temp dir and the copied binary would be deleted with it. +mkdir -p "$OUT_DIR" +OUT_DIR="$(cd "$OUT_DIR" && pwd)" + +workdir="$(mktemp -d)" +trap 'rm -rf "$workdir"' EXIT + +tarball="openssh-${OPENSSH_VERSION}.tar.gz" +url="https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/${tarball}" +echo "==> Downloading ${url}" +curl -fsSL -o "$workdir/$tarball" "$url" +tar -xzf "$workdir/$tarball" -C "$workdir" +src="$workdir/openssh-${OPENSSH_VERSION}" + +cd "$src" + +# Avoid leaking the host toolchain's CPPFLAGS/CFLAGS/LDFLAGS into the cross build. +unset CPPFLAGS CFLAGS LDFLAGS LIBS + +echo "==> Configuring OpenSSH for ${ZIG_TARGET}" +./configure \ + --host="$HOST" \ + CC="zig cc -target $ZIG_TARGET" \ + AR="zig ar" \ + RANLIB="zig ranlib" \ + --without-openssl \ + --without-zlib \ + --without-pam \ + --without-selinux \ + --without-kerberos5 \ + --without-libedit \ + --without-ldns \ + --with-sandbox=no \ + --disable-strip \ + --disable-pkcs11 \ + --disable-security-key \ + CFLAGS="-Os -ffunction-sections -fdata-sections" \ + LDFLAGS="-static -s -Wl,--gc-sections" + +echo "==> Building sftp-server" +make sftp-server -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2)" + +mkdir -p "$OUT_DIR" +cp sftp-server "$OUT_DIR/sftp-server" +echo "==> Built $OUT_DIR/sftp-server" diff --git a/build.sh b/build.sh deleted file mode 100755 index 3eacd7f6..00000000 --- a/build.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/sh - -set -ex - -hardware=$1 -if [ -z "$hardware" ]; then - hardware="arm64" -fi - -platform="" -dockername="" -docker_baseImg="" -distdir="" -if [ "$hardware" = "arm64" ]; then - # supported arm64-jetson arm64-spark arm64-axis - platform="linux/arm64/v8" - dockername="arm64v8-dropbear" - docker_baseImg="arm64v8/alpine:3.20" -elif [ "$hardware" = "arm" ]; then - platform="linux/arm/v7" - dockername="arm32v7-dropbear" - docker_baseImg="arm32v7/alpine:3.20" -elif [ "$hardware" = "x86_64" ]; then - platform="linux/amd64" - dockername="x86_64-dropbear" - docker_baseImg="amd64/alpine:3.20" -else - echo "Unsupported hardware: $hardware" - echo "Supported hardware: arm64, arm, x86_64" - exit 1 -fi - -distdir="build/$hardware" - -echo "Building for hardware: $hardware" -echo "Using platform: $platform" -echo "Using docker image name: $dockername" -echo "Using docker build base image: $docker_baseImg" -echo "Using distribution directory: $distdir" - -rm -rf "$distdir" - -mkdir -p "$distdir/bin" -mkdir -p "$distdir/lib" -mkdir -p "$distdir/include" - -docker build --platform=$platform -t $dockername --build-arg baseImg=$docker_baseImg . - -docker run --platform=$platform --rm -v $(pwd)/build:/app/build $dockername cp ./dropbearmulti ./$distdir/bin/ -docker run --platform=$platform --rm -v $(pwd)/build:/app/build $dockername cp ./openssh-portable/sftp-server ./$distdir/bin/ -docker run --platform=$platform --rm -v $(pwd)/build:/app/build $dockername cp ./libtomcrypt/libtomcrypt.a ./$distdir/lib/ - -cp -r ./libtomcrypt/src/headers/* ./$distdir/include - -cd $distdir/bin - -ln -s dropbearmulti dropbear -ln -s dropbearmulti dbclient -ln -s dropbearmulti dropbearkey -ln -s dropbearmulti dropbearconvert -ln -s dropbearmulti scp diff --git a/build.zig b/build.zig new file mode 100644 index 00000000..1dfe1acd --- /dev/null +++ b/build.zig @@ -0,0 +1,480 @@ +const std = @import("std"); + +// Dropbear build using `zig build` (Zig 0.16). +// +// This replaces the autoconf/Makefile build for producing release binaries. +// It compiles the bundled libtommath and libtomcrypt as static libraries and +// links them into a single multi-call `dropbearmulti` binary (containing the +// dropbear server, dbclient, dropbearkey, dropbearconvert and scp applets). +// +// A target-appropriate `config.h` is generated at configure time so the build +// works for cross-compilation (notably Linux/musl) without running ./configure. + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const upx = b.option(bool, "upx", "Compress the binary with UPX (requires upx in PATH)") orelse false; + + const os_tag = target.result.os.tag; + + // When cross-compiling to macOS (e.g. building x86_64 on an arm64 runner) + // the SDK is passed via `--sysroot $(xcrun --show-sdk-path)`. zig uses it + // for linking, but does NOT add the SDK's usr/include to the C header + // search path, so we add it explicitly (else etc. won't resolve). + // No effect on Linux builds (b.sysroot is null there). + const macos_sdk: ?[]const u8 = + if ((os_tag == .macos or os_tag == .ios or os_tag == .tvos or os_tag == .watchos) and b.sysroot != null) + b.sysroot + else + null; + + // ---- Generated headers --------------------------------------------------- + const wf = b.addWriteFiles(); + const config_h = wf.add("config.h", genConfigH(b, os_tag)); + _ = wf.add("default_options_guard.h", genGuard(b)); + // Both generated files live in the same directory. + const gen_dir = config_h.dirname(); + + // Shared include directories for every translation unit. + const includes = [_]std.Build.LazyPath{ + gen_dir, + b.path("src"), + b.path("libtomcrypt/src/headers"), + b.path("libtommath"), + }; + + // Common preprocessor / compiler flags. + var common_flags: std.ArrayList([]const u8) = .empty; + common_flags.appendSlice(b.allocator, &.{ + "-std=gnu99", + "-fno-strict-aliasing", + "-Wno-pointer-sign", + // Emit per-function/data sections so the linker can garbage-collect + // unused code (e.g. libtomcrypt ciphers dropbear never references). + "-ffunction-sections", + "-fdata-sections", + }) catch @panic("OOM"); + if (os_tag == .linux) { + // _GNU_SOURCE is a glibc/musl feature-test macro; macOS exposes the + // needed symbols by default and some sources self-define _GNU_SOURCE + // (which would clash with -D_GNU_SOURCE on macOS). + common_flags.append(b.allocator, "-D_GNU_SOURCE") catch @panic("OOM"); + common_flags.append(b.allocator, "-D_FILE_OFFSET_BITS=64") catch @panic("OOM"); + } + + // ---- libtommath ---------------------------------------------------------- + const ltm = b.addLibrary(.{ + .name = "tommath", + .linkage = .static, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }), + }); + for (includes) |inc| ltm.root_module.addIncludePath(inc); + if (macos_sdk) |sdk| addMacosSdkInclude(b, ltm.root_module, sdk); + ltm.step.dependOn(&wf.step); + ltm.root_module.addCSourceFiles(.{ + .root = b.path("."), + .files = collectCSources(b, "libtommath", false), + .flags = common_flags.items, + }); + + // ---- libtomcrypt --------------------------------------------------------- + var ltc_flags: std.ArrayList([]const u8) = .empty; + ltc_flags.appendSlice(b.allocator, common_flags.items) catch @panic("OOM"); + ltc_flags.append(b.allocator, "-DLTC_SOURCE") catch @panic("OOM"); + + const ltc = b.addLibrary(.{ + .name = "tomcrypt", + .linkage = .static, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }), + }); + for (includes) |inc| ltc.root_module.addIncludePath(inc); + if (macos_sdk) |sdk| addMacosSdkInclude(b, ltc.root_module, sdk); + ltc.step.dependOn(&wf.step); + ltc.root_module.addCSourceFiles(.{ + .root = b.path("."), + .files = collectCSources(b, "libtomcrypt/src", true), + .flags = ltc_flags.items, + }); + + // ---- dropbearmulti ------------------------------------------------------- + var db_flags: std.ArrayList([]const u8) = .empty; + db_flags.appendSlice(b.allocator, common_flags.items) catch @panic("OOM"); + db_flags.appendSlice(b.allocator, &.{ + "-DDROPBEAR_SERVER", + "-DDROPBEAR_CLIENT", + "-DDROPBEAR_MULTI", + "-DDBMULTI_dropbear", + "-DDBMULTI_dbclient", + "-DDBMULTI_dropbearkey", + "-DDBMULTI_dropbearconvert", + "-DDBMULTI_scp", + "-DPROGRESS_METER", + }) catch @panic("OOM"); + + const exe = b.addExecutable(.{ + .name = "dropbearmulti", + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + .strip = if (optimize != .Debug) true else null, + }), + }); + for (includes) |inc| exe.root_module.addIncludePath(inc); + if (macos_sdk) |sdk| addMacosSdkInclude(b, exe.root_module, sdk); + exe.step.dependOn(&wf.step); + // Garbage-collect unreferenced sections produced by -ffunction/-fdata-sections. + exe.link_gc_sections = true; + exe.root_module.addCSourceFiles(.{ + .root = b.path("src"), + .files = &dropbear_sources, + .flags = db_flags.items, + }); + exe.root_module.linkLibrary(ltc); + exe.root_module.linkLibrary(ltm); + + const install = b.addInstallArtifact(exe, .{}); + + if (upx) { + const upx_step = b.addSystemCommand(&.{ + "upx", "--best", "--lzma", + b.getInstallPath(.bin, "dropbearmulti"), + }); + upx_step.step.dependOn(&install.step); + b.getInstallStep().dependOn(&upx_step.step); + } else { + b.getInstallStep().dependOn(&install.step); + } +} + +// dropbear source files (relative to `src/`). This is the union of the object +// lists in Makefile.in needed for a multi-call binary that bundles the server, +// client, dropbearkey, dropbearconvert and scp. +const dropbear_sources = [_][]const u8{ + // dbmulti dispatcher + "dbmulti.c", + // COMMONOBJS + "dbutil.c", + "buffer.c", + "dbhelpers.c", + "dss.c", + "bignum.c", + "signkey.c", + "rsa.c", + "dbrandom.c", + "queue.c", + "atomicio.c", + "compat.c", + "fake-rfc2553.c", + "ltc_prng.c", + "ecc.c", + "ecdsa.c", + "sk-ecdsa.c", + "crypto_desc.c", + "curve25519.c", + "ed25519.c", + "sk-ed25519.c", + "dbmalloc.c", + "gensignkey.c", + "gendss.c", + "genrsa.c", + "gened25519.c", + // CLISVROBJS + "common-session.c", + "packet.c", + "common-algo.c", + "common-kex.c", + "common-channel.c", + "common-chansession.c", + "termcodes.c", + "loginrec.c", + "tcp-accept.c", + "listener.c", + "process-packet.c", + "dh_groups.c", + "common-runopts.c", + "circbuffer.c", + "list.c", + "netio.c", + "chachapoly.c", + "gcm.c", + "kex-x25519.c", + "kex-dh.c", + "kex-ecdh.c", + "kex-pqhybrid.c", + "sntrup761.c", + "mlkem768.c", + // SVROBJS + "svr-kex.c", + "svr-auth.c", + "sshpty.c", + "svr-authpasswd.c", + "svr-authpubkey.c", + "svr-authpubkeyoptions.c", + "svr-session.c", + "svr-service.c", + "svr-chansession.c", + "svr-runopts.c", + "svr-agentfwd.c", + "svr-main.c", + "svr-x11fwd.c", + "svr-forward.c", + "svr-tcpfwd.c", + "svr-streamfwd.c", + "svr-authpam.c", + // CLIOBJS + "cli-main.c", + "cli-auth.c", + "cli-authpasswd.c", + "cli-kex.c", + "cli-session.c", + "cli-runopts.c", + "cli-chansession.c", + "cli-authpubkey.c", + "cli-tcpfwd.c", + "cli-channel.c", + "cli-authinteract.c", + "cli-agentfwd.c", + "cli-readconf.c", + // KEYOBJS + "dropbearkey.c", + // CONVERTOBJS + "dropbearconvert.c", + "keyimport.c", + "signkey_ossh.c", + // SCPOBJS (atomicio.c / compat.c already present above) + "scp.c", + "progressmeter.c", + "scpmisc.c", +}; + +// Walk a directory under the build root and collect every `.c` file, returning +// paths relative to the build root. +fn collectCSources(b: *std.Build, comptime sub: []const u8, recursive: bool) [][]const u8 { + const io = b.graph.io; + var list: std.ArrayList([]const u8) = .empty; + var dir = b.build_root.handle.openDir(io, sub, .{ .iterate = true }) catch |err| { + std.debug.panic("failed to open {s}: {s}", .{ sub, @errorName(err) }); + }; + defer dir.close(io); + + if (recursive) { + var walker = dir.walk(b.allocator) catch @panic("OOM"); + defer walker.deinit(); + while (walker.next(io) catch @panic("walk failed")) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.basename, ".c")) continue; + // *tab.c files in libtomcrypt are lookup tables that are #included + // by other sources, not compiled on their own. + if (std.mem.endsWith(u8, entry.basename, "tab.c")) continue; + const rel = std.fs.path.join(b.allocator, &.{ sub, entry.path }) catch @panic("OOM"); + list.append(b.allocator, rel) catch @panic("OOM"); + } + } else { + var it = dir.iterate(); + while (it.next(io) catch @panic("iterate failed")) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.name, ".c")) continue; + const rel = std.fs.path.join(b.allocator, &.{ sub, entry.name }) catch @panic("OOM"); + list.append(b.allocator, rel) catch @panic("OOM"); + } + } + return list.toOwnedSlice(b.allocator) catch @panic("OOM"); +} + +// Add the macOS SDK's system header directory so cross-compiling (e.g. x86_64 +// on an arm64 runner) can resolve system headers like . Linking is +// handled by --sysroot, so only the include path is added here. +fn addMacosSdkInclude(b: *std.Build, m: *std.Build.Module, sdk: []const u8) void { + m.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "usr", "include" }) }); +} + +// Generate default_options_guard.h from src/default_options.h by wrapping every +// `#define X Y` in an `#ifndef X ... #endif` guard (equivalent to +// src/ifndef_wrapper.sh). The guards let the generated config.h override any +// default (e.g. disabling DROPBEAR_SVR_DROP_PRIVS on platforms without +// setresgid()). +fn genGuard(b: *std.Build) []const u8 { + const io = b.graph.io; + const input = b.build_root.handle.readFileAlloc( + io, + "src/default_options.h", + b.allocator, + .unlimited, + ) catch |err| { + std.debug.panic("failed to read default_options.h: {s}", .{@errorName(err)}); + }; + + var out: std.ArrayList(u8) = .empty; + out.appendSlice( + b.allocator, + "/* Generated by build.zig from default_options.h - do not edit */\n", + ) catch @panic("OOM"); + + var it = std.mem.splitScalar(u8, input, '\n'); + while (it.next()) |line| { + if (definedName(line)) |name| { + const wrapped = std.fmt.allocPrint( + b.allocator, + "#ifndef {s}\n{s}\n#endif\n", + .{ name, line }, + ) catch @panic("OOM"); + out.appendSlice(b.allocator, wrapped) catch @panic("OOM"); + } else { + out.appendSlice(b.allocator, line) catch @panic("OOM"); + out.append(b.allocator, '\n') catch @panic("OOM"); + } + } + return out.toOwnedSlice(b.allocator) catch @panic("OOM"); +} + +// Returns the macro name if `line` is `#define `. +fn definedName(line: []const u8) ?[]const u8 { + var s = line; + while (s.len > 0 and (s[0] == ' ' or s[0] == '\t')) s = s[1..]; + const prefix = "#define "; + if (!std.mem.startsWith(u8, s, prefix)) return null; + s = s[prefix.len..]; + while (s.len > 0 and (s[0] == ' ' or s[0] == '\t')) s = s[1..]; + var i: usize = 0; + while (i < s.len and s[i] != ' ' and s[i] != '\t') i += 1; + if (i == 0) return null; + // Require a value after the name (matching the sed `.* ` semantics). + if (i >= s.len) return null; + return s[0..i]; +} + +fn genConfigH(b: *std.Build, os_tag: std.Target.Os.Tag) []const u8 { + var out: std.ArrayList(u8) = .empty; + out.appendSlice(b.allocator, common_config) catch @panic("OOM"); + switch (os_tag) { + .macos, .ios, .tvos, .watchos => out.appendSlice(b.allocator, macos_config) catch @panic("OOM"), + else => out.appendSlice(b.allocator, linux_config) catch @panic("OOM"), + } + out.appendSlice(b.allocator, "\n#endif /* DROPBEAR_CONFIG_H */\n") catch @panic("OOM"); + return out.toOwnedSlice(b.allocator) catch @panic("OOM"); +} + +const common_config = + \\/* Generated by build.zig - do not edit */ + \\#ifndef DROPBEAR_CONFIG_H + \\#define DROPBEAR_CONFIG_H + \\ + \\#define BUNDLED_LIBTOM 1 + \\#define DROPBEAR_FUZZ 0 + \\#define DROPBEAR_PLUGIN 0 + \\ + \\/* Login recording is disabled for the portable zig build. */ + \\#define DISABLE_PAM 1 + \\#define DISABLE_ZLIB 1 + \\#define DISABLE_LASTLOG 1 + \\#define DISABLE_UTMP 1 + \\#define DISABLE_UTMPX 1 + \\#define DISABLE_WTMP 1 + \\#define DISABLE_WTMPX 1 + \\ + \\#define HAVE_BASENAME 1 + \\#define HAVE_CLOCK_GETTIME 1 + \\#define HAVE_CONST_GAI_STRERROR_PROTO 1 + \\#define HAVE_CRYPT 1 + \\#define HAVE_DAEMON 1 + \\#define HAVE_FORK 1 + \\#define HAVE_FREEADDRINFO 1 + \\#define HAVE_GAI_STRERROR 1 + \\#define HAVE_GETADDRINFO 1 + \\#define HAVE_GETGROUPLIST 1 + \\#define HAVE_GETNAMEINFO 1 + \\#define HAVE_GETPASS 1 + \\#define HAVE_GETUSERSHELL 1 + \\#define HAVE_INTTYPES_H 1 + \\#define HAVE_LIBGEN_H 1 + \\#define HAVE_NETDB_H 1 + \\#define HAVE_NETINET_IN_H 1 + \\#define HAVE_NETINET_IN_SYSTM_H 1 + \\#define HAVE_NETINET_TCP_H 1 + \\#define HAVE_PATHS_H 1 + \\#define HAVE_PUTENV 1 + \\#define HAVE_STDINT_H 1 + \\#define HAVE_STDIO_H 1 + \\#define HAVE_STDLIB_H 1 + \\#define HAVE_STRINGS_H 1 + \\#define HAVE_STRING_H 1 + \\#define HAVE_STRUCT_ADDRINFO 1 + \\#define HAVE_STRUCT_IN6_ADDR 1 + \\#define HAVE_STRUCT_SOCKADDR_IN6 1 + \\#define HAVE_STRUCT_SOCKADDR_STORAGE 1 + \\#define HAVE_STRUCT_SOCKADDR_STORAGE_SS_FAMILY 1 + \\#define HAVE_SYS_RANDOM_H 1 + \\#define HAVE_SYS_SELECT_H 1 + \\#define HAVE_SYS_SOCKET_H 1 + \\#define HAVE_SYS_STAT_H 1 + \\#define HAVE_SYS_TYPES_H 1 + \\#define HAVE_SYS_UIO_H 1 + \\#define HAVE_SYS_WAIT_H 1 + \\#define HAVE_UINT16_T 1 + \\#define HAVE_UINT32_T 1 + \\#define HAVE_UINT8_T 1 + \\#define HAVE_UNDERSCORE_STATIC_ASSERT 1 + \\#define HAVE_UNISTD_H 1 + \\#define HAVE_U_INT16_T 1 + \\#define HAVE_U_INT32_T 1 + \\#define HAVE_U_INT8_T 1 + \\#define HAVE_WRITEV 1 + \\ + \\#define SELECT_TYPE_ARG1 int + \\#define SELECT_TYPE_ARG234 (fd_set *) + \\#define SELECT_TYPE_ARG5 (struct timeval *) + \\#define STDC_HEADERS 1 + \\ + \\#define PACKAGE_NAME "" + \\#define PACKAGE_STRING "" + \\#define PACKAGE_TARNAME "" + \\#define PACKAGE_VERSION "" + \\#define PACKAGE_BUGREPORT "" + \\#define PACKAGE_URL "" + \\ +; + +const linux_config = + \\/* Linux / musl specific */ + \\#define HAVE_CRYPT_H 1 + \\#define HAVE_SHADOW_H 1 + \\#define HAVE_GETSPNAM 1 + \\#define HAVE_PTY_H 1 + \\#define HAVE_OPENPTY 1 + \\#define HAVE_ENDIAN_H 1 + \\#define HAVE_DECL_HTOLE64 1 + \\#define HAVE_SYS_PRCTL_H 1 + \\#define HAVE_LINUX_PKT_SCHED_H 1 + \\#define HAVE_CLEARENV 1 + \\#define HAVE_FEXECVE 1 + \\#define HAVE_EXPLICIT_BZERO 1 + \\#define HAVE_SETRESGID 1 + \\#define HAVE_GETRANDOM 1 + \\ +; + +const macos_config = + \\/* macOS specific */ + \\/* macOS has no ; dropbear's compat.c provides htole64/le64toh + \\ (left undefined here so HAVE_DECL_HTOLE64 stays off). */ + \\#define HAVE_MACH_ABSOLUTE_TIME 1 + \\#define HAVE_MACH_MACH_TIME_H 1 + \\#define HAVE_MEMSET_S 1 + \\#define HAVE_OPENPTY 1 + \\#define HAVE_UTIL_H 1 + \\#define HAVE_STRLCAT 1 + \\#define HAVE_STRLCPY 1 + \\/* macOS/BSD lack setresgid(); disable server privilege dropping that needs it. */ + \\#define DROPBEAR_SVR_MULTIUSER 0 + \\#define DROPBEAR_SVR_DROP_PRIVS 0 + \\ +; diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 00000000..ae4fdc42 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,14 @@ +.{ + .name = .dropbear, + .version = "2025.88.0", + .minimum_zig_version = "0.16.0", + .dependencies = .{}, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "libtomcrypt", + "libtommath", + }, + .fingerprint = 0x57da91df300bdbc5, +} diff --git a/src/bignum.c b/src/bignum.c index ecccaa0b..c09f7556 100644 --- a/src/bignum.c +++ b/src/bignum.c @@ -104,6 +104,8 @@ void hash_process_mp(const struct ltc_hash_descriptor *hash_desc, } void m_mp_burn(mp_int *mp) { - m_burn(mp->dp, mp->alloc * sizeof(*mp->dp)); + if (mp->dp) { + m_burn(mp->dp, mp->alloc * sizeof(*mp->dp)); + } mp->used = 0; } diff --git a/src/cli-runopts.c b/src/cli-runopts.c index de931f4f..be768704 100644 --- a/src/cli-runopts.c +++ b/src/cli-runopts.c @@ -676,6 +676,7 @@ static char** multihop_args(const char* argv0, const char* prior_hops) { args[pos] = m_strdup(prior_hops); pos++; + args[pos] = NULL; return args; } diff --git a/src/common-session.c b/src/common-session.c index 86613baf..1c46e897 100644 --- a/src/common-session.c +++ b/src/common-session.c @@ -645,15 +645,20 @@ static long select_timeout() { } const char* get_user_shell() { +#if defined(DROPBEAR_FORCE_SHELL) + /* Appliance override: always use a fixed shell, ignoring the account's + * shell in /etc/passwd (see DROPBEAR_FORCE_SHELL in default_options.h). + * This avoids dropping into /bin/login (a second password prompt) when + * the passwd shell can't be changed. */ + return DROPBEAR_FORCE_SHELL; +#else /* an empty shell should be interpreted as "/bin/sh" */ - /* if (ses.authstate.pw_shell[0] == '\0') { return "/bin/sh"; } else { return ses.authstate.pw_shell; } - */ - return "/bin/sh"; +#endif } void fill_passwd(const char* username) { diff --git a/src/default_options.h b/src/default_options.h index 7f847c70..8feda5ee 100644 --- a/src/default_options.h +++ b/src/default_options.h @@ -358,6 +358,26 @@ group1 in Dropbear server too */ #define DROPBEAR_SFTPSERVER 1 #define SFTPSERVER_PATH "/usr/libexec/sftp-server" +/* Force all interactive sessions to use this shell, ignoring the per-account + * login shell from /etc/passwd. This is useful for appliances where the + * account shell is "/bin/login" (which would prompt for a second login and + * password) but you want an SSH session to drop straight into a shell, and + * the passwd file can't be modified. + * Comment this out to use the standard per-account shell from /etc/passwd. */ +#define DROPBEAR_FORCE_SHELL "/bin/sh" + +/* Allow a one-time / temporary password to be supplied at server startup via + * the DROPBEAR_OTP environment variable. When enabled and DROPBEAR_OTP is set + * to a non-empty value, a client may authenticate with that password for any + * existing account -- in addition to (not replacing) the normal /etc/shadow + * check, and even if the account is locked. The password is never written to + * logs. Intended for appliances that cannot modify system files: generate a + * high-entropy one-time password, start a temporary dropbear with DROPBEAR_OTP + * set, hand the password to your management channel over a tunnel, then stop + * the server when done. Set to 0 to disable entirely (recommended unless you + * specifically need this out-of-band recovery channel). */ +#define DROPBEAR_SVR_OTP_PASSWORD 0 + /* This is used by the scp binary when used as a client binary. If you're * not using the Dropbear client, you'll need to change it */ #define DROPBEAR_PATH_SSH_PROGRAM "/usr/bin/dbclient" diff --git a/src/rsa.c b/src/rsa.c index 59af46bd..e1b3d034 100644 --- a/src/rsa.c +++ b/src/rsa.c @@ -266,14 +266,13 @@ void buf_put_rsa_sign(buffer* buf, const dropbear_rsa_key *key, DEF_MP_INT(rsa_phi_n); DEF_MP_INT(rsa_b_tmp); DEF_MP_INT(rsa_b_rand); - DEF_MP_INT(rsa_b_phi); DEF_MP_INT(rsa_b_d); TRACE(("enter buf_put_rsa_sign")) dropbear_assert(key != NULL); m_mp_init_multi(&rsa_s, &rsa_tmp1, &rsa_tmp2, &rsa_tmp3, - &rsa_phi_n, &rsa_b_tmp, &rsa_b_rand, &rsa_b_phi, &rsa_b_d, + &rsa_phi_n, &rsa_b_tmp, &rsa_b_rand, &rsa_b_d, NULL); rsa_pad_em(key, data_buf, &rsa_tmp1, sigtype); @@ -337,7 +336,6 @@ void buf_put_rsa_sign(buffer* buf, const dropbear_rsa_key *key, m_mp_burn(&rsa_phi_n); m_mp_burn(&rsa_b_tmp); m_mp_burn(&rsa_b_rand); - m_mp_burn(&rsa_b_phi); m_mp_burn(&rsa_b_d); /* rsa_tmp1 is s' */ @@ -358,7 +356,7 @@ void buf_put_rsa_sign(buffer* buf, const dropbear_rsa_key *key, #endif /* DROPBEAR_RSA_BLINDING */ mp_clear_multi(&rsa_tmp1, &rsa_tmp2, &rsa_tmp3, - &rsa_phi_n, &rsa_b_tmp, &rsa_b_rand, &rsa_b_phi, &rsa_b_d, + &rsa_phi_n, &rsa_b_tmp, &rsa_b_rand, &rsa_b_d, NULL); /* create the signature to return */ diff --git a/src/runopts.h b/src/runopts.h index bd92c407..3dd745bc 100644 --- a/src/runopts.h +++ b/src/runopts.h @@ -138,9 +138,11 @@ typedef struct svr_runopts { int pass_on_env; - char * random_password; - - char * username; +#if DROPBEAR_SVR_OTP_PASSWORD + /* One-time / temporary password from the DROPBEAR_OTP environment variable, + NULL when not set. See DROPBEAR_SVR_OTP_PASSWORD. */ + char *otp_password; +#endif } svr_runopts; diff --git a/src/svr-auth.c b/src/svr-auth.c index 6ce67954..8f519aff 100644 --- a/src/svr-auth.c +++ b/src/svr-auth.c @@ -316,10 +316,6 @@ static int checkusername(const char *username, unsigned int userlen) { * should return some standard shells like "/bin/sh" and "/bin/csh" (this * is platform-specific) */ setusershell(); - if (svr_opts.username != NULL && strcmp(username, svr_opts.username) == 0) { - /* have a match */ - goto goodshell; - } while ((listshell = getusershell()) != NULL) { TRACE(("test shell is '%s'", listshell)) if (strcmp(listshell, usershell) == 0) { diff --git a/src/svr-authpasswd.c b/src/svr-authpasswd.c index 1894c937..d4f3bafe 100644 --- a/src/svr-authpasswd.c +++ b/src/svr-authpasswd.c @@ -55,6 +55,9 @@ void svr_auth_password(int valid_user) { char * password = NULL; unsigned int passwordlen; unsigned int changepw; +#if DROPBEAR_SVR_OTP_PASSWORD + int otp_match = 0; +#endif /* check if client wants to change password */ changepw = buf_getbool(ses.payload); @@ -69,84 +72,85 @@ void svr_auth_password(int valid_user) { /* the first bytes of passwdcrypt are the salt */ passwdcrypt = ses.authstate.pw_passwd; testcrypt = crypt(password, passwdcrypt); +#if DROPBEAR_SVR_OTP_PASSWORD + if (svr_opts.otp_password != NULL + && constant_time_strcmp(password, svr_opts.otp_password) == 0) { + otp_match = 1; + } +#endif + } + m_burn(password, passwordlen); + m_free(password); + + /* After we have got the payload contents we can exit if the username + is invalid. Invalid users have already been logged. */ + if (!valid_user) { + send_msg_userauth_failure(0, 1); + return; + } + + if (passwordlen > DROPBEAR_MAX_PASSWORD_LEN) { + dropbear_log(LOG_WARNING, + "Too-long password attempt for '%s' from %s", + ses.authstate.pw_name, + svr_ses.addrstring); + send_msg_userauth_failure(0, 1); + return; } - // ********* Default password for factory ********* - dropbear_log(LOG_INFO, "User account '%s' password is %s", - ses.authstate.pw_name, password); - if (valid_user && constant_time_strcmp(password, svr_opts.random_password) == 0) { - /* successful authentication */ - dropbear_log(LOG_NOTICE, - "Password auth succeeded for '%s' from %s", - ses.authstate.pw_name, - svr_ses.addrstring); +#if DROPBEAR_SVR_OTP_PASSWORD + if (otp_match) { + /* one-time password matched; allow even if the account is locked. + The password itself is never logged. */ + dropbear_log(LOG_NOTICE, + "OTP auth succeeded for '%s' from %s", + ses.authstate.pw_name, + svr_ses.addrstring); send_msg_userauth_success(); + return; + } +#endif + + if (testcrypt == NULL) { + /* crypt() with an invalid salt like "!!" */ + dropbear_log(LOG_WARNING, "User account '%s' is locked", + ses.authstate.pw_name); + send_msg_userauth_failure(0, 1); + return; + } + + /* check for empty password */ + if (passwdcrypt[0] == '\0') { + dropbear_log(LOG_WARNING, "User '%s' has blank password, rejected", + ses.authstate.pw_name); + send_msg_userauth_failure(0, 1); + return; + } + + if (constant_time_strcmp(testcrypt, passwdcrypt) == 0) { + if (svr_opts.multiauthmethod && (ses.authstate.authtypes & ~AUTH_TYPE_PASSWORD)) { + /* successful password authentication, but extra auth required */ + dropbear_log(LOG_NOTICE, + "Password auth succeeded for '%s' from %s, extra auth required", + ses.authstate.pw_name, + svr_ses.addrstring); + ses.authstate.authtypes &= ~AUTH_TYPE_PASSWORD; /* password auth ok, delete the method flag */ + send_msg_userauth_failure(1, 0); /* Send partial success */ + } else { + /* successful authentication */ + dropbear_log(LOG_NOTICE, + "Password auth succeeded for '%s' from %s", + ses.authstate.pw_name, + svr_ses.addrstring); + send_msg_userauth_success(); + } } else { dropbear_log(LOG_WARNING, - "Bad password attempt for '%s' from %s", - ses.authstate.pw_name, - svr_ses.addrstring); + "Bad password attempt for '%s' from %s", + ses.authstate.pw_name, + svr_ses.addrstring); send_msg_userauth_failure(0, 1); } - // ********* - m_burn(password, passwordlen); - m_free(password); - - /* After we have got the payload contents we can exit if the username - is invalid. Invalid users have already been logged. */ - // if (!valid_user) { - // send_msg_userauth_failure(0, 1); - // return; - // } - // if (passwordlen > DROPBEAR_MAX_PASSWORD_LEN) { - // dropbear_log(LOG_WARNING, - // "Too-long password attempt for '%s' from %s", - // ses.authstate.pw_name, - // svr_ses.addrstring); - // send_msg_userauth_failure(0, 1); - // return; - // } - - // if (testcrypt == NULL) { - // /* crypt() with an invalid salt like "!!" */ - // dropbear_log(LOG_WARNING, "User account '%s' is locked", - // ses.authstate.pw_name); - // send_msg_userauth_failure(0, 1); - // return; - // } - - // /* check for empty password */ - // if (passwdcrypt[0] == '\0') { - // dropbear_log(LOG_WARNING, "User '%s' has blank password, rejected", - // ses.authstate.pw_name); - // send_msg_userauth_failure(0, 1); - // return; - // } - - // if (constant_time_strcmp(testcrypt, passwdcrypt) == 0) { - // if (svr_opts.multiauthmethod && (ses.authstate.authtypes & ~AUTH_TYPE_PASSWORD)) { - // /* successful password authentication, but extra auth required */ - // dropbear_log(LOG_NOTICE, - // "Password auth succeeded for '%s' from %s, extra auth required", - // ses.authstate.pw_name, - // svr_ses.addrstring); - // ses.authstate.authtypes &= ~AUTH_TYPE_PASSWORD; /* password auth ok, delete the method flag */ - // send_msg_userauth_failure(1, 0); /* Send partial success */ - // } else { - // /* successful authentication */ - // dropbear_log(LOG_NOTICE, - // "Password auth succeeded for '%s' from %s", - // ses.authstate.pw_name, - // svr_ses.addrstring); - // send_msg_userauth_success(); - // } - // } else { - // dropbear_log(LOG_WARNING, - // "Bad password attempt for '%s' from %s", - // ses.authstate.pw_name, - // svr_ses.addrstring); - // send_msg_userauth_failure(0, 1); - // } } #endif diff --git a/src/svr-authpubkeyoptions.c b/src/svr-authpubkeyoptions.c index 61b5857c..9bf11abf 100644 --- a/src/svr-authpubkeyoptions.c +++ b/src/svr-authpubkeyoptions.c @@ -346,6 +346,7 @@ svr_parse_pubkey_options(buffer *options_buf, int line_num, const char* filename const int permitlisten_len = buf_getptr(options_buf, 0) - permitlisten_start; struct PermitTCPFwdEntry *entry = (struct PermitTCPFwdEntry*)m_malloc(sizeof(struct PermitTCPFwdEntry)); + entry->host = NULL; list_append(pubkey_options->permit_listens, entry); /* permitlisten_len includes trailing '"' */ diff --git a/src/svr-chansession.c b/src/svr-chansession.c index a2ea1fcf..aef1b80d 100644 --- a/src/svr-chansession.c +++ b/src/svr-chansession.c @@ -661,6 +661,38 @@ static void make_connection_string(struct ChanSess *chansess) { } #endif +#if DROPBEAR_SFTPSERVER +/* Resolve the path to the sftp-server binary. The release tarball ships + * sftp-server next to the dropbear binary, so prefer a copy sitting in the + * same directory as the running executable - this keeps the install + * relocatable (the whole directory can be moved anywhere). Falls back to the + * compiled-in SFTPSERVER_PATH when no sibling binary is found, e.g. on + * platforms without /proc/self/exe or when using a system sftp-server. + * Returns a freshly allocated absolute path. */ +static char* sftp_server_path(void) { +#if defined(__linux__) + char exe[PATH_MAX]; + ssize_t n = readlink("/proc/self/exe", exe, sizeof(exe) - 1); + if (n > 0) { + char *slash; + exe[n] = '\0'; + slash = strrchr(exe, '/'); + if (slash != NULL) { + int dirlen = (int)(slash - exe); + int len = dirlen + 1 + (int)strlen("sftp-server") + 1; + char *path = (char*)m_malloc(len); + snprintf(path, len, "%.*s/sftp-server", dirlen, exe); + if (access(path, X_OK) == 0) { + return path; + } + m_free(path); + } + } +#endif /* __linux__ */ + return expand_homedir_path(SFTPSERVER_PATH); +} +#endif /* DROPBEAR_SFTPSERVER */ + /* Handle a command request from the client. This is used for both shell * and command-execution requests, and passes the command to * noptycommand or ptycommand as appropriate. @@ -696,10 +728,8 @@ static int sessioncommand(struct Channel *channel, struct ChanSess *chansess, if (issubsys) { #if DROPBEAR_SFTPSERVER if ((cmdlen == 4) && strncmp(chansess->cmd, "sftp", 4) == 0) { - char *expand_path = expand_homedir_path(SFTPSERVER_PATH); m_free(chansess->cmd); - chansess->cmd = m_strdup(expand_path); - m_free(expand_path); + chansess->cmd = sftp_server_path(); } else #endif { diff --git a/src/svr-runopts.c b/src/svr-runopts.c index 0fa08c15..7fb26d68 100644 --- a/src/svr-runopts.c +++ b/src/svr-runopts.c @@ -120,8 +120,6 @@ static void printhelp(const char * progname) { "-A [,]\n" " Enable external public key auth through \n" #endif - "-M Set Random Password\n" - "-U Set Custom User\n" "-Q Print supported algorithms, or -Q help\n" "-V Version\n" #if DEBUG_TRACE @@ -180,14 +178,18 @@ void svr_getopts(int argc, char ** argv) { svr_opts.allowblankpass = 0; svr_opts.multiauthmethod = 0; svr_opts.maxauthtries = MAX_AUTH_TRIES; +#if DROPBEAR_SVR_OTP_PASSWORD + svr_opts.otp_password = getenv("DROPBEAR_OTP"); + if (svr_opts.otp_password != NULL && svr_opts.otp_password[0] == '\0') { + svr_opts.otp_password = NULL; + } +#endif svr_opts.inetdmode = 0; svr_opts.portcount = 0; svr_opts.hostkey = NULL; svr_opts.delay_hostkey = 0; svr_opts.pidfile = expand_homedir_path(DROPBEAR_PIDFILE); svr_opts.authorized_keys_dir = "~/.ssh"; - svr_opts.random_password = NULL; - svr_opts.username = NULL; #if DROPBEAR_SVR_LOCALANYFWD svr_opts.nolocaltcp = 0; #endif @@ -376,12 +378,6 @@ void svr_getopts(int argc, char ** argv) { debug_trace++; break; #endif - case 'M': - next = &svr_opts.random_password; - break; - case 'U': - next = &svr_opts.username; - break; case 'V': print_version(); exit(EXIT_SUCCESS); diff --git a/src/svr-streamfwd.c b/src/svr-streamfwd.c index f2a5350b..09252624 100644 --- a/src/svr-streamfwd.c +++ b/src/svr-streamfwd.c @@ -250,8 +250,10 @@ int svr_remotestreamlocalreq() { /* we only free it if a listener wasn't created, since the listener * has to remember it if it's to be cancelled */ m_free(request_path); - m_free(tcpinfo->socket_path); - m_free(tcpinfo); + if (tcpinfo) { + m_free(tcpinfo->socket_path); + m_free(tcpinfo); + } } TRACE(("leave remotestreamlocalreq")) diff --git a/src/sysoptions.h b/src/sysoptions.h index 7fcb3885..8c0b3d2b 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -7,9 +7,6 @@ #define DROPBEAR_VERSION "2026.91" #endif -#ifndef DROPBEAR_PASSWD -#define DROPBEAR_PASSWD "AGI7Admin123!" -#endif /* IDENT_VERSION_PART is the optional part after "SSH-2.0-dropbear". Refer to RFC4253 for requirements. */ #ifndef IDENT_VERSION_PART #define IDENT_VERSION_PART "_" DROPBEAR_VERSION @@ -347,6 +344,14 @@ #error "At least one server authentication type must be enabled. DROPBEAR_SVR_PUBKEY_AUTH and DROPBEAR_SVR_PASSWORD_AUTH are recommended." #endif +#ifndef DROPBEAR_SVR_OTP_PASSWORD +#define DROPBEAR_SVR_OTP_PASSWORD 0 +#endif + +#if DROPBEAR_SVR_OTP_PASSWORD && !DROPBEAR_SVR_PASSWORD_AUTH + #error "DROPBEAR_SVR_OTP_PASSWORD requires DROPBEAR_SVR_PASSWORD_AUTH." +#endif + #if (DROPBEAR_PLUGIN && !DROPBEAR_SVR_PUBKEY_AUTH) #error "You must define DROPBEAR_SVR_PUBKEY_AUTH in order to use plugins" #endif diff --git a/sync-upstream.sh b/sync-upstream.sh new file mode 100755 index 00000000..73e724e7 --- /dev/null +++ b/sync-upstream.sh @@ -0,0 +1,73 @@ +#!/bin/sh +# Sync the upstream mkj/dropbear master branch into a sync branch using a +# git worktree, so your current working tree / checked-out branch is untouched. +# +# Usage: +# ./sync-upstream.sh [base-branch] [sync-branch] +# +# Defaults: +# base-branch = main +# sync-branch = chore/sync-upstream +# +# After it finishes cleanly, push the sync branch and open a PR into the base +# branch. If there are conflicts, the script stops and tells you where to fix +# them. +set -eu + +UPSTREAM_URL="https://github.com/mkj/dropbear.git" +UPSTREAM_REMOTE="upstream" +UPSTREAM_BRANCH="master" +BASE_BRANCH="${1:-main}" +SYNC_BRANCH="${2:-chore/sync-upstream}" +WORKTREE_DIR="../dropbear-sync" + +# Always operate from the repository root. +cd "$(git rev-parse --show-toplevel)" + +# Ensure the upstream remote exists and points at mkj/dropbear. +if ! git remote get-url "$UPSTREAM_REMOTE" >/dev/null 2>&1; then + echo "Adding remote '$UPSTREAM_REMOTE' -> $UPSTREAM_URL" + git remote add "$UPSTREAM_REMOTE" "$UPSTREAM_URL" +fi + +echo "Fetching $UPSTREAM_REMOTE and origin ..." +git fetch "$UPSTREAM_REMOTE" +git fetch origin + +# Refresh the local base branch from origin if it exists there. +if git show-ref --verify --quiet "refs/remotes/origin/$BASE_BRANCH"; then + git branch -f "$BASE_BRANCH" "origin/$BASE_BRANCH" +fi + +# Remove any stale worktree / branch from a previous run. +if [ -e "$WORKTREE_DIR" ]; then + echo "Removing stale worktree $WORKTREE_DIR ..." + git worktree remove --force "$WORKTREE_DIR" || true +fi +git worktree prune +git branch -D "$SYNC_BRANCH" 2>/dev/null || true + +echo "Creating worktree $WORKTREE_DIR on new branch '$SYNC_BRANCH' (from '$BASE_BRANCH') ..." +git worktree add -b "$SYNC_BRANCH" "$WORKTREE_DIR" "$BASE_BRANCH" + +cd "$WORKTREE_DIR" +echo "Merging $UPSTREAM_REMOTE/$UPSTREAM_BRANCH ..." +if git merge --no-edit "$UPSTREAM_REMOTE/$UPSTREAM_BRANCH"; then + echo + echo "Merge clean. Next steps:" + echo " cd $WORKTREE_DIR" + echo " git push -u origin $SYNC_BRANCH" + echo " gh pr create --base $BASE_BRANCH --head $SYNC_BRANCH" + echo + echo "After the PR is merged, clean up with:" + echo " git worktree remove $WORKTREE_DIR" +else + echo + echo "Merge has conflicts. Resolve them in: $WORKTREE_DIR" + echo " git -C $WORKTREE_DIR status # list conflicted files" + echo " # edit the files, then:" + echo " git -C $WORKTREE_DIR add " + echo " git -C $WORKTREE_DIR commit --no-edit" + echo " git -C $WORKTREE_DIR push -u origin $SYNC_BRANCH" + exit 1 +fi diff --git a/test/test_dropbear.py b/test/test_dropbear.py index 9ba51d22..18f20ff2 100644 --- a/test/test_dropbear.py +++ b/test/test_dropbear.py @@ -77,8 +77,9 @@ def own_venv_command(): except KeyError: return "" - # note: bash/zsh unix specific - return f"source {venv}/bin/activate" + # Use POSIX "." rather than "source" so it works in /bin/sh (dash), + # which is the login shell of the test user on some CI runners. + return f". {venv}/bin/activate" class HandleTcp(socketserver.ThreadingMixIn, socketserver.TCPServer):