Add option to provide a message digest in update center

The idea is, that an update center can provide message digests/hashes
for the modules it provides. This way, the NetBeans Platform can verify
a download of a module by checking the hash, even if the download
happens over an insecure link (http).

If a download does not yield the same digest, installation will be
blocked, just as if certification validation failed with a
SecurityException.
diff --git a/platform/autoupdate.services/libsrc/org/netbeans/updater/XMLUtil.java b/platform/autoupdate.services/libsrc/org/netbeans/updater/XMLUtil.java
index 7ce7088..b0e7560 100644
--- a/platform/autoupdate.services/libsrc/org/netbeans/updater/XMLUtil.java
+++ b/platform/autoupdate.services/libsrc/org/netbeans/updater/XMLUtil.java
@@ -183,6 +183,8 @@
                     return new InputSource(XMLUtil.class.getResource("resources/autoupdate-catalog-2_7.dtd").toString()); // NOI18N
                 } else if ("-//NetBeans//DTD Autoupdate Module Info 2.7//EN".equals(publicID)) { // NOI18N
                     return new InputSource(XMLUtil.class.getResource("resources/autoupdate-info-2_7.dtd").toString()); // NOI18N
+                } else if ("-//NetBeans//DTD Autoupdate Catalog 2.8//EN".equals(publicID)) { // NOI18N
+                    return new InputSource(XMLUtil.class.getResource("resources/autoupdate-catalog-2_8.dtd").toString()); // NOI18N
                 } else {
                     if (systemID.endsWith(".dtd")) { // NOI18N
                         return new InputSource(new ByteArrayInputStream(new byte[0]));
diff --git a/platform/autoupdate.services/libsrc/org/netbeans/updater/resources/autoupdate-catalog-2_8.dtd b/platform/autoupdate.services/libsrc/org/netbeans/updater/resources/autoupdate-catalog-2_8.dtd
new file mode 100644
index 0000000..074e636
--- /dev/null
+++ b/platform/autoupdate.services/libsrc/org/netbeans/updater/resources/autoupdate-catalog-2_8.dtd
@@ -0,0 +1,96 @@
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+<!-- -//NetBeans//DTD Autoupdate Catalog 2.8//EN -->
+<!-- XML representation of Autoupdate Modules/Updates Catalog -->
+
+<!ELEMENT module_updates ((notification?, content_description?, (module_group|module)*, license*)|error)>
+<!ATTLIST module_updates timestamp CDATA #REQUIRED>
+
+<!ELEMENT module_group ((module_group|module)*)>
+<!ATTLIST module_group name CDATA #REQUIRED>
+
+<!ELEMENT notification (#PCDATA)>
+<!ATTLIST notification url CDATA #IMPLIED>
+
+<!ELEMENT content_description (#PCDATA)>
+<!ATTLIST content_description url CDATA #IMPLIED>
+
+<!ELEMENT module (description?, module_notification?, external_package*, (manifest | l10n), message_digest* )>
+<!ATTLIST module codenamebase CDATA #REQUIRED
+                 homepage     CDATA #IMPLIED
+                 distribution CDATA #REQUIRED
+                 license      CDATA #IMPLIED
+                 downloadsize CDATA #REQUIRED
+                 needsrestart (true|false) #IMPLIED
+                 moduleauthor CDATA #IMPLIED
+                 releasedate  CDATA #IMPLIED
+                 global       (true|false) #IMPLIED
+                 targetcluster CDATA #IMPLIED
+                 preferredupdate (true|false) #IMPLIED
+                 eager (true|false) #IMPLIED
+                 autoload (true|false) #IMPLIED>
+
+<!ELEMENT description (#PCDATA)>
+
+<!ELEMENT module_notification (#PCDATA)>
+
+<!ELEMENT external_package EMPTY>
+<!ATTLIST external_package
+                 name CDATA #REQUIRED
+                 target_name  CDATA #REQUIRED
+                 start_url    CDATA #REQUIRED
+                 description  CDATA #IMPLIED>
+
+<!ELEMENT manifest EMPTY>
+<!ATTLIST manifest OpenIDE-Module CDATA #REQUIRED
+                   OpenIDE-Module-Name CDATA #REQUIRED
+                   OpenIDE-Module-Specification-Version CDATA #REQUIRED
+                   OpenIDE-Module-Implementation-Version CDATA #IMPLIED
+                   OpenIDE-Module-Module-Dependencies CDATA #IMPLIED
+                   OpenIDE-Module-Package-Dependencies CDATA #IMPLIED
+                   OpenIDE-Module-Java-Dependencies CDATA #IMPLIED
+                   OpenIDE-Module-IDE-Dependencies CDATA #IMPLIED
+                   OpenIDE-Module-Short-Description CDATA #IMPLIED
+                   OpenIDE-Module-Long-Description CDATA #IMPLIED
+                   OpenIDE-Module-Display-Category CDATA #IMPLIED
+                   OpenIDE-Module-Provides CDATA #IMPLIED
+                   OpenIDE-Module-Requires CDATA #IMPLIED
+                   OpenIDE-Module-Recommends CDATA #IMPLIED
+                   OpenIDE-Module-Needs CDATA #IMPLIED
+                   AutoUpdate-Show-In-Client (true|false) #IMPLIED
+                   AutoUpdate-Essential-Module (true|false) #IMPLIED
+                   OpenIDE-Module-Fragment-Host CDATA #IMPLIED>
+
+<!ELEMENT l10n EMPTY>
+<!ATTLIST l10n   langcode             CDATA #IMPLIED
+                 brandingcode         CDATA #IMPLIED
+                 module_spec_version  CDATA #IMPLIED
+                 module_major_version CDATA #IMPLIED
+                 OpenIDE-Module-Name  CDATA #IMPLIED
+                 OpenIDE-Module-Long-Description CDATA #IMPLIED>
+
+<!ELEMENT license (#PCDATA)>
+<!ATTLIST license name CDATA #REQUIRED
+                  url  CDATA #IMPLIED>
+
+<!ELEMENT message_digest EMPTY>
+<!ATTLIST message_digest algorithm CDATA #REQUIRED
+                         value     CDATA #REQUIRED>
\ No newline at end of file
diff --git a/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/services/InstallSupportImpl.java b/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/services/InstallSupportImpl.java
index 28263fc..7a37748 100644
--- a/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/services/InstallSupportImpl.java
+++ b/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/services/InstallSupportImpl.java
@@ -31,10 +31,8 @@
 import java.security.CodeSigner;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
-import java.security.cert.CertPath;
 import java.security.cert.Certificate;
 import java.security.cert.TrustAnchor;
-import java.security.cert.X509Certificate;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicLong;
@@ -51,6 +49,7 @@
 import org.netbeans.api.autoupdate.OperationSupport.Restarter;
 import org.netbeans.api.progress.ProgressHandle;
 import org.netbeans.modules.autoupdate.updateprovider.AutoupdateInfoParser;
+import org.netbeans.modules.autoupdate.updateprovider.MessageDigestValue;
 import org.netbeans.modules.autoupdate.updateprovider.ModuleItem;
 import org.netbeans.modules.autoupdate.updateprovider.NetworkAccess;
 import org.netbeans.modules.autoupdate.updateprovider.NetworkAccess.Task;
@@ -1017,7 +1016,7 @@
             //        + ") of is equal to estimatedSize (" + estimatedSize + ").";
             if (estimatedSize != increment) {
                 LOG.log (Level.FINEST, "Increment (" + increment + ") of is not equal to estimatedSize (" + estimatedSize + ").");
-            }            
+            }
         } catch (IOException ioe) {
             LOG.log (Level.INFO, "Writing content of URL " + source + " failed.", ioe);
         } finally {
@@ -1029,6 +1028,7 @@
                 LOG.log (Level.INFO, ioe.getMessage (), ioe);
             }
         }
+
         if (contentLength != -1 && increment != contentLength) {
             if(canceled) {
                 LOG.log(Level.INFO, "Download of " + source + " was cancelled");
@@ -1121,6 +1121,29 @@
                 res = Utilities.MODIFIED;
             }
 
+            {
+                MessageDigestChecker mdChecker = new MessageDigestChecker(impl.getMessageDigests());
+                byte[] buffer = new byte[102400];
+                int read;
+                try(FileInputStream fis = new FileInputStream(nbmFile)) {
+                    while((read = fis.read(buffer)) > 0) {
+                        mdChecker.update(buffer, 0, read);
+                    }
+                }
+                if(!mdChecker.validate()) {
+                    for (String algorithm : mdChecker.getFailingHashes()) {
+                        LOG.log(Level.INFO,
+                            "Failed to validate message digest for ''{0}'' expected ''{1}'' got ''{2}''",
+                            new Object[]{
+                                nbmFile.getAbsolutePath(),
+                                mdChecker.getExpectedHashAsString(algorithm),
+                                mdChecker.getCalculatedHashAsString(algorithm)
+                            });
+                    }
+                    res = Utilities.MODIFIED;
+                }
+            }
+
             if (res != null) {
                 switch (res) {
                     case Utilities.MODIFIED:
diff --git a/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/services/MessageDigestChecker.java b/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/services/MessageDigestChecker.java
new file mode 100644
index 0000000..53d3eaf
--- /dev/null
+++ b/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/services/MessageDigestChecker.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.autoupdate.services;
+
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.netbeans.modules.autoupdate.updateprovider.MessageDigestValue;
+
+public class MessageDigestChecker {
+
+    private static final Logger LOG = Logger.getLogger(MessageDigestChecker.class.getName());
+
+    private final Map<String,MessageDigest> messageDigest = new HashMap<>();
+    private final Map<String,byte[]> exptectedResult = new HashMap<>();
+    private final Map<String,byte[]> calculatedResult = new HashMap<>();
+    private Boolean overallValid = null;
+
+    public MessageDigestChecker(Collection<MessageDigestValue> referenceHashes) {
+        if (referenceHashes != null) {
+            for (MessageDigestValue h : referenceHashes) {
+                try {
+                    MessageDigest md = MessageDigest.getInstance(h.getAlgorithm());
+                    messageDigest.put(h.getAlgorithm(), md);
+                    exptectedResult.put(h.getAlgorithm(), hexDecode(h.getValue()));
+                } catch (NoSuchAlgorithmException ex) {
+                    LOG.log(Level.FINE, "Unsupported Hash Algorithm: {0}", h.getAlgorithm());
+                }
+            }
+        }
+    }
+
+    public void update(byte[] data) {
+        ensureValidateNotCalled();
+        for(MessageDigest md: messageDigest.values()) {
+            md.update(data);
+        }
+    }
+
+    public void update(byte[] data, int offset, int len) {
+        ensureValidateNotCalled();
+        for(MessageDigest md: messageDigest.values()) {
+            md.update(data, offset, len);
+        }
+    }
+
+    public void update(byte data) {
+        ensureValidateNotCalled();
+        for(MessageDigest md: messageDigest.values()) {
+            md.update(data);
+        }
+    }
+
+    private void ensureValidateNotCalled() throws IllegalStateException {
+        if(overallValid != null) {
+            throw new IllegalStateException("update must not be called after validate is invoked");
+        }
+    }
+
+    private void ensureValidateCalled() throws IllegalStateException {
+        if(overallValid == null) {
+            throw new IllegalStateException("This method must not be called before validate is invoked");
+        }
+    }
+
+    public boolean isDigestAvailable() {
+        return ! messageDigest.isEmpty();
+    }
+
+    public boolean validate() throws IOException {
+        if (overallValid == null) {
+            overallValid = true;
+            for (Entry<String, MessageDigest> e : messageDigest.entrySet()) {
+                String algorithm = e.getKey();
+                calculatedResult.put(algorithm, e.getValue().digest());
+                boolean localValid = Arrays.equals(
+                    exptectedResult.get(algorithm),
+                    calculatedResult.get(algorithm));
+                overallValid &= localValid;
+            }
+        }
+        return overallValid;
+    }
+
+    public List<String> getFailingHashes() {
+        ensureValidateCalled();
+        List<String> result = new ArrayList<>();
+        for(String algorithm: messageDigest.keySet()) {
+            if(! Arrays.equals(
+                exptectedResult.get(algorithm),
+                calculatedResult.get(algorithm))) {
+                result.add(algorithm);
+            }
+        }
+        return result;
+    }
+
+    public String getExpectedHashAsString(String algorithm) {
+        ensureValidateCalled();
+        return hexEncode(exptectedResult.get(algorithm));
+    }
+
+    public String getCalculatedHashAsString(String algorithm) {
+        ensureValidateCalled();
+        return hexEncode(calculatedResult.get(algorithm));
+    }
+
+    public static String hexEncode(byte[] input) {
+        StringBuilder sb = new StringBuilder(input.length * 2);
+        for(byte b: input) {
+            sb.append(Character.forDigit((b & 0xF0) >> 4, 16));
+            sb.append(Character.forDigit((b & 0x0F), 16));
+        }
+        return sb.toString();
+    }
+
+    public static byte[] hexDecode(String input) {
+        int length = input.length() / 2;
+        byte[] result = new byte[length];
+        for(int i = 0; i < length; i++) {
+            int b = Character.digit(input.charAt(i * 2), 16) << 4;
+            b |= Character.digit(input.charAt(i * 2 + 1), 16);
+            result[i] = (byte) (b & 0xFF);
+        }
+        return result;
+    }
+}
diff --git a/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/services/UpdateElementImpl.java b/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/services/UpdateElementImpl.java
index 7cc580c..d2ba5dd 100644
--- a/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/services/UpdateElementImpl.java
+++ b/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/services/UpdateElementImpl.java
@@ -19,10 +19,12 @@
 
 package org.netbeans.modules.autoupdate.services;
 
+import java.util.ArrayList;
 import java.util.List;
 import org.netbeans.api.autoupdate.UpdateElement;
 import org.netbeans.api.autoupdate.UpdateManager;
 import org.netbeans.api.autoupdate.UpdateUnit;
+import org.netbeans.modules.autoupdate.updateprovider.MessageDigestValue;
 import org.netbeans.modules.autoupdate.updateprovider.InstallInfo;
 import org.netbeans.modules.autoupdate.updateprovider.UpdateItemImpl;
 import org.openide.modules.ModuleInfo;
@@ -35,8 +37,13 @@
 public abstract class UpdateElementImpl extends Object {
     private UpdateUnit unit;
     private UpdateElement element;
+    private List<MessageDigestValue> messageDigests = new ArrayList<>();
     
-    public UpdateElementImpl (UpdateItemImpl item, String providerName) {}
+    public UpdateElementImpl (UpdateItemImpl item, String providerName) {
+        if(item.getMessageDigests() != null) {
+            messageDigests.addAll(item.getMessageDigests());
+        }
+    }
     
     public UpdateUnit getUpdateUnit () {
         return unit;
@@ -99,4 +106,11 @@
     // XXX: try to rid of this
     public abstract InstallInfo getInstallInfo ();
 
+    public List<MessageDigestValue> getMessageDigests() {
+        return messageDigests;
+    }
+
+    public void setMessageDigests(List<MessageDigestValue> messageDigests) {
+        this.messageDigests = messageDigests;
+    }
 }
diff --git a/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/updateprovider/AutoupdateCatalogParser.java b/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/updateprovider/AutoupdateCatalogParser.java
index d95e1b4..a6d737f 100644
--- a/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/updateprovider/AutoupdateCatalogParser.java
+++ b/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/updateprovider/AutoupdateCatalogParser.java
@@ -34,7 +34,6 @@
 import java.util.zip.GZIPInputStream;
 import javax.xml.parsers.SAXParser;
 import javax.xml.parsers.SAXParserFactory;
-import org.netbeans.Module;
 import org.netbeans.modules.autoupdate.services.Trampoline;
 import org.netbeans.modules.autoupdate.services.UpdateLicenseImpl;
 import org.netbeans.modules.autoupdate.services.Utilities;
@@ -52,6 +51,7 @@
     private final AutoupdateCatalogProvider provider;
     private final EntityResolver entityResolver;
     private final URI baseUri;
+    private final List<MessageDigestValue> messageDigestsBuffer = new ArrayList<>();
     
     private AutoupdateCatalogParser (Map<String, UpdateItem> items, AutoupdateCatalogProvider provider, URI base) {
         this.items = items;
@@ -111,7 +111,7 @@
     
     private static enum ELEMENTS {
         module_updates, module_group, notification, content_description, module, description,
-        module_notification, external_package, manifest, l10n, license
+        module_notification, external_package, manifest, l10n, license, message_digest
     }
     
     private static final String MODULE_UPDATES_ATTR_TIMESTAMP = "timestamp"; // NOI18N
@@ -149,7 +149,10 @@
     private static final String L10N_ATTR_MODULE_MAJOR_VERSION = "module_major_version"; // NOI18N
     private static final String L10N_ATTR_LOCALIZED_MODULE_NAME = "OpenIDE-Module-Name"; // NOI18N
     private static final String L10N_ATTR_LOCALIZED_MODULE_DESCRIPTION = "OpenIDE-Module-Long-Description"; // NOI18N
-    
+
+    private static final String MESSAGE_DIGEST_ATTR_ALGORITHM = "algorithm"; // NOI18N
+    private static final String MESSAGE_DIGEST_ATTR_VALUE = "value"; // NOI18N
+
     private static String GZIP_EXTENSION = ".gz"; // NOI18N
     private static String XML_EXTENSION = ".xml"; // NOI18N
     private static String GZIP_MIME_TYPE = "application/x-gzip"; // NOI18N
@@ -359,6 +362,8 @@
                 
                 currentLicense.pop ();
                 break;
+            case message_digest:
+                break;
             default:
                 ERR.warning ("Unknown element " + qName);
         }
@@ -454,6 +459,16 @@
                 map.put(attributes.getValue (LICENSE_ATTR_NAME), attributes.getValue (LICENSE_ATTR_URL));
                 currentLicense.push (map);
                 break;
+            case message_digest:
+                ModuleDescriptor desc2 = currentModule.peek ();
+                // At this point the manifest element must have been seen
+                UpdateItem ui = items.get (desc2.getId ());
+                UpdateItemImpl uiImpl = Trampoline.SPI.impl (ui);
+                uiImpl.getMessageDigests().add(new MessageDigestValue(
+                    attributes.getValue(MESSAGE_DIGEST_ATTR_ALGORITHM),
+                    attributes.getValue(MESSAGE_DIGEST_ATTR_VALUE)
+                ));
+                break;
             default:
                 ERR.warning ("Unknown element " + qName);
         }
@@ -511,6 +526,8 @@
         private String catalogDate;
         
         private String fragmentHost;
+
+        private List<MessageDigestValue> hashes;
         
         private static ModuleDescriptor md = null;
         
@@ -608,7 +625,7 @@
             return res;
         }
         
-        private void cleanUp (){
+        public void cleanUp (){
             this.specVersion = null;
             this.mf = null;
             this.notification = null;
diff --git a/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/updateprovider/MessageDigestValue.java b/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/updateprovider/MessageDigestValue.java
new file mode 100644
index 0000000..1351ef9
--- /dev/null
+++ b/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/updateprovider/MessageDigestValue.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.autoupdate.updateprovider;
+
+import java.util.Objects;
+
+public class MessageDigestValue {
+    private String algorithm;
+    private String value;
+
+    public MessageDigestValue() {
+    }
+
+    public MessageDigestValue(String algorithm, String value) {
+        this.algorithm = algorithm;
+        this.value = value;
+    }
+
+    public String getAlgorithm() {
+        return algorithm;
+    }
+
+    public void setAlgorithm(String algorithm) {
+        this.algorithm = algorithm;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 53 * hash + Objects.hashCode(this.algorithm);
+        hash = 53 * hash + Objects.hashCode(this.value);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final MessageDigestValue other = (MessageDigestValue) obj;
+        if (!Objects.equals(this.algorithm, other.algorithm)) {
+            return false;
+        }
+        if (!Objects.equals(this.value, other.value)) {
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/updateprovider/UpdateItemImpl.java b/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/updateprovider/UpdateItemImpl.java
index 0f639dd..bb4497c 100644
--- a/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/updateprovider/UpdateItemImpl.java
+++ b/platform/autoupdate.services/src/org/netbeans/modules/autoupdate/updateprovider/UpdateItemImpl.java
@@ -19,6 +19,8 @@
 
 package org.netbeans.modules.autoupdate.updateprovider;
 
+import java.util.ArrayList;
+import java.util.List;
 import org.netbeans.modules.autoupdate.services.UpdateLicenseImpl;
 import org.netbeans.spi.autoupdate.UpdateItem;
 
@@ -28,6 +30,7 @@
  */
 public abstract class UpdateItemImpl {
     private UpdateItem originalUpdateItem;
+    private List<MessageDigestValue> messageDigests = new ArrayList<>();
 
     /** Creates a new instance of UpdateItemImpl */
     UpdateItemImpl () {
@@ -64,4 +67,12 @@
     public String getFragmentHost() {
         return null;
     }
+
+    public List<MessageDigestValue> getMessageDigests() {
+        return messageDigests;
+    }
+
+    public void setMessageDigests(List<MessageDigestValue> messageDigests) {
+        this.messageDigests = new ArrayList<>(messageDigests);
+    }
 }
diff --git a/platform/autoupdate.services/test/unit/src/org/netbeans/modules/autoupdate/services/MessageDigestCheckerTest.java b/platform/autoupdate.services/test/unit/src/org/netbeans/modules/autoupdate/services/MessageDigestCheckerTest.java
new file mode 100644
index 0000000..79af4ab
--- /dev/null
+++ b/platform/autoupdate.services/test/unit/src/org/netbeans/modules/autoupdate/services/MessageDigestCheckerTest.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.autoupdate.services;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class MessageDigestCheckerTest {
+
+    public MessageDigestCheckerTest() {
+    }
+
+    @Test
+    public void testHexEncode() throws NoSuchAlgorithmException {
+        MessageDigest sha512 = MessageDigest.getInstance("SHA-512");
+        byte[] hash = sha512.digest(new byte[] {});
+        assertEquals(
+            "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e",
+            MessageDigestChecker.hexEncode(hash));
+    }
+
+    @Test
+    public void testHexDecode() throws NoSuchAlgorithmException {
+        MessageDigest sha512 = MessageDigest.getInstance("SHA-512");
+        byte[] hash = sha512.digest(new byte[] {});
+        assertArrayEquals(
+            hash,
+            MessageDigestChecker.hexDecode("cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"));
+    }
+}