diff --git a/fca-mount b/fca-mount new file mode 100755 index 0000000..4f4df3a --- /dev/null +++ b/fca-mount @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euo pipefail +for partition_id in $(fca-partitions); do + sudo mkdir -p "/mnt/${partition_id}" + sudo mount "/dev/disk/by-uuid/${partition_id}" "/mnt/${partition_id}" +done + +fca-sync diff --git a/fca-partitions b/fca-partitions new file mode 100755 index 0000000..6cf739c --- /dev/null +++ b/fca-partitions @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -euo pipefail + +partitions_found=0 + +for part in "1fa86f97-8161-433a-9f03-7ff80877932c" "caa1991c-af27-47c1-8148-5eb528c73376" "3fc9965e-fd79-4a4d-8a80-4d3b57acfc27"; do + if [[ -L "/dev/disk/by-uuid/${part}" ]]; then + echo "${part}" + partitions_found=$((partitions_found+1)) + fi +done + +if [[ "$partitions_found" == "0" ]]; then + echo "No CA stick found!" >&2 + exit 1 +fi diff --git a/fca-sync b/fca-sync new file mode 100755 index 0000000..66a1601 --- /dev/null +++ b/fca-sync @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -euo pipefail + +for partition in $(fca-partitions); do + cd "/mnt/${partition}/ca-data" + for other in $(fca-partitions); do + if [ "$partition" != "$other" ]; then + if ! git remote | grep -q "^${other}$"; then + git remote add "$other" "/mnt/${other}/ca-data" + fi + git fetch "${other}" + git merge --ff-only "${other}/main" + fi + done +done + +sync diff --git a/fca-tools b/fca-tools new file mode 100755 index 0000000..f764b5c --- /dev/null +++ b/fca-tools @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# +# Operations: +# init-root create the root CA +# sign-intermediate sign an intermediate CSR +# verify verify a certificate against the root +# list show what's in the depot +# +# Passphrase handling: +# - Interactive by default (certstrap prompts). +# - ROOT_CA_PASSPHRASE= + +set -euo pipefail + +readonly DEPOT="$(fca-working-directory)/ca-data" +readonly ORG="Famedly GmbH" +readonly CURVE="P-384" +readonly ROOT_CN="Famedly GmbH Root CA 2026" +readonly ROOT_EXPIRES="10 years" +readonly INT_EXPIRES="5 years" + +usage() { + sed -n '3,/^set -euo pipefail$/p' "$0" | sed -n 's/^# \{0,1\}//p' >&2 + exit "${1:-2}" +} + +slug_for_cn() { printf '%s' "${1// /_}"; } +root_slug() { slug_for_cn "$ROOT_CN"; } + +build_pass_args() { + if [[ -n "${ROOT_CA_PASSPHRASE:-}" ]]; then + printf '%s\n%s\n' "--passphrase" "$ROOT_CA_PASSPHRASE" + fi +} + +assert_no_clobber() { + [[ -e "$1" ]] && die "refusing to overwrite existing file: $1" + return 0 +} + +die() { + echo "$*" + exit 1 +} + +cmd_init_root() { + mkdir -p "$DEPOT" + local slug="$(root_slug)" + assert_no_clobber "$DEPOT/$slug.key" + assert_no_clobber "$DEPOT/$slug.crt" + + local pass_args=() + while IFS= read -r line; do pass_args+=("$line"); done < <(build_pass_args) + + certstrap --depot-path "$DEPOT" init \ + --common-name "$ROOT_CN" \ + --organization "$ORG" \ + --curve "$CURVE" \ + --expires "$ROOT_EXPIRES" \ + --path-length 1 \ + "${pass_args[@]}" +} + +cmd_sign_intermediate() { + local csr_file="$1" + [[ -f "$csr_file" ]] || die "CSR file not found: $csr_file" + + local root_slug="$(root_slug)" + [[ -f "$DEPOT/$root_slug.key" ]] || die "root key not found at $DEPOT/$root_slug.key" + + local cn="$(openssl req -in $csr_file -noout -subject | sed 's/subject=CN=//')" + [[ -z "$cn" ]] && die "Couldn't decode CN from $csr_file" + + local int_slug="$(slug_for_cn "$cn")" + assert_no_clobber "$DEPOT/$int_slug.crt" + + local pass_args=() + while IFS= read -r line; do pass_args+=("$line"); done < <(build_pass_args) + + certstrap --depot-path "$DEPOT" sign \ + --CA "$ROOT_CN" \ + --intermediate \ + --path-length 0 \ + --expires "$INT_EXPIRES" \ + --csr "$csr_file" \ + "${pass_args[@]}" \ + "$cn" +} + +cmd_verify() { + local cert="$1" + [[ -f "$cert" ]] || die "cert not found: $cert" + local root_slug root_crt + root_slug="$(root_slug)" + root_crt="$DEPOT/$root_slug.crt" + [[ -f "$root_crt" ]] || die "root cert not found at $root_crt" + + openssl verify -CAfile "$root_crt" "$cert" + openssl x509 -in "$cert" -noout -subject -issuer -dates -ext basicConstraints,keyUsage,subjectKeyIdentifier,authorityKeyIdentifier +} + +cmd_list() { + [[ -d "$DEPOT" ]] || die "depot $DEPOT does not exist" + echo "depot: $DEPOT" + echo + ls -lh "$DEPOT"/* + local root_crt; root_crt="$DEPOT/$(root_slug).crt" + if [[ -f "$root_crt" ]]; then + echo + openssl x509 -in "$root_crt" -noout -subject -dates -ext basicConstraints,keyUsage + fi + local root_crl; root_crl="$DEPOT/$(root_slug).crl" + if [[ -f "$root_crl" ]]; then + echo + openssl crl -in "$root_crl" -noout -text + fi +} + +[[ $# -ge 1 ]] || usage + +sub="$1"; shift +case "$sub" in + -h|--help|help) + usage 0 + ;; + init-root|sign-intermediate|verify|list) + ;; + *) + die "unknown subcommand: $sub (try $0 --help)" + ;; +esac + +case "$sub" in + init-root) [[ $# -eq 0 ]] || die "init-root takes no args"; cmd_init_root ;; + sign-intermediate) [[ $# -eq 1 ]] || die "usage: $0 sign-intermediate CSR"; cmd_sign_intermediate "$1" ;; + verify) [[ $# -eq 1 ]] || die "usage: $0 verify CERT"; cmd_verify "$1" ;; + list) [[ $# -eq 0 ]] || die "list takes no args"; cmd_list ;; +esac diff --git a/fca-working-directory b/fca-working-directory new file mode 100755 index 0000000..f60e85c --- /dev/null +++ b/fca-working-directory @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -euo pipefail + +partition="$(fca-partitions | head -n 1)" +echo "/mnt/${partition}" diff --git a/flake.nix b/flake.nix index 3d41acb..7315df0 100644 --- a/flake.nix +++ b/flake.nix @@ -43,6 +43,15 @@ { formatter = pkgs.nixpkgs-fmt; packages = { + # Famedly CA Scripts + fca-mount = pkgs.writeShellScriptBin "fca-mount" (builtins.readFile ./fca-mount); + fca-partitions = pkgs.writeShellScriptBin "fca-partitions" (builtins.readFile ./fca-partitions); + fca-sync = pkgs.writeShellScriptBin "fca-sync" (builtins.readFile ./fca-sync); + fca-tools = pkgs.writeShellScriptBin "fca-tools" (builtins.readFile ./fca-tools); + fca-working-directory = pkgs.writeShellScriptBin "fca-working-directory" ( + builtins.readFile ./fca-working-directory + ); + # Famedly OpenPGP Scripts fos-export = pkgs.writeShellScriptBin "fos-export" (builtins.readFile ./fos-export); fos-flash = pkgs.writeShellScriptBin "fos-flash" (builtins.readFile ./fos-flash); fos-generate = pkgs.writeShellScriptBin "fos-generate" (builtins.readFile ./fos-generate); diff --git a/fos-partitions b/fos-partitions index 414ace9..7243749 100755 --- a/fos-partitions +++ b/fos-partitions @@ -11,6 +11,6 @@ for part in "B9C8-D9A6" "EC3D-3102" "F055-B17B" "F665-64A4"; do done if [[ "$partitions_found" == "0" ]]; then - echo "No stick found!" >&2 + echo "No OCA stick found!" >&2 exit 1 fi diff --git a/iso.nix b/iso.nix index 1f47769..b76010a 100644 --- a/iso.nix +++ b/iso.nix @@ -147,9 +147,11 @@ in jq kdePackages.falkon kdePackages.okular + kdePackages.partitionmanager nano neovim openpgp-card-tools + openssl pcsc-tools pwgen rusty-diceware @@ -159,6 +161,13 @@ in wayland-utils wl-clipboard + # Famedly CA Scripts + flake.packages.${stdenv.hostPlatform.system}.fca-mount + flake.packages.${stdenv.hostPlatform.system}.fca-partitions + flake.packages.${stdenv.hostPlatform.system}.fca-sync + flake.packages.${stdenv.hostPlatform.system}.fca-tools + flake.packages.${stdenv.hostPlatform.system}.fca-working-directory + # Famedly OpenPGP Scripts flake.packages.${stdenv.hostPlatform.system}.fos-export flake.packages.${stdenv.hostPlatform.system}.fos-flash