binman: Support hashing entries

Sometimesi it us useful to be able to verify the content of entries with
a hash. Add an easy way to do this in binman. The hash information can be
retrieved from the device tree at run time.

Signed-off-by: Simon Glass <sjg@chromium.org>
diff --git a/tools/binman/README b/tools/binman/README
index cf1a06d..088f3a6 100644
--- a/tools/binman/README
+++ b/tools/binman/README
@@ -466,6 +466,28 @@
 	binman -E >tools/binman/README.entries
 
 
+Hashing Entries
+---------------
+
+It is possible to ask binman to hash the contents of an entry and write that
+value back to the device-tree node. For example:
+
+	binman {
+		u-boot {
+			hash {
+				algo = "sha256";
+			};
+		};
+	};
+
+Here, a new 'value' property will be written to the 'hash' node containing
+the hash of the 'u-boot' entry. Only SHA256 is supported at present. Whole
+sections can be hased if desired, by adding the 'hash' node to the section.
+
+The has value can be chcked at runtime by hashing the data actually read and
+comparing this has to the value in the device tree.
+
+
 Order of image creation
 -----------------------
 
diff --git a/tools/binman/bsection.py b/tools/binman/bsection.py
index 52ac31a..650e9ba 100644
--- a/tools/binman/bsection.py
+++ b/tools/binman/bsection.py
@@ -89,6 +89,8 @@
 
     def _ReadEntries(self):
         for node in self._node.subnodes:
+            if node.name == 'hash':
+                continue
             entry = Entry.Create(self, node)
             entry.SetPrefix(self._name_prefix)
             self._entries[node.name] = entry
@@ -112,6 +114,7 @@
         for prop in ['offset', 'size', 'image-pos']:
             if not prop in self._node.props:
                 state.AddZeroProp(self._node, prop)
+        state.CheckAddHashProp(self._node)
         for entry in self._entries.values():
             entry.AddMissingProperties()
 
diff --git a/tools/binman/entry.py b/tools/binman/entry.py
index 0915b47..fd72234 100644
--- a/tools/binman/entry.py
+++ b/tools/binman/entry.py
@@ -189,12 +189,16 @@
         for prop in ['offset', 'size', 'image-pos']:
             if not prop in self._node.props:
                 state.AddZeroProp(self._node, prop)
+        err = state.CheckAddHashProp(self._node)
+        if err:
+            self.Raise(err)
 
     def SetCalculatedProperties(self):
         """Set the value of device-tree properties calculated by binman"""
         state.SetInt(self._node, 'offset', self.offset)
         state.SetInt(self._node, 'size', self.size)
         state.SetInt(self._node, 'image-pos', self.image_pos)
