From: Matthias Kruk Date: Sat, 16 Jul 2022 04:45:44 +0000 (+0900) Subject: include/ipsec: Add functions for certificate management in IPsec VPNs X-Git-Url: https://git.corax.cc/?a=commitdiff_plain;h=29aaf9526b6910716aa725bf6dec33197d1d9359;p=toolbox-goodies include/ipsec: Add functions for certificate management in IPsec VPNs This commit adds the ipsec group of modules, which implements functions for easy management of certificate authorities for IPsec VPNs. The following modules are included in the ipsec group: - ipsec/key: Generation of RSA/ECDSA/Ed25519 keys - ipsec/ca: Generation of CAs and server/client certificates --- diff --git a/debian/control b/debian/control index 959fd71..63100ef 100644 --- a/debian/control +++ b/debian/control @@ -9,7 +9,7 @@ Homepage: https://m10k.eu/toolbox Package: toolbox-goodies Section: shells Architecture: all -Depends: toolbox, openssh-client +Depends: toolbox, openssh-client, strongswan-pki Description: Nice-to-have toolbox modules Additional modules that help perform common tasks from the shell. This package includes the these modules: diff --git a/include/ipsec.sh b/include/ipsec.sh new file mode 100644 index 0000000..6dfd93b --- /dev/null +++ b/include/ipsec.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# ipsec.sh - Toolbox module for IPsec VPNs +# Copyright (C) 2022 Matthias Kruk +# +# 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 . + +__init() { + if ! include "ipsec/key" \ + "ipsec/ca"; then + return 1 + fi + + return 0 +} diff --git a/include/ipsec/ca.sh b/include/ipsec/ca.sh new file mode 100644 index 0000000..6f6a26f --- /dev/null +++ b/include/ipsec/ca.sh @@ -0,0 +1,232 @@ +#!/bin/bash + +# ipsec/ca.sh - Toolbox module for CAs for use with IPsec +# Copyright (C) 2022 Matthias Kruk +# +# 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 . + +__init() { + if ! include "log" "ipsec/key"; then + return 1 + fi + + declare -grxi __ipsec_ca_default_lifetime_days=3650 + declare -grxi __ipsec_ca_default_rsa_keylen=4096 + + return 0 +} + +_ipsec_ca_init_cleanup() { + local root="$1" + + local cakey + local cacert + local -i err + + cakey="$root/private/cakey.der" + cacert="$root/cacerts/cacert.der" + err=0 + + if [ -e "$cakey" ] && + ! rm -f "$cakey"; then + log_error "Could not remove $cakey" + err=1 + + elif [ -d "$root/private" ] && + ! rmdir "$root/private"; then + log_error "Could not remove $root/private" + err=1 + fi + + if [ -e "$cacert" ] && + ! rm -f "$cacert"; then + log_error "Could not remove $cacert" + err=1 + + elif [ -d "$root/cacerts" ] && + ! rmdir "$root/cacerts"; then + log_error "Could not remove $root/cacerts" + err=1 + fi + + if [ -d "$root/certs" ] && + ! rmdir "$root/certs"; then + log_error "Could not remove $root/certs" + err=1 + fi + + return "$err" +} + +ipsec_ca_init() { + local root="$1" + local country="$2" + local organization="$3" + local common_name="$4" + local -i lifetime_days="$5" + local -i rsa_keylen="$6" + + local distinguished_name + local cakey + local cacert + local -i err + + distinguished_name="C=$country,O=$organization,CN=$common_name" + cakey="$root/private/cakey.pem" + cacert="$root/cacerts/cacert.pem" + err=0 + + if (( lifetime_days == 0 )); then + lifetime_days="$__ipsec_ca_default_lifetime_days" + fi + + if (( rsa_keylen == 0 )); then + rsa_keylen="$__ipsec_ca_default_rsa_keylen" + fi + + if ! mkdir -p "$root/private" \ + "$root/cacerts" \ + "$root/certs"; then + log_error "Could not create CA directory structure in $root" + err=1 + + elif [ -e "$cakey" ]; then + log_error "CA private key $cakey already exists" + err=2 + + elif ! ipsec_key_new "rsa" "$rsa_keylen" "pem" > "$cakey"; then + log_error "Could not generate CA private key $cakey (len: $rsa_keylen)" + err=3 + + elif ! pki --self --ca --type rsa --lifetime "$lifetime_days" \ + --dn "$distinguished_name" --in "$cakey" \ + --outform "pem" > "$cacert"; then + log_error "Coiuld not generate CA certificate $cacert" + err=4 + fi + + if (( err != 0 )); then + _ipsec_ca_init_cleanup "$root" + fi + + return "$err" +} + +ipsec_ca_generate_server_cert() { + local root="$1" + local country="$2" + local organization="$3" + local common_name="$4" + local -i lifetime_days="$5" + local -i rsa_keylen="$6" + + if ! _ipsec_ca_generate_cert "$root" "$country" "$organization" \ + "$common_name" "$lifetime_days" \ + "$rsa_keylen" \ + "serverAuth" "ikeIntermediate"; then + return 1 + fi + + return 0 +} + +ipsec_ca_generate_client_cert() { + local root="$1" + local country="$2" + local organization="$3" + local common_name="$4" + local -i lifetime_days="$5" + local -i rsa_keylen="$6" + + if ! _ipsec_ca_generate_cert "$root" "$country" "$organization" \ + "$common_name" "$lifetime_days" \ + "$rsa_keylen"; then + return 1 + fi + + return 0 +} + +_ipsec_ca_generate_cert() { + local root="$1" + local country="$2" + local organization="$3" + local common_name="$4" + local -i lifetime_days="$5" + local -i rsa_keylen="$6" + local flags=("${@:7}") + + local distinguished_name + local subject_key + local subject_cert + local cacert + local cakey + local pubkey_data + local pki_args + local flag + + if (( lifetime_days == 0 )); then + lifetime_days="$__ipsec_ca_default_lifetime_days" + fi + + if (( rsa_keylen == 0 )); then + rsa_keylen="$__ipsec_ca_default_rsa_keylen" + fi + + distinguished_name="C=$country,O=$organization,CN=$common_name" + subject_key="$root/private/$common_name.pem" + subject_cert="$root/certs/$common_name.pem" + cacert="$root/cacerts/cacert.pem" + cakey="$root/private/cakey.pem" + + if ! ipsec_key_new "rsa" "$rsa_keylen" "pem" > "$subject_key"; then + log_error "Could not generate key for $common_name" + return 1 + fi + + if ! pubkey_data=$(ipsec_key_get_pubkey "pem" < "$subject_key"); then + log_error "Could not get public key from $subject_key" + + if ! rm -f "$subject_key"; then + log_warn "Could not clean up $subject_key" + fi + + return 2 + fi + + pki_args=( + --issue + --lifetime "$lifetime_days" + --cacert "$cacert" + --cakey "$cakey" + --dn "$distinguished_name" + --san "$common_name" + --outform "pem" + ) + for flag in "${flags[@]}"; do + pki_args+=(--flag "$flag") + done + + if ! pki "${pki_args[@]}" <<< "$pubkey_data" > "$subject_cert"; then + log_error "Could not issue certificate for $common_name" + + if ! rm -f "$subject_key"; then + log_warn "Could not clean up $subject_key" + fi + + return 3 + fi + + return 0 +} diff --git a/include/ipsec/key.sh b/include/ipsec/key.sh new file mode 100644 index 0000000..be5460a --- /dev/null +++ b/include/ipsec/key.sh @@ -0,0 +1,86 @@ +#!/bin/bash + +# ipsec/key.sh - Toolbox module for key generation for use with IPsec +# Copyright (C) 2022 Matthias Kruk +# +# 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 . + +__init() { + if ! include "log" "array"; then + return 1 + fi + + declare -gxr __ipsec_key_default_type="rsa" + declare -gxra __ipsec_key_supported_types=( + "rsa" + "ecdsa" + "ed25519" + ) + declare -gxrA __ipsec_key_default_size=( + ["rsa"]=4096 + ["ecdsa"]=384 + ["ed25519"]=1 + ) + declare -gxr __ipsec_key_default_format="pem" + + return 0 +} + +ipsec_key_new() { + local type="$1" + local -i size="$2" + local format="$3" + + if [[ -z "$1" ]]; then + type="$__ipsec_key_default_type" + fi + + if ! array_contains "$type" "${__ipsec_key_supported_types[@]}"; then + log_error "Key type \"$type\" not supported" + return 1 + fi + + if (( size == 0 )); then + size="${__ipsec_key_default_size[$type]}" + fi + + if [[ -z "$format" ]]; then + format="$__ipsec_key_default_format" + fi + + if ! pki --gen --type "$type" --size "$size" --outform "$format"; then + return 1 + fi + + return 0 +} + +ipsec_key_get_pubkey() { + local format="$1" + local key="$2" + + if [[ -z "$format" ]]; then + format="$__ipsec_key_default_format" + fi + + if [[ -z "$key" ]]; then + key="/dev/stdin" + fi + + if ! pki --pub --outform "$format" < "$key"; then + return 1 + fi + + return 0 +}