From 0b36f71394956f2df6bc98b85c709ae3d3e43683 Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Wed, 27 May 2026 00:36:09 +0200 Subject: [PATCH] Remove Plan.md and simple-ca.sh files from the repository --- Plan.md | 369 ------------------------------------------ simple-ca.sh | 447 --------------------------------------------------- 2 files changed, 816 deletions(-) delete mode 100644 Plan.md delete mode 100755 simple-ca.sh diff --git a/Plan.md b/Plan.md deleted file mode 100644 index bc79893..0000000 --- a/Plan.md +++ /dev/null @@ -1,369 +0,0 @@ -# Plan: Debian Package Source for linux-cloud-router - -## Context - -The `azure-router` Terraform module provisions a Linux router VM using Cloud Init. Cloud Init -installs packages, writes config files (strongSwan swanctl, WireGuard, netplan, sysctl, UFW -rules), and runs a one-shot setup service. All site-specific values (IPs, FQDNs, CIDRs, PSK) -are injected at provision time via Terraform's `templatefile()`. - -The Debian package replicates the same outcome on Ubuntu 22.04, 24.04, and 26.04 hosts -(exact supported version list TBD — compatibility and feature availability must be verified -across all three; see Further Considerations). Site-specific values -are collected via **debconf** interactive prompts at `dpkg install` time. A `postinst` script -reads the debconf answers and generates all derived config files. - ---- - -## Decisions - -- Site config: **debconf** interactive prompts at install time -- P2S certificates: managed with `simple-ca.sh` directly -- Networking: Ubuntu only — netplan (not `/etc/network/interfaces.d/`) -- Build approach: full Debian source package (`dpkg-buildpackage` + debhelper ≥13) -- PKI library: `simple-ca.sh` (OpenSSL-based); currently RSA-4096, ECDSA/Ed25519 support - is a planned future enhancement to the script itself — not a concern for this package -- `strongswan-pki` is **not** a dependency; `openssl` covers all PKI needs (including PKCS#12 - which `pki` tool cannot produce) - ---- - -## Source Tree Layout - -``` -linux-cloud-router/ -├── debian/ -│ ├── control -│ ├── changelog -│ ├── rules -│ ├── copyright -│ ├── install -│ ├── dirs -│ ├── templates ← debconf question definitions -│ ├── config ← pre-install debconf prompts -│ ├── postinst ← generates all config files, enables services -│ └── prerm ← stops/disables services -└── src/ - ├── etc/ - │ ├── sysctl.d/ - │ │ └── 99-cloud-router.conf - │ ├── xdg/nvim/ - │ │ └── init.vim - │ └── systemd/system/ - │ └── cloud-router-setup.service - └── usr/local/ - ├── lib/ - │ └── simple-ca.sh - └── sbin/ - └── cloud-router-setup adapted from azure-router-setup -``` - ---- - -## Phase 0 — Package Availability Research - -Use Apple's `container` CLI to launch one-shot Ubuntu containers for each target release: - -```sh -PKGS="strongswan-swanctl charon-systemd libstrongswan-extra-plugins \ - libcharon-extra-plugins wireguard-tools ufw openssl debconf \ - debhelper netplan.io" - -for RELEASE in 22.04 24.04 26.04; do - echo "=== ubuntu:$RELEASE ===" - container run --rm ubuntu:$RELEASE \ - sh -c "apt-get update -qq && apt-cache policy $PKGS" -done -``` - -`container` is also the primary build environment: mount the source tree and run -`dpkg-buildpackage` inside the target Ubuntu container, keeping the macOS host clean. - -Record the candidate version for each package per release. Use the results to: -- Confirm all packages exist in the default repos (no PPAs needed) -- Verify `debhelper` version supports compat 14 (requires ≥13.10); fall back to compat 13 - if 22.04 support is required and its debhelper is too old -- Confirm strongSwan package names are consistent across releases -- Finalise the supported Ubuntu version list - ---- - -## Phase 1 — Package Scaffold - -1. `debian/control` — package name `cloud-router`, short description, `Build-Depends` and `Depends` (see below) -2. `debian/changelog` — initial `1.0.0-1` entry -3. `debian/rules` — minimal `dh $@` (debhelper) -4. `debian/copyright` — MIT (matches simple-ca.sh) - -**`debian/control` Build-Depends:** -``` -debhelper-compat (= 14) -``` - -**`debian/control` Depends:** -``` -strongswan-swanctl, charon-systemd, libstrongswan-extra-plugins, libcharon-extra-plugins, -wireguard-tools, ufw, debconf, openssl -``` - ---- - -## Phase 2 — Static Source Files - -6. `src/etc/sysctl.d/99-cloud-router.conf` - ``` - net.ipv4.ip_forward = 1 - net.ipv4.conf.all.rp_filter = 0 - net.ipv4.conf.default.rp_filter = 0 - ``` - (identical content to cloud-init) - -7. `src/etc/xdg/nvim/init.vim` — exact neovim settings from `router-cloud-init.tpl` - -8. `src/etc/systemd/system/cloud-router-setup.service` - Renamed from `azure-router-setup.service`. Same content: - ```ini - [Unit] - Description=Linux Cloud Router Setup - Wants=network-online.target - After=network-online.target - - [Service] - Type=oneshot - ExecStart=/usr/local/sbin/cloud-router-setup - RemainAfterExit=yes - - [Install] - WantedBy=multi-user.target - ``` - ---- - -## Phase 3 — `cloud-router-setup` Script - -9. Adapt `azure-router-setup` bash script from `router-cloud-init.tpl`: - - Add `source /etc/default/cloud-router` at the top - - Replace all Terraform-interpolated values (`${local_subnet}`, `${remote_cidrs}`, - `${wg_listen_port}`, `${p2s_address_pool}`, `${local_addrs}`, `${fqdn}`) with the - corresponding variable names sourced from `/etc/default/cloud-router` - - Rename script to `cloud-router-setup`; install to `src/usr/local/sbin/cloud-router-setup` - - The script remains idempotent (checks for existing UFW rule markers before inserting) - - WireGuard key generation and WireGuard UFW rules are guarded by `if [ "$WG_ENABLED" = "true" ]` - ---- - -## Phase 4 — PKI Library - -10. `src/usr/local/lib/simple-ca.sh` — Bash function module providing - `make_ca`, `make_cert`, `make_pfx`. Source it in any shell session with - `source /usr/local/lib/simple-ca.sh`; set `SIMPLE_CA_DIR=/etc/cloud-router/pki` - before use. - -11. `cloud-router-sync-pki` — standalone script that copies PKI output to swanctl's - expected locations: - - `/etc/cloud-router/pki/ca_bundle.pem` → `/etc/swanctl/x509ca/ca.pem` - - `/etc/cloud-router/pki/{server}_cert.pem` → `/etc/swanctl/x509/server.pem` - - `/etc/cloud-router/pki/{server}_key.pem` → `/etc/swanctl/private/server.key` - - The server cert name is read from `P2S_SERVER_NAME` in `/etc/default/cloud-router` - (defaults to the hostname component of `LOCAL_FQDN`). - ---- - -## Phase 5 — Debconf Templates and Config Script - -13. `debian/templates` — one `Template:` block per configurable value: - - | Question | Type | Default | - |---|---|---| - | `cloud-router/local_addrs` | string | | - | `cloud-router/local_fqdn` | string | | - | `cloud-router/local_id_mode` | select | `fqdn` (choices: fqdn, public_ip, internal_ip) | - | `cloud-router/local_cidrs` | string | | - | `cloud-router/remote_addrs` | string | | - | `cloud-router/remote_id` | string | | - | `cloud-router/psk` | password | | - | `cloud-router/remote_cidrs` | string | | - | `cloud-router/router_int_gateway_ip` | string | | - | `cloud-router/p2s_address_pool` | string | | - | `cloud-router/wg_enabled` | boolean | `false` | - | `cloud-router/wg_address` | string | `10.0.1.1/24` *(shown only when wg_enabled=true)* | - | `cloud-router/wg_listen_port` | string | `51820` *(shown only when wg_enabled=true)* | - -14. `debian/config` — pre-install shell script: - - Sources `/usr/share/debconf/confmodule` - - Calls `db_input` for each question in priority order - - Shows `wg_address` and `wg_listen_port` conditionally: `db_get cloud-router/wg_enabled; if [ "$RET" = "true" ]` - ---- - -## Phase 6 — Maintainer Scripts - -15. `debian/postinst` (triggered on `configure`): - 1. `db_get` all debconf answers - 2. Derive `LOCAL_SUBNET` = first element of `LOCAL_CIDRS` - 3. Derive `P2S_SERVER_NAME` = hostname part of `LOCAL_FQDN` - 4. Write `/etc/default/cloud-router` (mode 0644, root:root) — non-secret config: - ```sh - LOCAL_ADDRS="..." - LOCAL_FQDN="..." - LOCAL_ID_MODE="..." - LOCAL_CIDRS="..." - LOCAL_SUBNET="..." - REMOTE_ADDRS="..." - REMOTE_ID="..." - REMOTE_CIDRS="..." - ROUTER_INT_GATEWAY_IP="..." - P2S_ADDRESS_POOL="..." - P2S_SERVER_NAME="..." - WG_ENABLED="..." - WG_ADDRESS="..." # only relevant when WG_ENABLED=true - WG_LISTEN_PORT="..." # only relevant when WG_ENABLED=true - ``` - 5. Generate `/etc/swanctl/conf.d/remote-site.conf` (mode 0600, root:root) via bash heredoc — - includes both the `connections {}` block and a `secrets { ike-remote-site { secret = "$PSK" } }` - block; the file must be 0600 because it contains the PSK - 6. Generate `/etc/swanctl/conf.d/road-warrior.conf` via heredoc - 7. Generate `/etc/systemd/resolved.conf.d/p2s-forwarder.conf` via heredoc - 8. If `WG_ENABLED=true`: generate `/etc/wireguard/wg0.conf` via heredoc - 9. Generate `/etc/netplan/90-cloud-router.yaml` via heredoc - 10. `sysctl --system` - 11. `netplan apply` - 12. `systemctl daemon-reload` - 13. `systemctl enable --now cloud-router-setup.service` - 14. `ufw allow 22/tcp && ufw --force enable && ufw reload` - 15. `systemctl enable --now strongswan` - 16. `systemctl restart systemd-resolved` - -16. `debian/prerm` (on `remove`): - - `systemctl disable --now cloud-router-setup.service` - - `systemctl disable --now strongswan` - ---- - -## Phase 7 — Install Manifest - -17. `debian/install` — maps `src/` tree entries to absolute filesystem paths, e.g.: - ``` - src/etc/sysctl.d/99-cloud-router.conf etc/sysctl.d/ - src/etc/xdg/nvim/init.vim etc/xdg/nvim/ - src/etc/systemd/system/cloud-router-setup.service etc/systemd/system/ - src/usr/local/sbin/cloud-router-setup usr/local/sbin/ - src/usr/local/lib/simple-ca.sh usr/local/lib/ - ``` - -18. `debian/dirs` — directories that must pre-exist: - ``` - etc/cloud-router - etc/cloud-router/pki - etc/wireguard - etc/swanctl/conf.d - etc/swanctl/x509ca - etc/swanctl/x509 - etc/swanctl/private - etc/systemd/resolved.conf.d - ``` - ---- - -## Phase 8 — Cloud-Init YAML - -The `.deb` is delivered via a private apt repository (URL/GPG key TBD). The cloud-init -user-data pre-seeds all debconf answers before the package is installed, so `postinst` runs -fully unattended on first boot. - -Template file `cloud-router-cloud-init.yaml.tpl` (rendered by `templatefile()` at provision -time, Terraform variable names shown as `${…}`): - -```yaml -#cloud-config - -apt: - sources: - cloud-router: - source: "deb [signed-by=/etc/apt/keyrings/cloud-router.gpg] ${repo_url} ${ubuntu_codename} main" - key: | - ${repo_gpg_key} - -debconf_selections: | - cloud-router cloud-router/local_addrs string ${local_addrs} - cloud-router cloud-router/local_fqdn string ${fqdn} - cloud-router cloud-router/local_id_mode select ${local_id_mode} - cloud-router cloud-router/local_cidrs string ${local_cidrs} - cloud-router cloud-router/remote_addrs string ${remote_addrs} - cloud-router cloud-router/remote_id string ${remote_id} - cloud-router cloud-router/psk password ${psk} - cloud-router cloud-router/remote_cidrs string ${remote_cidrs} - cloud-router cloud-router/router_int_gateway_ip string ${router_int_gateway_ip} - cloud-router cloud-router/p2s_address_pool string ${p2s_address_pool} - cloud-router cloud-router/wg_enabled boolean ${wg_enabled} - cloud-router cloud-router/wg_address string ${wg_address} - cloud-router cloud-router/wg_listen_port string ${wg_listen_port} - -package_update: true - -packages: - - cloud-router -``` - -Notes: -- `debconf_selections` runs before `packages` in cloud-init's module order, so answers are - in place before `postinst` executes -- `DEBIAN_FRONTEND` does not need to be set explicitly; debconf detects the pre-seeded - answers and skips interactive prompts automatically -- `${ubuntu_codename}` (e.g. `noble`) must be passed as a Terraform variable or derived - from the VM image; `${repo_gpg_key}` is the ASCII-armoured public key of the signing key -- The template replaces the existing `router-cloud-init.tpl` in the `azure-router` module - ---- - -## Verification - -1. `dpkg-buildpackage -us -uc -b` from `linux-cloud-router/` completes without error; `.deb` produced -2. `dpkg -i cloud-router_1.0.0-1_all.deb` on a fresh VM for each target Ubuntu release - (22.04, 24.04, 26.04): debconf prompts appear; all deps satisfied -3. `cat /etc/default/cloud-router` shows non-secret config; `/etc/swanctl/conf.d/remote-site.conf` - and `road-warrior.conf` are present; `remote-site.conf` is mode 0600 and contains the - `secrets {}` block with the PSK; `/etc/wireguard/wg0.conf` exists only when `WG_ENABLED=true` -4. Source `simple-ca.sh`, then `make_ca "My Router CA"` → `/etc/cloud-router/pki/ca_cert.pem` - created; `make_cert --type server router.example.com` → cert/key produced; - `cloud-router-sync-pki` → files appear in `/etc/swanctl/` -5. `make_pfx ` → `.pfx` produced for a road-warrior client -6. `systemctl status cloud-router-setup.service` is `active (exited)`; UFW `before.rules` - contains the IPsec/NAT/forwarding sections; when `WG_ENABLED=true`, WireGuard keys are - present at `/etc/wireguard/wg0.key` and `wg0.pub` - ---- - -## Further Considerations - -- **Ubuntu version compatibility** (must verify before finalising supported version list): - items to check on each of 22.04, 24.04, 26.04: - - `debhelper-compat (= 14)` — Ubuntu 22.04 ships debhelper 13.6 (max compat 13); - 24.04 ships 13.11 (compat 14 supported); may need to drop to compat 13 for 22.04 support - - strongSwan package names (`strongswan-swanctl`, `charon-systemd`, plugin packages) — - verify availability and correct names on each release - - `wireguard-tools` availability and kernel module inclusion - - netplan API and YAML schema changes between releases - - `systemd-resolved` DNS stub behaviour differences - -- **`dpkg-reconfigure` behaviour**: re-running `dpkg-reconfigure cloud-router` re-prompts - all debconf questions and regenerates config files, overwriting any manual edits. Files - "owned" by debconf (remote-site.conf, wg0.conf, netplan yaml, env) should be documented - as managed; swanctl cert files (written via `simple-ca.sh`) are not touched by postinst. - -- **`simple-ca.sh` key algorithm support**: currently RSA-4096 only. ECDSA/Ed25519 support - is a planned future enhancement to `simple-ca.sh` itself and is independent of this package. - -- **CRL / revocation**: `simple-ca.sh` has no revocation support. If road-warrior client - revocation is needed, `strongswan-pki` can be added as an optional dependency for - `pki --signcrl`. Treat as a future enhancement. - -- **Build tooling**: `container` (Apple container CLI) is used to run `dpkg-buildpackage` - inside Ubuntu containers. The source tree is bind-mounted into the container; the built - `.deb` appears in the parent directory as usual. Inside the container: `apt-get install -y - build-essential debhelper`. No Ubuntu host or VM needed for builds. - -- **Development environment**: source files are authored on macOS. `container` handles all - build and install testing — run the same loop against 22.04, 24.04, and 26.04 containers - once the compatibility matrix is resolved. diff --git a/simple-ca.sh b/simple-ca.sh deleted file mode 100755 index 99abe3a..0000000 --- a/simple-ca.sh +++ /dev/null @@ -1,447 +0,0 @@ -# MIT License -# -# Copyright (c) 2026 Sławomir Koszewski -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -# These functions require OpenSSL to be installed on the system. -# -# SIMPLE_CA_DIR points to the root CA directory. It can be set in the environment -# before sourcing this file, or overridden per-call with --ca-dir. Once set by any -# call, subsequent calls in the same session inherit it. -# -# Directory layout: -# $SIMPLE_CA_DIR/ca_cert.pem — root CA certificate -# $SIMPLE_CA_DIR/ca_key.pem — root CA private key -# $SIMPLE_CA_DIR/{name}_cert.pem — certificates issued by the root CA -# $SIMPLE_CA_DIR/{issuing_ca}/ca_cert.pem — issuing CA certificate -# $SIMPLE_CA_DIR/{issuing_ca}/ca_key.pem — issuing CA private key -# $SIMPLE_CA_DIR/{issuing_ca}/{name}_cert.pem — certificates issued by that issuing CA -# -# Certificates are always written to the directory of the CA that signs them. -# Any subdirectory containing ca_cert.pem is treated as an issuing CA. - -SIMPLE_CA_DIR="${SIMPLE_CA_DIR:-}" - -_rebuild_ca_bundle() { - local BUNDLE="$SIMPLE_CA_DIR/ca_bundle.pem" - cat "$SIMPLE_CA_DIR/ca_cert.pem" > "$BUNDLE" - for issuing_ca_dir in $SIMPLE_CA_DIR/*; do - if [[ -d "$issuing_ca_dir" && -f "$issuing_ca_dir/ca_cert.pem" ]]; then - cat "$issuing_ca_dir/ca_cert.pem" >> "$BUNDLE" - fi - done -} - -_require_ca_dir() { - SIMPLE_CA_DIR="${SIMPLE_CA_DIR:-$(pwd)}" - if [[ ! -d "$SIMPLE_CA_DIR" ]]; then - echo "ERROR: CA directory '$SIMPLE_CA_DIR' does not exist." >&2 - return 1 - fi -} - -_is_ip() { [[ "$1" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; } -_is_dns() { [[ "$1" =~ ^[a-z0-9-]+(\.[a-z0-9-]+)*$ ]]; } -_is_email() { [[ "$1" =~ ^[^@]+@[^@]+\.[^@]+$ ]]; } - -make_ca() { - local CA_DAYS=3650 - local ISSUING_CA="" - local AIA_BASE_URL="" - - while [[ $# -gt 0 ]]; do - case "$1" in - --days) - if [[ -z "$2" || ! "$2" =~ ^[0-9]+$ ]]; then - echo "ERROR: --days requires a positive integer." >&2 - return 1 - fi - CA_DAYS="$2"; shift 2 ;; - --issuing-ca) - if [[ -z "$2" ]]; then - echo "ERROR: --issuing-ca requires a value." >&2 - return 1 - fi - if [[ "$2" == "ca" ]]; then - echo "ERROR: --issuing-ca cannot be 'ca'." >&2 - return 1 - fi - ISSUING_CA="$2"; shift 2 ;; - --aia-base-url) - if [[ -z "$2" ]]; then - echo "ERROR: --aia-base-url requires a value." >&2 - return 1 - fi - AIA_BASE_URL="$2"; shift 2 ;; - --ca-dir) - if [[ -z "$2" ]]; then - echo "ERROR: --ca-dir requires a value." >&2 - return 1 - fi - SIMPLE_CA_DIR="$2"; shift 2 ;; - *) break ;; - esac - done - - local CA_NAME="$1" - _require_ca_dir || return 1 - - if [[ -z "$CA_NAME" ]]; then - echo "ERROR: CA name is required." >&2 - return 1 - fi - - if [[ -z "$AIA_BASE_URL" && -f "$SIMPLE_CA_DIR/aia_base_url.txt" ]]; then - AIA_BASE_URL="$(cat "$SIMPLE_CA_DIR/aia_base_url.txt")" - fi - - local ROOT_CA_CERT="$SIMPLE_CA_DIR/ca_cert.pem" - local ROOT_CA_KEY="$SIMPLE_CA_DIR/ca_key.pem" - - # Create root CA - if [[ -z "$ISSUING_CA" ]]; then - if [[ -f "$ROOT_CA_CERT" && -f "$ROOT_CA_KEY" ]]; then - echo "Root CA already exists in $SIMPLE_CA_DIR, skipping." - return 0 - fi - echo "Generating root CA certificate '$CA_NAME' and key..." - if ! openssl req \ - -x509 \ - -newkey rsa:4096 \ - -keyout "$ROOT_CA_KEY" \ - -out "$ROOT_CA_CERT" \ - -days "$CA_DAYS" \ - -noenc \ - -subj "/CN=${CA_NAME}" \ - -text \ - -addext "basicConstraints=critical,CA:TRUE,pathlen:1" \ - -addext "keyUsage=critical,keyCertSign,cRLSign"; then - echo "ERROR: Failed to generate root CA certificate and key." >&2 - return 1 - fi - _rebuild_ca_bundle - if [[ -n "$AIA_BASE_URL" ]]; then - echo "$AIA_BASE_URL" > "$SIMPLE_CA_DIR/aia_base_url.txt" - fi - return 0 - fi - - # Create issuing CA - if [[ ! -f "$ROOT_CA_CERT" || ! -f "$ROOT_CA_KEY" ]]; then - echo "ERROR: Cannot create issuing CA '$CA_NAME' without an existing root CA. Create the root CA first." >&2 - return 1 - fi - - local ISSUING_DIR="$SIMPLE_CA_DIR/$ISSUING_CA" - local ISSUING_CERT="$ISSUING_DIR/ca_cert.pem" - local ISSUING_KEY="$ISSUING_DIR/ca_key.pem" - - if [[ -f "$ISSUING_CERT" && -f "$ISSUING_KEY" ]]; then - echo "Issuing CA '$ISSUING_CA' already exists in $ISSUING_DIR, skipping." - return 0 - fi - - mkdir -p "$ISSUING_DIR" - echo "Generating issuing CA certificate '$CA_NAME' and key..." - if ! openssl req \ - -newkey rsa:4096 \ - -keyout "$ISSUING_KEY" \ - -noenc \ - -subj "/CN=${CA_NAME}" \ - -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \ - -addext "keyUsage=critical,keyCertSign,cRLSign" \ - ${AIA_BASE_URL:+-addext "authorityInfoAccess=caIssuers;URI:${AIA_BASE_URL}/ca_cert.crt"} \ - | openssl x509 \ - -req \ - -CA "$ROOT_CA_CERT" \ - -CAkey "$ROOT_CA_KEY" \ - -copy_extensions copyall \ - -days "$CA_DAYS" \ - -text \ - -out "$ISSUING_CERT"; then - echo "ERROR: Failed to generate issuing CA certificate and key." >&2 - return 1 - fi - - _rebuild_ca_bundle - return 0 -} - -make_cert() { - local ISSUING_CA="" - local CERT_DAYS=365 - local CERT_DIR="" - local CERT_TYPE="server" - - while [[ $# -gt 0 ]]; do - case "$1" in - --ca-dir) - if [[ -z "$2" ]]; then - echo "ERROR: --ca-dir requires a value." >&2 - return 1 - fi - SIMPLE_CA_DIR="$2"; shift 2 ;; - --cert-dir) - if [[ -z "$2" ]]; then - echo "ERROR: --cert-dir requires a value." >&2 - return 1 - fi - CERT_DIR="$2"; shift 2 ;; - --issuing-ca) - if [[ -z "$2" ]]; then - echo "ERROR: --issuing-ca requires a value." >&2 - return 1 - fi - if [[ "$2" == "ca" ]]; then - echo "ERROR: --issuing-ca cannot be 'ca'." >&2 - return 1 - fi - ISSUING_CA="$2"; shift 2 ;; - --days) - if [[ -z "$2" || ! "$2" =~ ^[0-9]+$ ]]; then - echo "ERROR: --days requires a positive integer." >&2 - return 1 - fi - CERT_DAYS="$2"; shift 2 ;; - --type) - if [[ -z "$2" ]]; then - echo "ERROR: --type requires a value." >&2 - return 1 - fi - if [[ "$2" != "server" && "$2" != "user" ]]; then - echo "ERROR: --type must be 'server' or 'user'." >&2 - return 1 - fi - CERT_TYPE="$2"; shift 2 ;; - *) break ;; - esac - done - - local CERT_SUBJECT_NAME="$1" - if [[ $# -gt 0 ]]; then - shift - fi - - _require_ca_dir || return 1 - - if [[ -z "$CERT_SUBJECT_NAME" ]]; then - echo "ERROR: Subject name is required." >&2 - return 1 - fi - if [[ "$CERT_TYPE" == "server" ]] && ! _is_dns "$CERT_SUBJECT_NAME"; then - echo "ERROR: Invalid subject name '$CERT_SUBJECT_NAME'. Must be a valid DNS name." >&2 - return 1 - fi - - local SIGNING_DIR="$SIMPLE_CA_DIR${ISSUING_CA:+/$ISSUING_CA}" - local SIGNING_CERT="$SIGNING_DIR/ca_cert.pem" - local SIGNING_KEY="$SIGNING_DIR/ca_key.pem" - CERT_DIR="${CERT_DIR:-$SIGNING_DIR}" - - if [[ ! -f "$SIGNING_CERT" || ! -f "$SIGNING_KEY" ]]; then - echo "ERROR: Signing CA certificate and key not found in $SIGNING_DIR." >&2 - return 1 - fi - - local AIA_URL="" - if [[ -f "$SIMPLE_CA_DIR/aia_base_url.txt" ]]; then - local BASE_URL - BASE_URL="$(cat "$SIMPLE_CA_DIR/aia_base_url.txt")" - AIA_URL="${BASE_URL}${ISSUING_CA:+/$ISSUING_CA}/ca_cert.crt" - fi - - local CERT_NAME="${CERT_SUBJECT_NAME%%.*}" - local SANS=() - - if [[ "$CERT_TYPE" == "server" ]]; then - SANS=("DNS:${CERT_SUBJECT_NAME}") - fi - - while [[ $# -gt 0 ]]; do - if _is_ip "$1"; then - SANS+=("IP:$1") - elif _is_email "$1"; then - SANS+=("email:$1") - elif _is_dns "$1"; then - SANS+=("DNS:$1") - else - echo "ERROR: Invalid SAN entry '$1'." >&2 - return 1 - fi - shift - done - - if [[ "$CERT_TYPE" == "server" ]]; then - echo "Generating server certificate for '$CERT_SUBJECT_NAME' with SANs:" - else - echo "Generating user certificate for '$CERT_SUBJECT_NAME':" - fi - for san in "${SANS[@]}"; do echo " - $san"; done - - if [[ -f "$CERT_DIR/${CERT_NAME}_cert.pem" && -f "$CERT_DIR/${CERT_NAME}_key.pem" ]]; then - echo "Certificate already exists in $CERT_DIR, skipping." - return 0 - fi - - local REQ_ARGS=( - -newkey rsa:4096 - -keyout "$CERT_DIR/${CERT_NAME}_key.pem" - -noenc - -subj "/CN=${CERT_SUBJECT_NAME}" - -addext "basicConstraints=critical,CA:FALSE" - ) - - local X509_ARGS=( - -req - -CA "$SIGNING_CERT" - -CAkey "$SIGNING_KEY" - -copy_extensions copyall - -days "$CERT_DAYS" - -text - -out "$CERT_DIR/${CERT_NAME}_cert.pem" - ) - - if [[ "$CERT_TYPE" == "server" ]]; then - REQ_ARGS+=( - -addext "keyUsage=critical,digitalSignature,keyEncipherment" - -addext "extendedKeyUsage=serverAuth,clientAuth" - -addext "subjectAltName=$(IFS=,; echo "${SANS[*]}")" - ) - else - REQ_ARGS+=( - -addext "keyUsage=critical,digitalSignature,nonRepudiation" - -addext "extendedKeyUsage=clientAuth,emailProtection,codeSigning" - ) - if [[ ${#SANS[@]} -gt 0 ]]; then - REQ_ARGS+=(-addext "subjectAltName=$(IFS=,; echo "${SANS[*]}")") - fi - fi - - if [[ -n "$AIA_URL" ]]; then - REQ_ARGS+=(-addext "authorityInfoAccess=caIssuers;URI:${AIA_URL}") - fi - - echo "Generating certificate and key..." - if ! openssl req "${REQ_ARGS[@]}" | openssl x509 "${X509_ARGS[@]}"; then - echo "ERROR: Failed to generate certificate and key." >&2 - return 1 - fi -} - -make_pfx() { - local ISSUING_CA="" - local PFX_PASSWORD="" - local APPLE_OPENSSL=0 - - while [[ $# -gt 0 ]]; do - case "$1" in - --ca-dir) - if [[ -z "$2" ]]; then - echo "ERROR: --ca-dir requires a value." >&2 - return 1 - fi - SIMPLE_CA_DIR="$2"; shift 2 ;; - --issuing-ca) - if [[ -z "$2" ]]; then - echo "ERROR: --issuing-ca requires a value." >&2 - return 1 - fi - if [[ "$2" == "ca" ]]; then - echo "ERROR: --issuing-ca cannot be 'ca'." >&2 - return 1 - fi - ISSUING_CA="$2"; shift 2 ;; - --password) - if [[ -z "$2" ]]; then - echo "ERROR: --password requires a value." >&2 - return 1 - fi - PFX_PASSWORD="$2"; shift 2 ;; - --apple-openssl) - APPLE_OPENSSL=1; shift ;; - *) break ;; - esac - done - - local CERT_PATH="$1" - _require_ca_dir || return 1 - - if [[ -z "$CERT_PATH" ]]; then - echo "ERROR: Certificate path is required." >&2 - return 1 - fi - - local CERT_DIR CERT_NAME KEY_PATH - CERT_DIR="$(dirname "$CERT_PATH")" - CERT_NAME="$(basename "$CERT_PATH" _cert.pem)" - KEY_PATH="$CERT_DIR/${CERT_NAME}_key.pem" - - if [[ ! -d "$CERT_DIR" ]]; then - echo "ERROR: Certificate directory '$CERT_DIR' does not exist." >&2 - return 1 - fi - if [[ ! -f "$CERT_PATH" || ! -f "$KEY_PATH" ]]; then - echo "ERROR: Server certificate or key not found." >&2 - return 1 - fi - if [[ ! -f "$SIMPLE_CA_DIR/ca_cert.pem" ]]; then - echo "ERROR: Root CA certificate not found in $SIMPLE_CA_DIR." >&2 - return 1 - fi - if [[ -n "$ISSUING_CA" && ! -f "$SIMPLE_CA_DIR/$ISSUING_CA/ca_cert.pem" ]]; then - echo "ERROR: Issuing CA certificate not found in $SIMPLE_CA_DIR/$ISSUING_CA." >&2 - return 1 - fi - if [[ -f "$CERT_DIR/${CERT_NAME}.pfx" ]]; then - echo "PKCS#12 (PFX) file already exists, aborting generation." >&2 - return 1 - fi - - PFX_PASSWORD="${PFX_PASSWORD:-changeit}" - - local OPENSSL_BIN="openssl" - if [[ "$APPLE_OPENSSL" -eq 1 ]]; then - OPENSSL_BIN="/usr/bin/openssl" - fi - - echo -n "Generating PKCS#12 (PFX) file..." - - local CHAIN_FILE - CHAIN_FILE=$(mktemp) - trap "rm -f '$CHAIN_FILE'" EXIT QUIT KILL INT HUP - - cat "$SIMPLE_CA_DIR/ca_cert.pem" > "$CHAIN_FILE" - if [[ -n "$ISSUING_CA" ]]; then - cat "$SIMPLE_CA_DIR/$ISSUING_CA/ca_cert.pem" >> "$CHAIN_FILE" - fi - - if ! "$OPENSSL_BIN" pkcs12 \ - -export \ - -out "$CERT_DIR/${CERT_NAME}.pfx" \ - -inkey "$KEY_PATH" \ - -in "$CERT_PATH" \ - -certfile "$CHAIN_FILE" \ - -password "pass:${PFX_PASSWORD}"; then - echo "ERROR: Failed to generate PKCS#12 (PFX) file." >&2 - return 1 - fi - - echo "done." -}