Remove Plan.md and simple-ca.sh files from the repository

This commit is contained in:
2026-05-27 00:36:09 +02:00
parent 3c665c2b6c
commit 0b36f71394
2 changed files with 0 additions and 816 deletions
-369
View File
@@ -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 <cert-path>` → `.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.
-447
View File
@@ -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."
}