Skip to content

Instantly share code, notes, and snippets.

@jimdigriz
Last active October 10, 2023 12:17
Show Gist options
  • Save jimdigriz/28af0545c2cad461d56f89d59eb38ac8 to your computer and use it in GitHub Desktop.
Save jimdigriz/28af0545c2cad461d56f89d59eb38ac8 to your computer and use it in GitHub Desktop.
This script is used to maintain scrambled kdb+/q '*.q_' files within a git project
#!/bin/sh
# This script is used to maintain scrambled kdb+/q '*.q_' files within a git
# project. The unscrambled '*.q' files are retained as an encrypted CMS file
# using recipient certificates generated from a list of OpenSSH public keys
# stored at the top of the project in an '.authorized_keys' file.
#
# For this to work effectively, you need to include '*.q' in your .gitignore
#
# TODO sign the CMS and verify the signature is at least one that is listed
# TODO send PASSPHRASE over pipe (openssl can do it but not ssh-keygen)
#
# Upstream: https://gist.github.com/jimdigriz/28af0545c2cad461d56f89d59eb38ac8
set -eu
BASENAME=$(basename "$0")
usage () {
cat <<EOF
Usage: $BASENAME { { seal | unseal | edit } file ... | reseal }
Operation:
seal Encrypt a given .q file(s)
unseal Decrypt a given .q.cms (unscrambled .q_) file(s)
edit Edit in $EDITOR a given .q.cms (unscrambled .q_) file(s)
info Output debug for a given .q.cms file(s)
reseal Reseal all .q.cms files (ie. updating .authorized_keys)
Target user public keys are stored at '\$GIT_DIR/.authorized_keys'
in the format described by authorized_keys(5).
Note that SSH public keys must be of the type 'ssh-rsa'.
EOF
}
[ $# -ne 0 ] || { usage; exit 0; }
: "${EDITOR:=vi}"
: "${QHOME:=$HOME/q}"
Q_BIN=$(find "$QHOME/" -maxdepth 2 -mindepth 2 -type f -name q -perm -111 2>&-) || true
[ "${Q_BIN:-}" ] || { echo missing q command >&2; exit 1; }
PATH="$PATH:$(dirname "$Q_BIN")"
printf '' | q -q || { echo missing functioning q command >&2; exit 1; }
which git >/dev/null 2>&- || { echo missing git command >&2; exit 1; }
which openssl >/dev/null 2>&- || { echo missing openssl command >&2; exit 1; }
if openssl version | grep -q LibreSSL; then
echo 'openssl is LibreSSL which probably is really old and needs to be at least >=3.5.0 to support CMS...use Linux instead' >&2
exit 1
fi
which ssh-keygen >/dev/null 2>&- || { echo missing ssh-keygen command >&2; exit 1; }
test -f "$HOME/.ssh/id_rsa" || { echo missing file ~/.ssh/id_rsa >&2; exit 1; }
if which shred >/dev/null 2>&-; then
SHRED='shred -u'
else
SHRED='rm -f'
fi
: "${GIT_DIR:=$(git rev-parse --show-toplevel)}"
test -f "$GIT_DIR/.authorized_keys" || { echo missing file "$GIT_DIR/.authorized_keys" >&2; exit 1; }
[ "$(grep -c '\(^\| \)ssh-rsa ' "$GIT_DIR/.authorized_keys")" -gt 0 ] || { echo only ssh-rsa keys supported >&2; exit 1; }
WORKDIR=$(mktemp -d -t "${BASENAME%.sh}.XXXXXXXXXX")
cleanup () {
# *BSD/macOS does not support 'xargs -r ...' (╯°□°)╯ ┻━┻
[ $(find "$WORKDIR" -type f | wc -l) -eq 0 ] || find "$WORKDIR" -type f -print0 | xargs -0 $SHRED
rm -rf "$WORKDIR"
}
trap cleanup EXIT
cert () {
local PUBLICKEYFILE THUMBPRINT PUBLICKEY
PUBLICKEYFILE=$1
THUMBPRINT=$2
PUBLICKEY=$(openssl asn1parse -offset 19 -item ASN1_BIT_STRING -in "$PUBLICKEYFILE" -noout -out - | od -t x1 -v -A none | tr -d ' \n' | cut -b 11-)
# https://github.com/wllm-rbnt/asn1template
# openssl req -x509 -newkey rsa:1024 -subj '/CN=COMMONNAME/' -noenc -md5 -addext subjectKeyIdentifier=none -addext authorityKeyIdentifier=none -out cert.pem
# ./asn1template/asn1template.pl template.pem | sed -e 's/@[0-9-]\+//g'
# ...then manually removed the optional extensions field (field12) and its dependents
cat <<EOF | openssl asn1parse -noout -genconf - -out - | openssl x509 -inform DER -outform PEM
asn1 = SEQUENCE:seq1
[seq1]
field2 = SEQUENCE:seq2
field3 = SEQUENCE:seq3
field4 = FORMAT:HEX,BITSTRING:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
[seq2]
field5 = IMPLICIT:0C,SEQUENCE:seq4
field6 = INTEGER:0x$THUMBPRINT
field7 = SEQUENCE:seq5
field8 = SEQUENCE:seq6
field9 = SEQUENCE:seq7
field10 = SEQUENCE:seq8
field11 = SEQUENCE:seq9
[seq4]
field13 = INTEGER:0x02
[seq5]
field14 = OBJECT:md5WithRSAEncryption
field15 = NULL
[seq6]
field16 = SET:seq11
[seq11]
field17 = SEQUENCE:seq12
[seq12]
field18 = OBJECT:commonName
field19 = FORMAT:UTF8,UTF8String:"$BASENAME"
[seq7]
field20 = UTCTIME:$(date -u +%y%m%d)000000Z
field21 = UTCTIME:$(($(date -u +%y%m%d)+10000))000000Z
[seq8]
field22 = SET:seq13
[seq13]
field23 = SEQUENCE:seq14
[seq14]
field24 = OBJECT:commonName
field25 = FORMAT:UTF8,UTF8String:"$BASENAME"
[seq9]
field26 = SEQUENCE:seq15
field27 = FORMAT:HEX,BITSTRING:$PUBLICKEY
[seq15]
field28 = OBJECT:rsaEncryption
field29 = NULL
[seq3]
field35 = OBJECT:md5WithRSAEncryption
field36 = NULL
EOF
}
seal () {
for FILE in "$@"; do
FILE="${FILE%.cms}"
FILE="${FILE%.q_}"
FILE="${FILE%.q}"
FILE="$FILE.q"
test -f "$FILE" || { echo "'$FILE' does not exist" >&2; exit 1; }
done
test -f "$WORKDIR/dummy.key" || {
mkdir "$WORKDIR/p"
I=0
grep '\(^\| \)ssh-rsa ' "$GIT_DIR/.authorized_keys" | while IFS='
' read -r PUBKEY; do
I=$((I+1))
THUMBPRINT=$(echo "$PUBKEY" | ssh-keygen -l -E MD5 -f /dev/stdin | cut -d' ' -f 2 | cut -b 5- | tr -d :)
echo "$PUBKEY" | ssh-keygen -q -e -f /dev/stdin -m pem | openssl rsa -RSAPublicKey_in -pubout -out "$WORKDIR/p/$I.pub" 2>/dev/null
# 'openssl x509 -new ...' is not supported as well as -force_pubkey is not
# implemented correctly in 1.1.1, so instead we do the splicing work ourselves
# fortunately the signature is not used for our purposes so can be dummy data
cert "$WORKDIR/p/$I.pub" "$THUMBPRINT" > "$WORKDIR/p/$I.crt"
done
}
for FILE in "$@"; do
FILE="${FILE%.cms}"
FILE="${FILE%.q_}"
FILE="${FILE%.q}"
FILE="$FILE.q"
cd "$(dirname "$FILE")"
printf "\\_ %s\n" "$(basename "$FILE")" | q -q >&-
cd - >/dev/null
# aes-256-gcm does not interop between openssl 3.x and 1.1.1 so we stick to cbc
# probably as 1.1.1 was earlier than https://github.com/openssl/openssl/pull/8024
openssl cms -encrypt -aes256 -in "$FILE" -out "$FILE.cms" $(find "$WORKDIR/p" -type f -name '*.crt' | awk '{ print "-recip " $0 }') -keyopt rsa_padding_mode:oaep
$SHRED "$FILE"
done
}
unseal () {
for FILE in "$@"; do
FILE="${FILE%.cms}"
FILE="${FILE%.q_}"
FILE="${FILE%.q}"
FILE="$FILE.q.cms"
test -f "$FILE" || { echo "'$FILE' does not exist" >&2; exit 1; }
done
test -f "$WORKDIR/id_rsa.crt" || {
PASSPHRASE=$(openssl rand -base64 16)
cp -a "$HOME/.ssh/id_rsa" "$WORKDIR/id_rsa.key"
ssh-keygen -q -p -f "$WORKDIR/id_rsa.key" -m pem -N "$PASSPHRASE" >&-
THUMBPRINT=$(ssh-keygen -l -E MD5 -f "$HOME/.ssh/id_rsa.pub" | cut -d' ' -f 2 | cut -b 5- | tr -d :)
openssl req -x509 -key "$WORKDIR/id_rsa.key" -passin "pass:$PASSPHRASE" -out "$WORKDIR/id_rsa.crt" -set_serial "0x$THUMBPRINT" -subj "/CN=$BASENAME/"
}
for FILE in "$@"; do
FILE="${FILE%.cms}"
FILE="${FILE%.q_}"
FILE="${FILE%.q}"
openssl cms -decrypt -in "$FILE.q.cms" -out "$FILE.q" -recip "$WORKDIR/id_rsa.crt" -inkey "$WORKDIR/id_rsa.key" -passin "pass:$PASSPHRASE"
done
}
edit () {
for FILE in "$@"; do
FILE="${FILE%.cms}"
FILE="${FILE%.q_}"
FILE="${FILE%.q}"
FILE="$FILE.q.cms"
test -f "$FILE" || { echo "'$FILE' does not exist" >&2; exit 1; }
done
for FILE in "$@"; do
FILE="${FILE%.cms}"
FILE="${FILE%.q_}"
FILE="${FILE%.q}"
unseal "$FILE.q.cms"
CHKSUM=$(openssl dgst "$FILE.q")
"$EDITOR" "$FILE.q"
[ "$CHKSUM" = "$(openssl dgst "$FILE.q")" ] && $SHRED "$FILE.q" || seal "$FILE.q"
done
}
info () {
for FILE in "$@"; do
FILE="${FILE%.cms}"
FILE="${FILE%.q_}"
FILE="${FILE%.q}"
FILE="$FILE.q.cms"
test -f "$FILE" || { echo "'$FILE' does not exist" >&2; exit 1; }
done
for FILE in "$@"; do
FILE="${FILE%.cms}"
FILE="${FILE%.q_}"
FILE="${FILE%.q}"
openssl cms -print -cmsout -in "$FILE.q.cms"
done
}
reseal () {
OIFS="$IFS"
IFS='
'
set -- $(find "$GIT_DIR" -type f -name '*.q.cms')
IFS="$OIFS"
unseal "$@"
seal "$@"
}
case "${1:-}" in
seal|unseal|edit|info)
[ $# -gt 1 ] || { echo no files provided to process >&2; exit 1; }
"$@"
;;
reseal)
reseal
;;
*)
usage
exit 1
;;
esac
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment