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' \

Reply via email to