git-remote-gcrypt/git-remote-gcrypt
root 192befdb62 Request signature on manifest before uploading pack
Just to ensure that the user signs the push before we upload any files
(pack or manifest) to the remote.
2013-02-14 00:00:00 +00:00

593 lines
13 KiB
Bash
Executable file

#!/bin/sh
# git-remote-gcrypt
# Copyright 2013 by Ulrik
# License: GPLv2 or any later version, see http://www.gnu.org/licenses/
#
# See README
#set -x
set -e
Did_find_repo= # yes for connected, no for no repo
Localdir="${GIT_DIR:=.git}/remote-gcrypt"
export GITCEPTION="${GITCEPTION:-}+" # Reuse $Gref except when stacked
Gref="refs/gcrypt/gitception$GITCEPTION"
Gref_rbranch="refs/heads/master"
Repoid=
Hashpfx=":SHA224:"
UrlTag="G."
Packpfx="pack $Hashpfx"
Packkey_bytes=33
Branchlist=
Packlist=
Extension_list=
# compat/utility functions
xecho()
{
cat <<EOF
$@
EOF
}
xecho_n() { xecho "$@" | tr -d \\n ; } # kill newlines
echo_git() { xecho "$@" ; } # Code clarity
echo_info() { xecho "gcrypt:" "$@" >&2; }
echo_die() { echo_info "$@" ; exit 1; }
isnull() { case "${1}" in "") true;; *) false;; esac; }
isnonnull() { ! isnull "$1"; }
iseq() { isnull "${1#"$2"}"; }
isnoteq() { ! iseq "$@"; }
# Append $2 to $1 with a newline separator
append() { isnull "$1" || xecho "$1" && xecho "$2"; }
isurl() { isnull "${2%%$1://*}"; }
islocalrepo() { isnull "${1##/*}" && [ ! -e "$1/HEAD" ]; }
xgrep() { command grep "$@" || : ; }
sort_C() { LC_ALL=C command sort "$@"; }
sort_stable_k2()
{
awk '{ printf("%08d\t%s\n", NR, $0) }' | sort_C -k 3,3 -k 1,1 |cut -f 2-
}
tac() { sed '1!G;h;$!d'; }
# Split $1 into $prefix_:$suffix_
splitcolon()
{
prefix_=${1%%:*}
suffix_=${1#*:}
}
repoidstr() { xecho "repo $Hashpfx$Repoid 1"; }
## gitception part
# Fetch giturl $1, file $2
gitception_get()
{
# Take care to preserve FETCH_HEAD
local ret_=: obj_id= f_head="$GIT_DIR/FETCH_HEAD"
[ -e "$f_head" ] && command mv -f "$f_head" "$f_head.$$~" || :
git fetch -q -f "$1" "$Gref_rbranch:$Gref" 2>/dev/tty >/dev/null &&
obj_id="$(git ls-tree "$Gref" | xgrep -E '\b'"$2"'$' | awk '{print $3}')" &&
isnonnull "$obj_id" && git cat-file blob "$obj_id" && ret_=: ||
{ ret_=false && : ; }
[ -e "$f_head.$$~" ] && command mv -f "$f_head.$$~" "$f_head" || :
$ret_
}
anon_commit()
{
GIT_AUTHOR_NAME="root" GIT_AUTHOR_EMAIL="root@localhost" \
GIT_AUTHOR_DATE="1356994801 -0400" GIT_COMMITTER_NAME="root" \
GIT_COMMITTER_EMAIL="root@localhost" \
GIT_COMMITTER_DATE="1356994801 -0400" \
git commit-tree "$@" <<EOF
Initial commit
EOF
}
# Get 'tree' from $1, change file $2 to obj id $3
update_tree()
{
local tab_=" "
# $2 is a filename from the repo format
(git ls-tree "$1" | xgrep -v -E '\b'"$2"'$';
xecho "100644 blob $3$tab_$2") | git mktree
}
# Put giturl $1, file $2
# depends on previous GET to set $Gref and depends on PUT_FINAL later
gitception_put()
{
local obj_id= tree_id= commit_id=
obj_id=$(git hash-object -w --stdin) &&
tree_id=$(update_tree "$Gref" "$2" "$obj_id") &&
commit_id=$(anon_commit "$tree_id") &&
git update-ref "$Gref" "$commit_id"
}
gitception_new_repo()
{
local empty_tree=4b825dc642cb6eb9a060e54bf8d69288fbee4904
# get any file to update Gref, and if it's not updated we create empty
git update-ref -d "$Gref" || :
gitception_get "$1" "x" || :
git rev-parse -q --verify "$Gref" >/dev/null && return 0 ||
commit_id=$(anon_commit "$empty_tree") &&
git update-ref "$Gref" "$commit_id"
}
## end gitception
# Fetch repo $1, file $2, tmpfile in $3
GET()
{
if isurl ssh "$1"
then
splitcolon "${1#ssh://}"
(exec 0>&-; ssh "$prefix_" "cat $suffix_/$2") > "$3"
elif isurl sftp "$1"
then
(exec 0>&-; curl -s -S -k "$1/$2") > "$3"
elif isurl rsync "$1"
then
(exec 0>&-; rsync -I -W "${1#rsync://}"/"$2" "$3" >&2)
elif islocalrepo "$1"
then
cat "$1/$2" > "$3"
else
gitception_get "${1#gitception://}" "$2" > "$3"
fi
}
# Put repo $1, file $2 or fail, tmpfile in $3
PUT()
{
if isurl ssh "$1"
then
splitcolon "${1#ssh://}"
ssh "$prefix_" "cat > $suffix_/$2" < "$3"
elif isurl sftp "$1"
then
curl -s -S -k --ftp-create-dirs -T "$3" "$1/$2"
elif isurl rsync "$1"
then
rsync -I -W "$3" "${1#rsync://}"/"$2" >&2
elif islocalrepo "$1"
then
cat > "$1/$2" < "$3"
else
gitception_put "${1#gitception://}" "$2" < "$3"
fi
}
# Put all PUT changes for repo $1 at once
PUT_FINAL()
{
if isurl ssh "$1" || isurl sftp "$1" || islocalrepo "$1" || isurl rsync "$1"
then
:
else
git push --quiet -f "${1#gitception://}" "$Gref:$Gref_rbranch"
fi
}
# Put directory for repo $1
PUTREPO()
{
if isurl ssh "$1"
then
splitcolon "${1#ssh://}"
(exec 0>&- ; ssh "$prefix_" "mkdir -p $suffix_")
elif isurl sftp "$1"
then
:
elif isurl rsync "$1"
then
rsync -q -r --exclude='*' "$Localdir/" "${1#rsync://}" >&2
elif islocalrepo "$1"
then
mkdir -p "$1"
else
gitception_new_repo "${1#gitception://}"
fi
}
CLEAN_FINAL()
{
if isurl ssh "$1" || isurl sftp "$1" || islocalrepo "$1" || isurl rsync "$1"
then
:
else
git update-ref -d "$Gref" || :
fi
}
addsignkeyparam()
{
if isnull "$Conf_signkey"; then
"$@"
else
"$@" -u "$Conf_signkey"
fi
}
ENCRYPT()
{
gpg --batch --force-mdc --compress-algo none --passphrase-fd 3 -c 3<<EOF
$1
EOF
}
DECRYPT()
{
gpg -q --batch --no-default-keyring --secret-keyring /dev/null \
--keyring /dev/null --passphrase-fd 3 -d 3<<EOF
$1
EOF
}
# Encrypt to recipients $1
PRIVENCRYPT()
{
addsignkeyparam gpg --no-default-keyring --keyring "$Conf_keyring" \
--compress-algo none -se $1
}
PRIVDECRYPT()
{
local status_=
exec 4>&1 &&
status_=$(gpg --no-default-keyring --keyring "$Conf_keyring" \
--status-fd 3 -q -d 3>&1 1>&4) &&
xecho "$status_" | grep "^\[GNUPG:\] ENC_TO " >/dev/null &&
(xecho "$status_" | grep "^\[GNUPG:\] GOODSIG " >/dev/null || {
echo_info "Failed to verify manifest signature!" && return 1
})
}
# Generate $1 random bytes
genkey()
{
gpg --armor --gen-rand 1 "$1"
}
pack_hash()
{
local hash_=
hash_=$(gpg --with-colons --print-md SHA224 | tr A-F a-f)
hash_=${hash_#:*:}
xecho "${hash_%:}"
}
# Pass the branch/ref by pipe to git
safe_git_rev_parse()
{
git cat-file --batch-check 2>/dev/null |
xgrep -v "missing" | cut -f 1 -d ' '
}
check_recipients()
{
# Find which keys in the keyring we can encrypt to
Recipients=$(gpg --no-default-keyring --keyring "$Conf_keyring" \
--with-colons --fast-list -k | xgrep ^pub | \
while read rc_line; do
cap_=$(xecho "$rc_line" | cut -f 12 -d :)
keyid_=$(xecho "$rc_line" | cut -f 5 -d :)
iseq "${cap_#*E}" "$cap_" || xecho_n "-R $keyid_ "
done)
if isnull "$Recipients"
then
echo_info "You must configure a keyring for the repository."
echo_info "Use ::"
echo_info " gpg --export KEYID1 > <path-to-keyring>"
echo_info " git config gcrypt.keyring <path-to-keyring>"
exit 1
fi
}
make_new_repo()
{
local urlid_= fix_config=
echo_info "Setting up new repository at $URL"
PUTREPO "$URL"
# We need a relatively short ID for URL+REPO
# The manifest will be stored at SHA224(urlid_)
# Needed assumption: the same user should have no duplicate urlid_
# For now, we arbitrarily use 9 random bytes (72 bits)
urlid_=$(genkey 9 | tr '/+' 'Zz')
Repoid=$(xecho_n "$urlid_" | pack_hash)
echo_info "Repository ID is" "$urlid_"
iseq "${NAME#gcrypt::}" "$URL" || {
git config "remote.$NAME.url" "gcrypt::$URL/$UrlTag$urlid_"
fix_config=1
}
echo_info "Repository URL is" "gcrypt::$URL/$UrlTag$urlid_"
Extension_list=$(xecho "extn comment")
isnull "$fix_config" || echo_info "(configuration for $NAME updated)"
}
read_config()
{
Conf_keyring=$(git config --path gcrypt.keyring || xecho "/dev/null")
Conf_signkey=$(git config --path user.signingkey || :)
}
ensure_connected()
{
local manifest_= rcv_repoid= url_id=
if isnonnull "$Did_find_repo"
then
return
fi
Did_find_repo=no
read_config
# split out Repoid from URL
url_id=${URL##*/"$UrlTag"}
isnoteq "$url_id" "$URL" || return 0
URL=${URL%/"$UrlTag$url_id"}
Repoid=$(xecho_n "$url_id" | pack_hash)
TmpManifest_Enc="$Localdir/manifest.$$"
trap 'rm -f "$TmpManifest_Enc"' EXIT
GET "$URL" "$Repoid" "$TmpManifest_Enc" 2>/dev/null ||
echo_die "Repository not found: $url_id at $URL"
Did_find_repo=yes
echo_info "Decrypting manifest"
manifest_=$(PRIVDECRYPT < "$TmpManifest_Enc") &&
isnonnull "$manifest_" || {
echo_info "Failed to decrypt manifest!"
echo_info "Using keyring $Conf_keyring"
if iseq "$Conf_keyring" "/dev/null"
then
echo_info "NOTE: Please configure gcrypt.keyring"
fi
exit 1
}
rm -f "$TmpManifest_Enc"
trap - EXIT
Branchlist=$(xecho "$manifest_" | xgrep -E '^[0-9a-f]{40} ')
Packlist=$(xecho "$manifest_" | xgrep "^$Packpfx")
Extension_list=$(xecho "$manifest_" | xgrep "^extn ")
rcv_repoid=$(xecho "$manifest_" | xgrep "^repo ")
iseq "$(repoidstr)" "$rcv_repoid" || echo_die "Repository id mismatch!"
}
do_capabilities()
{
echo_git fetch
echo_git push
echo_git
}
do_list()
{
local obj_id= ref_name= line_=
ensure_connected
xecho "$Branchlist" | while read line_
do
isnonnull "$line_" || break
obj_id=${line_%% *}
ref_name=${line_##* }
echo_git "$obj_id" "$ref_name"
if iseq "$ref_name" "refs/heads/master"
then
echo_git "@refs/heads/master HEAD"
fi
done
# end with blank line
echo_git
}
do_fetch()
{
# Security protocol:
# The PACK id is the hash of the encrypted git packfile.
# We only download packs mentioned in the encrypted manifest,
# and check their digest when received.
local pack_= rcv_id= packline_= pneed_= pboth_= phave_= premote_= key_=
ensure_connected
if isnull "$Packlist"
then
echo_git # end with blank line
return
fi
TmpPack_Encrypted="$Localdir/tmp_pack_ENCRYPTED_.$$"
trap 'rm -f "$TmpPack_Encrypted"' EXIT
premote_=$(xecho "$Packlist" | cut -f 1-2 -d ' ')
# Needed packs is Packlist - (phave & Packlist)
# The `+` for $GITCEPTION is pointless but we will be safe for stacking
phave_="$(cat "$Localdir/have_packs+" 2>/dev/null || :)"
pboth_="$(xecho "$premote_" | xgrep -F -x -e "$phave_")"
pneed_="$(xecho "$premote_" | xgrep -F -vx -e "$phave_")"
xecho "$pneed_" | while read packline_
do
isnonnull "$packline_" || continue
pack_=${packline_#"$Packpfx"}
GET "$URL" "$pack_" "$TmpPack_Encrypted"
rcv_id=$(pack_hash < "$TmpPack_Encrypted")
if isnoteq "$rcv_id" "$pack_"
then
echo_die "Packfile $pack_ does not match digest!"
fi
key_=$(xecho "$Packlist" | grep "$pack_" | cut -f 3 -d ' ')
DECRYPT "$key_" < "$TmpPack_Encrypted" |
git index-pack -v --stdin >/dev/null
# add to local pack list
xecho "$Packpfx$pack_" >> "$Localdir/have_packs$GITCEPTION"
done
rm -f "$TmpPack_Encrypted"
trap - EXIT
echo_git # end with blank line
}
# do_push PUSHARGS (multiple lines like +src:dst, with both + and src opt.)
do_push()
{
# Security protocol:
# Each git packfile is encrypted and then named for the encrypted
# file's hash. The manifest is updated with the pack id.
# The manifest is encrypted.
local remote_has= remote_want= prefix_= suffix_= line_= pack_id= key_=
del_hash=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ensure_connected
check_recipients
if iseq "$Did_find_repo" "no"
then
make_new_repo
fi
trap 'rm -f "$TmpPack_Encrypted" "$TmpObjlist" "$TmpManifest_Enc"' EXIT
if isnonnull "$Branchlist"
then
# filter through batch-check to mark only the commits we have
remote_has=$(xecho "$Branchlist" | cut -f 1 -d ' ' |
safe_git_rev_parse | sed -e 's/^\(.\)/^&/')
fi
while read line_ # from <<
do
# +src:dst -- remove leading + then split at :
splitcolon "${line_#+}"
if isnonnull "$prefix_"
then
remote_want=$(append "$remote_want" "$prefix_")
Branchlist=$(append "$Branchlist" \
"$(xecho "$prefix_" | safe_git_rev_parse) $suffix_")
else
# Mark branch for deletion
Branchlist=$(append "$Branchlist" "$del_hash $suffix_")
fi
done <<EOF
$1
EOF
Branchlist=$(xecho "$Branchlist" | sort_stable_k2 | tac | uniq -s 40 |
xgrep -v "^$del_hash")
TmpPack_Encrypted="$Localdir/tmp_pack_ENCRYPTED_.$$"
TmpObjlist="$Localdir/tmp_packrevlist.$$"
key_=$(genkey "$Packkey_bytes")
append "$remote_has" "$remote_want" |
git rev-list --objects --stdin -- |
tee "$TmpObjlist" |
git pack-objects --stdout | ENCRYPT "$key_">"$TmpPack_Encrypted"
# Only send pack if we have any objects to send
if [ -s "$TmpObjlist" ]
then
pack_id=$(pack_hash < "$TmpPack_Encrypted")
Packlist=$(append "$Packlist" "$Packpfx$pack_id $key_")
fi
# Generate manifest
echo_info "Encrypting manifest to \"$Recipients\""
echo_info "Requesting manifest signature"
TmpManifest_Enc="$Localdir/manifest.$$"
(xecho "$Branchlist"; xecho "$Packlist";
repoidstr; xecho "$Extension_list") |
PRIVENCRYPT "$Recipients" > "$TmpManifest_Enc"
# Upload pack
if [ -s "$TmpObjlist" ]
then
PUT "$URL" "$pack_id" "$TmpPack_Encrypted"
fi
rm -f "$TmpPack_Encrypted"
rm -f "$TmpObjlist"
# Upload manifest
PUT "$URL" "$Repoid" "$TmpManifest_Enc"
PUT_FINAL "$URL"
rm -f "$TmpManifest_Enc"
trap - EXIT
# ok all updates
xecho "$1" | while read line_
do
# +src:dst -- remove leading + then split at :
splitcolon "${line_#+}"
echo_git "ok $suffix_"
done
echo_git
}
NAME=$1 # Remote name
URL=$2 # Remote URL
mkdir -p "$Localdir"
while read Input
do
case "$Input" in
capabilities)
do_capabilities
;;
list|list\ for-push)
do_list
;;
fetch\ *)
args_="${Input##fetch }"
while read InputX
do
case "$InputX" in
fetch*)
args_= #ignored
;;
*)
break
;;
esac
done
do_fetch "$args_"
;;
push\ *)
args_="${Input##push }"
while read InputX
do
case "$InputX" in
push\ *)
args_=$(append "$args_" "${InputX#push }")
;;
*)
break
;;
esac
done
do_push "$args_"
;;
?*)
echo_die "Unknown input!"
;;
*)
CLEAN_FINAL "$URL"
exit 0
;;
esac
done