Created
April 29, 2018 14:36
-
-
Save pts/13da122cd324faa623d812902ef293e0 to your computer and use it in GitHub Desktop.
sesamessh
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /bin/sh -- | |
# Passphrase-based SSH client. https://github.com/pts/sesamessh | |
# This is free software, GNU GPL >=2.0. There is NO WARRANTY. Use at your risk. | |
# | |
# wget https://github.com/pts/sesamessh/blob/master/sesamessh | sh | |
# | |
if true; then # Read entire script before running it. | |
rmtmp() { # Will be overwritten later. | |
: | |
} | |
die() { | |
echo "fatal: $*" >&2 | |
rmtmp | |
exit 1 | |
} | |
# Needed in $SHCK_ARGS below. | |
test "$ZSH_VERSION" && setopt shwordsplit 2>/dev/null | |
unset TARGET UK HK IS_SETUP STATUS EXPECTED_OUTPUT PYCODE ARG SHKC_ARGS Q_ARGS # Unexport. | |
IS_SETUP= | |
if test "$1" = setup || test "$1" = --setup; then | |
IS_SETUP=1 | |
shift | |
fi | |
# Find a Python which can do the operations we need. | |
# Currently only Python 2.4, 2.5, 2.6 and 2.7 are supported, Pythen 3 isn't. | |
# For Python 2.4, the hashlib package needs to be installed as an extra. | |
PYCODE='import base64, hashlib, os, struct, sys; print base64.b64encode(hashlib.sha512(struct.pack(">H", int(os.getenv("HK")))).digest().encode("hex").upper().decode("hex"))' | |
PYTHON= | |
for PYTHON in python python2 python2.7 python2.6 python2.5 python2.4; do | |
TARGET="$(HK=26740 "$PYTHON" -c "$PYCODE" 2>/dev/null)" | |
test "$?" = 0 && test "$TARGET" = wSjUFoJTnlc9TWYgmAGwSpXZR9r95C9ybC+W2fU1EGCmfAYl+fvJBELX+YRv0eGlhqzEQRpvUIqWUqhLA6oXvw== && break | |
PYTHON= | |
done | |
test "$PYTHON" || die 'working Python not found' | |
if test $# = 0; then | |
echo -n 'Enter sesamessh target: ' | |
read TARGET </dev/tty | |
test "$TARGET" || die 'target must not be empty' | |
else | |
TARGET= | |
fi | |
if test "$SESAMESSH_UK"; then | |
# As a user of sesamessh, don't type `SESAMESSH_UK=...' to your shell | |
# command-line, to prevent the key from being saved to the shell history. | |
# Do this instead: `read SESAMESSH_UK; export SESAMESSH_UK'. | |
UK="$SESAMESSH_UK" | |
unset SESAMESSH_UK # Prevent exporting. | |
else | |
# We could use `read -s' here, but Dash doesn't support it. | |
echo -n 'Enter sesamessh user key: ' | |
# Any byte except for \0 and \n is supported in $UK. Accented characters | |
# (non-ASCII bytes) are passed as is, but they are not recommended in case | |
# the encoding of the TTY changes in the future. | |
read UK </dev/tty | |
fi | |
test "$UK" || die 'user key must not be empty' | |
if test "$SESAMESSH_HK"; then | |
HK="$SESAMESSH_HK" | |
unset SESAMESSH_HK # Prevent exporting. | |
else | |
echo -n 'Enter sesamessh host key: ' | |
read HK </dev/tty | |
fi | |
test "$HK" = . && HK= | |
T="${TMPDIR:-/tmp}/sesamessh.tmp.$$" | |
mkdir -- "$T" || die "cannot create temporary directory: $T" | |
rmtmp() { | |
rm -f -- "$T"/* 2>/dev/null | |
rmdir -- "$T" 2>/dev/null | |
} | |
chmod 700 -- "$T" || die "cannot chmod temporary directory: $T" | |
(: >"$T/id_sesamessh" && chmod 600 -- "$T/id_sesamessh") || die "cannot create or chmod private key file: $T/id_sesamessh" | |
if test "$IS_SETUP"; then | |
(: >"$T/id_sesamessh.setup" && chmod 600 -- "$T/id_sesamessh.setup") || die "cannot create or chmod setup info file: $T/id_sesamessh.setup" | |
fi | |
(: >"$T/known_hosts" && chmod 600 -- "$T/known_hosts") || die "cannot create or chmod known_hosts file: $T/known_hosts" | |
# TODO(pts): Create SSH config file ($T/config), hide even more options from command-line. | |
PYCODE='import base64, hashlib, os, struct, sys | |
def get_public_key_ed25519_unsafe(private_key, _bpow=[]): | |
h = hashlib.sha512(private_key).digest()[:32] | |
e = ((1 << 254) | (int(h[::-1].encode("hex"), 16) & ~(7 | 1 << 255))) % ( | |
(1 << 252) + 0x14def9dea2f79cd65812631a5cf5d3ed) | |
q = (1 << 255) - 19 | |
if not _bpow: # Compute it only for the first time. | |
_bpow.append(( | |
0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51a, | |
0x6666666666666666666666666666666666666666666666666666666666666658, 1, | |
0x67875f0fd78b766566ea4e8e64abe37d20f09f80775152f56dde8ab3a5b7dda3)) | |
for i in xrange(252): # _bpow[i] == scalarmult(B, 2**i). | |
x1, y1, z1, t1 = _bpow[-1] | |
a, b, c = x1 * x1 % q, y1 * y1 % q, ((z1 * z1) << 1) % q | |
hh, g = -a - b, b - a | |
ee, f = ((x1 + y1) * (x1 + y1) + hh) % q, g - c | |
_bpow.append((ee * f % q, g * hh % q, f * g % q, ee * hh % q)) | |
x, y, z, t = 0, 1, 1, 0 | |
m = 0xa406d9dc56dffce7198e80f2eef3d13000e0149a8283b156ebd69b9426b2f146 | |
for i in xrange(253): | |
if e & 1: | |
x2, y2, z2, t2 = _bpow[i] | |
a, b = (y - x) * (y2 - x2) % q, (y + x) * (y2 + x2) % q | |
c, dd = t * m % q * t2 % q, ((z * z2) << 1) % q | |
ee, f, g, hh = b - a, dd - c, dd + c, b + a | |
x, y, z, t = ee * f % q, g * hh % q, f * g % q, ee * hh % q | |
e >>= 1 | |
zi = pow(z, q - 2, q) | |
x, y = (x * zi) % q, (y * zi) % q | |
return ("%064x" % (y & ~(1 << 255) | ((x & 1) << 255))).decode("hex")[::-1] | |
def build_openssh_private_key_ed25519( | |
public_key, private_key, comment="SeSc", checkstr="SeSr"): | |
data = base64.b64encode("".join(( # No newlines. | |
"openssh-key-v1\0\0\0\0\4none\0\0\0\4none\0\0\0\0\0\0\0\1\0\0\0\x33" | |
"\0\0\0\x0bssh-ed25519\0\0\0 ", public_key, | |
struct.pack(">L", 131 + len(comment) + (-(len(comment) + 3) & 7)), | |
checkstr, checkstr, "\0\0\0\x0bssh-ed25519\0\0\0 ", public_key, | |
"\0\0\0@", private_key[:32], public_key, struct.pack(">L", len(comment)), | |
comment, "\1\2\3\4\5\6\7"[:-(len(comment) + 3) & 7]))) | |
output = ["-----BEGIN OPENSSH PRIVATE KEY-----\n"] | |
for i in xrange(0, len(data), 70): | |
output.append(data[i : i + 70]) | |
output.append("\n") | |
output.append("-----END OPENSSH PRIVATE KEY-----\n") | |
return "".join(output) | |
def build_openssh_public_key_ed25519(public_key, comment="SeSc"): | |
return "ssh-ed25519 %s %s\n" % ( | |
base64.b64encode("".join(("\0\0\0\x0bssh-ed25519\0\0\0 ", public_key))), | |
comment) | |
def main_pre(uk): | |
tdir = os.getenv("T", "") | |
assert tdir, "Empty tmp dirname." | |
hk = os.getenv("HK", "").strip() | |
if hk: | |
host_key = "" | |
if not host_key and hk.startswith("ssh-ed25519"): | |
hks = hk.split() | |
assert len(hks) == 3, "Bad host key format: %r" % hk | |
assert hks[0] == "h" and hks[1] == "ssh-ed25519", "Bad host key fields: %r" % hk | |
try: | |
host_key_data = base64.b64decode(hks[2]) | |
except (TypeError, ValueError): | |
assert 0, "Bad host key syntax: %r" % hk | |
assert len(host_key_data) == 51 and host_key_data.startswith("\0\0\0\x0bssh-ed25519\0\0\0 "), "Bad host key encoded format: %r" % hk | |
host_key = host_key_data[19:] | |
if not host_key and len(hk) == 64: | |
try: | |
host_key = hk.decode("hex") | |
if len(host_key) != 32 or host_key.encode("hex") != hk.lower(): | |
host_key = "" | |
except (TypeError, ValueError): | |
host_key = "" | |
if not host_key and hk.endswith("=") and len(hk) == 44: | |
try: | |
host_key = base64.b64decode(hk) | |
if len(host_key) != 32 or base64.b64encode(host_key) != hk: | |
host_key = "" | |
except (TypeError, ValueError): | |
host_key = "" | |
assert len(host_key) == 32, "Bad host key: %r" % hk | |
open(tdir + "/known_hosts", "ab").write("h %s\n" % build_openssh_public_key_ed25519(host_key, comment="").rstrip()) | |
assert uk, "Empty user key." | |
private_key = "" | |
if not private_key and len(uk) == 64: | |
try: | |
private_key = uk.decode("hex") | |
if len(private_key) != 32 or private_key.encode("hex") != uk.lower(): | |
private_key = "" | |
except (TypeError, ValueError): | |
private_key = "" | |
if not private_key and uk.endswith("=") and len(uk) == 44: | |
try: | |
private_key = base64.b64decode(uk) | |
if len(private_key) != 32 or base64.b64encode(private_key) != uk: | |
private_key = "" | |
except (TypeError, ValueError): | |
private_key = "" | |
if not private_key: | |
# Do many iterations to make dictionary attacks harder. | |
digest_cons, iterations = hashlib.sha512, 100000 | |
private_key = uk + "SeKd" | |
for i in xrange(iterations): | |
h = digest_cons(str(i)) | |
h.update("SeKd") | |
h.update(private_key) | |
h.update("SeKD") | |
h.update(str(i)) | |
private_key = h.digest() | |
private_key = private_key[:32] | |
assert len(private_key) == 32 | |
if len(private_key) != 32: | |
private_key = (private_key * (32 / len(private_key) + 1))[:32] | |
public_key = get_public_key_ed25519_unsafe(private_key) | |
open(tdir + "/id_sesamessh", "wb").write(build_openssh_private_key_ed25519(public_key, private_key)) | |
if os.getenv("IS_SETUP", ""): | |
open(tdir + "/id_sesamessh.setup", "wb").write("".join(( | |
"info: sesamessh user key: %s\n" % uk, | |
"info: sesamessh user key b64: %s\n" % base64.b64encode(private_key), | |
"info: sesamessh user key hex: %s\n" % private_key.encode("hex"), | |
"info: sesamessh user public key: %s" % build_openssh_public_key_ed25519(public_key), | |
))) | |
def main_post_setup(): | |
hk = os.getenv("HK", "").strip() | |
hks = hk.split() | |
assert len(hks) == 3, "Bad host key format: %r" % hk | |
assert hks[0] == "h" and hks[1] == "ssh-ed25519", "Bad host key fields: %r" % hk | |
try: | |
host_key_data = base64.b64decode(hks[2]) | |
except (TypeError, ValueError): | |
assert 0, "Bad host key syntax: %r" % hk | |
assert len(host_key_data) == 51 and host_key_data.startswith("\0\0\0\x0bssh-ed25519\0\0\0 "), "Bad host key encoded format: %r" % hk | |
host_key = host_key_data[19:] | |
tdir = os.getenv("T", "") | |
assert tdir, "Empty tmp dirname." | |
open(tdir + "/id_sesamessh.setup", "wb").write("".join(( | |
"info: sesamessh host key b64: %s\n" % base64.b64encode(host_key), | |
"info: sesamessh host key hex: %s\n" % host_key.encode("hex"), | |
"info: sesamessh host public key: %s\n" % build_openssh_public_key_ed25519(host_key, comment="").rstrip(), | |
)))' | |
export HK T IS_SETUP | |
# Creates final contents of id_sesamessh and id_sesamessh.setup. | |
# | |
# We can't use echo here, because the echo builtin in Dash interprets \\ | |
# etc., while in other shells it doesn't. | |
# | |
# We pass the secret key $UK on stdin (rather than argv or environ) to make | |
# it for the attacker harder to intercept by running ps(1) or inspecting | |
# /proc. | |
printf '%s' "$UK | |
$PYCODE" | "$PYTHON" -c "import sys; _uk = sys.stdin.readline().strip(); exec sys.stdin; main_pre(uk=_uk)" | |
test "$?" = 0 || die "Python failed: $PYTHON" | |
unset SSH_AUTH_SOCK SSH_AUTH_SOCK_FAST # Disable use of ssh-agent. | |
SHKC_ARGS='-o StrictHostKeyChecking=no' | |
Q_ARGS= | |
test "$HK" && SHKC_ARGS='-o StrictHostKeyChecking=yes' | |
# Unfortunately -q suppresses this ssh error as well: ``..: Permission | |
# denied (publickey).'', so we won't add -q automatically. | |
#if test -z "$HK"; then | |
# Q_ARGS='-q' # Suppress: Warning: Permanently added 'h' (ED25519) to the list of known hosts.' | |
# for ARG in $TARGET "$@"; do | |
# test "${ARG#-v}" = "$ARG" || Q_ARGS= # -v takes precedence. | |
# done | |
#fi | |
SLEEP_PID= | |
if test -z "$IS_SETUP"; then | |
# This will remove the private key files etc. early, just 10 seconds after | |
# ssh has started. This makes long-running SSH sessions more secure. | |
(sleep 10 2>/dev/null && rmtmp) & | |
SLEEP_PID="$!" | |
fi | |
# Not supporting dbclient(1) instead of OpenSSH's ssh(1), because v2017.75 | |
# has some problems: | |
# | |
# * Alsways opens ~/.ssh/id_dropbear, even if we specify another $HOME. | |
# * Doesn't append to "$HOME"/.ssh/known_hosts if the connection fails. | |
# * Password authentication can't be disabled. | |
# * Connection error: ed25519 message to sign too long | |
# * Uses a different private key format. (We could convert.) | |
# | |
# No space after -F to make ssh_connect_fast ignore ~/.ssh/config . | |
# | |
# This SSH message is normal if $HK is empty, can't be suppressed selectively: | |
# ``Warning: Permanently added 'h' (ED25519) to the list of known hosts.''. | |
ssh -F/dev/null \ | |
-o UserKnownHostsFile="$T/known_hosts" \ | |
-o GlobalKnownHostsFile=/dev/null \ | |
-o HostKeyAlias=h \ | |
-o HostKeyAlgorithms=ssh-ed25519 \ | |
-o [email protected] \ | |
-o IdentitiesOnly=yes \ | |
-o BatchMode=yes \ | |
-o HashKnownHosts=no \ | |
-o UpdateHostKeys=no \ | |
-o CheckHostIP=no \ | |
$Q_ARGS $SHKC_ARGS -i "$T/id_sesamessh" $TARGET "$@" | |
STATUS="$?" | |
if test "$SLEEP_PID"; then | |
kill "$SLEEP_PID" 2>/dev/null && | |
wait "$SLEEP_PID" 2>/dev/null # Suppress ``Terminated'' job control message from Bash. | |
fi | |
if test "$IS_SETUP"; then | |
TARGET_SPACE= | |
test "$TARGET" && TARGET_SPACE=' ' | |
echo "info: sesamessh target: $TARGET$TARGET_SPACE$*" >&2 | |
cat "$T/id_sesamessh.setup" >&2 | |
HK="$(cat "$T/known_hosts")" | |
test "$?" = 0 || die "cat failed: $T/known_hosts" | |
if test "$HK"; then | |
export HK | |
# Writes info about the host key to "$T/id_sesamessh.setup". | |
printf '%s' "$PYCODE | |
main_post_setup()" | "$PYTHON" - | |
test "$?" = 0 || die "Python for setup failed: $PYTHON" | |
cat "$T/id_sesamessh.setup" >&2 | |
else | |
echo "error: known_hosts not populated by ssh: $T/known_hosts" >&2 | |
fi | |
fi | |
rmtmp | |
exit "$STATUS" | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment