#!/bin/sh

# Turris Sentinel Certgen-sh
# Copyright (C) 2023 CZ.NIC z.s.p.o
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

[ -n "$SENTINEL_API_URL" ] || SENTINEL_API_URL="https://sentinel.turris.cz/v1/certs"
API_URL="$SENTINEL_API_URL"
TYPE="b2b"
[ -n "$SENTINEL_CERT_DIR" ] || SENTINEL_CERT_DIR="/etc/sentinel"
WD="$SENTINEL_CERT_DIR"
MASTER_KEY="master_key.pem"
MASTER_PUB="master_key.pub"
sid=""
TARGET_USER="sentinel"
TARGET_GROUP="sentinel"
OWNER="$TARGET_USER:$TARGET_GROUP"

die() {
    echo "$@" >&2
    exit 1
}

[ -d "$WD" ] || mkdir -p "$WD" || die "Directory $WD doesn't exist and couldn't be created, please check your rights."
cd "$WD" || die "Can't enter $WD, please check your rights."
SN="$(cat sn.txt 2>/dev/null)"

# Check whether we have keys to use and provide user with instructions
register() {
    [ -f "$MASTER_KEY" ] || openssl ecparam -genkey -name secp521r1 -out "$MASTER_KEY" > /dev/null || {
        rm -f "$MASTER_KEY"
        die "Can't create a master key"
    }
    chmod 0600 "$MASTER_KEY"
    [ -f "$MASTER_PUB" ] || openssl ec -in "$MASTER_KEY" -pubout -out "$MASTER_PUB" > /dev/null || {
        rm -f "$MASTER_PUB"
        die "Can't create a public key"
    }
    chmod 0640 "$MASTER_PUB"
    try_chown "$MASTER_PUB"
    pubkey="$(openssl ec -in "$MASTER_PUB" -pubin -text -noout -conv_form compressed 2> /dev/null | sed -n 's|^[[:blank:]]\+||p' | tr '\n' ':' | sed 's|:||g')"
    cat << EOF
Please mail the following to info@turris.com and wait for the reply to continue.

Subject: Sentinel registration $SN

Hi,

our public key is the following:

$pubkey

$(echo "mailto:info@turris.com?subject=Sentinel registration $SN&body=Hi, our public key is $pubkey" | \
qrencode -o - -8 -t UTF8 2>/dev/null # if it fails the above text should be ok
)

EOF
    exit 0
}

# Ask user to provide his ID for the system and proceed with registration
b2b_register() {
    if [ -z "$SN" ]; then
        echo -en "Please enter your company ID.\nFor companies in Czech Republic it is the ICO, otherwise it is ID assigned by our sales representative.\nCompany ID: "
        if echo 1 | read -n 1 TEST 2> /dev/null; then
            read -n 13 ID
        else
            read ID
        fi
        expr "$ID" : '[0-9a-fA-F]*$' > /dev/null || die 'Not a number!'
        SN_LEN="$(echo -n "$ID" | wc -c)"
        # Make sure the SN is long enough
        while [ "$SN_LEN" -lt 13 ]; do
            ID="0$ID"
            SN_LEN="$(echo -n "$ID" | wc -c)"
        done
        SN="B2B$ID"
        echo "$SN" > sn.txt
    fi
    register
}

# Check whether certificate is still valid and if so, exit
check_validity() {
    [ -f mqtt_key.pem ] || return
    openssl ec -in mqtt_key.pem -check > /dev/null || return
    [ -f mqtt_cert.pem ] || return
    VALIDITY="$(openssl x509 -in ./mqtt_cert.pem -text -noout | sed -n 's|.*Not After[[:blank:]]*:[[:blank:]]*||p')"
    VALIDITY="$(date -d "$VALIDITY" +%s)"
    [ 0"$VALIDITY" -gt 0 ] || die "Can't get validity information from certificate"
    if [ $(($VALIDITY - 24 * 3600)) -gt $(date +%s) ]; then
        echo "We are safe for at least a day"
        exit 0
    fi
}

# Create a new mqtt key (if needed) and new certification request
gen_new_cert() {
    if [ \! -f mqtt_key.pem ] || ! openssl ec -in mqtt_key.pem -check > /dev/null; then
        openssl ecparam -genkey -name prime256v1 -out mqtt_key.pem || die "Can't create a new key"
        # MQTT key needs to be readable by group - by proxy
        chmod 0600 mqtt_key.pem
        try_chown mqtt_key.pem
    fi
    openssl req -new -key mqtt_key.pem -subj "/CN=$SN" -out mqtt_csr.pem || die "Can't create CSR"
    chmod 0640 mqtt_csr.pem
    try_chown mqtt_csr.pem
}

# Pack certificate into one-liner string that can be used inside json
cert_str() {
    cat "$1" | \
        sed '$ ! s|$|\\|g' | tr '\n' 'n' |\
        sed 's|n$||'
}

# Get specific element from the json
get_el() {
    echo "$MSG" | sed -n 's|.*"'"$1"'":"\([^"]*\)".*|\1|p'
}

# Die and display message
die_msg() {
    die "$(get_el message)"
}

# If we are root, call chown, if only one argument is provided, use default owner
try_chown() {
    [ "$(id -u)" -eq 0 ] || return
    getent passwd "$TARGET_USER" > /dev/null || return
    getent passwd "$TARGET_GROUP" > /dev/null || return
    if [ $# -gt 1 ]; then
        chown "$@"
    else
        chown "$OWNER" "$@"
    fi
}

get_cert() {
    status=""
    delay=0
    # We might be told to wait till our request is processed
    while [ -z "$status" ] || [ "$status" = wait ]; do
        # Inform user about waiting and actually wait
        if [ 0"$delay" -gt 0 ]; then
            echo "Certification in process, waiting for the result another $delay seconds."
            sleep "$delay"
        fi
        # Send request for certificate
        MSG="$(curl -X POST -H "Content-Type: application/json" \
                     -H "Accept: application/json" \
                     -d '{
                         "type": "get",
                         "auth_type": "'"$TYPE"'",
                         "sn": "'"$SN"'",
                         "csr_str": "'"$(cert_str "mqtt_csr.pem")"'",
                         "sid": "'"$sid"'",
                         "flags": "" }' "$API_URL")"
        # Get the final status
        status="$(get_el status)"
        # Handle 500 server error
        if echo "$MSG" | grep -q '500 Internal Server Error'; then
            die "Server is unavailable, please try again later"
        fi
        # fail or error means that something went wrong, details are in messsage field
        [ "$status" \!= fail ] || die_msg
        [ "$status" \!= error ] || die_msg
        # If we are told to waint, we need to know for how long
        delay="$(echo "$MSG" | sed -n 's|.*"delay":\([0-9]\+\),.*|\1|p')"
        # We might also succeed and get the certificate
        if [ "$status" = ok ] && [ -n "$(get_el cert)" ]; then
            # We need to convert it to proper format and store it
            get_el cert | sed 's|\\n|\n|g' > mqtt_cert.pem || die "Can't store the final certificate"
            chmod 0600 mqtt_cert.pem
            try_chown mqtt_cert.pem
            # On systemnd systems, we want to enable and start proxy and minipots
            if [ -x /usr/bin/systemctl ] && [ "$(id -u)" -eq 0 ]; then
                systemctl enable sentinel-proxy
                systemctl restart sentinel-proxy
                systemctl restart sentinel-minipots.target
                echo "Everything is set and your minipots should be up and running."
            else
                echo "Everything is set, you can restart your proxy now."
            fi
            # If we have certificate, we are done
            exit 0
        fi
    done
    # If we don't have a certificate, we need nonce and sid to authenticate ourselves
    nonce="$(get_el nonce)"
    sid="$(get_el sid)"
}

# Sign data with our key
sign() {
    local signature=""
    # We need 264 bytes long signature
    # openssl dgst omits padding therefore sometimes produces too short signatures
    # As it uses randomness while signing, we can iterate few times till we get a correct signature
    while [ "$(echo -n "$signature" | wc -c)" -ne 264 ]; do
        signature="$(echo -n "$1" | \
            openssl dgst -sha512 -sign "$MASTER_KEY" -keyform PEM | \
            openssl asn1parse -in - -inform DER | \
            sed -n 's|.*66 prim:.*INTEGER[[:blank:]]*:||p' | \
            tr '\n[A-Z]' 'x[a-z]' | \
            sed 's|x||g')"
    done
    echo -n "$signature"
}

# Authenticate ourselves by sending signed nonce
authenticate() {
    signature="$(sign "$nonce")"
    MSG="$(curl -X POST -H "Content-Type: application/json" \
                 -H "Accept: application/json" \
                 -d '{
                     "type": "auth",
                     "auth_type": "'"$TYPE"'",
                     "sn": "'"$SN"'",
                     "sid": "'"$sid"'",
                     "signature": "'"$signature"'" }' "$API_URL")"
    delay="$(echo "$MSG" | sed -n 's|.*"delay":\([0-9]*\),.*|\1|p')"
    [ 0"$delay" -gt 0 ] || delay=2
    sleep "$delay"
    status="$(get_el status)"
    [ "$status" \!= fail ] || die_msg
    [ "$status" \!= error ] || die_msg
}

# During first run, ask for a serial and create a master key
[ -n "$SN" ] || b2b_register

# Show fingerprint of our public key and instruction
if [ "$1" = register ] || [ "$1" = fingerprint ]; then
    register
fi

# Do we need a new MQTT certificate?
check_validity
# Generate it
gen_new_cert
# Request a signed certificate
get_cert
# Sanity checks
[ -n "$nonce" ] || die "No nonce"
[ -n "$sid" ] || die "No session id"
# Send authentication token
authenticate
# Request a signed certificate
get_cert
