diff --git a/release-tools/do-copyright-year b/release-tools/do-copyright-year index 4aa0407c..9e82182c 100755 --- a/release-tools/do-copyright-year +++ b/release-tools/do-copyright-year @@ -44,13 +44,23 @@ process_files() { count=0 sp="/-\|" sc=0 - spin() { - printf "\r${sp:sc++:1} %s" "$@" - ((sc==${#sp})) && sc=0 - } - endspin() { - printf "\r%s\n" "$@" - } + # In a non-TTY context (e.g. Jenkins log capture), `\r` is not + # treated as a line clear and each spin tick shows up as its own + # line. Skip the spinner there. + if [ -t 1 ]; then + spin() { + printf "\r${sp:sc++:1} %s" "$@" + ((sc==${#sp})) && sc=0 + } + endspin() { + printf "\r%s\n" "$@" + } + else + spin() { :; } + endspin() { + printf "%s\n" "$@" + } + fi while read STATUS FILE ; do if [ -d "$FILE" ]; then continue; fi diff --git a/release-tools/openpgp/revocation-recipients/134C02E813889057DA2F3FDBEDDD4C5DAA149BBE.pgp b/release-tools/openpgp/revocation-recipients/134C02E813889057DA2F3FDBEDDD4C5DAA149BBE.pgp new file mode 100644 index 00000000..49939c3a --- /dev/null +++ b/release-tools/openpgp/revocation-recipients/134C02E813889057DA2F3FDBEDDD4C5DAA149BBE.pgp @@ -0,0 +1,210 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: 134C 02E8 1388 9057 DA2F 3FDB EDDD 4C5D AA14 9BBE +Comment: Anton Arapov +Comment: Anton Arapov +Comment: Anton Arapov +Comment: Anton Arapov +Comment: Anton Arapov + +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 +bdQSk8M5iGJw8JJ1XEZttg== +=U6t/ +-----END PGP PUBLIC KEY BLOCK----- diff --git a/release-tools/openpgp/revocation-recipients/A21FAB74B0088AA361152586B8EF1A6BA9DA2D5C.pgp b/release-tools/openpgp/revocation-recipients/A21FAB74B0088AA361152586B8EF1A6BA9DA2D5C.pgp new file mode 100644 index 00000000..919d1d97 --- /dev/null +++ b/release-tools/openpgp/revocation-recipients/A21FAB74B0088AA361152586B8EF1A6BA9DA2D5C.pgp @@ -0,0 +1,115 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: Hostname: +Version: Hockeypuck 2.2 + +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 +TIdjvGbJ/k4qxw== +=fnGl +-----END PGP PUBLIC KEY BLOCK----- diff --git a/release-tools/openpgp/revocation-recipients/CCDDFC774D9B306B3B3F1D956F3A67A0A75C7C08.pgp b/release-tools/openpgp/revocation-recipients/CCDDFC774D9B306B3B3F1D956F3A67A0A75C7C08.pgp new file mode 100644 index 00000000..096d7f1b --- /dev/null +++ b/release-tools/openpgp/revocation-recipients/CCDDFC774D9B306B3B3F1D956F3A67A0A75C7C08.pgp @@ -0,0 +1,78 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: Hostname: +Version: Hockeypuck 2.2 + +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 +=Wl25 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/release-tools/openpgp/revocation-recipients/README.md b/release-tools/openpgp/revocation-recipients/README.md new file mode 100644 index 00000000..0a003de8 --- /dev/null +++ b/release-tools/openpgp/revocation-recipients/README.md @@ -0,0 +1,22 @@ +# Revocation Recipient Certificates + +This directory contains the public OpenPGP certificates used by the OpenSSL +OpenPGP certificate initialization pipeline to encrypt the primary-key +revocation certificate artifact. + +`recipients.txt` is the authoritative recipient manifest. Each non-comment +line contains: + +```text +<40-hex-character fingerprint> +``` + +For each manifest entry, this directory must contain a public certificate named: + +```text +.pgp +``` + +The pipeline validates that each certificate contains the expected fingerprint +and User ID email, and that `sq encrypt --for-file` can encrypt to the +certificate under the current `sq` policy. diff --git a/release-tools/openpgp/revocation-recipients/recipients.txt b/release-tools/openpgp/revocation-recipients/recipients.txt new file mode 100644 index 00000000..cbd827f8 --- /dev/null +++ b/release-tools/openpgp/revocation-recipients/recipients.txt @@ -0,0 +1,5 @@ +# Primary-key revocation certificate encryption recipients. +# Format: +CCDDFC774D9B306B3B3F1D956F3A67A0A75C7C08 dmitry@openssl.org +A21FAB74B0088AA361152586B8EF1A6BA9DA2D5C tomas@openssl.org +134C02E813889057DA2F3FDBEDDD4C5DAA149BBE anton@openssl.org diff --git a/release-tools/openssl-pgp b/release-tools/openssl-pgp new file mode 100755 index 00000000..8a0051ae --- /dev/null +++ b/release-tools/openssl-pgp @@ -0,0 +1,728 @@ +#!/usr/bin/env bash +# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved. +# +# Licensed under the Apache License 2.0 (the "License"). You may not use +# this file except in compliance with the License. You can obtain a copy +# in the file LICENSE in the source distribution or at +# https://www.openssl.org/source/license.html +# +# openssl-pgp -- OpenSSL release-artifacts OpenPGP key/cert/sign helper. +# +# Implements the release-artifact OpenPGP policy on top of sq-pkcs11 +# (which speaks PKCS#11 to the nShield HSM). Capabilities: +# +# - generate the policy primary key and current signing subkey +# - issue / rotate the published OpenPGP certificate +# - sign release artifacts with the current signing subkey +# - issue primary-key and subkey-revocation certificates +# +# Requires sq-pkcs11 on PATH (or OPENSSL_PGP_SQ_PKCS11 set). + +set -euo pipefail + +PROG=${0##*/} + +usage() { + cat <<'EOF' +Usage: + openssl-pgp primary-generate [--label LABEL] --cardset CARDSET [--module MODULE] + openssl-pgp subkey-generate [--label LABEL] [--module MODULE] + openssl-pgp cert-init [--generate-keys] [--output FILE] [--revocation-output FILE] + openssl-pgp subkey-rotate --new-subkey-label LABEL [--generate-subkey] [--input-cert FILE] [--output FILE] + openssl-pgp sign [--output FILE] [--binary] FILE... + openssl-pgp cert-revoke [--output FILE] [--reason REASON] [--message TEXT] + openssl-pgp subkey-revoke --subkey-fingerprint FPR [--input-cert FILE] [--output FILE] [--reason REASON] [--message TEXT] + +Required configuration (per command): + cert-init OPENSSL_PGP_PRIMARY_LABEL, OPENSSL_PGP_CURRENT_SUBKEY_LABEL, + OPENSSL_PGP_USERID, OPENSSL_PGP_PRIMARY_CARDSET + subkey-rotate OPENSSL_PGP_PRIMARY_LABEL, OPENSSL_PGP_PRIMARY_CARDSET + (does NOT use OPENSSL_PGP_CURRENT_SUBKEY_LABEL — old subkey + comes from the input cert, new one from --new-subkey-label) + sign OPENSSL_PGP_CURRENT_SUBKEY_LABEL + cert-revoke OPENSSL_PGP_PRIMARY_LABEL, OPENSSL_PGP_PRIMARY_CARDSET + subkey-revoke OPENSSL_PGP_PRIMARY_LABEL, OPENSSL_PGP_PRIMARY_CARDSET, + plus --subkey-fingerprint identifying which subkey in + the input cert to revoke (compromise scenario: subkey + private key is NOT touched, only the primary signs) + primary-generate label and cardset (CLI flags or env vars) + subkey-generate label (CLI flag or OPENSSL_PGP_CURRENT_SUBKEY_LABEL) + +Every label / cardset value is validated against the Security World +(`cklist` / `nfkminfo --cardset-list`) before any HSM action runs. + +Optional configuration: + OPENSSL_PGP_CONFIG Config file to source before running + OPENSSL_PGP_CERT Published cert path (default: release.asc) + OPENSSL_PGP_GENERATEKEY generatekey path (default: /opt/nfast/bin/generatekey) + OPENSSL_PGP_GENERATE_MODULE Optional nShield module number/name for generatekey module= + OPENSSL_PGP_SQ_PKCS11 sq-pkcs11 binary (default: looked up as "sq-pkcs11" on PATH) + OPENSSL_PGP_PRELOAD preload path (default: /opt/nfast/bin/preload) — wraps + primary-key operations so the K/N OCS quorum ceremony + runs in preload's interactive UI and sq-pkcs11 inherits + the preloaded session + OPENSSL_PGP_PRELOAD_MODULE nShield module preload pins to via -m (default: 1). + Avoids spurious LogTokenNotPresent errors on multi- + module hosts where one set of cards is shuttled + between readers. Set to "" to load on all modules. + PKCS11_MODULE_PATH PKCS#11 module path (default: /opt/nfast/toolkits/pkcs11/libcknfast.so) + OPENSSL_PGP_CKLIST cklist path (default: /opt/nfast/bin/cklist) + OPENSSL_PGP_NFKMINFO nfkminfo path (default: /opt/nfast/bin/nfkminfo) + OPENSSL_PGP_GENTIME_TZ Timezone of nfkminfo gentime (default: UTC) + +This wrapper implements the OpenSSL release-artifacts signing policy: + - generated OpenPGP keys are nShield PKCS#11 RSA-4096 keys + - generated keys are created with generatekey --generate --batch + - generated keys always include logkeyusage=yes for Security World audit logging + - primary key is generated protect=token under the configured 2-of-4 OCS + - signing subkeys are generated protect=module + - primary validity is fixed to 5y; signing subkey validity is fixed to 1y + - cert creation/signing timestamps are derived from nShield gentime + - artifact signing always uses OPENSSL_PGP_CURRENT_SUBKEY_LABEL + - per-signing audit (key identity, timestamp, requester, result, error + condition) is captured by the nShield Security World audit log; keys + must be generated with logkeyusage=yes + - the artifact-name and artifact-digest fields required by the policy + come from the operator's release pipeline log (whichever script / + automation invokes "openssl-pgp sign"), correlated to the HSM log by + (key, timestamp); the wrapper itself does not duplicate this +EOF +} + +die() { + printf '%s: %s\n' "$PROG" "$*" >&2 + exit 1 +} + +note() { + printf '%s\n' "$*" >&2 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +repo_dir() { + cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd +} + +load_config() { + if [[ -n "${OPENSSL_PGP_CONFIG:-}" ]]; then + [[ -r "$OPENSSL_PGP_CONFIG" ]] || die "cannot read OPENSSL_PGP_CONFIG=$OPENSSL_PGP_CONFIG" + # shellcheck source=/dev/null + . "$OPENSSL_PGP_CONFIG" + elif [[ -r "$(repo_dir)/openssl-pgp.conf" ]]; then + # shellcheck source=/dev/null + . "$(repo_dir)/openssl-pgp.conf" + fi +} + +default_config() { + : "${PKCS11_MODULE_PATH:=/opt/nfast/toolkits/pkcs11/libcknfast.so}" + : "${OPENSSL_PGP_CERT:=release.asc}" + : "${OPENSSL_PGP_GENERATEKEY:=/opt/nfast/bin/generatekey}" + : "${OPENSSL_PGP_CKLIST:=/opt/nfast/bin/cklist}" + : "${OPENSSL_PGP_NFKMINFO:=/opt/nfast/bin/nfkminfo}" + : "${OPENSSL_PGP_GENTIME_TZ:=UTC}" + : "${OPENSSL_PGP_SQ_PKCS11:=sq-pkcs11}" + : "${OPENSSL_PGP_PRELOAD:=/opt/nfast/bin/preload}" +} + +# Each command calls only the prerequisite checks it actually needs, so a +# command never demands an env var it does not use (e.g. subkey-rotate does +# not consume OPENSSL_PGP_CURRENT_SUBKEY_LABEL). + +require_pkcs11_module() { + [[ -n "${PKCS11_MODULE_PATH:-}" ]] || die "PKCS11_MODULE_PATH is required" + [[ -r "$PKCS11_MODULE_PATH" ]] || die "PKCS11_MODULE_PATH is not readable: $PKCS11_MODULE_PATH" +} + +require_sq_pkcs11_binary() { + [[ -x "$OPENSSL_PGP_SQ_PKCS11" ]] || command -v "$OPENSSL_PGP_SQ_PKCS11" >/dev/null 2>&1 \ + || die "sq-pkcs11 not found: $OPENSSL_PGP_SQ_PKCS11" +} + +# preload wraps every sq-pkcs11 invocation that touches the primary key. +# It runs the K/N OCS quorum ceremony interactively (slot polling, +# per-card prompts) and exec()s sq-pkcs11 with the preloaded session +# available to libcknfast — sq-pkcs11 then sees the OCS slot as +# already-authenticated and calls C_Login with a NULL PIN (the PKCS#11 +# protected-authentication-path idiom) to flip the session into User +# state. Sign operations use module-protected subkeys and do not need +# preload — only the four primary-key commands below. +require_preload() { + [[ -x "$OPENSSL_PGP_PRELOAD" ]] || die "preload not executable: $OPENSSL_PGP_PRELOAD" +} + +require_userid() { + [[ -n "${OPENSSL_PGP_USERID:-}" ]] || die "OPENSSL_PGP_USERID is required" +} + +# Validate that the env var named by $1 is set AND that its value is a +# CKA_LABEL present in the Security World. Catches typos that +# require-var-only checks would silently pass through. +require_hsm_label() { + local var_name=$1 + local label=${!var_name:-} + [[ -n "$label" ]] || die "$var_name is required" + [[ -x "$OPENSSL_PGP_CKLIST" ]] || die "cklist not executable: $OPENSSL_PGP_CKLIST" + if ! label_exists "$label"; then + die "$var_name=$label is not present in the Security World; run \"$OPENSSL_PGP_CKLIST -n --cka-label=$label\" to verify" + fi +} + +require_primary_cardset() { + [[ -n "${OPENSSL_PGP_PRIMARY_CARDSET:-}" ]] \ + || die "OPENSSL_PGP_PRIMARY_CARDSET is required to address the OCS slot for primary-key operations" +} + +primary_key_uri() { + require_primary_cardset + printf 'pkcs11:token=%s;object=%s;type=private\n' \ + "$OPENSSL_PGP_PRIMARY_CARDSET" "$OPENSSL_PGP_PRIMARY_LABEL" +} + +# Confirm OPENSSL_PGP_PRIMARY_CARDSET names a real OCS in this Security World. +# A typo or stale value can otherwise cause sq-pkcs11 to fail later, after +# destructive HSM actions (e.g. subkey-rotate generating a fresh key). +verify_primary_cardset_exists() { + require_primary_cardset + [[ -x "$OPENSSL_PGP_NFKMINFO" ]] || die "nfkminfo not executable: $OPENSSL_PGP_NFKMINFO" + local cardset=$OPENSSL_PGP_PRIMARY_CARDSET nfkm_out + if ! nfkm_out=$("$OPENSSL_PGP_NFKMINFO" --cardset-list 2>&1); then + die "could not list Security World cardsets: $nfkm_out" + fi + # nfkminfo --cardset-list emits two header lines, then one line per + # cardset: k/n flags . Match the requested name as a + # whitespace-separated word from column 4 onwards so the hash column + # cannot collide with a name that happens to be hex. + if ! awk -v want="$cardset" ' + NR <= 2 { next } + { + for (i = 4; i <= NF; i++) + if ($i == want) { found = 1; exit } + } + END { exit found ? 0 : 1 } + ' <<<"$nfkm_out"; then + die "OPENSSL_PGP_PRIMARY_CARDSET=$cardset is not a known OCS in this Security World; run \"$OPENSSL_PGP_NFKMINFO --cardset-list\" to see available cardsets" + fi +} + +require_nshield_metadata_tools() { + [[ -x "$OPENSSL_PGP_CKLIST" ]] || die "cklist not executable: $OPENSSL_PGP_CKLIST" + [[ -x "$OPENSSL_PGP_NFKMINFO" ]] || die "nfkminfo not executable: $OPENSSL_PGP_NFKMINFO" +} + +require_generatekey() { + [[ -x "$OPENSSL_PGP_GENERATEKEY" ]] || die "generatekey not executable: $OPENSSL_PGP_GENERATEKEY" +} + +sq() { + PKCS11_MODULE_PATH=$PKCS11_MODULE_PATH "$OPENSSL_PGP_SQ_PKCS11" "$@" +} + +# sq() wrapped in preload — for primary-key operations (cert-init, +# subkey-rotate, cert-revoke, subkey-revoke). preload runs the OCS +# quorum ceremony and exec()s sq-pkcs11 with the preloaded session +# inherited via libcknfast; OPENSSL_PGP_PRIMARY_CARDSET names the OCS. +# Subkey operations (sign) use the plain sq() above — the signing +# subkey is module-protected and needs no preload. +# +# OPENSSL_PGP_PRELOAD_MODULE pins preload to a single nShield module +# (-m flag). Default "1" — when the agent has more than one module and +# operators shuttle cards between readers, preload would otherwise try +# to load the keys into every module and trip LogTokenNotPresent on +# whichever module lost its cards first. Pinning to one module +# sidesteps that. Override with OPENSSL_PGP_PRELOAD_MODULE=N (or empty +# string to use preload's default, all-modules behaviour). +sq_with_preload() { + require_primary_cardset + # ${VAR-default} (no colon) defaults on unset only — preserves an + # explicit empty string so the operator can opt out of -m entirely. + local module=${OPENSSL_PGP_PRELOAD_MODULE-1} + local -a preload_args=() + [[ -n "$module" ]] && preload_args+=(-m "$module") + PKCS11_MODULE_PATH=$PKCS11_MODULE_PATH \ + "$OPENSSL_PGP_PRELOAD" "${preload_args[@]}" -c "$OPENSSL_PGP_PRIMARY_CARDSET" -- \ + "$OPENSSL_PGP_SQ_PKCS11" "$@" +} + +cklist_for_label() { + local label=$1 + "$OPENSSL_PGP_CKLIST" -n --cka-label="$label" +} + +nfkm_ident_for_label() { + local label=$1 ident + ident=$(cklist_for_label "$label" | awk -F'"' '/CKA_NFKM_ID/ { print $2; exit }') + [[ -n "$ident" ]] || die "could not find CKA_NFKM_ID for label: $label" + printf '%s\n' "$ident" +} + +label_exists() { + local label=$1 + cklist_for_label "$label" 2>/dev/null \ + | awk -F'"' '/CKA_NFKM_ID/ { found = 1 } END { exit found ? 0 : 1 }' +} + +require_label_absent_for_generation() { + local label=$1 + if label_exists "$label"; then + die "refusing to generate key; label already exists in Security World: $label" + fi +} + +hsm_gentime() { + local label=$1 ident gentime + ident=$(nfkm_ident_for_label "$label") + gentime=$("$OPENSSL_PGP_NFKMINFO" -k pkcs11 "$ident" \ + | awk '/^[ \t]*gentime/ { print $2 " " $3; exit }') + [[ -n "$gentime" ]] || die "could not find nfkminfo gentime for label: $label ident: $ident" + + if [[ "$OPENSSL_PGP_GENTIME_TZ" == "UTC" ]]; then + printf '%sT%sZ\n' "${gentime%% *}" "${gentime##* }" + else + need_cmd date + date -u -d "TZ=\"$OPENSSL_PGP_GENTIME_TZ\" $gentime" +"%Y-%m-%dT%H:%M:%SZ" + fi +} + +verify_rsa4096_label() { + local label=$1 out key_type bits hex + out=$(cklist_for_label "$label") + + key_type=$(awk ' + /CKA_KEY_TYPE/ { + if ($0 ~ /CKK_RSA|RSA/) { print "RSA"; exit } + print "NOT_RSA"; exit + } + ' <<<"$out") + [[ "$key_type" == "RSA" ]] || die "label $label is not reported as RSA by cklist" + + bits=$(awk ' + /CKA_MODULUS_BITS|CKA_MODULUS_BITS/ { + for (i = 1; i <= NF; i++) { + if ($i ~ /^[0-9]+$/) { print $i; exit } + } + } + ' <<<"$out") + + if [[ -z "$bits" ]]; then + hex=$(awk ' + /CKA_MODULUS/ { + line = $0 + sub(/^.*CKA_MODULUS[^0-9A-Fa-f]*/, "", line) + gsub(/[^0-9A-Fa-f]/, "", line) + print line + exit + } + ' <<<"$out") + if [[ -n "$hex" ]]; then + bits=$(( ${#hex} * 4 )) + fi + fi + + if [[ -z "$bits" ]]; then + if [[ "${OPENSSL_PGP_ALLOW_UNVERIFIED_KEY_ATTRS:-0}" == "1" ]]; then + note "warning: could not verify RSA modulus size for $label from cklist output" + return 0 + fi + die "could not verify RSA-4096 modulus size for $label; set OPENSSL_PGP_ALLOW_UNVERIFIED_KEY_ATTRS=1 to override after manual verification" + fi + + [[ "$bits" == "4096" ]] || die "label $label is RSA-$bits, policy requires RSA-4096" +} + +require_cert_exists() { + local path=$1 + [[ -r "$path" ]] || die "certificate not readable: $path" +} + +require_output_absent() { + local path=$1 + [[ ! -e "$path" ]] || die "refusing to overwrite existing file: $path" +} + +default_sig_path() { + local artifact=$1 + if [[ "$artifact" == *.* ]]; then + printf '%s.asc\n' "$artifact" + else + printf '%s.asc\n' "$artifact" + fi +} + +generatekey_pkcs11() { + require_generatekey + note "running nShield generatekey in batch mode; ACS/OCS prompts may follow" + "$OPENSSL_PGP_GENERATEKEY" --generate --batch pkcs11 "$@" +} + +append_generate_module_arg() { + local -n argv=$1 + local module=${2:-} + if [[ -n "$module" ]]; then + argv+=("module=$module") + elif [[ -n "${OPENSSL_PGP_GENERATE_MODULE:-}" ]]; then + argv+=("module=$OPENSSL_PGP_GENERATE_MODULE") + fi +} + +cmd_primary_generate() { + local label=${OPENSSL_PGP_PRIMARY_LABEL:-} cardset=${OPENSSL_PGP_PRIMARY_CARDSET:-} module= + while [[ $# -gt 0 ]]; do + case "$1" in + --label) label=${2:?missing value for --label}; shift 2 ;; + --cardset) cardset=${2:?missing value for --cardset}; shift 2 ;; + --module) module=${2:?missing value for --module}; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown primary-generate argument: $1" ;; + esac + done + [[ -n "$label" ]] || die "primary-generate requires --label or OPENSSL_PGP_PRIMARY_LABEL" + [[ -n "$cardset" ]] || die "primary-generate requires --cardset or OPENSSL_PGP_PRIMARY_CARDSET" + require_generatekey + require_nshield_metadata_tools + # Validate that --cardset / OPENSSL_PGP_PRIMARY_CARDSET names a real OCS in + # this Security World *before* generatekey starts prompting for cards. + OPENSSL_PGP_PRIMARY_CARDSET=$cardset verify_primary_cardset_exists + require_label_absent_for_generation "$label" + + local args=( + protect=token + "cardset=$cardset" + type=RSA + size=4096 + "plainname=$label" + logkeyusage=yes + ) + append_generate_module_arg args "$module" + + generatekey_pkcs11 "${args[@]}" + verify_rsa4096_label "$label" + note "generated OCS-protected RSA-4096 primary key with logkeyusage=yes: $label" +} + +cmd_subkey_generate() { + local label=${OPENSSL_PGP_CURRENT_SUBKEY_LABEL:-} module= + while [[ $# -gt 0 ]]; do + case "$1" in + --label) label=${2:?missing value for --label}; shift 2 ;; + --module) module=${2:?missing value for --module}; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown subkey-generate argument: $1" ;; + esac + done + [[ -n "$label" ]] || die "subkey-generate requires --label or OPENSSL_PGP_CURRENT_SUBKEY_LABEL" + require_generatekey + require_nshield_metadata_tools + require_label_absent_for_generation "$label" + + local args=( + protect=module + type=RSA + size=4096 + "plainname=$label" + logkeyusage=yes + ) + append_generate_module_arg args "$module" + + generatekey_pkcs11 "${args[@]}" + verify_rsa4096_label "$label" + note "generated module-protected RSA-4096 signing subkey with logkeyusage=yes: $label" +} + +cmd_cert_init() { + local output=$OPENSSL_PGP_CERT revocation_output='' generate_keys=0 + while [[ $# -gt 0 ]]; do + case "$1" in + --generate-keys) generate_keys=1; shift ;; + --output) output=${2:?missing value for --output}; shift 2 ;; + --revocation-output) revocation_output=${2:?missing value for --revocation-output}; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown cert-init argument: $1" ;; + esac + done + [[ -n "$revocation_output" ]] || revocation_output="${output%.asc}-primary-revocation.asc" + # Validate every prerequisite before any destructive HSM action. cert-init + # uses both labels and the cardset (to generate when --generate-keys is set, + # and to address them via the PKCS#11 URI when calling sq cert-export / + # sq cert-revoke). The label-existence check is gated on --generate-keys: + # in generate mode the labels must NOT exist yet (asserted inside the + # cmd_*_generate calls), otherwise they MUST already exist. + require_pkcs11_module + require_sq_pkcs11_binary + require_preload + require_nshield_metadata_tools + require_userid + [[ -n "${OPENSSL_PGP_PRIMARY_LABEL:-}" ]] || die "OPENSSL_PGP_PRIMARY_LABEL is required" + [[ -n "${OPENSSL_PGP_CURRENT_SUBKEY_LABEL:-}" ]] || die "OPENSSL_PGP_CURRENT_SUBKEY_LABEL is required" + verify_primary_cardset_exists + require_output_absent "$output" + require_output_absent "$revocation_output" + if [[ "$generate_keys" == "1" ]]; then + require_generatekey + cmd_primary_generate \ + --label "$OPENSSL_PGP_PRIMARY_LABEL" \ + --cardset "$OPENSSL_PGP_PRIMARY_CARDSET" + cmd_subkey_generate \ + --label "$OPENSSL_PGP_CURRENT_SUBKEY_LABEL" + else + label_exists "$OPENSSL_PGP_PRIMARY_LABEL" \ + || die "OPENSSL_PGP_PRIMARY_LABEL=$OPENSSL_PGP_PRIMARY_LABEL is not present in the Security World; pass --generate-keys to create it" + label_exists "$OPENSSL_PGP_CURRENT_SUBKEY_LABEL" \ + || die "OPENSSL_PGP_CURRENT_SUBKEY_LABEL=$OPENSSL_PGP_CURRENT_SUBKEY_LABEL is not present in the Security World; pass --generate-keys to create it" + fi + verify_rsa4096_label "$OPENSSL_PGP_PRIMARY_LABEL" + verify_rsa4096_label "$OPENSSL_PGP_CURRENT_SUBKEY_LABEL" + + local primary_created subkey_created + primary_created=$(hsm_gentime "$OPENSSL_PGP_PRIMARY_LABEL") + subkey_created=$(hsm_gentime "$OPENSSL_PGP_CURRENT_SUBKEY_LABEL") + + local primary_uri + primary_uri=$(primary_key_uri) + + sq_with_preload cert-export \ + --key-uri "$primary_uri" \ + --subkey-label "$OPENSSL_PGP_CURRENT_SUBKEY_LABEL" \ + --userid "$OPENSSL_PGP_USERID" \ + --creation-time "$primary_created" \ + --validity-period 5y \ + --subkey-creation-time "$subkey_created" \ + --subkey-validity-period 1y \ + --output "$output" + + sq_with_preload cert-revoke \ + --key-uri "$primary_uri" \ + --creation-time "$primary_created" \ + --reason compromised \ + --message "offline primary-key revocation certificate generated during cert-init" \ + --output "$revocation_output" + + note "certificate written: $output" + note "offline primary revocation written: $revocation_output" +} + +cmd_subkey_rotate() { + local input=$OPENSSL_PGP_CERT output='' new_label='' generate_subkey=0 + while [[ $# -gt 0 ]]; do + case "$1" in + --new-subkey-label) new_label=${2:?missing value for --new-subkey-label}; shift 2 ;; + --generate-subkey) generate_subkey=1; shift ;; + --input-cert) input=${2:?missing value for --input-cert}; shift 2 ;; + --output) output=${2:?missing value for --output}; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown subkey-rotate argument: $1" ;; + esac + done + [[ -n "$new_label" ]] || die "subkey-rotate requires --new-subkey-label" + [[ -n "$output" ]] || output="${input%.asc}-rotated.asc" + # Validate every prerequisite before any destructive HSM action so a missing + # or wrong env var cannot leave a half-rotated state (e.g. a freshly + # generated subkey with no cert binding it). Note: subkey-rotate does NOT + # consume OPENSSL_PGP_CURRENT_SUBKEY_LABEL — the existing subkey is read + # from the input cert and the new one comes from --new-subkey-label. + require_pkcs11_module + require_sq_pkcs11_binary + require_preload + require_nshield_metadata_tools + require_hsm_label OPENSSL_PGP_PRIMARY_LABEL + verify_primary_cardset_exists + require_cert_exists "$input" + require_output_absent "$output" + verify_rsa4096_label "$OPENSSL_PGP_PRIMARY_LABEL" + if [[ "$generate_subkey" == "1" ]]; then + cmd_subkey_generate --label "$new_label" + else + label_exists "$new_label" || die "--new-subkey-label $new_label is not a key in the Security World; pass --generate-subkey to generate one, or run \"$OPENSSL_PGP_CKLIST -n --cka-label=$new_label\" to confirm the label" + fi + verify_rsa4096_label "$new_label" + + local primary_created subkey_created + primary_created=$(hsm_gentime "$OPENSSL_PGP_PRIMARY_LABEL") + subkey_created=$(hsm_gentime "$new_label") + + local primary_uri + primary_uri=$(primary_key_uri) + + sq_with_preload cert-export \ + --merge-cert "$input" \ + --key-uri "$primary_uri" \ + --subkey-label "$new_label" \ + --creation-time "$primary_created" \ + --subkey-creation-time "$subkey_created" \ + --subkey-validity-period 1y \ + --output "$output" + + note "rotated certificate written: $output" + note "update OPENSSL_PGP_CURRENT_SUBKEY_LABEL after publication and cutover" +} + +cmd_sign() { + local output='' binary=0 + while [[ $# -gt 0 ]]; do + case "$1" in + --output) output=${2:?missing value for --output}; shift 2 ;; + --binary) binary=1; shift ;; + -h|--help) usage; exit 0 ;; + --*) die "unknown sign argument: $1" ;; + *) break ;; + esac + done + [[ $# -gt 0 ]] || die "sign requires at least one file" + if [[ $# -gt 1 && -n "$output" ]]; then + die "sign --output may only be used with one input file" + fi + + # sign uses only the current signing subkey; no primary auth, no UID, no + # cardset. Validate that the env var names a key that exists in the HSM. + require_pkcs11_module + require_sq_pkcs11_binary + require_nshield_metadata_tools + require_hsm_label OPENSSL_PGP_CURRENT_SUBKEY_LABEL + verify_rsa4096_label "$OPENSSL_PGP_CURRENT_SUBKEY_LABEL" + require_cert_exists "$OPENSSL_PGP_CERT" + local subkey_created + subkey_created=$(hsm_gentime "$OPENSSL_PGP_CURRENT_SUBKEY_LABEL") + + # Pre-flight: confirm the configured HSM signing key is a current + # valid signer in the published cert before any artefact is signed. + # Catches a stale, unrelated, revoked, or expired + # OPENSSL_PGP_CURRENT_SUBKEY_LABEL — without this check, a typo in + # the env var would silently produce a release signature that fails + # verification against the published cert. + sq verify-signing-key \ + --key-label "$OPENSSL_PGP_CURRENT_SUBKEY_LABEL" \ + --creation-time "$subkey_created" \ + --input-cert "$OPENSSL_PGP_CERT" \ + || die "OPENSSL_PGP_CURRENT_SUBKEY_LABEL=$OPENSSL_PGP_CURRENT_SUBKEY_LABEL is not a current valid signer in $OPENSSL_PGP_CERT (see message above); refusing to sign" + + local artifact sig args=() + [[ "$binary" == "1" ]] && args+=(--binary) + for artifact in "$@"; do + [[ -r "$artifact" ]] || die "artifact not readable: $artifact" + if [[ -n "$output" ]]; then + sig=$output + require_output_absent "$sig" + sq sign "${args[@]}" \ + --key-label "$OPENSSL_PGP_CURRENT_SUBKEY_LABEL" \ + --creation-time "$subkey_created" \ + --output "$sig" \ + "$artifact" + else + sig=$(default_sig_path "$artifact") + require_output_absent "$sig" + sq sign "${args[@]}" \ + --key-label "$OPENSSL_PGP_CURRENT_SUBKEY_LABEL" \ + --creation-time "$subkey_created" \ + "$artifact" + fi + note "signed: $artifact -> $sig" + note "audit source: nShield Security World key usage log (key must be generated with logkeyusage=yes)" + done +} + +cmd_cert_revoke() { + local output="${OPENSSL_PGP_CERT%.asc}-primary-revocation.asc" reason=compromised message="primary key suspected compromised" + while [[ $# -gt 0 ]]; do + case "$1" in + --output) output=${2:?missing value for --output}; shift 2 ;; + --reason) reason=${2:?missing value for --reason}; shift 2 ;; + --message) message=${2:?missing value for --message}; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown cert-revoke argument: $1" ;; + esac + done + # cert-revoke uses only the primary key + OCS; no UID, no subkey label. + require_pkcs11_module + require_sq_pkcs11_binary + require_preload + require_nshield_metadata_tools + require_hsm_label OPENSSL_PGP_PRIMARY_LABEL + verify_primary_cardset_exists + require_output_absent "$output" + verify_rsa4096_label "$OPENSSL_PGP_PRIMARY_LABEL" + local primary_created + primary_created=$(hsm_gentime "$OPENSSL_PGP_PRIMARY_LABEL") + + local primary_uri + primary_uri=$(primary_key_uri) + + sq_with_preload cert-revoke \ + --key-uri "$primary_uri" \ + --creation-time "$primary_created" \ + --reason "$reason" \ + --message "$message" \ + --output "$output" + note "primary revocation written: $output" +} + +cmd_subkey_revoke() { + # subkey-revoke now identifies the subkey by fingerprint inside the + # published cert (--input-cert + --subkey-fingerprint), so a + # compromised or deleted subkey can still be revoked using only the + # primary OCS-protected key. No HSM access for the subkey itself. + local input=$OPENSSL_PGP_CERT + local fingerprint='' output='' reason=compromised message="signing subkey suspected compromised" + while [[ $# -gt 0 ]]; do + case "$1" in + --subkey-fingerprint) fingerprint=${2:?missing value for --subkey-fingerprint}; shift 2 ;; + --input-cert) input=${2:?missing value for --input-cert}; shift 2 ;; + --output) output=${2:?missing value for --output}; shift 2 ;; + --reason) reason=${2:?missing value for --reason}; shift 2 ;; + --message) message=${2:?missing value for --message}; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown subkey-revoke argument: $1" ;; + esac + done + [[ -n "$fingerprint" ]] || die "subkey-revoke requires --subkey-fingerprint (look it up in the published cert with \`sq inspect\` or \`gpg --list-keys --with-subkey-fingerprint\`)" + require_pkcs11_module + require_sq_pkcs11_binary + require_preload + require_nshield_metadata_tools + require_hsm_label OPENSSL_PGP_PRIMARY_LABEL + verify_primary_cardset_exists + require_cert_exists "$input" + [[ -n "$output" ]] || output="${input%.asc}-subkey-revocation.asc" + require_output_absent "$output" + verify_rsa4096_label "$OPENSSL_PGP_PRIMARY_LABEL" + + local primary_created + primary_created=$(hsm_gentime "$OPENSSL_PGP_PRIMARY_LABEL") + + local primary_uri + primary_uri=$(primary_key_uri) + + sq_with_preload subkey-revoke \ + --key-uri "$primary_uri" \ + --input-cert "$input" \ + --subkey-fingerprint "$fingerprint" \ + --creation-time "$primary_created" \ + --reason "$reason" \ + --message "$message" \ + --output "$output" + note "subkey revocation written: $output" +} + +main() { + load_config + default_config + local cmd=${1:-} + [[ -n "$cmd" ]] || { usage; exit 2; } + shift + + # Each cmd_* function validates its own preconditions at the top, so main + # only dispatches. -h / --help short-circuits before any validation runs + # because the command's arg loop handles -h before reaching its + # require_* / verify_* calls. + case "$cmd" in + -h|--help|help) usage ;; + primary-generate|subkey-generate|cert-init|subkey-rotate|sign|cert-revoke|subkey-revoke) + "cmd_${cmd//-/_}" "$@" + ;; + *) die "unknown command: $cmd" ;; + esac +} + +main "$@" diff --git a/release-tools/openssl-pgp-ceremony-run b/release-tools/openssl-pgp-ceremony-run new file mode 100755 index 00000000..c2192ac0 --- /dev/null +++ b/release-tools/openssl-pgp-ceremony-run @@ -0,0 +1,255 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Run attended nShield OCS ceremonies from a CI or automation server without +# collecting card passphrases in that server. +# +# The automation server starts this script in start-and-wait mode. The script +# creates a detached tmux session on the HSM client, runs the requested OpenPGP +# command inside that session, and waits for the command's exit status. +# Custodians SSH to the same HSM client, attach to the printed tmux session, +# present their cards through nShield Remote Administration/TVD, and type OCS +# passphrases into the shared terminal when the nShield tooling asks for them. +# +# This keeps the automation server as the orchestrator and audit collector only. +# It sees the command, logs, outputs, and final exit status, but OCS passphrases +# are not passed through CI parameters, CI credentials, or a web UI. + +usage() { + cat <<'EOF' +Usage: + openssl-pgp-ceremony-run start-and-wait --socket PATH --session NAME --state-root DIR [--timeout SECONDS] [--allow-user USER]... -- COMMAND [ARG...] + openssl-pgp-ceremony-run exec STATE_DIR -- COMMAND [ARG...] + openssl-pgp-ceremony-run STATE_DIR COMMAND [ARG...] + +The start-and-wait subcommand starts COMMAND inside a detached tmux session, +prints attach instructions for custodians, waits for COMMAND to finish, and +exits with COMMAND's exit status. + +The exec subcommand is the tmux-side runner. It records COMMAND output in +STATE_DIR/ceremony.log and writes COMMAND's exit status to STATE_DIR/rc. +EOF +} + +die() { + printf 'error: %s\n' "$*" >&2 + exit 2 +} + +require_nonempty() { + local value=$1 + local name=$2 + + [[ -n $value ]] || die "$name must not be empty" +} + +shell_quote() { + local arg + + for arg in "$@"; do + printf ' %q' "$arg" + done +} + +run_exec() { + local state_dir=$1 + shift + + if [[ ${1:-} == "--" ]]; then + shift + fi + + (($# > 0)) || die "missing command" + + mkdir -p "$state_dir" + chmod 0770 "$state_dir" + + local log="$state_dir/ceremony.log" + local rc_file="$state_dir/rc" + + rm -f "$rc_file" + + { + date -u '+started=%Y-%m-%dT%H:%M:%SZ' + printf 'command=' + printf '%q ' "$@" + printf '\n\n' + + set +e + "$@" + local rc=$? + set -e + + printf '\n' + date -u '+finished=%Y-%m-%dT%H:%M:%SZ' + printf 'rc=%s\n' "$rc" + + printf '%s\n' "$rc" > "$rc_file" + exit "$rc" + } 2>&1 | tee "$log" +} + +run_start_and_wait() { + local socket= + local session= + local state_root= + local timeout_seconds=3600 + local allow_users=() + + while (($# > 0)); do + case $1 in + --socket) + (($# >= 2)) || die "--socket requires a value" + socket=$2 + shift 2 + ;; + --session) + (($# >= 2)) || die "--session requires a value" + session=$2 + shift 2 + ;; + --state-root) + (($# >= 2)) || die "--state-root requires a value" + state_root=$2 + shift 2 + ;; + --timeout) + (($# >= 2)) || die "--timeout requires a value" + timeout_seconds=$2 + shift 2 + ;; + --allow-user) + (($# >= 2)) || die "--allow-user requires a value" + allow_users+=("$2") + shift 2 + ;; + --) + shift + break + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown start-and-wait option: $1" + ;; + esac + done + + require_nonempty "$socket" "--socket" + require_nonempty "$session" "--session" + require_nonempty "$state_root" "--state-root" + (($# > 0)) || die "missing command" + [[ $timeout_seconds =~ ^[0-9]+$ ]] || die "--timeout must be a non-negative integer" + local user + for user in "${allow_users[@]}"; do + id -u "$user" >/dev/null 2>&1 || die "unknown --allow-user: $user" + done + + local socket_dir + socket_dir=$(dirname "$socket") + [[ -d $socket_dir ]] || die "tmux socket directory does not exist: $socket_dir" + [[ -w $socket_dir ]] || die "tmux socket directory is not writable: $socket_dir" + [[ -d $state_root ]] || die "state root does not exist: $state_root" + [[ -w $state_root ]] || die "state root is not writable: $state_root" + command -v tmux >/dev/null 2>&1 || die "tmux not found" + + local state_dir="$state_root/$session" + mkdir -p "$state_dir" + chmod 0770 "$state_dir" + + local rc_file="$state_dir/rc" + rm -f "$rc_file" + + if tmux -S "$socket" has-session -t "$session" 2>/dev/null; then + die "tmux session already exists: $session" + fi + + local script_path + script_path=$(readlink -f -- "${BASH_SOURCE[0]}") || die "cannot resolve script path" + local tmux_command + tmux_command=$(printf '%q' "$script_path") + tmux_command+=" exec" + tmux_command+=$(shell_quote "$state_dir" -- "$@") + + local socket_group + socket_group=$(stat -c %G "$socket_dir") + + tmux -S "$socket" new-session -d -s "$session" "$tmux_command" + chgrp "$socket_group" "$socket" 2>/dev/null || true + chmod 0660 "$socket" 2>/dev/null || true + + local server_owner + server_owner=$(id -un) + + for user in "${allow_users[@]}"; do + if [[ $user == "$server_owner" ]]; then + continue + fi + if ! tmux -S "$socket" server-access -a "$user"; then + tmux -S "$socket" kill-session -t "$session" 2>/dev/null || true + die "failed to grant tmux access to user: $user" + fi + if ! tmux -S "$socket" server-access -w "$user"; then + tmux -S "$socket" kill-session -t "$session" 2>/dev/null || true + die "failed to grant tmux write access to user: $user" + fi + done + + printf 'OCS ceremony started.\n' + printf 'Custodians should SSH to this host and run:\n' + printf ' tmux -S %q attach-session -t %q\n' "$socket" "$session" + printf '\n' + printf 'State directory:\n' + printf ' %s\n' "$state_dir" + printf '\n' + + local started + started=$(date +%s) + + while [[ ! -s $rc_file ]]; do + if ((timeout_seconds > 0)); then + local now + now=$(date +%s) + if ((now - started >= timeout_seconds)); then + printf 'error: ceremony timed out after %s seconds\n' "$timeout_seconds" >&2 + tmux -S "$socket" kill-session -t "$session" 2>/dev/null || true + exit 124 + fi + fi + sleep 5 + done + + local rc + rc=$(cat "$rc_file") + [[ $rc =~ ^[0-9]+$ ]] || die "invalid rc file content: $rc_file" + exit "$rc" +} + +main() { + (($# > 0)) || { + usage + exit 2 + } + + case $1 in + start-and-wait) + shift + run_start_and_wait "$@" + ;; + exec) + shift + (($# >= 1)) || die "missing state directory" + run_exec "$@" + ;; + -h|--help) + usage + ;; + *) + run_exec "$@" + ;; + esac +} + +main "$@" diff --git a/release-tools/openssl-pgp-revocation-recipients b/release-tools/openssl-pgp-revocation-recipients new file mode 100755 index 00000000..bfae4e52 --- /dev/null +++ b/release-tools/openssl-pgp-revocation-recipients @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved. +# +# Licensed under the Apache License 2.0 (the "License"). You may not use +# this file except in compliance with the License. You can obtain a copy +# in the file LICENSE in the source distribution or at +# https://www.openssl.org/source/license.html + +set -euo pipefail + +PROG=${0##*/} + +usage() { + cat <<'EOF' +Usage: + openssl-pgp-revocation-recipients bundle --source DIR --bundle FILE --manifest FILE --bundle-sha256 FILE + +Validate OpenPGP certificate recipients for primary-key revocation certificate +encryption, concatenate the public recipient certificates into a bundle, write a +recipient manifest, and write the bundle SHA-256 checksum. + +The source directory must contain recipients.txt and one .pgp file +per recipient. +EOF +} + +die() { + printf '%s: error: %s\n' "$PROG" "$*" >&2 + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +require_value() { + local value=$1 + local name=$2 + + [[ -n $value ]] || die "$name must not be empty" +} + +make_parent_dir() { + local path=$1 + local dir + + dir=$(dirname -- "$path") + mkdir -p -- "$dir" +} + +validate_recipient_cert() { + local fingerprint=$1 + local email=$2 + local cert_file=$3 + local cert_details + + [[ -s $cert_file ]] || die "recipient certificate is missing or empty: $cert_file" + + cert_details=$(sq inspect "$cert_file") + + printf '%s' "$cert_details" | tr -d '[:space:]' | grep -Fi -- "$fingerprint" >/dev/null || { + die "certificate does not appear to contain expected fingerprint: $fingerprint" + } + + printf '%s' "$cert_details" | grep -Fi -- "$email" >/dev/null || { + die "certificate does not appear to contain expected User ID email: $email" + } + + printf 'revocation recipient validation\n' | sq --batch --quiet encrypt \ + --without-signature \ + --for-file "$cert_file" \ + - >/dev/null || { + die "recipient certificate is not usable for encryption: $fingerprint $email" + } +} + +bundle_recipients() { + local source_dir= + local bundle= + local manifest= + local bundle_sha256= + + while (($# > 0)); do + case $1 in + --source) + (($# >= 2)) || die "--source requires a value" + source_dir=$2 + shift 2 + ;; + --bundle) + (($# >= 2)) || die "--bundle requires a value" + bundle=$2 + shift 2 + ;; + --manifest) + (($# >= 2)) || die "--manifest requires a value" + manifest=$2 + shift 2 + ;; + --bundle-sha256) + (($# >= 2)) || die "--bundle-sha256 requires a value" + bundle_sha256=$2 + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown bundle option: $1" + ;; + esac + done + + require_value "$source_dir" "--source" + require_value "$bundle" "--bundle" + require_value "$manifest" "--manifest" + require_value "$bundle_sha256" "--bundle-sha256" + + need_cmd sq + need_cmd sha256sum + + [[ -d $source_dir ]] || die "recipient source directory does not exist: $source_dir" + + local recipient_list="$source_dir/recipients.txt" + [[ -s $recipient_list ]] || die "recipient list does not exist or is empty: $recipient_list" + + make_parent_dir "$bundle" + make_parent_dir "$manifest" + make_parent_dir "$bundle_sha256" + + local bundle_tmp="${bundle}.tmp.$$" + local manifest_tmp="${manifest}.tmp.$$" + local checksum_tmp="${bundle_sha256}.tmp.$$" + + cleanup() { + rm -f -- "$bundle_tmp" "$manifest_tmp" "$checksum_tmp" + } + trap cleanup EXIT + + : > "$bundle_tmp" + : > "$manifest_tmp" + + local recipient_count=0 + local line fingerprint email extra cert_file + declare -A seen_fingerprints=() + + while IFS= read -r line || [[ -n $line ]]; do + [[ $line =~ ^[[:space:]]*($|#) ]] && continue + + fingerprint= + email= + extra= + read -r fingerprint email extra <<< "$line" + + [[ -z ${extra:-} ]] || die "recipient list line has too many fields: $line" + [[ $fingerprint =~ ^[0-9A-Fa-f]{40}$ ]] || die "invalid recipient fingerprint: $fingerprint" + + fingerprint=${fingerprint^^} + + case $email in + ''|*[[:space:]]*) + die "invalid recipient email for fingerprint $fingerprint: $email" + ;; + esac + + [[ -z ${seen_fingerprints[$fingerprint]+x} ]] || { + die "duplicate revocation recipient fingerprint: $fingerprint" + } + seen_fingerprints[$fingerprint]=1 + + cert_file="$source_dir/$fingerprint.pgp" + validate_recipient_cert "$fingerprint" "$email" "$cert_file" + + printf '%s %s\n' "$fingerprint" "$email" >> "$manifest_tmp" + cat -- "$cert_file" >> "$bundle_tmp" + recipient_count=$((recipient_count + 1)) + done < "$recipient_list" + + [[ -s $bundle_tmp ]] || die "recipient bundle is empty: $bundle" + + if [[ $recipient_count -eq 0 ]]; then + die "no revocation recipients were configured" + fi + + mv -f -- "$bundle_tmp" "$bundle" + mv -f -- "$manifest_tmp" "$manifest" + + sha256sum "$bundle" > "$checksum_tmp" + mv -f -- "$checksum_tmp" "$bundle_sha256" + trap - EXIT +} + +main() { + local command=${1:-} + + case $command in + bundle) + shift + bundle_recipients "$@" + ;; + -h|--help) + usage + ;; + '') + usage >&2 + exit 2 + ;; + *) + die "unknown command: $command" + ;; + esac +} + +main "$@" diff --git a/release-tools/sq-pkcs11-git-shim b/release-tools/sq-pkcs11-git-shim new file mode 100755 index 00000000..b7871561 --- /dev/null +++ b/release-tools/sq-pkcs11-git-shim @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved. +# +# Licensed under the Apache License 2.0 (the "License"). You may not use +# this file except in compliance with the License. You can obtain a copy +# in the file LICENSE in the source distribution or at +# https://www.openssl.org/source/license.html +# +# sq-pkcs11-git-shim -- gpg-CLI-compatible signing shim that routes git's +# `gpg.program` invocations to sq-pkcs11. +# +# Usage: +# +# git -c gpg.program=$TOOLS/release-tools/sq-pkcs11-git-shim \ +# tag -s -u +# +# git invokes the configured gpg program as roughly: +# +# gpg --status-fd=2 -bsau < tagbody > armored_detached_sig +# +# We accept that flag set, treat as a CKA_LABEL on the HSM, drain +# stdin, hand it to `sq-pkcs11 sign --output -`, and stream the armored +# detached signature back on stdout. Anything else (verify, list-keys, +# decrypt, …) is forwarded to a real gpg so `git tag -v` etc. continue +# to work on the operator's machine. +# +# Environment overrides: +# +# OPENSSL_PGP_SQ_PKCS11 sq-pkcs11 binary (default: looked up in PATH) +# OPENSSL_PGP_FALLBACK_GPG gpg binary used for non-sign modes +# (default: "gpg" in PATH) +# PKCS11_MODULE_PATH passed straight through to sq-pkcs11 +# (default: sq-pkcs11's own default) + +set -euo pipefail + +PROG=${0##*/} + +die() { + printf '%s: %s\n' "$PROG" "$*" >&2 + exit 1 +} + +sq=${OPENSSL_PGP_SQ_PKCS11:-sq-pkcs11} +gpg=${OPENSSL_PGP_FALLBACK_GPG:-gpg} + +# Parse just the flags git emits when signing, plus a few common ones. +# Anything matching a non-sign mode (verify, decrypt, list, etc.) flips +# us to fallback mode; we re-exec gpg with the original argv via a saved +# copy. Everything else is silently ignored — we infer "sign" from the +# combination of -b/-s and from git's pipeline shape. +saved_argv=("$@") +keyid= +status_fd= +mode=sign + +while [[ $# -gt 0 ]]; do + case $1 in + # Clustered short-option forms git emits (-b -s -a -u ) when + # signing. In gpg's CLI, when short options are clustered the + # last one (-u) consumes the next argv as its operand. Match the + # specific clusters git is known to emit and pull the keyid. + -bsau|-bsau) + [[ $# -ge 2 ]] || die "$1 needs an argument" + keyid=$2 + shift 2 + ;; + + # Sign-mode flag clusters that don't consume a keyid argument. + # Treat as no-ops; the keyid will arrive separately via -u. + -b|--detach-sign|-s|--sign|-a|--armor|-bsa|-sba) + shift + ;; + + # Key id (CKA_LABEL in our world). + -u|--local-user) + [[ $# -ge 2 ]] || die "$1 needs an argument" + keyid=$2 + shift 2 + ;; + --local-user=*) + keyid=${1#*=} + shift + ;; + + # Status protocol fd. We honour it by emitting a SIG_CREATED + # line on success so older git versions stay happy. + --status-fd) + [[ $# -ge 2 ]] || die "--status-fd needs an argument" + status_fd=$2 + shift 2 + ;; + --status-fd=*) + status_fd=${1#*=} + shift + ;; + + # Anything that isn't a sign operation gets handed to real gpg. + --verify|-d|--decrypt|--list-keys|--list-secret-keys|--list-sigs|\ + --gen-key|--quick-gen-key|--keyserver|--recv-keys|--export|--import|\ + --edit-key|--clearsign) + mode=fallback + break + ;; + + # End-of-options sentinel; stop scanning. + --) + shift + break + ;; + + # Anything else we don't recognise — ignore. git shouldn't + # surprise us in sign mode, but be tolerant of new flags. + *) + shift + ;; + esac +done + +if [[ "$mode" == "fallback" ]]; then + command -v "$gpg" >/dev/null 2>&1 \ + || die "non-sign mode requested but $gpg is not installed; \ +override with OPENSSL_PGP_FALLBACK_GPG" + exec "$gpg" "${saved_argv[@]}" +fi + +[[ -n "$keyid" ]] \ + || die "no -u/--local-user CKA_LABEL given (the key id git would normally pass)" + +command -v "$sq" >/dev/null 2>&1 \ + || die "sq-pkcs11 not found: $sq (set OPENSSL_PGP_SQ_PKCS11 to override)" + +# sq-pkcs11 sign takes a file path, not stdin, so drain stdin to a temp +# file. The temp file also gets the right name extension so any +# downstream debugging cues remain meaningful. +tmp=$(mktemp -t "sq-pkcs11-git-shim.XXXXXX") || die "mktemp failed" +trap 'rm -f "$tmp"' EXIT +cat > "$tmp" + +# --output - streams the armored detached signature to stdout, which is +# exactly what git expects to read back. +"$sq" sign --key-label "$keyid" --output - "$tmp" + +# git's gpg-interface.c parses GnuPG's status protocol when --status-fd +# was supplied. Modern git (since ~2.31) also accepts a missing +# SIG_CREATED line and just checks our exit code, but emitting the line +# costs nothing and keeps older git versions happy. Format per +# GnuPG's doc/DETAILS: +# SIG_CREATED +# We don't have a meaningful fingerprint to expose here (the HSM does), +# so we emit a placeholder; git only matches against the keyword. +if [[ -n "$status_fd" ]]; then + printf '[GNUPG:] SIG_CREATED D 1 10 00 %d sq-pkcs11\n' "$(date -u +%s)" \ + >&"$status_fd" 2>/dev/null || true +fi diff --git a/release-tools/stage-release.sh b/release-tools/stage-release.sh index dd0ac706..c29943ae 100755 --- a/release-tools/stage-release.sh +++ b/release-tools/stage-release.sh @@ -24,45 +24,30 @@ Usage: stage-release.sh [ options ... ] It can only be given with --alpha. --beta Start or increase the "beta" pre-release tag. --final Get out of "alpha" or "beta" and make a final release. - Implies --branch. - ---branch Create a release branch 'openssl-{major}.{minor}', - where '{major}' and '{minor}' are the major and minor - version numbers. - ---clean-worktree - Expect the current worktree to be clean, and uses it directly. - This implies the current branch of the worktree will be updated. - ---branch-fmt= - Format for branch names. - Default is "%b" for the release branch. ---tag-fmt= Format for tag names. - Default is "%t" for the release tag. --reviewer= The reviewer of the commits. ---local-user= - For the purpose of signing tags and tar files, use this - key (default: use the default e-mail address’ key). +--key-label=