From a19dce5c18f6d6370b58aa3b9dd26550a589fc8c Mon Sep 17 00:00:00 2001 From: Andrew Cady Date: Fri, 22 Oct 2021 15:22:37 -0400 Subject: Minimal Git/SSH Rhizome Requires Sudo access to configure OpenSSH. It would be possible to run OpenSSH on a non-default port and not require root access. That is not implemented. --- AnonymousAccessCommand | 126 +++++++++++++++++++++++++++++++++++++++++++++++++ AuthorizedKeysCommand | 15 ++++++ Makefile | 24 ++++++++++ README.txt | 1 + anonymous-access.conf | 10 ++++ tests.makefile | 34 +++++++++++++ tests.sh | 66 ++++++++++++++++++++++++++ 7 files changed, 276 insertions(+) create mode 100755 AnonymousAccessCommand create mode 100755 AuthorizedKeysCommand create mode 100644 Makefile create mode 100644 README.txt create mode 100644 anonymous-access.conf create mode 100644 tests.makefile create mode 100644 tests.sh diff --git a/AnonymousAccessCommand b/AnonymousAccessCommand new file mode 100755 index 0000000..443d25e --- /dev/null +++ b/AnonymousAccessCommand @@ -0,0 +1,126 @@ +#!/bin/sh +default_msg() +{ + sshfpline="$(get_sshfp_authline ${SSH_CLIENT%% *})" + cat <&2 + + You are: + + $authline + $sshfpline + +EOF +} + +get_sshfp_authline() +{ + ( + r=${1:-.} + key=$(mktemp) || exit + trap 'rm -rf "$key"' EXIT + echo "$authline" > "$key" + get_sshfp "$key" "$r" + ) +} + +get_sshfp() +{ + ( + key="$1" + r="${2:-.}" + dns=$(mktemp) || exit + trap 'rm -rf "$dns"' EXIT + + ssh-keygen -r "$r" -f "$key" > "$dns" + exec < "$dns" + while read line + do + set -- $line + if [ "$3 $5" = "SSHFP 2" ] + then + echo "$line" + break + fi + done + ) +} + +ssh_client_fingerprint_base16() +{ + set -- $(get_sshfp_authline) + [ "$6" ] + echo $6 +} + +check_if_self_forge() +{ + # TODO: don't use description, but something else. + local dir="$1" + [ -d "$dir" ] || exit + [ -r "$dir"/description ] || exit + read description < "$dir"/description + if [ "$description" != self-forge ] && [ "$(GIT_DIR=$dir git config core.self-forge)" != true ] + then + echo 'Error: access denied. The specified directory is not a self-forge.' >&2 + exit + fi +} + +read authtype authline < "$SSH_USER_AUTH" || exit +[ "$authtype" = publickey ] || exit + +cmd=${SSH_ORIGINAL_COMMAND%% *} + +case "$cmd" in + git-send-pack | git-upload-pack) + GIT_NAMESPACE= + ;; + git-receive-pack) + export GIT_NAMESPACE="$(ssh_client_fingerprint_base16)" + [ "$GIT_NAMESPACE" ] || exit + ;; + *) + default_msg + exit + ;; +esac + +arg=${SSH_ORIGINAL_COMMAND#* } +arg=${arg%\'} +arg=${arg#\'} +case "$arg" in + *\'*) exit ;; + *.git) ;; + *) arg=$arg/.git ;; +esac + +dir=$(readlink -e "$arg") || exit + +check_if_self_forge "$dir" + +with_allowCurrentBranch() +{ + local cmd="$1" dir="$2" + ( + set -eC + lockfile=$GIT_DIR/index.lock + echo $$ > "$lockfile" + trap 'rm -f "$lockfile"' EXIT + + # This doesn't seem very secure. Need to patch git probably. + for deny in CurrentBranch # DeleteCurrent + do git config receive.deny$deny false + done + "$@" + for deny in CurrentBranch # DeleteCurrent + do git config receive.deny$deny true + done + ) +} + +if [ "$GIT_NAMESPACE" ] +then + GIT_DIR=$dir with_allowCurrentBranch "$cmd" "$dir" +else + "$cmd" "$dir" +fi diff --git a/AuthorizedKeysCommand b/AuthorizedKeysCommand new file mode 100755 index 0000000..6e13063 --- /dev/null +++ b/AuthorizedKeysCommand @@ -0,0 +1,15 @@ +#!/bin/sh +username=$1 +userhome=$2 +fingerprint=$3 +authline="$4 $5" + +case "$userhome" in + *'"'*) exit ;; +esac + +usercommand=$userhome/.ssh/AnonymousAccessCommand + +[ -x "$usercommand" ] || exit + +printf 'command="%s",no-port-forwarding %s\n' "$usercommand $fingerprint" "$authline" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bd63693 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +ifeq ($(shell id -u),0) +SUDO = +else +SUDO = sudo +endif + +ROOT_INSTALL = $(SUDO) install + +USER != echo "$${SUDO_USER:-$$(id -un)}" + +SSH_CONFIG_DIR = /etc/ssh +SSHD_CONFIG_DIR = $(SSH_CONFIG_DIR)/sshd_config.d +SSH_LIB_DIR = /usr/lib/ssh +USER_SSH_CONFIG_DIR = ~$(USER)/.ssh + +.PHONY: install +install: + install -t $(USER_SSH_CONFIG_DIR) AnonymousAccessCommand + $(ROOT_INSTALL) -d "$(SSH_CONFIG_DIR)" "$(SSHD_CONFIG_DIR)" "$(SSH_LIB_DIR)" || true + $(ROOT_INSTALL) -m0644 -t "$(SSHD_CONFIG_DIR)" anonymous-access.conf || true + $(ROOT_INSTALL) -t "$(SSH_LIB_DIR)" AuthorizedKeysCommand || true + [ -e "$(SSH_LIB_DIR)"/AuthorizedKeysCommand ] || $(SUDO) ln -s -t /etc/ssh "$(SSH_LIB_DIR)"/AuthorizedKeysCommand + +include tests.makefile diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..a1c0dd8 --- /dev/null +++ b/README.txt @@ -0,0 +1 @@ +minimal rhizome diff --git a/anonymous-access.conf b/anonymous-access.conf new file mode 100644 index 0000000..5cd6b6a --- /dev/null +++ b/anonymous-access.conf @@ -0,0 +1,10 @@ +ExposeAuthInfo=yes +AuthorizedKeysCommandUser=root +AuthorizedKeysCommand=/etc/ssh/AuthorizedKeysCommand %u %h %f "%t %k" + +# %u The username. +# %h The home directory of the user. +# %f The fingerprint of the key or certificate. +# %t The key or certificate type. +# %k The base64-encoded key or certificate for authentication. + diff --git a/tests.makefile b/tests.makefile new file mode 100644 index 0000000..b152f01 --- /dev/null +++ b/tests.makefile @@ -0,0 +1,34 @@ +testuser = testuser + +SU = $(SUDO) su + +.PHONY: test useradd cleanuser + +useradd: + $(SUDO) useradd $(testuser) --shell /bin/bash --create-home + $(SU) - $(testuser) -c 'ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""' + $(SU) - $(testuser) -c 'git config --global user.name $(testuser)' + $(SU) - $(testuser) -c 'git config --global user.email $(testuser)' + +test: install $(shell getent passwd $(testuser) >/dev/null || echo useradd) + $(ROOT_INSTALL) -t ~$(testuser) tests.sh + $(SU) - $(testuser) -c ./tests.sh + +ifeq ($(testuser),) +$(error testuser not defined) +endif +cleanuser_command = $(SUDO) rm -I -r ~$(testuser) + +cleanuser: + : Preparing to run destructive command: + : + : + : $(cleanuser_command) + : + : + : Press ctrl-c to abort. + : + @for n in 5 4 3 2 1; do printf ' %d\r' "$$n"; sleep 1; done + $(cleanuser_command) || true + $(SUDO) userdel testuser || true + diff --git a/tests.sh b/tests.sh new file mode 100644 index 0000000..5d221b6 --- /dev/null +++ b/tests.sh @@ -0,0 +1,66 @@ +#!/bin/sh +set -ex +USER=u +DIR=src/anonymous-ssh +HOST=localhost +SSH_ID=~/.ssh/id_ed25519 + +get_sshfp() +{ + ( + key="$1" + r="${2:-.}" + dns=$(mktemp) || exit + trap 'rm -rf "$dns"' EXIT + + ssh-keygen -r "$r" -f "$key" > "$dns" + exec < "$dns" + while read line + do + set -- $line + if [ "$3 $5" = "SSHFP 2" ] + then + echo "$line" + break + fi + done + ) +} + +make_test_commit() +{ + newfile=newfile.$(date -Ins|tr -d :) + touch "$newfile" + git add "$newfile" + git commit -m "$newfile" +} + +[ -e "$SSH_ID" ] || ssh-keygen -t ed25519 -f "$SSH_ID" -P '' + +git_namespace=$(set -- $(get_sshfp "$SSH_ID") && echo $6) + +ssh -o NoHostAuthenticationForLocalhost=yes $USER@$HOST -- test || true +[ ! -e anonymous-ssh ] || rm -rf anonymous-ssh +export GIT_SSH_COMMAND="ssh -o NoHostAuthenticationForLocalhost=yes -i $SSH_ID" +git clone -v ${USER}@${HOST}:${DIR} +cd anonymous-ssh + +make + +git pull --ff-only +make_test_commit +git push -f +make_test_commit +git push +git log -n4 +git pull --ff-only +git log -n4 +git push + +# branch=$(git branch -q --show-current) +# forkname=origin-myfork +# ns_branch=refs/namespaces/$git_namespace/refs/heads/$branch +# git remote add -m "$ns_branch" "$forkname" $(git remote get-url origin) +# git push "$forkname" +# git pull "$forkname" --ff-only "$branch" +exit -- cgit v1.2.3