This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/ant-antlibs-cyclonedx.git
commit 9f81746ffe0caa2c9f77b3b4a9da27c6315f52e2 Author: Stefan Bodewig <[email protected]> AuthorDate: Mon Jul 20 20:19:37 2026 +0200 calculate a stable serial number if SOURCE_DATE_EPOCH is set --- changes.xml | 13 +++--- docs/componentbom.html | 9 ++-- ivy.xml | 1 + .../org/apache/ant/cyclonedx/ComponentBomTask.java | 52 ++++++++++++++++++---- 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/changes.xml b/changes.xml index 1ed2fb7..832c0c1 100644 --- a/changes.xml +++ b/changes.xml @@ -72,11 +72,6 @@ A new specialized task "archivebom" simplifies creation of SBOMs for distribution zips/tarballs by providing a few defaults. </action> - <action type="add"> - A new attribute "serialNumber" of the componentbom task allows - the serial number of the generated SBOM to be set to a fixed - value for reproducible builds. - </action> <action type="update"> The componentbom and archivebom tasks honor the SOURCE_DATE_EPOCH environment variable as well as Ant's magic @@ -84,6 +79,14 @@ as timestamp for the generated SBOM for reproducible builds. The default still is to use the current point in time as timestamp. </action> + <action type="add"> + A new attribute "serialNumber" of the componentbom task allows + the serial number of the generated SBOM to be set to a fixed + value for reproducible builds. + If the attribute is not set but SOURCE_DATE_EPOCH is defined the + serial number will be calculated from SOURCE_DATE_EPOCH and the + main component's coordinates. + </action> <action type="update"> The array valued parts of the generated SBOM now have a stable sort order to support reproducible builds. diff --git a/docs/componentbom.html b/docs/componentbom.html index 0170590..4905e50 100644 --- a/docs/componentbom.html +++ b/docs/componentbom.html @@ -85,9 +85,12 @@ <h3>Attributes</h3> number, even if the rest of the content doesn't change. This is what this task does by default (i.e. when the attribute is not set).<br/> - In the context of reproducible builds you may want to have - more control and this is a way to set a fixed serial - number.<br/> + For reproducible builds if the attribute is not set but the + environment variable <code>SOURCE_DATE_EPOCH</code> is set, + the task will generate a stable value that takes the + timestamp and the main component's coordinates into + account.<br/> + Another option for reproducible builds is to set this value explicitly.<br/> The special value "random" can be used to explicitly trigger the task's default behavior.<br/> <em>since CycloneDX Antlib 0.2</em> diff --git a/ivy.xml b/ivy.xml index 9dd6a2b..e789134 100644 --- a/ivy.xml +++ b/ivy.xml @@ -62,6 +62,7 @@ <dependency org="org.apache.ant" name="ant" rev="1.10.17" conf="provided->default"/> <dependency org="org.apache.ant" name="ant-launcher" rev="1.10.17" conf="provided->default"/> <dependency org="org.apache.ivy" name="ivy" rev="2.6.0" conf="provided->default"/> + <dependency org="commons-codec" name="commons-codec" rev="1.22.0" conf="default"/> <dependency org="org.apache.ant" name="ant-antunit" rev="1.5.0" conf="test->default"/> </dependencies> </ivy-module> diff --git a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java index 25acc67..b9603e2 100644 --- a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java +++ b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java @@ -44,6 +44,8 @@ import org.apache.tools.ant.Task; import org.apache.tools.ant.types.Resource; import org.apache.tools.ant.types.ResourceCollection; import org.apache.tools.ant.types.resources.Union; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.codec.digest.MessageDigestAlgorithms; import org.cyclonedx.Format; import org.cyclonedx.exception.GeneratorException; @@ -278,13 +280,11 @@ public class ComponentBomTask extends Task { private Bom createBom() throws IOException { Bom bom = new Bom(); - if (serialNumber != null) { - bom.setSerialNumber(serialNumber); - } else { - bom.setSerialNumber("urn:uuid:" + UUID.randomUUID()); - } + Map.Entry<Date, Boolean> currentTimeAndReproducibleBuildsFlag = DateUtils.getNow(getProject()); + bom.setSerialNumber(getSerialNumber(currentTimeAndReproducibleBuildsFlag.getKey(), + Boolean.TRUE.equals(currentTimeAndReproducibleBuildsFlag.getValue()))); - Metadata meta = createMetadata(); + Metadata meta = createMetadata(currentTimeAndReproducibleBuildsFlag.getKey()); Map<String, String> knownComponents = new HashMap<>(); List<Component> resolvedComponents = new ArrayList<>(); @@ -346,9 +346,45 @@ public class ComponentBomTask extends Task { return bom; } - private Metadata createMetadata() throws IOException { + private String getSerialNumber(Date timestamp, boolean reproducibleBuild) { + if (serialNumber != null) { + return serialNumber; + } else { + UUID uuid = reproducibleBuild ? getReproducibleUuid(timestamp) : UUID.randomUUID(); + return "urn:uuid:" + uuid; + } + } + + // https://www.rfc-editor.org/info/rfc9562/#section-5.1 + private UUID getReproducibleUuid(Date timestamp) { + long timestampMillis = timestamp.getTime(); + long timeLow = (timestampMillis & 0xFFFFFFFFL) << 32; + long timeMid = ((timestampMillis >> 32) & 0xFFFF) << 16; + long version = 1l << 12; + long timeHigh = ((timestampMillis >> 48) & 0x0FFF); + long msb = timeLow | timeMid | version | timeHigh; + + long variant = 1l << 63; + // for clock_seq and node we create a hash of the main compoment's bom-ref (or something close) + String componentId = component.getBomRef(); + if (componentId == null) { + componentId = component.getName() + + ":" + (component.getGroup() == null ? "group" : component.getGroup()) + + ":" + (component.getVersion() == null ? "group" : component.getVersion()); + } + byte[] componentIdHash = new DigestUtils(MessageDigestAlgorithms.MD5).digest(componentId); + long clockseq = ((long)(componentIdHash[0] & 0x3F) << 56) | ((long)(componentIdHash[1] & 0xFF) << 48); + long nodeLow = ((long)(componentIdHash[2] & 0xFFl) << 40) | ((long)(componentIdHash[3] & 0xFF) << 32); + long nodeHigh = ((long)(componentIdHash[4] & 0xFFl) << 24) | ((componentIdHash[5] & 0xFF) << 16) + | ((componentIdHash[6] & 0xFF) << 8) | (componentIdHash[5] & 0xFF); + long lsb = variant | clockseq | nodeLow | nodeHigh; + + return new UUID(msb, lsb); + } + + private Metadata createMetadata(Date timestamp) throws IOException { Metadata meta = new Metadata(); - meta.setTimestamp(DateUtils.getNow(getProject()).getKey()); + meta.setTimestamp(timestamp); ToolInformation antlibToolInformation = ToolData.getToolInformation(specVersion.getVersion()); if (!toolComponents.isEmpty()) { List<org.cyclonedx.model.Component> tools =
