#!/usr/bin/env bash set -e # String formatting subsystem formatting_init() { endl=$'\n' } enquote() { while (( $# > 1 )); do printf "%q " "${1}"; shift done if (( $# == 1 )); then printf "%q" "${1}"; shift fi } multiline() { # shellcheck disable=SC1004 echo "${1} " | awk ' function pm(a, b, l) { return length(a) > l \ && length(b) > l \ && substr(a, 1, l+1) == substr(b, 1, l+1) \ ? pm(a, b, l+1) : l; } !started && $0 !~ /^[ \t]*$/ { started=1 match($0, /^[ \t]*/) prefix=substr($0, 1, RLENGTH) } started { print(substr($0, 1 + pm($0, prefix))); } ' } dbg() { echo >&2 "$@" } detect_git_dir() { # https://stackoverflow.com/questions/3618078/pipe-only-stderr-through-a-filter ( git -C "${scriptdir}" rev-parse --show-toplevel 3>&1 1>&2 2>&3 3>&- \ | sed ' /not a git repository/d; s/^/WARNING: /' ) 3>&1 1>&2 2>&3 3>&- } # Cleanup subsystem (sigterm) cleanup_init() { cleanup_actions=() trap cleanup_apply exit } cleanup_apply() { local f for f in "${cleanup_actions[@]}"; do eval "${f}" done } cleanup() { cleanup_actions+=("$(multiline "${1}")") } # Transactional execution subsystem frag_init() { explain=0 frag_transaction=() frag " #! /bin/bash set -e" } frag_apply() { local f for f in "${frag_transaction[@]}"; do if (( explain == 1 )); then dbg "${f}" fi eval "${f}" done } frag() { frag_transaction+=("$(multiline "${1}")") } frag_append() { local len; len="${#frag_transaction[@]}" frag_transaction=("${frag_transaction[@]:0:len-1}" "${frag_transaction[len-1]}${1}") } frag_append_esc() { frag_append " \\${endl}${1}" } # Usage documentation subsystem usage_init() { usagestack=("${script}") } usage_snap() { echo "${#usagestack}" } usage_restore() { local n; n="${1}" dbg REST "${1}" usagestack=("${usagestack[@]:0:n-2}") } usage() { dbg "Usage: ${usagestack[*]}" } fatal() { dbg "FATAL: $*" usage exit 1 } genkey() { usagestack+=("PRIVATE_KEYS_DIR") local skdir skdir="${1%/}"; shift || fatal "Required positional argument: PRIVATE_KEYS_DIR" while (( $# > 0 )); do local arg; arg="$1"; shift case "${arg}" in -h | -help | --help | help) usage; return 0 ;; *) fatal "Unknown option ${arg}";; esac done if test -e "${skdir}"; then fatal "PRIVATE_KEYS_DIR \"${skdir}\" already exists" fi frag " umask 077 mkdir -p $(enquote "${skdir}") wg genkey > $(enquote "${skdir}"/wgsk) $(enquote "${binary}") gen-keys \\ -s $(enquote "${skdir}"/pqsk) \\ -p $(enquote "${skdir}"/pqpk)" } pubkey() { usagestack+=("PRIVATE_KEYS_DIR" "PUBLIC_KEYS_DIR") local skdir pkdir skdir="${1%/}"; shift || fatal "Required positional argument: PRIVATE_KEYS_DIR" pkdir="${1%/}"; shift || fatal "Required positional argument: PUBLIC_KEYS_DIR" while (( $# > 0 )); do local arg; arg="$1"; shift case "${arg}" in -h | -help | --help | help) usage; exit 0;; *) fatal "Unknown option ${arg}";; esac done if test -e "${pkdir}"; then fatal "PUBLIC_KEYS_DIR \"${pkdir}\" already exists" fi frag " mkdir -p $(enquote "${pkdir}") wg pubkey < $(enquote "${skdir}"/wgsk) > $(enquote "${pkdir}/wgpk") cp $(enquote "${skdir}"/pqpk) $(enquote "${pkdir}/pqpk")" } exchange() { usagestack+=("PRIVATE_KEYS_DIR" "[dev ]" "[listen :]" "[peer PUBLIC_KEYS_DIR [endpoint :] [persistent-keepalive ] [allowed-ips /[,/]...]]...") local skdir dev lport dev="${project_name}0" skdir="${1%/}"; shift || fatal "Required positional argument: PRIVATE_KEYS_DIR" while (( $# > 0 )); do local arg; arg="$1"; shift case "${arg}" in dev) dev="${1}"; shift || fatal "dev option requires parameter";; peer) set -- "peer" "$@"; break;; # Parsed down below listen) local listen; listen="${1}"; lip="${listen%:*}"; lport="${listen/*:/}"; if [[ "$lip" = "$lport" ]]; then lip="[::]" fi shift;; -h | -help | --help | help) usage; return 0;; *) fatal "Unknown option ${arg}";; esac done if (( $# == 0 )); then fatal "Needs at least one peer specified" fi # os dependent setup case "$OSTYPE" in linux-*) # could be linux-gnu or linux-musl frag " # Create the WireGuard interface ip link add dev $(enquote "${dev}") type wireguard || true" cleanup " ip link del dev $(enquote "${dev}") || true" frag " ip link set dev $(enquote "${dev}") up" ;; freebsd*) frag " # load the WireGuard kernel module kldload -n if_wg || fatal 'Cannot load if_wg kernel module'" frag " # Create the WireGuard interface ifconfig wg create name $(enquote "${dev}") || true" cleanup " ifconfig $(enquote "${dev}") destroy || true" frag " ifconfig $(enquote "${dev}") up" ;; *) fatal "Your system $OSTYPE is not yet supported. We are happy to receive patches to address this :)" ;; esac frag " # Deploy the classic wireguard private key wg set $(enquote "${dev}") private-key $(enquote "${skdir}/wgsk")" if test -n "${lport}"; then frag_append "listen-port $(enquote "$(( lport + 1 ))")" fi frag " # Launch the post quantum wireguard exchange daemon $(enquote "${binary}") exchange" if (( verbose == 1 )); then frag_append "verbose" fi frag_append_esc " secret-key $(enquote "${skdir}/pqsk")" frag_append_esc " public-key $(enquote "${skdir}/pqpk")" if test -n "${lport}"; then frag_append_esc " listen $(enquote "${lip}:${lport}")" fi usagestack+=("peer" "PUBLIC_KEYS_DIR endpoint IP:PORT") while (( $# > 0 )); do shift; # Skip "peer" argument local peerdir ip port keepalive allowedips peerdir="${1%/}"; shift || fatal "Required peer argument: PUBLIC_KEYS_DIR" while (( $# > 0 )); do local arg; arg="$1"; shift case "${arg}" in peer) set -- "peer" "$@"; break;; # Next peer endpoint) ip="${1%:*}"; port="${1##*:}"; shift;; persistent-keepalive) keepalive="${1}"; shift;; allowed-ips) allowedips="${1}"; shift;; -h | -help | --help | help) usage; return 0;; *) fatal "Unknown option ${arg}";; esac done # Public key frag_append_esc " peer public-key $(enquote "${peerdir}/pqpk")" # PSK local pskfile; pskfile="${peerdir}/psk" if test -f "${pskfile}"; then frag_append_esc " preshared-key $(enquote "${pskfile}")" fi if test -n "${ip}"; then frag_append_esc " endpoint $(enquote "${ip}:${port}")" fi frag_append_esc " wireguard $(enquote "${dev}") $(enquote "$(cat "${peerdir}/wgpk")")" if test -n "${ip}"; then frag_append_esc " endpoint $(enquote "${ip}:$(( port + 1 ))")" fi if test -n "${keepalive}"; then frag_append_esc " persistent-keepalive $(enquote "${keepalive}")" fi if test -n "${allowedips}"; then frag_append_esc " allowed-ips $(enquote "${allowedips}")" fi done } find_rosenpass_binary() { local binary; binary="" if [[ -n "${gitdir}" ]]; then # If rp is run from the git repo, use the newest build artifact binary=$( find "${gitdir}/result/bin/${project_name}" \ "${gitdir}"/target/{release,debug}/"${project_name}" \ -printf "%T@ %p\n" 2>/dev/null \ | sort -nr \ | awk 'NR==1 { print($2) }' ) elif [[ -n "${nixdir}" ]]; then # If rp is run from nix, use the nix-installed rosenpass version binary="${nixdir}/bin/${project_name}" fi if [[ -z "${binary}" ]]; then binary="${project_name}" fi echo "${binary}" } main() { formatting_init cleanup_init usage_init frag_init project_name="rosenpass" verbose=0 scriptdir="$(dirname "${script}")" gitdir="$(detect_git_dir)" || true if [[ -d /nix ]]; then nixdir="$(readlink -f result/bin/rp | grep -Pio '^/nix/store/[^/]+(?=/bin/[^/]+)')" || true fi binary="$(find_rosenpass_binary)" # Parse command usagestack+=("[explain]" "[verbose]" "genkey|pubkey|exchange" "[ARGS]...") local cmd while (( $# > 0 )); do local arg; arg="$1"; shift case "${arg}" in genkey|pubkey|exchange) cmd="${arg}"; break;; explain) explain=1;; verbose) verbose=1;; -h | -help | --help | help) usage; return 0 ;; *) fatal "Unknown command ${arg}";; esac done test -n "${cmd}" || fatal "No command supplied" usagestack=("${script}") # Execute command usagestack+=("${cmd}") "${cmd}" "$@" usagestack=("${script}") # Apply transaction frag_apply } script="$0" main "$@"