+        state.CheckSetHashValue(self._node, self.GetData)
 
     def ProcessFdt(self, fdt):
         """Allow entries to adjust the device tree
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index b156943..c46a065 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -6,6 +6,7 @@
 #
 #    python -m unittest func_test.TestFunctional.testHelp
 
+import hashlib
 from optparse import OptionParser
 import os
 import shutil
@@ -1608,6 +1609,41 @@
         self.assertIn("Node '/binman/_testing': Cannot obtain contents when "
                       'expanding entry', str(e.exception))
 
+    def testHash(self):
+        """Test hashing of the contents of an entry"""
+        _, _, _, out_dtb_fname = self._DoReadFileDtb('90_hash.dts',
+                use_real_dtb=True, update_dtb=True)
+        dtb = fdt.Fdt(out_dtb_fname)
+        dtb.Scan()
+        hash_node = dtb.GetNode('/binman/u-boot/hash').props['value']
+        m = hashlib.sha256()
+        m.update(U_BOOT_DATA)
+        self.assertEqual(m.digest(), ''.join(hash_node.value))
+
+    def testHashNoAlgo(self):
+        with self.assertRaises(ValueError) as e:
+            self._DoReadFileDtb('91_hash_no_algo.dts', update_dtb=True)
+        self.assertIn("Node \'/binman/u-boot\': Missing \'algo\' property for "
+                      'hash node', str(e.exception))
+
+    def testHashBadAlgo(self):
+        with self.assertRaises(ValueError) as e:
+            self._DoReadFileDtb('92_hash_bad_algo.dts', update_dtb=True)
+        self.assertIn("Node '/binman/u-boot': Unknown hash algorithm",
+                      str(e.exception))
+
+    def testHashSection(self):
+        """Test hashing of the contents of an entry"""
+        _, _, _, out_dtb_fname = self._DoReadFileDtb('99_hash_section.dts',
+                use_real_dtb=True, update_dtb=True)
+        dtb = fdt.Fdt(out_dtb_fname)
+        dtb.Scan()
+        hash_node = dtb.GetNode('/binman/section/hash').props['value']
+        m = hashlib.sha256()
+        m.update(U_BOOT_DATA)
+        m.update(16 * 'a')
+        self.assertEqual(m.digest(), ''.join(hash_node.value))
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tools/binman/state.py b/tools/binman/state.py
index 2f8c086..d945e4b 100644
--- a/tools/binman/state.py
+++ b/tools/binman/state.py
@@ -5,6 +5,7 @@
 # Holds and modifies the state information held by binman
 #
 
+import hashlib
 import re
 from sets import Set
 
@@ -226,3 +227,27 @@
     """
     for n in GetUpdateNodes(node):
         n.SetInt(prop, value)
+
+def CheckAddHashProp(node):
+    hash_node = node.FindNode('hash')
+    if hash_node:
+        algo = hash_node.props.get('algo')
+        if not algo:
+            return "Missing 'algo' property for hash node"
+        if algo.value == 'sha256':
+            size = 32
+        else:
+            return "Unknown hash algorithm '%s'" % algo
+        for n in GetUpdateNodes(hash_node):
+            n.AddEmptyProp('value', size)
+
+def CheckSetHashValue(node, get_data_func):
+    hash_node = node.FindNode('hash')
+    if hash_node:
+        algo = hash_node.props.get('algo').value
+        if algo == 'sha256':
+            m = hashlib.sha256()
+            m.update(get_data_func())
+            data = m.digest()
+        for n in GetUpdateNodes(hash_node):
+            n.SetData('value', data)
diff --git a/tools/binman/test/90_hash.dts b/tools/binman/test/90_hash.dts
new file mode 100644
index 0000000..2003045
--- /dev/null
+++ b/tools/binman/test/90_hash.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+	binman {
+		u-boot {
+			hash {
+				algo = "sha256";
+			};
+		};
+	};
+};
diff --git a/tools/binman/test/91_hash_no_algo.dts b/tools/binman/test/91_hash_no_algo.dts
new file mode 100644
index 0000000..b64df20
--- /dev/null
+++ b/tools/binman/test/91_hash_no_algo.dts
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+	binman {
+		u-boot {
+			hash {
+			};
+		};
+	};
+};
diff --git a/tools/binman/test/92_hash_bad_algo.dts b/tools/binman/test/92_hash_bad_algo.dts
new file mode 100644
index 0000000..d240200
--- /dev/null
+++ b/tools/binman/test/92_hash_bad_algo.dts
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+	binman {
+		u-boot {
+			hash {
+				algo = "invalid";
+			};
+		};
+	};
+};
diff --git a/tools/binman/test/99_hash_section.dts b/tools/binman/test/99_hash_section.dts
new file mode 100644
index 0000000..dcd8683
--- /dev/null
+++ b/tools/binman/test/99_hash_section.dts
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: GPL-2.0+
+/dts-v1/;
+
+/ {
+	binman {
+		section {
+			u-boot {
+			};
+			fill {
+				size = <0x10>;
+				fill-byte = [61];
+			};
+			hash {
+				algo = "sha256";
+			};
+		};
+	};
+};