From 99ab18fb92aec6a2361b1906009ebc36af6f2d1b Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Thu, 7 May 2026 08:59:12 +0200 Subject: [PATCH 01/18] release-tools: add openssl-pgp wrapper Operator-facing helper for the release-artifact OpenPGP signing policy on top of sq-pkcs11 (which speaks PKCS#11 to the nShield HSM). Capabilities: - generate the policy primary key (RSA-4096, OCS-protected) and current signing subkey (RSA-4096, module-protected) via nShield generatekey - issue / rotate the published OpenPGP certificate (5-year primary, 1-year subkey, with --merge-cert for subkey rotation that preserves predecessor subkeys) - sign release artifacts with the current signing subkey - issue primary-key and subkey-revocation certificates Each command validates only the env vars it actually consumes (e.g. subkey-rotate doesn't demand OPENSSL_PGP_CURRENT_SUBKEY_LABEL), and every label / cardset value is checked against the Security World via cklist / nfkminfo before any HSM action runs, so a typo cannot leave a half-finished state on the HSM. Co-Authored-By: Claude Opus 4.7 (1M context) --- release-tools/openssl-pgp | 666 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 666 insertions(+) create mode 100755 release-tools/openssl-pgp diff --git a/release-tools/openssl-pgp b/release-tools/openssl-pgp new file mode 100755 index 00000000..ae38a934 --- /dev/null +++ b/release-tools/openssl-pgp @@ -0,0 +1,666 @@ +#!/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-label LABEL [--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 a subkey label from --subkey-label or + OPENSSL_PGP_CURRENT_SUBKEY_LABEL + 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) + 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 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}" +} + +# 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" +} + +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" "$@" +} + +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; ACS/OCS prompts may follow" + "$OPENSSL_PGP_GENERATEKEY" 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_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 cert-export \ + --key-uri "$primary_uri" --ocs \ + --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 cert-revoke \ + --key-uri "$primary_uri" --ocs \ + --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_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 cert-export \ + --merge-cert "$input" \ + --key-uri "$primary_uri" --ocs \ + --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" + local subkey_created + subkey_created=$(hsm_gentime "$OPENSSL_PGP_CURRENT_SUBKEY_LABEL") + + 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_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 cert-revoke \ + --key-uri "$primary_uri" --ocs \ + --creation-time "$primary_created" \ + --reason "$reason" \ + --message "$message" \ + --output "$output" + note "primary revocation written: $output" +} + +cmd_subkey_revoke() { + local label='' output='' reason=compromised message="signing subkey suspected compromised" + while [[ $# -gt 0 ]]; do + case "$1" in + --subkey-label) label=${2:?missing value for --subkey-label}; 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 + # subkey-revoke needs the primary (signs the revocation) and a subkey + # label. The label comes from --subkey-label, otherwise defaults to + # OPENSSL_PGP_CURRENT_SUBKEY_LABEL — which then must be set AND exist. + require_pkcs11_module + require_sq_pkcs11_binary + require_nshield_metadata_tools + require_hsm_label OPENSSL_PGP_PRIMARY_LABEL + verify_primary_cardset_exists + if [[ -z "$label" ]]; then + require_hsm_label OPENSSL_PGP_CURRENT_SUBKEY_LABEL + label=$OPENSSL_PGP_CURRENT_SUBKEY_LABEL + else + label_exists "$label" \ + || die "--subkey-label $label is not present in the Security World; run \"$OPENSSL_PGP_CKLIST -n --cka-label=$label\" to verify" + fi + [[ -n "$output" ]] || output="${label}-revocation.asc" + require_output_absent "$output" + verify_rsa4096_label "$OPENSSL_PGP_PRIMARY_LABEL" + verify_rsa4096_label "$label" + + local primary_created subkey_created + primary_created=$(hsm_gentime "$OPENSSL_PGP_PRIMARY_LABEL") + subkey_created=$(hsm_gentime "$label") + + local primary_uri + primary_uri=$(primary_key_uri) + + sq subkey-revoke \ + --key-uri "$primary_uri" --ocs \ + --subkey-label "$label" \ + --creation-time "$primary_created" \ + --subkey-creation-time "$subkey_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 "$@" From 3b9a73f1698fbbeab8abda5e1fb54ab5af658aca Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Thu, 7 May 2026 09:00:05 +0200 Subject: [PATCH 02/18] release-tools: add sq-pkcs11-git-shim A gpg-CLI-compatible signing shim that translates git's gpg.program invocation shape (gpg --status-fd=N -bsau < tagbody) into a sq-pkcs11 sign call against an HSM-resident key identified by CKA_LABEL. The shim drains stdin to a temp file (sq-pkcs11 takes a path, not stdin), runs sq-pkcs11 sign --output - so the armored signature streams straight back on stdout where git expects it, and emits a SIG_CREATED status line for older git versions that parse GnuPG's status protocol. Anything that isn't a sign operation (verify, decrypt, list-keys, ...) is forwarded to a real gpg via OPENSSL_PGP_FALLBACK_GPG (default "gpg"), so `git tag -v` and similar continue to work on the operator's machine. Used by stage-release.sh via --gpg-program; can also be configured directly with `git config gpg.program ` for non-release tag signing scenarios. Co-Authored-By: Claude Opus 4.7 (1M context) --- release-tools/sq-pkcs11-git-shim | 155 +++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100755 release-tools/sq-pkcs11-git-shim 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 From 53fa85967b2112b25d015cb23adf72eceb875f6d Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Thu, 7 May 2026 09:00:43 +0200 Subject: [PATCH 03/18] stage-release.sh: --gpg-program option for tag signing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add --gpg-program= to redirect git tag signing through a custom program by invoking `git -c gpg.program= tag -s ...`. Direct gpg invocations elsewhere in this script (tarball detached signature, announcement clearsign) are deliberately untouched; combine with --unsigned if those should be skipped and signed out-of-band. Primary motivation is to route tag signing through release-tools/sq-pkcs11-git-shim so the release tag can be signed by an HSM-resident private key without involving the local gpg keyring. The option is general — it's just a `gpg.program` override — so any gpg-CLI-compatible signer can be plugged in. When --gpg-program is used together with --local-user, the keyid form expected by --local-user follows whatever the configured program expects (key id / fingerprint for gpg, CKA_LABEL for the sq-pkcs11 shim). This is documented in the help text and the manual. Co-Authored-By: Claude Opus 4.7 (1M context) --- release-tools/stage-release.sh | 50 +++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/release-tools/stage-release.sh b/release-tools/stage-release.sh index dd0ac706..58bd6f3b 100755 --- a/release-tools/stage-release.sh +++ b/release-tools/stage-release.sh @@ -44,6 +44,14 @@ Usage: stage-release.sh [ options ... ] --local-user= For the purpose of signing tags and tar files, use this key (default: use the default e-mail address’ key). + The exact form of depends on the configured signer: + gpg expects a key id or fingerprint; the sq-pkcs11 git + shim (--gpg-program) expects a CKA_LABEL. +--gpg-program= + Override the program git uses to sign tags. Set this to + tools/release-tools/sq-pkcs11-git-shim to route tag + signing through sq-pkcs11 (HSM-backed). Direct gpg + invocations elsewhere in this script are unaffected. --unsigned Do not sign anything. --staging-address=
@@ -96,6 +104,11 @@ do_manual=false do_signed=true tagkey=' -s' gpgkey= +# When set, `git tag -s` is run with `gpg.program=$gpg_program`, redirecting +# tag signing through a custom signer (e.g. tools/release-tools/sq-pkcs11-git-shim +# for HSM-backed signing). Direct gpg invocations elsewhere in this script +# are unaffected by this setting. +gpg_program= reviewers= staging_address=upload@dev.openssl.org @@ -106,6 +119,7 @@ TEMP=$(getopt -l 'alpha,next-beta,beta,final' \ -l 'branch-fmt:,tag-fmt:' \ -l 'reviewer:' \ -l 'local-user:,unsigned' \ + -l 'gpg-program:' \ -l 'staging-address:' \ -l 'no-upload,no-update' \ -l 'quiet,verbose,debug' \ @@ -169,6 +183,11 @@ while true; do tagkey=" -a" gpgkey= ;; + --gpg-program ) + shift + gpg_program="$1" + shift + ;; --staging-address ) shift staging_address="$1" @@ -628,7 +647,12 @@ if [ -n "$reviewers" ]; then addrev --release --nopr $reviewers fi $ECHO "Tagging release with tag $release_tag. You may need to enter a pass phrase" -git tag$tagkey "$release_tag" -m "OpenSSL $release release tag" +if [ -n "$gpg_program" ] && $do_signed; then + git -c "gpg.program=$gpg_program" tag$tagkey "$release_tag" \ + -m "OpenSSL $release release tag" +else + git tag$tagkey "$release_tag" -m "OpenSSL $release release tag" +fi tarfile=openssl-$release.tar tgzfile=$tarfile.gz @@ -1017,6 +1041,7 @@ B<--clean-worktree> | B<--branch-fmt>=I | B<--tag-fmt>=I | B<--local-user>=I | +B<--gpg-program>=I | B<--unsigned> | B<--reviewer>=I | B<--staging-address>=I
| @@ -1160,6 +1185,29 @@ Use I as the local user for C and for signing with C. If not given, then the default e-mail address' key is used. +The form of I depends on the program performing the signing. When +the default C is used, I is a key id, fingerprint, or e-mail +address as understood by C. When B<--gpg-program> points at the +sq-pkcs11 shim (see below), I is the C of a private key +on the configured PKCS#11 token. + +=item B<--gpg-program>=I + +Override the program C uses to produce its OpenPGP signature. +By default C calls whatever C resolves to (typically +C). Setting this option redirects tag signing — and only tag +signing — to I. The most common use is + + --gpg-program=$TOOLS/release-tools/sq-pkcs11-git-shim + +which routes tag signing through C against an HSM-resident +private key identified by B<--local-user> as a C. + +Direct C invocations later in this script (tarball detached +signature, announcement clearsign) are not affected by this option; +combine with B<--unsigned> if those should be skipped and signed by +another tool out-of-band. + =item B<--unsigned> Do not sign the tarball or announcement file. This leaves it for other From f455ceb7758e1885b3815a0b0d712abc79e02ffe Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Thu, 7 May 2026 15:26:10 +0200 Subject: [PATCH 04/18] release-tools/openssl-pgp: subkey-revoke takes --subkey-fingerprint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sq-pkcs11's subkey-revoke API changed: instead of identifying the subkey by CKA_LABEL (which required HSM access to the subkey's private key — broken for the compromise scenario), the new shape takes --input-cert + --subkey-fingerprint and only exercises the primary's private key. The wrapper follows. Operational change: callers of \`openssl-pgp subkey-revoke\` now pass the subkey fingerprint they want revoked, looked up from the published cert via \`sq inspect release.asc\` or \`gpg --list-keys --with-subkey-fingerprint\`. The lost / compromised subkey path no longer requires the HSM to still hold its private key. Co-Authored-By: Claude Opus 4.7 (1M context) --- release-tools/openssl-pgp | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/release-tools/openssl-pgp b/release-tools/openssl-pgp index ae38a934..7efe3fe8 100755 --- a/release-tools/openssl-pgp +++ b/release-tools/openssl-pgp @@ -31,7 +31,7 @@ Usage: 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-label LABEL [--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, @@ -42,8 +42,9 @@ Required configuration (per command): 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 a subkey label from --subkey-label or - OPENSSL_PGP_CURRENT_SUBKEY_LABEL + 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) @@ -594,10 +595,16 @@ cmd_cert_revoke() { } cmd_subkey_revoke() { - local label='' output='' reason=compromised message="signing subkey suspected compromised" + # 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-label) label=${2:?missing value for --subkey-label}; shift 2 ;; + --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 ;; @@ -605,38 +612,28 @@ cmd_subkey_revoke() { *) die "unknown subkey-revoke argument: $1" ;; esac done - # subkey-revoke needs the primary (signs the revocation) and a subkey - # label. The label comes from --subkey-label, otherwise defaults to - # OPENSSL_PGP_CURRENT_SUBKEY_LABEL — which then must be set AND exist. + [[ -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_nshield_metadata_tools require_hsm_label OPENSSL_PGP_PRIMARY_LABEL verify_primary_cardset_exists - if [[ -z "$label" ]]; then - require_hsm_label OPENSSL_PGP_CURRENT_SUBKEY_LABEL - label=$OPENSSL_PGP_CURRENT_SUBKEY_LABEL - else - label_exists "$label" \ - || die "--subkey-label $label is not present in the Security World; run \"$OPENSSL_PGP_CKLIST -n --cka-label=$label\" to verify" - fi - [[ -n "$output" ]] || output="${label}-revocation.asc" + require_cert_exists "$input" + [[ -n "$output" ]] || output="${input%.asc}-subkey-revocation.asc" require_output_absent "$output" verify_rsa4096_label "$OPENSSL_PGP_PRIMARY_LABEL" - verify_rsa4096_label "$label" - local primary_created subkey_created + local primary_created primary_created=$(hsm_gentime "$OPENSSL_PGP_PRIMARY_LABEL") - subkey_created=$(hsm_gentime "$label") local primary_uri primary_uri=$(primary_key_uri) sq subkey-revoke \ --key-uri "$primary_uri" --ocs \ - --subkey-label "$label" \ + --input-cert "$input" \ + --subkey-fingerprint "$fingerprint" \ --creation-time "$primary_created" \ - --subkey-creation-time "$subkey_created" \ --reason "$reason" \ --message "$message" \ --output "$output" From d26538fd6f591419c772e9879618008ca65131d1 Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Thu, 7 May 2026 17:09:34 +0200 Subject: [PATCH 05/18] release-tools/openssl-pgp: gate sign on cert-bound signing-key check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this change, openssl-pgp sign would happily use whatever HSM key OPENSSL_PGP_CURRENT_SUBKEY_LABEL pointed at, as long as it was RSA-4096 and present on the HSM. A typo'd or stale label could therefore produce a "valid"-looking release signature that fails verification against the published cert — exactly the kind of error a release-engineering tool should refuse to make. Add a pre-flight call to `sq-pkcs11 verify-signing-key`, before any real signing operation, that confirms the configured HSM key is a current valid signer of $OPENSSL_PGP_CERT (alive, not revoked, signing-flagged binding under Sequoia's StandardPolicy). On mismatch, openssl-pgp refuses with a message naming the offending env var; no HSM signing operation is consumed. require_cert_exists $OPENSSL_PGP_CERT is also added — sign now needs a published cert on disk to validate against (it was previously unused on the sign path). Co-Authored-By: Claude Opus 4.7 (1M context) --- release-tools/openssl-pgp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/release-tools/openssl-pgp b/release-tools/openssl-pgp index 7efe3fe8..0412801e 100755 --- a/release-tools/openssl-pgp +++ b/release-tools/openssl-pgp @@ -532,9 +532,22 @@ cmd_sign() { 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 From f0a07955f2320d86de89d27585d3a4a1d1088ea3 Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Sat, 9 May 2026 06:33:33 +0200 Subject: [PATCH 06/18] release-tools: add openssl-pgp-ceremony-run A small wrapper invoked as `openssl-pgp-ceremony-run [args...]` that runs an OpenPGP ceremony command, tees its combined stdout/stderr to /ceremony.log, and persists the final exit status to /rc. The state directory acts as a handshake point between the orchestrator that starts the ceremony and the operator who attaches to type passphrases: the orchestrator polls for `rc` to know when the command has finished and what status it returned, while the log file gives post-hoc audit material. This commit introduces the bare runner shape only; the tmux-based session sharing for attended OCS passphrase entry comes in the next commit so the skeleton is reviewable on its own. --- release-tools/openssl-pgp-ceremony-run | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 release-tools/openssl-pgp-ceremony-run diff --git a/release-tools/openssl-pgp-ceremony-run b/release-tools/openssl-pgp-ceremony-run new file mode 100644 index 00000000..defa8651 --- /dev/null +++ b/release-tools/openssl-pgp-ceremony-run @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +state_dir=$1 +shift + +mkdir -p "$state_dir" +chmod 0770 "$state_dir" + +log="$state_dir/ceremony.log" +rc_file="$state_dir/rc" + +rm -f "$rc_file" + +{ + date -u '+started=%Y-%m-%dT%H:%M:%SZ' + echo "command=$*" + echo + + set +e + "$@" + rc=$? + set -e + + echo + date -u '+finished=%Y-%m-%dT%H:%M:%SZ' + echo "rc=$rc" + + printf '%s\n' "$rc" > "$rc_file" + exit "$rc" +} 2>&1 | tee "$log" From a48214ea04e6c79cbe8ec5b17a1d62649b1191d7 Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Sat, 9 May 2026 21:27:07 +0200 Subject: [PATCH 07/18] release-tools/openssl-pgp-ceremony-run: add tmux start-and-wait orchestration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure the runner into two subcommands: start-and-wait: invoked by an automation server (Jenkins agent). Creates a detached tmux session on the HSM client via a named socket, runs the requested OpenPGP command inside that session, prints attach instructions, and waits for the command's exit status (polls the state directory's rc file with a configurable --timeout). exec: the tmux-side runner that actually executes the command, records output to ceremony.log, and writes the exit status to rc. Equivalent to the original single-mode behaviour and used as the tmux session command by start-and-wait. This split 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 typed into the shared tmux terminal by custodians (over SSH + nShield Remote Administration / TVD), never passed through CI parameters, credentials, or a web UI. The --allow-user flag uses `tmux server-access -a/-w` to grant named users read/write access to the session via the shared socket. Socket permissions are set to 0660 with the group inherited from the socket directory so attaching from a fixed Unix group is sufficient — no chmod 0666 / world-readable sockets. --- release-tools/openssl-pgp-ceremony-run | 258 +++++++++++++++++++++++-- 1 file changed, 238 insertions(+), 20 deletions(-) mode change 100644 => 100755 release-tools/openssl-pgp-ceremony-run diff --git a/release-tools/openssl-pgp-ceremony-run b/release-tools/openssl-pgp-ceremony-run old mode 100644 new mode 100755 index defa8651..30ccc92d --- a/release-tools/openssl-pgp-ceremony-run +++ b/release-tools/openssl-pgp-ceremony-run @@ -1,31 +1,249 @@ #!/usr/bin/env bash set -euo pipefail -state_dir=$1 -shift +# 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. -mkdir -p "$state_dir" -chmod 0770 "$state_dir" +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...] -log="$state_dir/ceremony.log" -rc_file="$state_dir/rc" +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. -rm -f "$rc_file" +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 +} -{ - date -u '+started=%Y-%m-%dT%H:%M:%SZ' - echo "command=$*" - echo +die() { + printf 'error: %s\n' "$*" >&2 + exit 2 +} - set +e - "$@" - rc=$? - set -e +require_nonempty() { + local value=$1 + local name=$2 - echo - date -u '+finished=%Y-%m-%dT%H:%M:%SZ' - echo "rc=$rc" + [[ -n $value ]] || die "$name must not be empty" +} - printf '%s\n' "$rc" > "$rc_file" +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 + + for user in "${allow_users[@]}"; do + 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" -} 2>&1 | tee "$log" +} + +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 "$@" From cdfa6157fbd49276b1bdc591870e372a258a9261 Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Sat, 9 May 2026 22:02:04 +0200 Subject: [PATCH 08/18] release-tools/openssl-pgp: run generatekey in batch mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch generatekey_pkcs11 to invoke `generatekey --generate --batch pkcs11 ...` so the operator is not prompted to confirm each parameter on stdin before the OCS prompts appear. Two scenarios where this matters: - Automated cert-init runs from Jenkins, where there is no interactive operator at the generatekey step; the wrapper has already validated every parameter (labels, cardset existence, key-size, logkeyusage policy) before calling generatekey. - Tmux-shared ceremonies via openssl-pgp-ceremony-run, where the expected operator interaction is OCS card presentation and passphrase entry — not parameter confirmation. Without --batch the custodians would have to press Enter through generatekey's parameter-review prompt before the card prompts appeared, cluttering the shared terminal with non-decision input. The arguments fed into generatekey come exclusively from the wrapper (constructed from validated env vars and CLI flags), so the interactive confirmation step --batch suppresses is redundant in this context. The note() string is updated to reflect the new flag for parity with what the operator will see in the ceremony log. --- release-tools/openssl-pgp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/release-tools/openssl-pgp b/release-tools/openssl-pgp index 0412801e..6aa4c906 100755 --- a/release-tools/openssl-pgp +++ b/release-tools/openssl-pgp @@ -64,6 +64,7 @@ Optional configuration: 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 @@ -306,8 +307,8 @@ default_sig_path() { generatekey_pkcs11() { require_generatekey - note "running nShield generatekey; ACS/OCS prompts may follow" - "$OPENSSL_PGP_GENERATEKEY" pkcs11 "$@" + note "running nShield generatekey in batch mode; ACS/OCS prompts may follow" + "$OPENSSL_PGP_GENERATEKEY" --generate --batch pkcs11 "$@" } append_generate_module_arg() { From e89cf28b385a8d2f787b86909a2a0aa64618c58b Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Mon, 11 May 2026 11:00:28 +0200 Subject: [PATCH 09/18] release-tools: add openssl-pgp-revocation-recipients bundle helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A helper invoked as `openssl-pgp-revocation-recipients bundle --source --bundle --manifest --bundle-sha256 ` that collects the trusted set of public OpenPGP certificates authorised to decrypt the offline primary-key revocation certificate produced by cert-init. The source directory contains: - recipients.txt — authoritative manifest, one "<40-hex fingerprint> " line per recipient - .pgp — one file per manifest entry, the recipient's public certificate For each manifest entry the helper: - confirms the cert file exists and is non-empty - runs `sq inspect` and asserts both the declared fingerprint and User ID email are present in the cert (catches stale fingerprints or accidentally-swapped files) - test-encrypts a probe message via `sq encrypt --for-file` to confirm the cert is currently usable for encryption under sq's policy (catches expired keys, policy-rejected ciphersuites, etc.) - appends the cert to the output bundle and the (fingerprint, email) pair to the output manifest, finally writing the bundle's SHA-256 The cert-init Jenkins pipeline runs this helper before the ceremony so the encrypted offline-revocation artifact can only be produced if the recipient set is fully valid — no half-encrypted artifacts slipping through if one recipient's key has expired or been withdrawn since the last run. Three initial recipients are checked in under release-tools/openpgp/revocation-recipients/ with a README documenting the manifest format and the validation contract above. --- ...4C02E813889057DA2F3FDBEDDD4C5DAA149BBE.pgp | 210 +++++++++++++++++ ...1FAB74B0088AA361152586B8EF1A6BA9DA2D5C.pgp | 115 ++++++++++ ...DDFC774D9B306B3B3F1D956F3A67A0A75C7C08.pgp | 78 +++++++ .../openpgp/revocation-recipients/README.md | 22 ++ .../revocation-recipients/recipients.txt | 5 + .../openssl-pgp-revocation-recipients | 215 ++++++++++++++++++ 6 files changed, 645 insertions(+) create mode 100644 release-tools/openpgp/revocation-recipients/134C02E813889057DA2F3FDBEDDD4C5DAA149BBE.pgp create mode 100644 release-tools/openpgp/revocation-recipients/A21FAB74B0088AA361152586B8EF1A6BA9DA2D5C.pgp create mode 100644 release-tools/openpgp/revocation-recipients/CCDDFC774D9B306B3B3F1D956F3A67A0A75C7C08.pgp create mode 100644 release-tools/openpgp/revocation-recipients/README.md create mode 100644 release-tools/openpgp/revocation-recipients/recipients.txt create mode 100755 release-tools/openssl-pgp-revocation-recipients 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-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 "$@" From 0e99f098d1a0cce46b134b48ed2daccde3246a0e Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Mon, 11 May 2026 11:10:55 +0200 Subject: [PATCH 10/18] release-tools/openssl-pgp-ceremony-run: skip server owner in access grant `tmux server-access -a ` fails when invoked for the user who owns the running tmux server: the server starts that user implicitly in the allow-list, and tmux refuses to add a duplicate entry. In the ceremony runner, this manifested as a hard failure of the access-grant loop the moment --allow-user named the CI agent's own account. Detect the case (compare each --allow-user value to `id -un`) and skip the redundant grant. The loop then continues to grant access to the remaining custodians, leaving the session alive. Without this fix, an operator list that included the server-owning account would kill the session and bail on what is structurally a no-op. --- release-tools/openssl-pgp-ceremony-run | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/release-tools/openssl-pgp-ceremony-run b/release-tools/openssl-pgp-ceremony-run index 30ccc92d..c2192ac0 100755 --- a/release-tools/openssl-pgp-ceremony-run +++ b/release-tools/openssl-pgp-ceremony-run @@ -180,7 +180,13 @@ run_start_and_wait() { 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" From dbb9f021380ca424d3d3fbf063261c8912bc7dbc Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Mon, 11 May 2026 17:06:13 +0200 Subject: [PATCH 11/18] openssl-pgp: route OCS quorum operations through preload --- release-tools/openssl-pgp | 54 +++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/release-tools/openssl-pgp b/release-tools/openssl-pgp index 6aa4c906..66a2a236 100755 --- a/release-tools/openssl-pgp +++ b/release-tools/openssl-pgp @@ -57,6 +57,10 @@ Optional configuration: 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 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) @@ -117,6 +121,7 @@ default_config() { : "${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 @@ -133,6 +138,18 @@ require_sq_pkcs11_binary() { || 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" } @@ -200,6 +217,19 @@ 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. +sq_with_preload() { + require_primary_cardset + PKCS11_MODULE_PATH=$PKCS11_MODULE_PATH \ + "$OPENSSL_PGP_PRELOAD" -c "$OPENSSL_PGP_PRIMARY_CARDSET" -- \ + "$OPENSSL_PGP_SQ_PKCS11" "$@" +} + cklist_for_label() { local label=$1 "$OPENSSL_PGP_CKLIST" -n --cka-label="$label" @@ -405,6 +435,7 @@ cmd_cert_init() { # 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" @@ -435,8 +466,8 @@ cmd_cert_init() { local primary_uri primary_uri=$(primary_key_uri) - sq cert-export \ - --key-uri "$primary_uri" --ocs \ + sq_with_preload cert-export \ + --key-uri "$primary_uri" \ --subkey-label "$OPENSSL_PGP_CURRENT_SUBKEY_LABEL" \ --userid "$OPENSSL_PGP_USERID" \ --creation-time "$primary_created" \ @@ -445,8 +476,8 @@ cmd_cert_init() { --subkey-validity-period 1y \ --output "$output" - sq cert-revoke \ - --key-uri "$primary_uri" --ocs \ + 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" \ @@ -477,6 +508,7 @@ cmd_subkey_rotate() { # 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 @@ -497,9 +529,9 @@ cmd_subkey_rotate() { local primary_uri primary_uri=$(primary_key_uri) - sq cert-export \ + sq_with_preload cert-export \ --merge-cert "$input" \ - --key-uri "$primary_uri" --ocs \ + --key-uri "$primary_uri" \ --subkey-label "$new_label" \ --creation-time "$primary_created" \ --subkey-creation-time "$subkey_created" \ @@ -588,6 +620,7 @@ cmd_cert_revoke() { # 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 @@ -599,8 +632,8 @@ cmd_cert_revoke() { local primary_uri primary_uri=$(primary_key_uri) - sq cert-revoke \ - --key-uri "$primary_uri" --ocs \ + sq_with_preload cert-revoke \ + --key-uri "$primary_uri" \ --creation-time "$primary_created" \ --reason "$reason" \ --message "$message" \ @@ -629,6 +662,7 @@ cmd_subkey_revoke() { [[ -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 @@ -643,8 +677,8 @@ cmd_subkey_revoke() { local primary_uri primary_uri=$(primary_key_uri) - sq subkey-revoke \ - --key-uri "$primary_uri" --ocs \ + sq_with_preload subkey-revoke \ + --key-uri "$primary_uri" \ --input-cert "$input" \ --subkey-fingerprint "$fingerprint" \ --creation-time "$primary_created" \ From fb98905c1d98d3d7a93ddb5f66ca3b6d1e16541a Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Mon, 11 May 2026 17:21:42 +0200 Subject: [PATCH 12/18] openssl-pgp: pin preload to module 1 by default --- release-tools/openssl-pgp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/release-tools/openssl-pgp b/release-tools/openssl-pgp index 66a2a236..8a0051ae 100755 --- a/release-tools/openssl-pgp +++ b/release-tools/openssl-pgp @@ -61,6 +61,10 @@ Optional configuration: 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) @@ -223,10 +227,23 @@ sq() { # 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" -c "$OPENSSL_PGP_PRIMARY_CARDSET" -- \ + "$OPENSSL_PGP_PRELOAD" "${preload_args[@]}" -c "$OPENSSL_PGP_PRIMARY_CARDSET" -- \ "$OPENSSL_PGP_SQ_PKCS11" "$@" } From cc5708e9e5e88f2742b7e110865dbe3d9399a789 Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Wed, 13 May 2026 14:52:05 +0200 Subject: [PATCH 13/18] stage-release.sh: route all signing through openssl-pgp The "Signing the release files" step shelled out to gpg, which fails on Jenkins agents that do not have the release private key in a local gpg keyring -- the key lives on the HSM. Replace the two gpg invocations (tarball detached signature, announcement clearsign) with calls to the openssl-pgp wrapper, which signs via sq-pkcs11 against the HSM key. Also default --gpg-program to release-tools/sq-pkcs11-git-shim when not explicitly set, so git tag signing goes through the HSM by the same mechanism; the flag is still overridable (e.g. --gpg-program=gpg) for operators using a local gpg keyring. --local-user now also exports OPENSSL_PGP_CURRENT_SUBKEY_LABEL so the openssl-pgp subprocesses pick up the same key without a second env-var hop. openssl-pgp / sq-pkcs11 produce detached, ASCII-armored signatures only, so the announcement is no longer cleartext-signed. Both the plain .txt and the detached .txt.asc are uploaded; FILES section of the manual is updated to reflect this. Co-Authored-By: Claude Opus 4.7 (1M context) --- release-tools/stage-release.sh | 105 +++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/release-tools/stage-release.sh b/release-tools/stage-release.sh index 58bd6f3b..ff8db740 100755 --- a/release-tools/stage-release.sh +++ b/release-tools/stage-release.sh @@ -42,16 +42,21 @@ Usage: stage-release.sh [ options ... ] --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). - The exact form of depends on the configured signer: - gpg expects a key id or fingerprint; the sq-pkcs11 git - shim (--gpg-program) expects a CKA_LABEL. + For the purpose of signing tags and release artifacts, + use this key. The default signer is openssl-pgp (and + the sq-pkcs11-git-shim for tag signing), so is + a CKA_LABEL on the configured HSM. When --gpg-program + points at a classic gpg, follows gpg's rules + (key id, fingerprint, or e-mail). + Also exported as OPENSSL_PGP_CURRENT_SUBKEY_LABEL so + openssl-pgp picks up the same key for artifact signing. --gpg-program= - Override the program git uses to sign tags. Set this to - tools/release-tools/sq-pkcs11-git-shim to route tag - signing through sq-pkcs11 (HSM-backed). Direct gpg - invocations elsewhere in this script are unaffected. + Override the program git uses to sign tags. Defaults + to tools/release-tools/sq-pkcs11-git-shim, which routes + tag signing through sq-pkcs11 (HSM-backed). Set to + "gpg" to fall back to a local gpg keyring. Tarball and + announcement signing always go through openssl-pgp and + are not affected by this option. --unsigned Do not sign anything. --staging-address=
@@ -103,11 +108,10 @@ do_manual=false do_signed=true tagkey=' -s' -gpgkey= -# When set, `git tag -s` is run with `gpg.program=$gpg_program`, redirecting -# tag signing through a custom signer (e.g. tools/release-tools/sq-pkcs11-git-shim -# for HSM-backed signing). Direct gpg invocations elsewhere in this script -# are unaffected by this setting. +# gpg.program for `git tag -s`. Defaults below to sq-pkcs11-git-shim +# (HSM-backed signing) once $RELEASE_TOOLS is known; override with +# --gpg-program to use a different signer. Tarball and announcement +# signing always go through openssl-pgp and are unaffected by this. gpg_program= reviewers= @@ -174,14 +178,15 @@ while true; do shift do_signed=true tagkey=" -u $1" - gpgkey=" -u $1" + # openssl-pgp reads the signing-key label from the env, so a + # CLI override needs to propagate to subprocess env too. + export OPENSSL_PGP_CURRENT_SUBKEY_LABEL="$1" shift ;; --unsigned ) shift do_signed=false tagkey=" -a" - gpgkey= ;; --gpg-program ) shift @@ -290,7 +295,9 @@ RELEASE_AUX="$RELEASE_TOOLS/release-aux" # Check that we have external scripts that we use found=true -for fn in "$RELEASE_TOOLS/do-copyright-year"; do +for fn in "$RELEASE_TOOLS/do-copyright-year" \ + "$RELEASE_TOOLS/openssl-pgp" \ + "$RELEASE_TOOLS/sq-pkcs11-git-shim"; do if ! [ -f "$fn" ]; then echo >&2 "'$fn' is missing" found=false @@ -300,6 +307,13 @@ if ! $found; then exit 1 fi +# Default tag-signing shim. sq-pkcs11-git-shim adapts git's gpg-CLI +# expectations to sq-pkcs11 (HSM-backed signing); --gpg-program can +# override (e.g. --gpg-program=gpg to fall back to a local keyring). +if [ -z "$gpg_program" ]; then + gpg_program="$RELEASE_TOOLS/sq-pkcs11-git-shim" +fi + # Check that we have the scripts that define functions we use found=true for fn in "$RELEASE_AUX/release-version-fn.sh" \ @@ -647,7 +661,7 @@ if [ -n "$reviewers" ]; then addrev --release --nopr $reviewers fi $ECHO "Tagging release with tag $release_tag. You may need to enter a pass phrase" -if [ -n "$gpg_program" ] && $do_signed; then +if $do_signed; then git -c "gpg.program=$gpg_program" tag$tagkey "$release_tag" \ -m "OpenSSL $release release tag" else @@ -705,10 +719,10 @@ cat "$RELEASE_AUX/$announce_template" \ $VERBOSE "== Generating signatures: $tgzfile.asc $announce.asc" rm -f "../$tgzfile.asc" "../$announce.asc" -$ECHO "Signing the release files. You may need to enter a pass phrase" if $do_signed; then - gpg$gpgkey --use-agent -sba "../$tgzfile" - gpg$gpgkey --use-agent -sta --clearsign "../$announce" + $ECHO "Signing the release files via openssl-pgp." + "$RELEASE_TOOLS/openssl-pgp" sign "../$tgzfile" + "$RELEASE_TOOLS/openssl-pgp" sign "../$announce" fi if ! $clean_worktree; then @@ -719,7 +733,7 @@ fi if $do_signed; then staging_files=( "$tgzfile" "$tgzfile.sha1" "$tgzfile.sha256" - "$tgzfile.asc" "$announce.asc" ) + "$tgzfile.asc" "$announce" "$announce.asc" ) else staging_files=( "$tgzfile" "$tgzfile.sha1" "$tgzfile.sha256" "$announce" ) fi @@ -1181,32 +1195,30 @@ means retagging a release commit manually as well. =item B<--local-user>=I -Use I as the local user for C and for signing with C. +Use I as the local user for C and as the signing key +for the C wrapper. The value is also exported as +C so artifact signing picks up the +same key without an extra environment hop. -If not given, then the default e-mail address' key is used. - -The form of I depends on the program performing the signing. When -the default C is used, I is a key id, fingerprint, or e-mail -address as understood by C. When B<--gpg-program> points at the -sq-pkcs11 shim (see below), I is the C of a private key -on the configured PKCS#11 token. +The form of I depends on the configured signer. With the +default tag-signing shim (C) and with the +C wrapper, I is the C of a private +key on the configured PKCS#11 token. When B<--gpg-program> points +at a classic C, I is a key id, fingerprint, or e-mail +address as understood by C. =item B<--gpg-program>=I -Override the program C uses to produce its OpenPGP signature. -By default C calls whatever C resolves to (typically -C). Setting this option redirects tag signing — and only tag -signing — to I. The most common use is - - --gpg-program=$TOOLS/release-tools/sq-pkcs11-git-shim - -which routes tag signing through C against an HSM-resident -private key identified by B<--local-user> as a C. +Override the program C uses to produce its OpenPGP +signature. By default this script sets it to +C<$TOOLS/release-tools/sq-pkcs11-git-shim>, which routes tag signing +through C against an HSM-resident private key identified +by B<--local-user> as a C. Override with C<--gpg-program=gpg> +to fall back to a local C keyring. -Direct C invocations later in this script (tarball detached -signature, announcement clearsign) are not affected by this option; -combine with B<--unsigned> if those should be skipped and signed by -another tool out-of-band. +Tarball and announcement signing always run through C +and are unaffected by this option; combine with B<--unsigned> if +those should be skipped and signed out-of-band by another tool. =item B<--unsigned> @@ -1378,9 +1390,12 @@ The SHA1 and SHA256 checksums for F. The detached PGP signature for F. -=item F +=item F, F -The announcement text, clear signed with PGP. +The announcement text and its detached PGP signature. Earlier +versions of this script clear-signed the announcement; with the +move to C (which does not support cleartext signatures) +the signature is now detached, so both files are uploaded. =item F From eb7d738fce9463d24ebdb1400c66abd924d5e988 Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Wed, 13 May 2026 15:45:44 +0200 Subject: [PATCH 14/18] stage-release.sh: drop unused CLI flags and upload machinery The script grew options over the years that the Jenkins release job does not exercise. Remove them and the code paths they gated: --clean-worktree The script now always operates on the current worktree (refusing to run if dirty). Drops the sibling-clone setup and the "cd $release_clone" dance, plus the three `git push parent HEAD` back-pushes to the original repo. --branch-fmt Branch and tag names follow the standard scheme --tag-fmt (%b / %t). The two formats were always identical under --clean-worktree, so format_string and release-aux/string-fn.sh are no longer needed. --staging-address No more uploads from this script -- artifacts --no-upload land in the parent directory and the caller is responsible for shipping them. Drops the staging_address parser and the upload-fn.sh backends entirely. --no-update `make update` and `make update-fips-checksums` always run. --force The branch-name check is now strict. The --debug flag no longer implies --no-upload (no upload step exists). The metadata file's `upload_files` key becomes `release_files`; the now-impossible staging_update_branch / staging_release_branch keys are removed. POD manual is trimmed to match. The cleanup leaves the script ~400 lines shorter and the supported invocation matches what the Jenkins job actually uses: stage-release.sh \\ $ALPHA_FLAG $BETA_FLAG $FINAL_FLAG \\ --branch \\ --gpg-program="$GPG_PROGRAM" \\ --local-user="$SIGNING_KEY_LABEL" \\ --reviewer="$REVIEWER_1" --reviewer="$REVIEWER_2" Co-Authored-By: Claude Opus 4.7 (1M context) --- release-tools/stage-release.sh | 535 ++++----------------------------- 1 file changed, 57 insertions(+), 478 deletions(-) diff --git a/release-tools/stage-release.sh b/release-tools/stage-release.sh index ff8db740..38230185 100755 --- a/release-tools/stage-release.sh +++ b/release-tools/stage-release.sh @@ -30,16 +30,6 @@ Usage: stage-release.sh [ options ... ] 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 release artifacts, @@ -59,23 +49,21 @@ Usage: stage-release.sh [ options ... ] are not affected by this option. --unsigned Do not sign anything. ---staging-address=
- The staging location to upload release files to (default: - upload@dev.openssl.org) ---no-upload Don't upload the staging release files. ---no-update Don't perform 'make update' and 'make update-fips-checksums'. --quiet Really quiet, only the final output will still be output. --verbose Verbose output. ---debug Include debug output. Implies --no-upload. +--debug Include debug output. --porcelain Give the output in an easy-to-parse format for scripts. ---force Force execution - --help This text --manual The manual If none of --alpha, --beta, or --final are given, this script tries to figure out the next step. + +The worktree this script runs in must be clean -- the script operates on +the current branch directly and produces release artifacts in the parent +directory. Upload of those artifacts is out of scope; the caller is +responsible for shipping them. EOF exit 0 } @@ -87,22 +75,12 @@ next_method2= do_branch=false warn_branch=false -do_upload=true -do_update=true - -clean_worktree=false - -default_branch_fmt='OSSL--%b--%v' -default_tag_fmt='%t' - ECHO=echo DEBUG=: VERBOSE=: git_quiet=-q do_porcelain=false -force=false - do_help=false do_manual=false @@ -115,20 +93,13 @@ tagkey=' -s' gpg_program= reviewers= -staging_address=upload@dev.openssl.org - TEMP=$(getopt -l 'alpha,next-beta,beta,final' \ -l 'branch' \ - -l 'clean-worktree' \ - -l 'branch-fmt:,tag-fmt:' \ -l 'reviewer:' \ -l 'local-user:,unsigned' \ -l 'gpg-program:' \ - -l 'staging-address:' \ - -l 'no-upload,no-update' \ -l 'quiet,verbose,debug' \ -l 'porcelain' \ - -l 'force' \ -l 'help,manual' \ -n stage-release.sh -- - "$@") eval set -- "$TEMP" @@ -153,22 +124,6 @@ while true; do warn_branch=true shift ;; - --clean-worktree ) - clean_worktree=true - default_branch_fmt='%b' - default_tag_fmt='%t' - shift - ;; - --branch-fmt ) - shift - branch_fmt="$1" - shift - ;; - --tag-fmt ) - shift - tag_fmt="$1" - shift - ;; --reviewer ) reviewers="$reviewers $1=$2" shift @@ -193,19 +148,6 @@ while true; do gpg_program="$1" shift ;; - --staging-address ) - shift - staging_address="$1" - shift - ;; - --no-upload ) - do_upload=false - shift - ;; - --no-update ) - do_update=false - shift - ;; --quiet ) ECHO=: VERBOSE=: @@ -219,17 +161,12 @@ while true; do ;; --debug ) DEBUG=echo - do_upload=false shift ;; --porcelain ) do_porcelain=true shift ;; - --force ) - force=true - shift - ;; --help ) usage exit 0 @@ -254,16 +191,11 @@ while true; do esac done -if [ -z "$branch_fmt" ]; then branch_fmt="$default_branch_fmt"; fi -if [ -z "$tag_fmt" ]; then tag_fmt="$default_tag_fmt"; fi - $DEBUG >&2 "DEBUG: \$next_method=$next_method" $DEBUG >&2 "DEBUG: \$next_method2=$next_method2" $DEBUG >&2 "DEBUG: \$do_branch=$do_branch" -$DEBUG >&2 "DEBUG: \$do_upload=$do_upload" -$DEBUG >&2 "DEBUG: \$do_update=$do_update" $DEBUG >&2 "DEBUG: \$DEBUG=$DEBUG" $DEBUG >&2 "DEBUG: \$VERBOSE=$VERBOSE" $DEBUG >&2 "DEBUG: \$git_quiet=$git_quiet" @@ -318,9 +250,7 @@ fi found=true for fn in "$RELEASE_AUX/release-version-fn.sh" \ "$RELEASE_AUX/release-state-fn.sh" \ - "$RELEASE_AUX/release-data-fn.sh" \ - "$RELEASE_AUX/string-fn.sh" \ - "$RELEASE_AUX/upload-fn.sh"; do + "$RELEASE_AUX/release-data-fn.sh"; do if ! [ -f "$fn" ]; then echo >&2 "'$fn' is missing" found=false @@ -334,10 +264,6 @@ fi . $RELEASE_AUX/release-version-fn.sh . $RELEASE_AUX/release-state-fn.sh . $RELEASE_AUX/release-data-fn.sh -# Load string manipulation functions -. $RELEASE_AUX/string-fn.sh -# Load upload backend functions -. $RELEASE_AUX/upload-fn.sh # Make sure we're in the work directory, and remember it if HERE=$(git rev-parse --show-toplevel); then @@ -373,8 +299,6 @@ if (echo "$orig_branch" \ -e '^OpenSSL_[0-9]+_[0-9]+_[0-9]+[a-z]*-stable$' \ -e '^openssl-[0-9]+\.[0-9]+$'); then : -elif $force; then - : else echo >&2 "Not in master or any recognised release branch" echo >&2 "Please 'git checkout' an appropriate branch" @@ -401,69 +325,16 @@ if ! $found; then exit 1 fi -# We turn staging_address into a few variables, which can be used -# by backends that must understand a subset of the SFTP commands -staging_directory= -staging_backend= -case "$staging_address" in - *:* ) - # Something with a colon is interpreted as the typical SCP - # location. We reinterpret that in our terms - staging_directory="${staging_address#*:}" - staging_address="${staging_address%%:*}" - staging_backend=sftp - ;; - *@* ) - staging_backend=sftp - ;; - sftp://?*/* | sftp://?* ) - # First, remove the URI scheme - staging_address="${staging_address#sftp://}" - # Now we know that we have a host, followed by a slash, followed by - # a directory spec. If there is no slash, there's no directory. - staging_directory="${staging_address#*/}" - if [ "$staging_directory" = "$staging_address" ]; then - # There was nothing with a slash to remove, so no directory. - staging_directory= - fi - staging_address="${staging_address%%/*}" - staging_backend=sftp - ;; - sftp:* ) - echo >&2 "Invalid staging address $staging_address" - exit 1 - ;; - * ) - if $do_upload && ! [ -d "$staging_address" ]; then - echo >&2 "Not an existing directory: $staging_address" - exit 1 - fi - staging_backend=file - ;; -esac - # Initialize ######################################################### $ECHO "== Initializing work tree" -release_clone= -if $clean_worktree; then - if [ -n "$(git status -s)" ]; then - echo >&2 "You've specified --clean-worktree, but your worktree is unclean" - exit 1 - fi -else - # Generate a cloned directory name - release_clone="$orig_branch-release-tmp" - - $ECHO "== Work tree will be in $release_clone" - - # Make a clone in a subdirectory and move there - if ! [ -d "$release_clone" ]; then - $VERBOSE "== Cloning to $release_clone" - git clone $git_quiet -b "$orig_branch" -o parent . "$release_clone" - fi - cd "$release_clone" +# This script operates on the current branch directly, so refuse to run +# if the worktree is dirty. Jenkins jobs run in a fresh workspace, so +# in practice this is just a sanity check. +if [ -n "$(git status -s)" ]; then + echo >&2 "Worktree is not clean; refusing to run" + exit 1 fi get_version @@ -500,29 +371,13 @@ else orig_release_branch="$orig_update_branch" fi -# Check that the current branch is still on the same branch as our parent repo, -# or on a release branch +# Sanity check: we should still be on the branch the worktree started +# on (or, after a --branch switch later in the script, on the release +# branch). current_branch=$(git rev-parse --abbrev-ref HEAD) -if [ "$current_branch" = "$orig_update_branch" ]; then - : -elif [ "$current_branch" = "$orig_release_branch" ]; then - : -else - # It is an error to end up here. Let's try to figure out what went wrong - - if $clean_worktree; then - # We should never get here. If we do, something is incorrect in - # the code above. - echo >&2 "Unexpected current branch: $current_branch" - else - echo >&2 "The cloned sub-directory '$release_clone' is on a branch" - if [ "$orig_update_branch" = "$orig_release_branch" ]; then - echo >&2 "other than '$orig_update_branch'." - else - echo >&2 "other than '$orig_update_branch' or '$orig_release_branch'." - fi - echo >&2 "Please 'cd \"$(pwd)\"; git checkout $orig_update_branch'" - fi +if [ "$current_branch" != "$orig_update_branch" ] \ + && [ "$current_branch" != "$orig_release_branch" ]; then + echo >&2 "Unexpected current branch: $current_branch" exit 1 fi @@ -533,8 +388,7 @@ $DEBUG >&2 "DEBUG: Source directory is $SOURCEDIR" # We always expect to start from a state of development if [ "$TYPE" != 'dev' ]; then - if $clean_worktree; then - cat >&2 <&2 <&2 < ../$metadata -if $do_upload; then - $ECHO "== Upload tar, hash, announcement and metadata files to staging location" -fi - -( - # With sftp, the progress meter is enabled by default, - # so we turn it off unless --verbose was given - if [ "$VERBOSE" == ':' ]; then - echo "progress" - fi - if [ -n "$staging_directory" ]; then - echo "cd $staging_directory" - fi - for uf in "${staging_files[@]}" "$metadata"; do - echo "put ../$uf" - done -) | upload_backend_$staging_backend "$staging_address" $do_upload - # Post-release ####################################################### # Reset the files to their pre-release contents. This doesn't affect @@ -824,12 +622,6 @@ if [ -n "$reviewers" ]; then addrev --release --nopr $reviewers fi -if ! $clean_worktree; then - # Push everything to the parent repo - $VERBOSE "== Push what we have to the parent repository" - git push parent HEAD -fi - if [ "$release_branch" != "$update_branch" ]; then $VERBOSE "== Going back to the update branch $update_branch" git checkout $git_quiet "$update_branch" @@ -861,48 +653,27 @@ if [ "$release_branch" != "$update_branch" ]; then fi fi -if ! $clean_worktree; then - # Push everything to the parent repo - $VERBOSE "== Push what we have to the parent repository" - git push parent HEAD -fi - # Done ############################################################### $VERBOSE "== Done" cd $HERE if $do_porcelain; then - if [ -n "$release_clone" ]; then - echo "clone_directory='$release_clone'" - fi echo "orig_head='$orig_head'" echo "metadata='$metadata'" else cat < | B<--beta> | B<--final> | B<--branch> | -B<--clean-worktree> | -B<--branch-fmt>=I | -B<--tag-fmt>=I | B<--local-user>=I | B<--gpg-program>=I | B<--unsigned> | B<--reviewer>=I | -B<--staging-address>=I
| -B<--no-upload> | -B<--no-update> | B<--quiet> | B<--verbose> | B<--debug> | @@ -1085,20 +772,13 @@ next. When B<--porcelain> is given, it finishes off with script friendly data instead, see the description of that option. When finishing commands are given, they must be followed exactly. -B normally leaves behind a clone of the local repository, -as a subdirectory in the current worktree, as well as an extra branch with -the results of running this script in the local repository. This extra -branch is useful to create a pull request from, which will also be mentioned -at the end of the run of B. This local clone subdirectory -as well as this extra branch can safely be removed after all instructions -have been successfully followed. - -When the option B<--clean-worktree> is given, B has a -different behaviour. In this case, it doesn't create that clone or any -extra branch, and it will update the current branch of the worktree -directly. This is useful when it's desirable to push the changes directly -to a remote repository without having to go through a pull request and -approval process. +B operates on the current worktree directly: it +refuses to run if the worktree is not clean, and updates the current +branch in place. The release artifacts (tarball, hashes, signature, +announcement, metadata) are written to the parent directory. Pushing +the resulting commits and tag, and shipping the artifacts, are the +caller's responsibility -- nothing is uploaded or pushed by this +script. =head1 OPTIONS @@ -1133,58 +813,6 @@ Create a branch specific for the I release series, if it doesn't already exist, and switch to it when making the release files. The exact branch name will be C<< openssl-I >>. -=item B<--clean-worktree> - -This indicates that the current worktree is clean and can be acted on -directly, instead of creating a clone of the local repository or creating -any extra branch. - -=item B<--branch-fmt>=I - -=item B<--tag-fmt>=I - -Format for branch and tag names. This can be used to tune the names of -branches and tags that are updated or added by this script. - -I can include printf-like formating directives: - -=over 4 - -=item %b - -is replaced with a branch name. This branch name is usually the current -branch of the current repository, but may also be the default release -branch name that is generated when B<--branch> is given. - -=item %t - -is replaced with the generated release tag name. - -=item %v - -is replaced with the version number. The exact version number varies -through the process of this script. - -=back - -This script uses the following defaults: - -=over 4 - -=item * Without B<--clean-worktree> - -For branches: C - -For tags: C<%t> - -=item * With B<--clean-worktree> - -For branches: C<%b> - -For tags: C<%t> - -=back - =item B<--reviewer>=I Add I to the set of reviewers for the commits performed by this script. @@ -1225,35 +853,6 @@ those should be skipped and signed out-of-band by another tool. Do not sign the tarball or announcement file. This leaves it for other scripts to sign the files later. -=item B<--staging-address>=I
- -The staging location that the release files are to be uploaded to. -Supported values are: - -=over 4 - -=item - - -an existing local directory - -=item - - -something that can be interpreted as an SCP/SFTP address. In this case, -SFTP will always be used. Typical SCP remote file specs will be translated -into something that makes sense for SFTP. - -=back - -The default staging address is C. - -=item B<--no-upload> - -Don't upload the release files to the staging location. - -=item B<--no-update> - -Don't run C and C. - =item B<--quiet> Really quiet, only bare necessity output, which is the final instructions, @@ -1268,7 +867,7 @@ Verbose output. =item B<--debug> -Display extra debug output. Implies B<--no-upload> +Display extra debug output. =item B<--porcelain> @@ -1277,11 +876,6 @@ in a form reminicent of shell variable assignments. Currently supported are: =over 4 -=item B=I - -The directory for the clone that this script creates. This is not given when -the option B<--clean-worktree> is used. - =item B=I The metadata file. See L for a description of all generated files @@ -1289,11 +883,6 @@ as well as the contents of the metadata file. =back -=item B<--force> - -Force execution. Precisely, the check that the current branch is C -or a release branch is not done. - =item B<--help> Display a quick help text and exit. @@ -1373,8 +962,9 @@ release date in the tar file of any release. =head1 FILES -The following files are produced and normally uploaded to the staging -address: +The following files are produced in the parent directory of the +worktree. Shipping them is the caller's responsibility; this script +does not upload anything. =over 4 @@ -1395,7 +985,7 @@ The detached PGP signature for F. The announcement text and its detached PGP signature. Earlier versions of this script clear-signed the announcement; with the move to C (which does not support cleartext signatures) -the signature is now detached, so both files are uploaded. +the signature is now detached, so both files are produced. =item F @@ -1409,30 +999,19 @@ such as a script to promote this release to an actual release: The update branch. This is always given. -=item B=I - -If a staging update branch was used (because B<--clean-worktree> wasn't -given or because B<--branch-fmt> was used), it's given here. - =item B=I The release branch, if it differs from the update branch (i.e. B<--branch> was given or implied). -=item B=I - -If a staging release branch was used (because B<--clean-worktree> wasn't -given or because B<--branch-fmt> was used), it's given here. - =item B=I The release tag. This is always given. -=item B='I' +=item B='I' -The space separated list of files that were or would have been uploaded -to the staging location (depending on the presence of B<--no-upload>). This -list doesn't include the metadata file itself. +The space-separated list of release files produced. Does not include +the metadata file itself. =item B='I' From 4dc2a1add014a34d12de0fa28ae17bc5ea12f7f3 Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Wed, 13 May 2026 15:52:43 +0200 Subject: [PATCH 15/18] stage-release.sh: drop announcement file, quiet copyright spinner stage-release.sh: stop producing openssl-.txt and its detached signature. The announcement-template machinery (template selection, sed expansion, fix-title.pl, the second openssl-pgp sign call) is gone, along with the `length` checksum that only the announcement consumed. The release_files manifest, FILES section of the POD, and the leftover comments in --gpg-program / tagkey header follow suit. do-copyright-year: the progress spinner used \r to overwrite a single status line, which works on a TTY but not in Jenkins -- every tick of the spinner showed up as its own log line, polluting the build output. Skip the spinner entirely when stdout is not a TTY, keeping just the "Updating copyright" / "Files considered: N" / "Files changed: N" lines. Co-Authored-By: Claude Opus 4.7 (1M context) --- release-tools/do-copyright-year | 24 +++++++---- release-tools/stage-release.sh | 72 ++++++++++----------------------- 2 files changed, 38 insertions(+), 58 deletions(-) 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/stage-release.sh b/release-tools/stage-release.sh index 38230185..89f54f3e 100755 --- a/release-tools/stage-release.sh +++ b/release-tools/stage-release.sh @@ -44,9 +44,9 @@ Usage: stage-release.sh [ options ... ] Override the program git uses to sign tags. Defaults to tools/release-tools/sq-pkcs11-git-shim, which routes tag signing through sq-pkcs11 (HSM-backed). Set to - "gpg" to fall back to a local gpg keyring. Tarball and - announcement signing always go through openssl-pgp and - are not affected by this option. + "gpg" to fall back to a local gpg keyring. Tarball + signing always goes through openssl-pgp and is not + affected by this option. --unsigned Do not sign anything. --quiet Really quiet, only the final output will still be output. @@ -88,8 +88,8 @@ do_signed=true tagkey=' -s' # gpg.program for `git tag -s`. Defaults below to sq-pkcs11-git-shim # (HSM-backed signing) once $RELEASE_TOOLS is known; override with -# --gpg-program to use a different signer. Tarball and announcement -# signing always go through openssl-pgp and are unaffected by this. +# --gpg-program to use a different signer. Tarball signing always +# goes through openssl-pgp and is unaffected by this. gpg_program= reviewers= @@ -463,11 +463,8 @@ set_version release="$FULL_VERSION" if [ -n "$PRE_LABEL" ]; then release_text="$SERIES$_BUILD_METADATA $PRE_LABEL $PRE_NUM" - announce_template=openssl-announce-pre-release.tmpl else - release_type=$(std_release_type $VERSION) release_text="$release" - announce_template=openssl-announce-release-$release_type.tmpl fi $VERBOSE "== Updated version information to $release" @@ -499,10 +496,9 @@ fi tarfile=openssl-$release.tar tgzfile=$tarfile.gz metadata=openssl-$release.dat -announce=openssl-$release.txt -$ECHO "== Generating tar, hash, announcement and metadata files." -$ECHO "== This make take a bit of time..." +$ECHO "== Generating tar, hash, and metadata files." +$ECHO "== This may take a bit of time..." $VERBOSE "== Making tarfile: $tgzfile" @@ -528,36 +524,18 @@ echo $sha1hash "$tgzfile" > "../$tgzfile.sha1" sha256hash=$(openssl sha256 < "../$tgzfile" | \ (IFS='= '; while read X H; do echo $H; done)) echo $sha256hash "$tgzfile" > "../$tgzfile.sha256" -length=$(wc -c < "../$tgzfile") - -$VERBOSE "== Generating announcement text: $announce" -# Hack the announcement template -cat "$RELEASE_AUX/$announce_template" \ - | sed -e "s|\\\$release_text|$release_text|g" \ - -e "s|\\\$release_tag|$release_tag|g" \ - -e "s|\\\$release|$release|g" \ - -e "s|\\\$series|$SERIES|g" \ - -e "s|\\\$label|$PRE_LABEL|g" \ - -e "s|\\\$tarfile|$tgzfile|" \ - -e "s|\\\$length|$length|" \ - -e "s|\\\$sha1hash|$sha1hash|" \ - -e "s|\\\$sha256hash|$sha256hash|" \ - | perl -p "$RELEASE_AUX/fix-title.pl" \ - > "../$announce" - -$VERBOSE "== Generating signatures: $tgzfile.asc $announce.asc" -rm -f "../$tgzfile.asc" "../$announce.asc" + +$VERBOSE "== Generating signature: $tgzfile.asc" +rm -f "../$tgzfile.asc" if $do_signed; then - $ECHO "Signing the release files via openssl-pgp." + $ECHO "Signing the release tarball via openssl-pgp." "$RELEASE_TOOLS/openssl-pgp" sign "../$tgzfile" - "$RELEASE_TOOLS/openssl-pgp" sign "../$announce" fi if $do_signed; then - release_files=( "$tgzfile" "$tgzfile.sha1" "$tgzfile.sha256" - "$tgzfile.asc" "$announce" "$announce.asc" ) + release_files=( "$tgzfile" "$tgzfile.sha1" "$tgzfile.sha256" "$tgzfile.asc" ) else - release_files=( "$tgzfile" "$tgzfile.sha1" "$tgzfile.sha256" "$announce" ) + release_files=( "$tgzfile" "$tgzfile.sha1" "$tgzfile.sha256" ) fi $VERBOSE "== Generating metadata file: $metadata" @@ -775,10 +753,9 @@ are given, they must be followed exactly. B operates on the current worktree directly: it refuses to run if the worktree is not clean, and updates the current branch in place. The release artifacts (tarball, hashes, signature, -announcement, metadata) are written to the parent directory. Pushing -the resulting commits and tag, and shipping the artifacts, are the -caller's responsibility -- nothing is uploaded or pushed by this -script. +metadata) are written to the parent directory. Pushing the resulting +commits and tag, and shipping the artifacts, are the caller's +responsibility -- nothing is uploaded or pushed by this script. =head1 OPTIONS @@ -844,14 +821,14 @@ through C against an HSM-resident private key identified by B<--local-user> as a C. Override with C<--gpg-program=gpg> to fall back to a local C keyring. -Tarball and announcement signing always run through C -and are unaffected by this option; combine with B<--unsigned> if -those should be skipped and signed out-of-band by another tool. +Tarball signing always runs through C and is unaffected +by this option; combine with B<--unsigned> if it should be skipped +and signed out-of-band by another tool. =item B<--unsigned> -Do not sign the tarball or announcement file. This leaves it for other -scripts to sign the files later. +Do not sign the tarball. This leaves it for other scripts to sign +the file later. =item B<--quiet> @@ -980,13 +957,6 @@ The SHA1 and SHA256 checksums for F. The detached PGP signature for F. -=item F, F - -The announcement text and its detached PGP signature. Earlier -versions of this script clear-signed the announcement; with the -move to C (which does not support cleartext signatures) -the signature is now detached, so both files are produced. - =item F The metadata file for F. It contains shell From ce8e4f78409604fb10e7f6943cbf57f3f37d3a28 Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Wed, 13 May 2026 15:57:15 +0200 Subject: [PATCH 16/18] stage-release.sh: hardcode signers, drop --gpg-program Tarball signing was already inlined as $RELEASE_TOOLS/openssl-pgp, but tag signing was routed through a --gpg-program flag whose only sensible value was the in-repo sq-pkcs11-git-shim. Make the pair symmetric: inline sq-pkcs11-git-shim at the `git tag -s` call and remove --gpg-program entirely (from getopt, case dispatch, --help text, and the POD manual). --unsigned still covers the skip-signing escape hatch; --local-user remains the single knob for the signing key. The Jenkins invocation can drop its --gpg-program="$GPG_PROGRAM" arg. Co-Authored-By: Claude Opus 4.7 (1M context) --- release-tools/stage-release.sh | 76 +++++++--------------------------- 1 file changed, 16 insertions(+), 60 deletions(-) diff --git a/release-tools/stage-release.sh b/release-tools/stage-release.sh index 89f54f3e..2bc7a7a2 100755 --- a/release-tools/stage-release.sh +++ b/release-tools/stage-release.sh @@ -32,21 +32,10 @@ Usage: stage-release.sh [ options ... ] --reviewer= The reviewer of the commits. --local-user= - For the purpose of signing tags and release artifacts, - use this key. The default signer is openssl-pgp (and - the sq-pkcs11-git-shim for tag signing), so is - a CKA_LABEL on the configured HSM. When --gpg-program - points at a classic gpg, follows gpg's rules - (key id, fingerprint, or e-mail). - Also exported as OPENSSL_PGP_CURRENT_SUBKEY_LABEL so - openssl-pgp picks up the same key for artifact signing. ---gpg-program= - Override the program git uses to sign tags. Defaults - to tools/release-tools/sq-pkcs11-git-shim, which routes - tag signing through sq-pkcs11 (HSM-backed). Set to - "gpg" to fall back to a local gpg keyring. Tarball - signing always goes through openssl-pgp and is not - affected by this option. + CKA_LABEL of the HSM key used for both tag signing + (via release-tools/sq-pkcs11-git-shim) and release + artifact signing (via release-tools/openssl-pgp). + Also exported as OPENSSL_PGP_CURRENT_SUBKEY_LABEL. --unsigned Do not sign anything. --quiet Really quiet, only the final output will still be output. @@ -86,18 +75,12 @@ do_manual=false do_signed=true tagkey=' -s' -# gpg.program for `git tag -s`. Defaults below to sq-pkcs11-git-shim -# (HSM-backed signing) once $RELEASE_TOOLS is known; override with -# --gpg-program to use a different signer. Tarball signing always -# goes through openssl-pgp and is unaffected by this. -gpg_program= reviewers= TEMP=$(getopt -l 'alpha,next-beta,beta,final' \ -l 'branch' \ -l 'reviewer:' \ -l 'local-user:,unsigned' \ - -l 'gpg-program:' \ -l 'quiet,verbose,debug' \ -l 'porcelain' \ -l 'help,manual' \ @@ -143,11 +126,6 @@ while true; do do_signed=false tagkey=" -a" ;; - --gpg-program ) - shift - gpg_program="$1" - shift - ;; --quiet ) ECHO=: VERBOSE=: @@ -239,13 +217,6 @@ if ! $found; then exit 1 fi -# Default tag-signing shim. sq-pkcs11-git-shim adapts git's gpg-CLI -# expectations to sq-pkcs11 (HSM-backed signing); --gpg-program can -# override (e.g. --gpg-program=gpg to fall back to a local keyring). -if [ -z "$gpg_program" ]; then - gpg_program="$RELEASE_TOOLS/sq-pkcs11-git-shim" -fi - # Check that we have the scripts that define functions we use found=true for fn in "$RELEASE_AUX/release-version-fn.sh" \ @@ -485,9 +456,10 @@ git commit $git_quiet -m "Prepare for release of $release_text"$'\n\nRelease: ye if [ -n "$reviewers" ]; then addrev --release --nopr $reviewers fi -$ECHO "Tagging release with tag $release_tag. You may need to enter a pass phrase" +$ECHO "Tagging release with tag $release_tag." if $do_signed; then - git -c "gpg.program=$gpg_program" tag$tagkey "$release_tag" \ + git -c "gpg.program=$RELEASE_TOOLS/sq-pkcs11-git-shim" \ + tag$tagkey "$release_tag" \ -m "OpenSSL $release release tag" else git tag$tagkey "$release_tag" -m "OpenSSL $release release tag" @@ -723,7 +695,6 @@ B<--beta> | B<--final> | B<--branch> | B<--local-user>=I | -B<--gpg-program>=I | B<--unsigned> | B<--reviewer>=I | B<--quiet> | @@ -800,30 +771,15 @@ means retagging a release commit manually as well. =item B<--local-user>=I -Use I as the local user for C and as the signing key -for the C wrapper. The value is also exported as -C so artifact signing picks up the -same key without an extra environment hop. - -The form of I depends on the configured signer. With the -default tag-signing shim (C) and with the -C wrapper, I is the C of a private -key on the configured PKCS#11 token. When B<--gpg-program> points -at a classic C, I is a key id, fingerprint, or e-mail -address as understood by C. - -=item B<--gpg-program>=I - -Override the program C uses to produce its OpenPGP -signature. By default this script sets it to -C<$TOOLS/release-tools/sq-pkcs11-git-shim>, which routes tag signing -through C against an HSM-resident private key identified -by B<--local-user> as a C. Override with C<--gpg-program=gpg> -to fall back to a local C keyring. - -Tarball signing always runs through C and is unaffected -by this option; combine with B<--unsigned> if it should be skipped -and signed out-of-band by another tool. +The C of the HSM-resident private key used for signing. +Tag signing goes through C +(plugged into C via C); tarball signing goes +through C. Both ultimately invoke +C against the same PKCS#11 token. + +The value is also exported as C +so C picks up the same key without an extra +environment hop. =item B<--unsigned> From 0893ac8c077886790afed996139ae2fda55b6ce2 Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Wed, 13 May 2026 16:03:14 +0200 Subject: [PATCH 17/18] stage-release.sh: drop --branch, always create release branch when needed The Jenkins job passed --branch unconditionally; the existing patch-number / same-branch checks already collapsed it to a no-op when not applicable. Make do_branch=true the default and remove the flag. - on master at PATCH == 0: release branch openssl-X.Y is created (same as --branch was honored before) - on a release branch or PATCH != 0: release commit lands on the current branch (same as --branch was ignored before, just without the "--branch ignored" warning) This removes --branch from getopt/case dispatch/--help/POD, drops the warn_branch bookkeeping that only fed that warning, removes the "--final implies --branch" plumbing and the "--branch is invalid unless current branch is master" guard (the do_branch=false branch now handles non-master worktrees identically). Co-Authored-By: Claude Opus 4.7 (1M context) --- release-tools/stage-release.sh | 63 +++++++++------------------------- 1 file changed, 17 insertions(+), 46 deletions(-) diff --git a/release-tools/stage-release.sh b/release-tools/stage-release.sh index 2bc7a7a2..4ee9ffdf 100755 --- a/release-tools/stage-release.sh +++ b/release-tools/stage-release.sh @@ -24,11 +24,6 @@ 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. --reviewer= The reviewer of the commits. --local-user= @@ -61,8 +56,10 @@ EOF next_method= next_method2= -do_branch=false -warn_branch=false +# Always try to create the release branch. The post-arg-parsing +# logic below resets this to false when we're already on a release +# branch or when PATCH != 0, so passing --branch was redundant. +do_branch=true ECHO=echo DEBUG=: @@ -78,7 +75,6 @@ tagkey=' -s' reviewers= TEMP=$(getopt -l 'alpha,next-beta,beta,final' \ - -l 'branch' \ -l 'reviewer:' \ -l 'local-user:,unsigned' \ -l 'quiet,verbose,debug' \ @@ -94,19 +90,11 @@ while true; do next_method2=$next_method fi shift - if [ "$next_method" = 'final' ]; then - do_branch=true - fi ;; --next-beta ) next_method2=$(echo "x$1" | sed -e 's|^x--next-||') shift ;; - --branch ) - do_branch=true - warn_branch=true - shift - ;; --reviewer ) reviewers="$reviewers $1=$2" shift @@ -312,33 +300,24 @@ get_version # Branches to start from. The release branch is where the changes for the # release are made, and the update branch is where the post-release changes are -# made. If --branch was given and is relevant, they should be different (and -# the update branch should be 'master'), otherwise they should be the same. +# made. When releasing from master at PATCH == 0 they differ (the update +# branch stays as master, the release branch becomes openssl-X.Y); otherwise +# they are the same. orig_update_branch="$orig_branch" orig_release_branch="$(std_branch_name)" -# among others, we only create a release branch if the patch number is zero +# We create a release branch only when we're on master at PATCH == 0; +# otherwise (already on a release branch, or this is a patch release) +# we make the release commit on the current branch. if [ "$orig_update_branch" = "$orig_release_branch" ] \ || [ -n "$PATCH" -a "$PATCH" != 0 ]; then - if $do_branch && $warn_branch; then - echo >&2 "Warning! We're already in a release branch; --branch ignored" - fi do_branch=false fi -if $do_branch; then - if [ "$orig_update_branch" != "master" ]; then - echo >&2 "--branch is invalid unless the current branch is 'master'" - exit 1 - fi - # No need to check if $orig_update_branch and $orig_release_branch differ, - # 'cause the code a few lines up guarantee that if they are the same, - # $do_branch becomes false -else - # In this case, the computed release branch may differ from the update branch, - # even if it shouldn't... this is the case when alpha or beta releases are - # made in the master branch, which is perfectly ok. Therefore, simply reset - # the release branch to be the same as the update branch and carry on. +if ! $do_branch; then + # The computed release branch may differ from the update branch when + # alpha or beta releases are made on master, which is fine -- in that + # case we keep operating on the current branch. orig_release_branch="$orig_update_branch" fi @@ -693,7 +672,6 @@ B<--alpha> | B<--next-beta> | B<--beta> | B<--final> | -B<--branch> | B<--local-user>=I | B<--unsigned> | B<--reviewer>=I | @@ -753,14 +731,6 @@ release is done. Set the state of this branch to indicate that regular releases are to be done. This is only valid if alpha or beta releases are currently ongoing. -This implies B<--branch>. - -=item B<--branch> - -Create a branch specific for the I release series, if it doesn't -already exist, and switch to it when making the release files. The exact -branch name will be C<< openssl-I >>. - =item B<--reviewer>=I Add I to the set of reviewers for the commits performed by this script. @@ -927,8 +897,9 @@ The update branch. This is always given. =item B=I -The release branch, if it differs from the update branch (i.e. B<--branch> -was given or implied). +The release branch, if a new one was created (i.e. when releasing from +C at PATCH == 0). Omitted when the release commit is made on +the current branch. =item B=I From 680fab688c3dca9944fa2831da229c4f99f67f18 Mon Sep 17 00:00:00 2001 From: Dmitry Misharov Date: Thu, 14 May 2026 11:09:58 +0200 Subject: [PATCH 18/18] stage-release.sh: rename --local-user to --key-label The flag's value is a PKCS#11 CKA_LABEL, not a GnuPG local-user spec. Reflect that in the option name. The shim still accepts --local-user because git invokes gpg.program with that flag. --- release-tools/stage-release.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/release-tools/stage-release.sh b/release-tools/stage-release.sh index 4ee9ffdf..c29943ae 100755 --- a/release-tools/stage-release.sh +++ b/release-tools/stage-release.sh @@ -26,7 +26,7 @@ Usage: stage-release.sh [ options ... ] --final Get out of "alpha" or "beta" and make a final release. --reviewer= The reviewer of the commits. ---local-user= +--key-label=