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 =

Reply via email to