This is an automated email from the ASF dual-hosted git repository. stevedlawrence pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/daffodil-infrastructure.git
commit 57417d2bfa0db374e31d4eb3ae5a346d7d4031f3 Author: Steve Lawrence <[email protected]> AuthorDate: Wed Jul 15 09:51:50 2026 -0400 Enable support for signing windows executables using SSL.com - Update the release-candidate action to add support for optionally signing Windows exe files using SSL.com credentials. ASF Infra must provide SSL.com credentials as secrets, and workflows using this action must be provided the secrets. If not provided, exe files are not signed. - Update the check-release script to verify exe signatures and also strip signatures to verify reproducibility. DAFFODIL-3090 --- actions/release-candidate/README.md | 11 +++++--- actions/release-candidate/action.yml | 9 +++++++ actions/release-candidate/dist/main/index.js | 7 +++++ actions/release-candidate/dist/post/index.js | 16 ++++++++++- actions/release-candidate/src/main.js | 7 +++++ actions/release-candidate/src/post.js | 16 ++++++++++- containers/check-release/Dockerfile | 2 ++ containers/check-release/src/check-release.sh | 39 ++++++++++++++++++++++++--- 8 files changed, 99 insertions(+), 8 deletions(-) diff --git a/actions/release-candidate/README.md b/actions/release-candidate/README.md index 81e8a10..93c89c3 100644 --- a/actions/release-candidate/README.md +++ b/actions/release-candidate/README.md @@ -9,7 +9,7 @@ is not intended to be used by all projects. * Apache Security Team has approved the project for [Automated Release Signing](https://infra.apache.org/release-signing.html#automated-release-signing) and INFRA has set secrets for the repository, including a GPG signing key, - SVN username/password, and nexus username/password. + SVN username/password, nexus username/password, and optionally SSL.com credentials * The `runs-on` workflow setting should be Linux based (e.g. `ubuntu-latest`) * The repository must be checked out using `actions/checkout` prior to triggering this action @@ -53,9 +53,11 @@ dropped. If the workflow job successfully completes, the following actions are performed at the end of the workflow: +* Sign all rpm artifacts with the GPG key with rpmsign +* Sign all exe artifacts with the ssl.com certificates, if publishing is + enabled and `ssl_com_username` is defined * Create sha512 checksum files for all artifacts * Create detached ASCII armored GPG signatures for all artifacts -* Sign all rpm artifacts with the GPG key with rpmsign * Commit all files added to `dist/dev/` to SVN Note that committing to SVN is is disabled if any of the following are true: @@ -77,6 +79,9 @@ This is useful for testing the workflow using workflow dispatch. | project_id | yes | | ID of the project, used in source artifact file name | | project_dir | no | "" | Directory for the project in dev/dist/<tlp_dir>/. Omit if at the root | | gpg_signing_key | if publishing | | Key used to sign artifacts | +| ssl_com_username| no | | Username for signing .exe artifacts using SSL.com | +| ssl_com_password| no | | Password for signing .exe artifacts using SSL.com | +| ssl_com_secret | no | | Secret for signing .exe artifacts using SSL.com | | svn_username | if publishing | | Username for publishing release artifacts to SVN dev/dist | | svn_password | if publishing | | Password for publishing release artifacts to SVN dev/dist | | nexus_username | if publishing | | Username for publishing release artifacts to Nexus | @@ -183,4 +188,4 @@ podman run -it --rm \ --volume <ARTIFACT-DIR>:/release \ daffodil-check-release "NA" "NA" /release ``` -> Note: The public key needed to verify the artifacts is available in the release-download directory. \ No newline at end of file +> Note: The public key needed to verify the artifacts is available in the release-download directory. diff --git a/actions/release-candidate/action.yml b/actions/release-candidate/action.yml index 170d860..5e4132a 100644 --- a/actions/release-candidate/action.yml +++ b/actions/release-candidate/action.yml @@ -34,6 +34,15 @@ inputs: gpg_signing_key: description: Key used to sign artifacts required: false + ssl_com_usename: + description: Username for signing .exe artifacts using SSL.com + required: false + ssl_com_password: + description: Password for signing .exe artifacts using SSL.com + required: false + ssl_com_secret: + description: Secret for signing .exe artifacts using SSL.com + required: false svn_username: description: Username for publishing release artifacts to SVN dev/dist required: false diff --git a/actions/release-candidate/dist/main/index.js b/actions/release-candidate/dist/main/index.js index 5e951cd..23e4097 100644 --- a/actions/release-candidate/dist/main/index.js +++ b/actions/release-candidate/dist/main/index.js @@ -33019,6 +33019,13 @@ async function run() { fs.appendFileSync(`${ sbt_dir }/plugins/build.sbt`, 'addSbtPlugin("com.github.sbt" %% "sbt-sbom" % "0.4.0")\n'); fs.appendFileSync(`${ sbt_dir }/build.sbt`, 'bomFormat := "xml"\n'); + // download and install jsign for signing exe files, ignore dependencies assuming that the + // workflow already has java installed or will install java at some point + const jsign_version = "7.5"; + const jsign_deb_path = `${ os.tmpdir() }/jsign_${jsign_version}_all.deb`; + await exec("curl", ["-L", "-o", jsign_deb_path, `https://github.com/ebourg/jsign/releases/download/${jsign_version}/jsign_${jsign_version}_all.deb`]); + await exec("sudo", ["dpkg", "--ignore-depends=ALL", "--install", jsign_deb_path]); + if (do_publish) { // if publishing is enabled, we configure SVN and SBT so future commands and // workflow tasks can publish artifacts without needing to pass in diff --git a/actions/release-candidate/dist/post/index.js b/actions/release-candidate/dist/post/index.js index 97f1d48..a7c90b3 100644 --- a/actions/release-candidate/dist/post/index.js +++ b/actions/release-candidate/dist/post/index.js @@ -125625,14 +125625,28 @@ async function run() { const do_publish = core.getState("do_publish") === "true"; const release_version = core.getState("release_version"); + // ssl.com credentials are optional, if not provided we will not sign + // exe artifacts. Also, each ssl.com signature costs ASF money, so we + // only sign exe artifacts if we are actually going to publish things + // (e.g. this isn't a test run) + const ssl_com_username = core.getInput("ssl_com_username"); + const ssl_com_password = core.getInput("ssl_com_password"); + const ssl_com_secret = core.getInput("ssl_com_secret"); + const do_ssl_com_sign = do_publish && ssl_com_username; + // sign/checksum all artifacts const artifacts = fs.readdirSync(artifact_dir, { recursive: true, withFileTypes: true }); for(const artifact of artifacts) { if (artifact.isFile()) { - // must sign rpms before sha/gpg since rpmsign modifies the RPM + // must sign rpms and exes before sha/gpg since rpmsign/jsign modifies the files if (artifact.name.endsWith(".rpm")) { await exec("rpmsign", ["--define", `_gpg_name ${ gpg_signing_key_id }`, "--define", "_binary_filedigest_algorithm 10", "--addsign", `${ artifact.parentPath }/${ artifact.name }`]); } + if (artifact.name.endsWith(".exe") && do_ssl_com_sign) { + // see https://infra.apache.org/code-signing-use.html for more information + const cert_uuid = "d97c5110-c66a-4c0c-ac0c-1cd6af812ee6"; + await exec("jsign", ["--storetype", "ESIGNER", "--alias", cert_uuid, "--storepass", `${ssl_com_username}|${ssl_com_password}`, "--keypass", ssl_com_secret, "--tsaurl=http://ts.ssl.com", "--tsmode", "RFC3161", "--alg", "SHA512", `${ artifact.parentPath }/${ artifact.name }`]); + } const shasum_output = await getExecOutput("sha512sum", ["--binary", artifact.name], { cwd: artifact.parentPath }); diff --git a/actions/release-candidate/src/main.js b/actions/release-candidate/src/main.js index 5d25507..154d1bb 100644 --- a/actions/release-candidate/src/main.js +++ b/actions/release-candidate/src/main.js @@ -137,6 +137,13 @@ async function run() { fs.appendFileSync(`${ sbt_dir }/plugins/build.sbt`, 'addSbtPlugin("com.github.sbt" %% "sbt-sbom" % "0.4.0")\n'); fs.appendFileSync(`${ sbt_dir }/build.sbt`, 'bomFormat := "xml"\n'); + // download and install jsign for signing exe files, ignore dependencies assuming that the + // workflow already has java installed or will install java at some point + const jsign_version = "7.5"; + const jsign_deb_path = `${ os.tmpdir() }/jsign_${jsign_version}_all.deb`; + await exec("curl", ["-L", "-o", jsign_deb_path, `https://github.com/ebourg/jsign/releases/download/${jsign_version}/jsign_${jsign_version}_all.deb`]); + await exec("sudo", ["dpkg", "--ignore-depends=ALL", "--install", jsign_deb_path]); + if (do_publish) { // if publishing is enabled, we configure SVN and SBT so future commands and // workflow tasks can publish artifacts without needing to pass in diff --git a/actions/release-candidate/src/post.js b/actions/release-candidate/src/post.js index 60055e2..f3902e2 100644 --- a/actions/release-candidate/src/post.js +++ b/actions/release-candidate/src/post.js @@ -34,14 +34,28 @@ async function run() { const do_publish = core.getState("do_publish") === "true"; const release_version = core.getState("release_version"); + // ssl.com credentials are optional, if not provided we will not sign + // exe artifacts. Also, each ssl.com signature costs ASF money, so we + // only sign exe artifacts if we are actually going to publish things + // (e.g. this isn't a test run) + const ssl_com_username = core.getInput("ssl_com_username"); + const ssl_com_password = core.getInput("ssl_com_password"); + const ssl_com_secret = core.getInput("ssl_com_secret"); + const do_ssl_com_sign = do_publish && ssl_com_username; + // sign/checksum all artifacts const artifacts = fs.readdirSync(artifact_dir, { recursive: true, withFileTypes: true }); for(const artifact of artifacts) { if (artifact.isFile()) { - // must sign rpms before sha/gpg since rpmsign modifies the RPM + // must sign rpms and exes before sha/gpg since rpmsign/jsign modifies the files if (artifact.name.endsWith(".rpm")) { await exec("rpmsign", ["--define", `_gpg_name ${ gpg_signing_key_id }`, "--define", "_binary_filedigest_algorithm 10", "--addsign", `${ artifact.parentPath }/${ artifact.name }`]); } + if (artifact.name.endsWith(".exe") && do_ssl_com_sign) { + // see https://infra.apache.org/code-signing-use.html for more information + const cert_uuid = "d97c5110-c66a-4c0c-ac0c-1cd6af812ee6"; + await exec("jsign", ["--storetype", "ESIGNER", "--alias", cert_uuid, "--storepass", `${ssl_com_username}|${ssl_com_password}`, "--keypass", ssl_com_secret, "--tsaurl=http://ts.ssl.com", "--tsmode", "RFC3161", "--alg", "SHA512", `${ artifact.parentPath }/${ artifact.name }`]); + } const shasum_output = await getExecOutput("sha512sum", ["--binary", artifact.name], { cwd: artifact.parentPath }); diff --git a/containers/check-release/Dockerfile b/containers/check-release/Dockerfile index 25cfe75..2afdbdf 100644 --- a/containers/check-release/Dockerfile +++ b/containers/check-release/Dockerfile @@ -20,6 +20,8 @@ RUN \ apt-get install -qy \ diffutils \ gpg \ + osslsigncode \ + python3-pefile \ rpm \ wget diff --git a/containers/check-release/src/check-release.sh b/containers/check-release/src/check-release.sh index dff9d4e..ffa9beb 100755 --- a/containers/check-release/src/check-release.sh +++ b/containers/check-release/src/check-release.sh @@ -147,14 +147,20 @@ require_command() { command -v "$1" &> /dev/null || { echo "error: command $1 not found in PATH"; exit 1; } } +require_python() { + python3 -c "import $1" &> /dev/null || { echo "error: python module $1 not found"; exit 1; } +} + # error early if needed tools are missing require_command cmp require_command gpg require_command md5sum +require_command osslsigncode require_command rpm require_command sha1sum require_command sha512sum require_command wget +require_python pefile RELEASE_DIR=release-download @@ -219,6 +225,11 @@ test_files <(find "$DIST_DIR" -type f -name '*.rpm') <<-'CMD' rpm -K '{}' | grep 'digests signatures OK' CMD +printf "\n==== EXE Embedded Signatures ====\n" +test_files <(find "$DIST_DIR" -type f -name '*.exe') <<-'CMD' + osslsigncode verify '{}' +CMD + if [ -n "$MAVEN_URL" ] then printf "\n==== Maven SHA1 Checksums ====\n" @@ -242,6 +253,10 @@ if [ -z "$LOCAL_RELEASE_DIR" ] then echo -e "$WARN no local release directory provided, skipping reproducible build check" else + # Directory to store backups of files that require modifications for the + # reproduciblity checks. + BACKUP_DIR="$(mktemp -d)" + # RPM files have an embedded signature which makes reproducibility checking # difficult since locally built RPMs will not have the embedded signature. The # RPMs should be identical if we delete that signature, but unfortunately @@ -268,7 +283,6 @@ else # data_length fields at a known offset (skipping the lead and magic number), # then add together the length of 3 fixed length fields (16 bytes), the length # of the index (16 * index_count) and the length of the data (data_length). - BACKUP_DIR="$(mktemp -d)" find "$RELEASE_DIR" -name '*.rpm' -exec cp --parents '{}' "$BACKUP_DIR" \; while IFS= read -r RPM_PATH do @@ -281,15 +295,34 @@ else bs=1 count=$SIG_HEADER_LENGTH conv=notrunc &> /dev/null done < <(find "$LOCAL_RELEASE_DIR/" -name '*.rpm' -printf '%P\n') + # EXE files have an embedded signature which makes reproducibility checking + # difficult since locally built EXEs will not have the embedded signature. + # However, the EXEs should be identical if we delete the signature. The + # only exception to this is the optional header checksum field in the exe + # is normally zero when we build it, but tools that add and remove + # signatures calculate and embed an actual checksum value. So we remove the + # signature using osslsigncode and then using python3-pefile to set that + # checksum field to zero. This should make it exactly the same as locally + # built files. + find "$RELEASE_DIR" -name '*.exe' -exec cp --parents '{}' "$BACKUP_DIR" \; + while IFS= read -r EXE_PATH + do + RELEASE_EXE="$RELEASE_DIR/$EXE_PATH" + osslsigncode remove-signature -in "$RELEASE_EXE" -out "$RELEASE_EXE.unsigned" + python3 -c "import pefile; pe=pefile.PE('$RELEASE_EXE.unsigned'); pe.OPTIONAL_HEADER.CheckSum=0; pe.write('$RELEASE_EXE')" + rm "$RELEASE_EXE.unsigned" + done < <(find "$LOCAL_RELEASE_DIR/" -name '*.exe' -printf '%P\n') + # Reasons for excluding files from the diff check: - # - The downloaded .rpm file has an embedded signature (which we removed), - # locally built RPM does not so checksums will be different. RPMs should be + # - The downloaded .rpm and .exe files have an embedded signature (which we removed), + # locally built files do not so checksums will be different. These files should be # exactly the same with the signature removed though. # - The .asc files can only be generated by the system with the secret key, the # locally built releases are not signed test_files <(find "$RELEASE_DIR/" "$LOCAL_RELEASE_DIR/" \ -type f \ ! -name '*.rpm.sha512' \ + ! -name '*.exe.sha512' \ ! -name '*.asc' \ ! -name '*.asc.md5' \ ! -name '*.asc.sha1' \
