According to Wikipedia, a software supply chain is the components, libraries, tools, and processes used to develop, build, and publish a software artifact. The Java space provides thousands upon thousands of libraries that may be consumed as dependencies for building projects. Many of these libraries rely on Apache Maven as their build tool of choice, followed by Gradle and Apache Ant. There are of course a few other more, the Java ecosystem provides plenty of options (bld, JBang, etc).
While these build tools continue to improve their own development practices, their use alone is not enough to ensure that your artifacts, build pipelines, compiler tool chains, and other working parts of your software supply chain are secure and resistant to tampering or attacks. The Supply-chain Levels for Software Artifacts project, or SLSA (pronounced “salsa”) is a it’s a security framework, a checklist of standards and controls to prevent tampering, improve integrity, and secure packages and infrastructure. In its landing page you’ll find the following description regarding places where potential attacks may occur
Yes, attacks may come from many places but also happen at many locations within the software supply chain.
We must take action at different locations, fortunately the previously mentioned build tools provide some support but we need a few more things. Do you recall the news that shocked the IT world in December 2021? That’s right, it was Log4Shell. Or the failed XZ Backdoor from 2024? These and many other attacks could have been prevented or have their impact lessened if specific techniques, additional metadata, and tools were also available and applied on time.
Let’s begin with the straight forward ones, then move on with more complex options. But before we do, I would like to remark that JReleaser, while not strictly a build tool but rather a release tool, provides support for all of the features we’ll cover. Onward.
Reproducible Artifacts
Reproducible artifacts come from Reproducible Builds, a software development practice where building the exact same source code with the same tools and instructions always results in a bit-for-bit identical binary output. If you happen to have a compatible reproducible environment (similar or identical OS, Java distribution, environment variables, etc) then you may rebuild a given codebase at an specific point of its history (say a tagged release) and obtain a bit-by-bit identical set of artifacts that may be compared at different build dates.
Luckily for many Java developers, Maven makes it quite easy to generate reproducible artifacts (JARs, ZIPs, TARs) by simply setting a fixed timestamp. Gradle offers similar capabilities although the value of the timestamp can’t be changed. JReleaser also lets you assemble reproducible archives with any of its assemblers.
For example, this repository contains a simple Helloworld project. Its build creates an executable JAR file, while its release configuration defines a binary distribution as a ZIP file. Here’s a short snippet of said configuration
assemble:
javaArchive:
helloworld:
active: ALWAYS
formats: [ ZIP ]
fileSets:
- input: '.'
includes: [ 'LICENSE' ]
mainJar:
path: target/{{distributionName}}-{{projectVersion}}.jar
Invoking the assemble command results in archive containing the following entries:
Archive: java-archive/helloworld-1.0.0.zip
Length Date Time Name
--------- ---------- ----- ----
11357 01-06-2025 19:30 helloworld-1.0.0/LICENSE
4533 01-06-2025 19:30 helloworld-1.0.0/bin/helloworld
1889 01-06-2025 19:30 helloworld-1.0.0/bin/helloworld.bat
3777 01-06-2025 19:30 helloworld-1.0.0/lib/helloworld-1.0.0.jar
--------- -------
21556 4 files
That timestamp may look arbitrary but its guaranteed to be reproducible, as it happens to match the tagged commit for that release, which is
commit eaa60a9314e3555db6e44da8442808d46f2fcd35 Author: Andres Almiray <aalmiray@gmail.com> Date: Mon Jan 6 19:30:58 2025 +0100
Neat, isn’t it? Let’s continue.
Digital Signatures
Another way to add an extra layer of security is by providing digital signatures for a given set of artifacts. We’ve been using PGP for decades since its inception. Pretty much every build tool supports generating these kind of signatures with either explicit mechanism (plugins) or calling out to external commands (gnupg for example).
This works fine, except when it doesn’t, which is when key management comes into play. Some developers are lazy and set their keys to never expire, while some are too concerned that they rotate their keys quite often. No matter where you stand in the key management spectrum, it requires performing additional tasks as a burden. For this reason, a new idea emerged a bit more than a decade ago: Sigstore.
Sigstore provides an alternative for automating artifact signing and signature verification. While the signing tool was originally created for signing container images (hence the name cosign) and written in Go, nowadays there’s a Java API that enables Maven and Gradle support. JReleaser in turn also supports generating signatures for all artifacts to be released as either PGP or Sigstore, making it easier to provide such signatures as it doesn’t matter how those artifacts were built/assembled or which tool did it.
Enabling digital signatures for a release, whether with PGP or Sigstore, is quite easy with JReleaser, for example, the minimum configuration would be something similar to
signing: active: ALWAYS armored: true
Where’s the rest? Well, the values of the public & secret keys, as well as an associated passphrase (when required) may be supplied via environment variables, to keep these value as secrets.
SBOMs and SWID Tags
The next level of protection is comprised of additional metadata files that may be used to inspect an artifact for its set of dependencies, or in the case of an archive, for its contained files. By now I hope you’ve heard of SBOMs and how to procure them during a build. SBOMs have become more important these days as software vendors and makers will be required to provide them upon request, according to CRA and DORA.
Both Maven and Gradle provide plugins that generate SBOMs in different formats. You may also use Syft to create such files. These SBOMs should provide a list of all dependencies required for building the matching artifact, a JAR file in this case. But what about ZIPs and TARs, or other kind of archives? In this case JReleaser can generate SBOMs for all archives created via any of its assemblers.
Here’s a snippet showing how you can configure SBOM generation for artifacts using both cyclonedx and Syft. The resulting SBOMs will be packaged in a single ZIP and will be available to be uploaded as release assets
catalog:
sbom:
cyclonedx:
active: ALWAYS
pack:
enabled: true
name: '{{projectName}}-{{projectVersion}}-cyclonedx-sboms'
syft:
active: ALWAYS
pack:
enabled: true
name: '{{projectName}}-{{projectVersion}}-syft-sboms'
You only need one format or the other, this particular example shows how easy is to configure either format.
Additionally to SBOMs there’s also SWID Tags, created by the National Institute of Standards and Technology (NIST). This format provides a thorough list of all entries found inside a given archive, with a checksum per entry. Computing both SBOMs and SWID Tags provides a level of redundancy as the same checksum values should appear in both files, making it harder for an attacker to fake values. Also, you may be contractually obligated to deliver such files depending on the type of projects and customers you may be working with. As far as I recall there’s a Maven plugin for generating SWID tags but no Gradle plugin.
Activating SWID tag generation for assembled archives is as easy as configuring the following snippet in your JReleaser configuration file
catalog:
swid:
swid-tag:
active: ALWAYS
Now, adding SWID tag to a given assembly is done as follows
assemble:
javaArchive:
helloworld:
active: ALWAYS
formats: [ ZIP ]
fileSets:
- input: '.'
includes: [ 'LICENSE' ]
mainJar:
path: target/{{distributionName}}-{{projectVersion}}.jar
swid:
tagRef: swid-tag
Notice the reference to swid-tag, which is the custom name that we defined in a previous snippet.
Attestation Files
Attestations bind some subject (a named artifact along with its digest) to a predicate (some assertion about that subject) using the in-toto format. Predicates consist of a type URI and a JSON object containing type-dependent parameters.
Attestation files are usually digitally signed, and guess what, this is where Sigstore appears again. Attestation providers also offer a command line tool that may be used to verify the signatures of the attestation files, as well as their contents.
If you happen to use GitHub (whether free or enterprise) then you have access to GitHub Attestations. Making use of this feature is straight forward and requires you to use the actions/attest action. There are a few ways to supply the list of files to be included for attestation, one of them is a checksums file. It so happens that JReleaser automatically calculates such a file when a release is performed, or when the checksum command is explicitly invoked. This makes it quite easy to combine it with the actions/attest action, like so
- name: Release
uses: jreleaser/release-action@v2
with:
arguments: release
env:
# change this value to your own version number
JRELEASER_PROJECT_VERSION: 1.2.3
- name: Attestations
uses: actions/attest-build-provenance@v1
with:
subject-checksums: out/jreleaser/checksums/. checksums_sha256.txt
predicate-type: 'https://example.com/predicate/v1'
predicate: '{}'
The SLSA framework also provides its own attestation feature. You may run it directly from GitHub via the slsa-framework/slsa-github-generator, for which you’d need to configure more things depending on your build requirements. Luckily the SLSA team made things easier by enabling other tools to create their own sanctioned SLSA builders (BYOB), as explained in this link.
JReleaser is an early adopter of the BYOB feature, providing a seamless integration with GitHub Actions. The jreleaser/helloworld-java-slsa repository showcases how this integration works. There is one thing to take into consideration, the SLSA builder must be provided with both build and release instructions, as they must happen within a controlled environment. I said earlier that JReleaser is a release tool, not a build tool, however it does allow custom commands to be invoked at certain stages of its execution. Thus if we’re able to supply build instructions that are guaranteed to be invoked before a release then we should be good to go. And that’s exactly what we can do.
The release configuration file in the helloworld-java-slsa repository has a section specifying the required build instructions, and that they should be invoked during the assemble step.
hooks:
script:
before:
- run: './mvnw -ntp verify'
condition: '"{{ Env.CI }}" == true'
verbose: true
filter:
includes: ['assemble']
Now, the next bit of configuration is found in a GitHub Actions workflow such as this one
release:
name: Release
needs: [ precheck ]
permissions:
contents: write
id-token: write
actions: read
packages: write
uses: jreleaser/jreleaser-slsa/.github/workflows/builder_slsa3_java.yml@v1.1.0
with:
project-version: ${{ needs.precheck.outputs.VERSION }}
rekor-log-public: true
secrets:
github-token: ${{ secrets.GITHUB_TOKEN }}
This snippet defines specific permissions granted to the default GITHUB_TOKEN, such that the invoked trusted workflow (builder_slsa4_java.yml) can not access more than it shouldn’t. This trusted workflow will in turn invoke the SLSA generator and make sure that the attestation file is attached as a release asset. It all just happens automagically as seen in this release:
And tough this is a Java Advent entry, I would be remiss it I did not mention that JReleaser can be used with any kind of project independent of its source code, its not just for Java. Likewise, the SLSA support offered by JReleaser may be used with Go, Rust, and Zig, with additional languages coming later.
Summary
Securing your Software Supply Chain requires a paradigm shift, adapting build and releases processes, learning new tools and techniques. The build tools you’re already used to may provide answers to some of these concerns, while JReleaser provides additional support, specifically during the release portion of the chain.
Author: Andres Almiray
Andres is a Java/Groovy developer and a Java Champion with more than 2 decades of experience in software design and development. He has been involved in web and desktop application development since the early days of Java. Andres is a true believer in open source and has participated on popular projects like Groovy, Griffon, and DbUnit, as well as starting his own projects. Founding member of the Griffon framework and Hackergarten community event.


steinhauer.software