[lc_ctrl/otp_ctrl] Update mmap and state generation scripts

This moves the OTP scrambling constant generation (for keys and
digests) into the memory map generation script instead of using
the topgen RndCnt mechanism.

Likewise, the generation of the RAW unlock token is moved into
the life cycle state generation script.

These changes have a couple of advantages later on:
- The scrambling keys are conveniently available in the OTP memory map
configuration, which eases generation of the OTP preload image later on.
- The RAW unlock token does not have to be "statically" hashed in RTL,
but can be hashed in Python as a preprocessing step before dumping it
into the template.
- It will be easier to replace the token hashing algo with KMAC later
on.

Note that we do not expect to have multiple instances of the OTP or life
cycle controllers in our top-level, hence moving away from the RndCnst
mechanism (which uses instantiation paramters) is safe.

Signed-off-by: Michael Schaffner <msf@opentitan.org>
diff --git a/hw/ip/lc_ctrl/data/lc_ctrl_state.hjson b/hw/ip/lc_ctrl/data/lc_ctrl_state.hjson
index ad0d611..5550320 100644
--- a/hw/ip/lc_ctrl/data/lc_ctrl_state.hjson
+++ b/hw/ip/lc_ctrl/data/lc_ctrl_state.hjson
@@ -37,4 +37,17 @@
     min_hw       : 5,
     max_hw       : 17,
     min_hd       : 5
+
+    // LC token size in bit
+    token_size   : 128
+    tokens       : [
+        {
+            name:  "AllZeroToken"
+            value: "0x0"
+        }
+        {
+            name:  "RndCnstRawUnlockToken"
+            value: "<random>"
+        }
+    ]
 }
diff --git a/hw/ip/lc_ctrl/rtl/lc_ctrl_state_pkg.sv.tpl b/hw/ip/lc_ctrl/rtl/lc_ctrl_state_pkg.sv.tpl
index b7977a1..9ea44f4 100644
--- a/hw/ip/lc_ctrl/rtl/lc_ctrl_state_pkg.sv.tpl
+++ b/hw/ip/lc_ctrl/rtl/lc_ctrl_state_pkg.sv.tpl
@@ -13,6 +13,11 @@
 data_width = lc_st_enc.config['secded']['data_width']
 ecc_width  = lc_st_enc.config['secded']['ecc_width']
 %>
+
+  /////////////////////////////////////////////
+  // Life cycle manufacturing state encoding //
+  /////////////////////////////////////////////
+
   // These values have been generated such that they are incrementally writeable with respect
   // to the ECC polynomial specified. The values are used to define the life cycle manufacturing
   // state and transition counter encoding in lc_ctrl_pkg.sv.
@@ -56,4 +61,14 @@
 
 % endfor
 
+  ///////////////////////////////////////////
+  // Hashed RAW unlock and all-zero tokens //
+  ///////////////////////////////////////////
+<% token_size = lc_st_enc.config['token_size'] %>
+% for token in lc_st_enc.config['tokens']:
+  parameter logic [${token_size-1}:0] ${token['name']} = {
+    ${"{0:}'h{1:0X}".format(token_size, token['value'])}
+  };
+% endfor
+
 endpackage : lc_ctrl_state_pkg
diff --git a/hw/ip/otp_ctrl/data/otp_ctrl.hjson.tpl b/hw/ip/otp_ctrl/data/otp_ctrl.hjson.tpl
index eb8eebe..4885a6f 100644
--- a/hw/ip/otp_ctrl/data/otp_ctrl.hjson.tpl
+++ b/hw/ip/otp_ctrl/data/otp_ctrl.hjson.tpl
@@ -72,30 +72,6 @@
       randcount: "40",
       randtype:  "perm", // random permutation for randcount elements
     }
-    { name:      "RndCnstKey",
-      desc:      "Compile-time random scrambling keys",
-      type:      "otp_ctrl_pkg::key_array_t"
-      randcount: "384",  // 3*128
-      randtype:  "data", // randomize randcount databits
-    }
-    { name:      "RndCnstDigestConst",
-      desc:      "Compile-time random digest constant",
-      type:      "otp_ctrl_pkg::digest_const_array_t"
-      randcount: "640",  // 5*128
-      randtype:  "data", // randomize randcount databits
-    }
-    { name:      "RndCnstDigestIV",
-      desc:      "Compile-time random digest IV",
-      type:      "otp_ctrl_pkg::digest_iv_array_t"
-      randcount: "320",  // 5*64
-      randtype:  "data", // randomize randcount databits
-    }
-    { name:      "RndCnstRawUnlockToken",
-      desc:      "Compile-time random value for RAW unlock token.",
-      type:      "lc_ctrl_pkg::lc_token_t"
-      randcount: "128",
-      randtype:  "data", // randomize randcount databits
-    }
     // Normal parameters
     { name: "NumSramKeyReqSlots",
       desc: "Number of key slots",
diff --git a/hw/ip/otp_ctrl/data/otp_ctrl_mmap.hjson b/hw/ip/otp_ctrl/data/otp_ctrl_mmap.hjson
index c785172..ed13db6 100644
--- a/hw/ip/otp_ctrl/data/otp_ctrl_mmap.hjson
+++ b/hw/ip/otp_ctrl/data/otp_ctrl_mmap.hjson
@@ -22,6 +22,58 @@
         width: "2", // bytes
         depth: "1024"
     }
+
+    // Definition of scrambling and digest constants and keys.
+    scrambling: {
+        key_size:  "16",
+        iv_size:   "8",
+        cnst_size: "16",
+        keys: [
+            {
+                name:  "Secret0Key",
+                value: "<random>",
+            }
+            {
+                name:  "Secret1Key",
+                value: "<random>",
+            }
+            {
+                name:  "Secret2Key",
+                value: "<random>",
+            }
+        ]
+        digests: [
+            // This is the consistency digest used by all partitions.
+            {
+                name:       "CnstyDigest",
+                iv_value:   "<random>",
+                cnst_value: "<random>",
+            }
+            // The other digest configurations below are used for
+            // key derivation and token hashing.
+            {
+                name:       "LcRawDigest",
+                iv_value:   "<random>",
+                cnst_value: "<random>",
+            }
+            {
+                name:       "FlashDataKey",
+                iv_value:   "<random>",
+                cnst_value: "<random>",
+            }
+            {
+                name:       "FlashAddrKey",
+                iv_value:   "<random>",
+                cnst_value: "<random>",
+            }
+            {
+                name:       "SramDataKey",
+                iv_value:   "<random>",
+                cnst_value: "<random>",
+            }
+        ]
+    }
+
     // The enumeration order below defines the address map of the OTP controller,
     // if the offsets are not defined explicitly via the "offset" key.
     // Note that the digest items are added automatically to the address map.
@@ -89,7 +141,7 @@
                     // Default value to be output in case partition has not
                     // initialized or is in error state. If not specified,
                     // a value of '0 will be used.
-                    inv_default: "{256{1'b1}}",
+                    inv_default: "<random>",
                 }
                 {
                     name: "HW_CFG_CONTENT",
@@ -118,12 +170,12 @@
                     // This will generate a random default to be output in
                     // case partition has not initialized or is in error state.
                     // If not specified, a value of '0 will be used.
-                    rand_inv_default: "True",
+                    inv_default: "<random>",
                     size: "16"
                 }
                 {
                     name: "TEST_EXIT_TOKEN",
-                    rand_inv_default: "True",
+                    inv_default: "<random>",
                     size: "16"
                 }
             ],
@@ -144,17 +196,17 @@
             items: [
                 {
                     name: "FLASH_ADDR_KEY_SEED",
-                    rand_inv_default: "True",
+                    inv_default: "<random>",
                     size: "32"
                 }
                 {
                     name: "FLASH_DATA_KEY_SEED",
-                    rand_inv_default: "True",
+                    inv_default: "<random>",
                     size: "32"
                 }
                 {
                     name: "SRAM_DATA_KEY_SEED",
-                    rand_inv_default: "True",
+                    inv_default: "<random>",
                     size: "16"
                 }
             ],
@@ -176,17 +228,17 @@
             items: [
                 {
                     name: "RMA_TOKEN",
-                    rand_inv_default: "True",
+                    inv_default: "<random>",
                     size: "16"
                 }
                 {
                     name: "CREATOR_ROOT_KEY_SHARE0",
-                    rand_inv_default: "True",
+                    inv_default: "<random>",
                     size: "32"
                 }
                 {
                     name: "CREATOR_ROOT_KEY_SHARE1",
-                    rand_inv_default: "True",
+                    inv_default: "<random>",
                     size: "32"
                 }
             ],
diff --git a/hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv.tpl b/hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv.tpl
index 8b0c72e..84f823a 100644
--- a/hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv.tpl
+++ b/hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv.tpl
@@ -10,21 +10,113 @@
 //
 <%
   def PascalCase(inp):
+    # if input is in snake case format
+    # or ALLCAPS
     oup = ''
-    upper = True
-    for k in inp.lower():
-      if k == '_':
-        upper = True
-      else:
-        oup += k.upper() if upper else k
-        upper = False
+    if '_' in inp or inp == inp.upper():
+      upper = True
+      for k in inp:
+        if k == '_':
+          upper = True
+        else:
+          oup += k.upper() if upper else k.lower()
+          upper = False
+    # assume its a possibly PascalCased string.
+    # just make sure the first char is capitalized.
+    else:
+      oup = inp[0].upper()
+      if len(inp) > 1:
+        oup += inp[1:]
+
     return oup
+
 %>
 package otp_ctrl_part_pkg;
 
+  import prim_util_pkg::vbits;
   import otp_ctrl_reg_pkg::*;
   import otp_ctrl_pkg::*;
 
+  ////////////////////////////////////
+  // Scrambling Constants and Types //
+  ////////////////////////////////////
+
+  parameter int NumScrmblKeys = ${len(otp_mmap.config["scrambling"]["keys"])};
+  parameter int NumDigestSets = ${len(otp_mmap.config["scrambling"]["digests"])};
+  parameter int ConstSelWidth = (NumScrmblKeys > NumDigestSets) ?
+                                vbits(NumScrmblKeys) :
+                                vbits(NumDigestSets);
+
+  typedef enum logic [ConstSelWidth-1:0] {
+    StandardMode,
+    ChainedMode
+  } digest_mode_e;
+
+  typedef logic [NumScrmblKeys-1:0][ScrmblKeyWidth-1:0] key_array_t;
+  typedef logic [NumDigestSets-1:0][ScrmblKeyWidth-1:0] digest_const_array_t;
+  typedef logic [NumDigestSets-1:0][ScrmblBlockWidth-1:0] digest_iv_array_t;
+
+  typedef enum logic [ConstSelWidth-1:0] {
+% for key in otp_mmap.config["scrambling"]["keys"]:
+    ${PascalCase(key["name"])}${"" if loop.last else ","}
+% endfor
+  } key_sel_e;
+
+  typedef enum logic [ConstSelWidth-1:0] {
+% for dig in otp_mmap.config["scrambling"]["digests"]:
+    ${PascalCase(dig["name"])}${"" if loop.last else ","}
+% endfor
+  } digest_sel_e;
+
+  parameter key_array_t RndCnstKey = {
+% for key in otp_mmap.config["scrambling"]["keys"][::-1]:
+    ${"{0:}'h{1:0X}".format(otp_mmap.config["scrambling"]["key_size"] * 8, key["value"])}${"" if loop.last else ","}
+% endfor
+  };
+
+  // Note: digest set 0 is used for computing the partition digests. Constants at
+  // higher indices are used to compute the scrambling keys.
+  parameter digest_const_array_t RndCnstDigestConst = {
+% for dig in otp_mmap.config["scrambling"]["digests"][::-1]:
+    ${"{0:}'h{1:0X}".format(otp_mmap.config["scrambling"]["cnst_size"] * 8, dig["cnst_value"])}${"" if loop.last else ","}
+% endfor
+  };
+
+  parameter digest_iv_array_t RndCnstDigestIV = {
+% for dig in otp_mmap.config["scrambling"]["digests"][::-1]:
+    ${"{0:}'h{1:0X}".format(otp_mmap.config["scrambling"]["iv_size"] * 8, dig["iv_value"])}${"" if loop.last else ","}
+% endfor
+  };
+
+
+  /////////////////////////////////////
+  // Typedefs for Partition Metadata //
+  /////////////////////////////////////
+
+  typedef enum logic [1:0] {
+    Unbuffered,
+    Buffered,
+    LifeCycle
+  } part_variant_e;
+
+  typedef struct packed {
+    part_variant_e variant;
+    // Offset and size within the OTP array, in Bytes.
+    logic [OtpByteAddrWidth-1:0] offset;
+    logic [OtpByteAddrWidth-1:0] size;
+    // Key index to use for scrambling.
+    key_sel_e key_sel;
+    // Attributes
+    logic secret;     // Whether the partition is secret (and hence scrambled)
+    logic hw_digest;  // Whether the partition has a hardware digest
+    logic write_lock; // Whether the partition is write lockable (via digest)
+    logic read_lock;  // Whether the partition is read lockable (via digest)
+  } part_info_t;
+
+  ////////////////////////
+  // Partition Metadata //
+  ////////////////////////
+
   localparam part_info_t PartInfo [NumPart] = '{
 % for part in otp_mmap.config["partitions"]:
     // ${part["name"]}
@@ -77,9 +169,8 @@
   % for k, part in enumerate(otp_mmap.config["partitions"][::-1]):
     ${int(part["size"])*8}'({
     % for item in part["items"][::-1]:
-      ${item["inv_default"]}${("\n    })," if k < len(otp_mmap.config["partitions"])-1 else "\n    })});") if loop.last else ","}
+      ${"{}'h{:0X}".format(item["size"] * 8, item["inv_default"])}${("\n    })," if k < len(otp_mmap.config["partitions"])-1 else "\n    })});") if loop.last else ","}
     % endfor
   % endfor
 
-
 endpackage : otp_ctrl_part_pkg
diff --git a/util/design/lib/LcStEnc.py b/util/design/lib/LcStEnc.py
index 9b92ea2..3526153 100644
--- a/util/design/lib/LcStEnc.py
+++ b/util/design/lib/LcStEnc.py
@@ -6,9 +6,10 @@
 """
 import logging as log
 import random
+from collections import OrderedDict
 
 from lib.common import (check_int, ecc_encode, get_hd, hd_histogram,
-                        is_valid_codeword, scatter_bits)
+                        is_valid_codeword, scatter_bits, random_or_hexvalue)
 
 
 def _is_incremental_codeword(word1, word2):
@@ -153,6 +154,10 @@
             log.error('Missing secded configuration')
             exit(1)
 
+        if 'tokens' not in config:
+            log.error('Missing token configuration')
+            exit(1)
+
         config['secded'].setdefault('data_width', 0)
         config['secded'].setdefault('ecc_width', 0)
         config['secded'].setdefault('ecc_matrix', [[]])
@@ -162,6 +167,7 @@
         config.setdefault('min_hw', 0)
         config.setdefault('max_hw', 0)
         config.setdefault('min_hd', 0)
+        config.setdefault('token_size', 128)
 
         config['seed'] = check_int(config['seed'])
 
@@ -178,6 +184,7 @@
         config['min_hw'] = check_int(config['min_hw'])
         config['max_hw'] = check_int(config['max_hw'])
         config['min_hd'] = check_int(config['min_hd'])
+        config['token_size'] = check_int(config['token_size'])
 
         total_width = config['secded']['data_width'] + config['secded'][
             'ecc_width']
@@ -206,6 +213,18 @@
 
         log.info('')
 
+        hashed_tokens = []
+        for token in config['tokens']:
+            random_or_hexvalue(token, 'value', config['token_size'])
+            hashed_token = OrderedDict()
+            hashed_token['name'] = token['name'] + 'Hashed'
+            # TODO: plug in PRESENT-based hashing algo or KMAC
+            hashed_token['value'] = token['value']
+            hashed_tokens.append(hashed_token)
+
+        config['tokens'] += hashed_tokens
+
+        print(config['tokens'])
         self.config = config
 
         # Re-initialize with seed to make results reproducible.
diff --git a/util/design/lib/OtpMemMap.py b/util/design/lib/OtpMemMap.py
index 0601481..ab5731a 100644
--- a/util/design/lib/OtpMemMap.py
+++ b/util/design/lib/OtpMemMap.py
@@ -11,12 +11,211 @@
 from math import ceil, log2
 
 from tabulate import tabulate
-from lib.common import check_bool, check_int
+from lib.common import check_bool, check_int, random_or_hexvalue
 
 DIGEST_SUFFIX = "_DIGEST"
 DIGEST_SIZE = 8
 
 
+def _validate_otp(otp):
+    '''Validate OTP entry'''
+    otp.setdefault("depth", "1024")
+    otp.setdefault("width", "2")
+    otp["depth"] = check_int(otp["depth"])
+    otp["width"] = check_int(otp["width"])
+    otp["size"] = otp["depth"] * otp["width"]
+    otp["addr_width"] = ceil(log2(check_int(otp["depth"])))
+    otp["byte_addr_width"] = ceil(log2(otp["size"]))
+
+
+def _validate_scrambling(scr):
+    '''Validate SCrambling entry'''
+    scr.setdefault("key_size", "16")
+    scr.setdefault("iv_size", "8")
+    scr.setdefault("cnst_size", "16")
+    scr["key_size"] = check_int(scr["key_size"])
+    scr["iv_size"] = check_int(scr["iv_size"])
+    scr["cnst_size"] = check_int(scr["cnst_size"])
+
+    if "keys" not in scr:
+        log.error("Missing key configuration.")
+        exit(1)
+    if "digests" not in scr:
+        log.error("Missing digest configuration.")
+        exit(1)
+
+    for key in scr["keys"]:
+        key.setdefault("name", "unknown_key_name")
+        key.setdefault("value", "<random>")
+        random_or_hexvalue(key, "value", scr["key_size"] * 8)
+
+    for dig in scr["digests"]:
+        dig.setdefault("name", "unknown_key_name")
+        dig.setdefault("iv_value", "<random>")
+        dig.setdefault("cnst_value", "<random>")
+        random_or_hexvalue(dig, "iv_value", scr["iv_size"] * 8)
+        random_or_hexvalue(dig, "cnst_value", scr["cnst_size"] * 8)
+
+
+def _validate_part(part, offset, key_names):
+    '''Validates a partition within the OTP memory map'''
+    part.setdefault("offset", offset)
+    part.setdefault("name", "unknown_name")
+    part.setdefault("variant", "Unbuffered")
+    part.setdefault("size", "0")
+    part.setdefault("secret", "false")
+    part.setdefault("sw_digest", "false")
+    part.setdefault("hw_digest", "false")
+    part.setdefault("write_lock", "none")
+    part.setdefault("read_lock", "none")
+    part.setdefault("key_sel", "NoKey")
+    log.info("Partition {} at offset {} with size {}".format(
+        part["name"], part["offset"], part["size"]))
+
+    # Make sure these are boolean types (simplifies the mako templates)
+    part["secret"] = check_bool(part["secret"])
+    part["sw_digest"] = check_bool(part["sw_digest"])
+    part["hw_digest"] = check_bool(part["hw_digest"])
+    part["bkout_type"] = check_bool(part["bkout_type"])
+
+    # Make sure this has integer type.
+    part["size"] = check_int(part["size"])
+
+    # basic checks
+    if part["variant"] not in ["Unbuffered", "Buffered", "LifeCycle"]:
+        log.error("Invalid partition type {}".format(part["variant"]))
+        exit(1)
+
+    if part["key_sel"] not in (["NoKey"] + key_names):
+        log.error("Invalid key sel {}".format(part["key_sel"]))
+        exit(1)
+
+    if check_bool(part["secret"]) and part["key_sel"] == "NoKey":
+        log.error(
+            "A secret partition needs a key select value other than NoKey"
+        )
+        exit(1)
+
+    if part["write_lock"].lower() not in ["digest", "csr", "none"]:
+        log.error("Invalid value for write_lock")
+        exit(1)
+
+    if part["read_lock"].lower() not in ["digest", "csr", "none"]:
+        log.error("Invalid value for read_lock")
+        exit(1)
+
+    if part["sw_digest"] and part["hw_digest"]:
+        log.error(
+            "Partition cannot support both a SW and a HW digest at the same time."
+        )
+        exit(1)
+
+    if part["variant"] == "Unbuffered" and not part["sw_digest"]:
+        log.error(
+            "Unbuffered partitions without digest are not supported at the moment."
+        )
+        exit(1)
+
+    if not part["sw_digest"] and not part["hw_digest"]:
+        if part["write_lock"].lower(
+        ) == "digest" or part["read_lock"].lower() == "digest":
+            log.error(
+                "A partition can only be write/read lockable if it has a hw or sw digest."
+            )
+            exit(1)
+
+    if check_int(part["offset"]) % 8:
+        log.error("Partition offset must be 64bit aligned")
+        exit(1)
+
+    if check_int(part["size"]) % 8:
+        log.error("Partition size must be 64bit aligned")
+        exit(1)
+
+    if len(part["items"]) == 0:
+        log.warning("Partition does not contain any items.")
+
+
+def _validate_item(item, offset):
+    '''Validates an item within a partition'''
+    item.setdefault("name", "unknown_name")
+    item.setdefault("size", "0")
+    item.setdefault("isdigest", "false")
+    item.setdefault("offset", offset)
+
+    # Make sure this has integer type.
+    item["size"] = check_int(item["size"])
+
+    # Generate random constant to be used when partition has
+    # not been initialized yet or when it is in error state.
+    random_or_hexvalue(item, "inv_default", check_int(item["size"]) * 8)
+
+
+def _validate_mmap(config):
+    '''Validate the memory map configuration'''
+
+    # Get valid key names.
+    key_names = []
+    for key in config["scrambling"]["keys"]:
+        key_names.append(key["name"])
+
+    offset = 0
+    num_part = 0
+    for part in config["partitions"]:
+        num_part += 1
+        _validate_part(part, offset, key_names)
+
+        # Loop over items within a partition
+        for item in part["items"]:
+            _validate_item(item, offset)
+            log.info("> Item {} at offset {} with size {}".format(
+                item["name"], offset, item["size"]))
+            offset += check_int(item["size"])
+
+        # Place digest at the end of a partition.
+        if part["sw_digest"] or part["hw_digest"]:
+            part["items"].append({
+                "name":
+                part["name"] + DIGEST_SUFFIX,
+                "size":
+                DIGEST_SIZE,
+                "offset":
+                check_int(part["offset"]) + check_int(part["size"]) -
+                DIGEST_SIZE,
+                "isdigest":
+                "True",
+                "inv_default": "<random>"
+            })
+            # Randomize the digest default.
+            random_or_hexvalue(part["items"][-1], "inv_default", DIGEST_SIZE * 8)
+
+            log.info("> Adding digest {} at offset {} with size {}".format(
+                part["name"] + DIGEST_SUFFIX, offset, DIGEST_SIZE))
+            offset += DIGEST_SIZE
+
+        # check offsets and size
+        if offset > check_int(part["offset"]) + check_int(part["size"]):
+            log.error("Not enough space in partitition "
+                      "{} to accommodate all items. Bytes available "
+                      "= {}, bytes requested = {}".format(
+                          part["name"], part["size"],
+                          offset - part["offset"]))
+            exit(1)
+
+        offset = check_int(part["offset"]) + check_int(part["size"])
+
+    if offset > config["otp"]["size"]:
+        log.error(
+            "OTP is not big enough to store all partitions. "
+            "Bytes available {}, bytes required {}",
+            config["otp"]["size"], offset)
+        exit(1)
+
+    log.info("Total number of partitions: {}".format(num_part))
+    log.info("Bytes available in OTP: {}".format(config["otp"]["size"]))
+    log.info("Bytes required for partitions: {}".format(offset))
+
+
 class OtpMemMap():
 
     # This holds the config dict.
@@ -37,154 +236,22 @@
         # Initialize RNG.
         random.seed(int(config['seed']))
 
-        offset = 0
-        num_part = 0
-        for part in config["partitions"]:
-            num_part += 1
-            # Defaults
-            part.setdefault("offset", offset)
-            part.setdefault("name", "unknown_name")
-            part.setdefault("variant", "Unbuffered")
-            part.setdefault("size", "0")
-            part.setdefault("secret", "false")
-            part.setdefault("sw_digest", "false")
-            part.setdefault("hw_digest", "false")
-            part.setdefault("write_lock", "none")
-            part.setdefault("read_lock", "none")
-            part.setdefault("key_sel", "NoKey")
-            log.info("Partition {} at offset {} with size {}".format(
-                part["name"], part["offset"], part["size"]))
-
-            # make sure these are boolean types (simplifies the mako templates)
-            part["secret"] = check_bool(part["secret"])
-            part["sw_digest"] = check_bool(part["sw_digest"])
-            part["hw_digest"] = check_bool(part["hw_digest"])
-            part["bkout_type"] = check_bool(part["bkout_type"])
-
-            # basic checks
-            if part["variant"] not in ["Unbuffered", "Buffered", "LifeCycle"]:
-                log.error("Invalid partition type {}".format(part["variant"]))
-                exit(1)
-
-            if part["key_sel"] not in [
-                    "NoKey", "Secret0Key", "Secret1Key", "Secret2Key"
-            ]:
-                log.error("Invalid key sel {}".format(part["key_sel"]))
-                exit(1)
-
-            if check_bool(part["secret"]) and part["key_sel"] == "NoKey":
-                log.error(
-                    "A secret partition needs a key select value other than NoKey"
-                )
-                exit(1)
-
-            if part["write_lock"].lower() not in ["digest", "csr", "none"]:
-                log.error("Invalid value for write_lock")
-                exit(1)
-
-            if part["read_lock"].lower() not in ["digest", "csr", "none"]:
-                log.error("Invalid value for read_lock")
-                exit(1)
-
-            if part["sw_digest"] and part["hw_digest"]:
-                log.error(
-                    "Partition cannot support both a SW and a HW digest at the same time."
-                )
-                exit(1)
-
-            if part["variant"] == "Unbuffered" and not part["sw_digest"]:
-                log.error(
-                    "Unbuffered partitions without digest are not supported at the moment."
-                )
-                exit(1)
-
-            if not part["sw_digest"] and not part["hw_digest"]:
-                if part["write_lock"].lower(
-                ) == "digest" or part["read_lock"].lower() == "digest":
-                    log.error(
-                        "A partition can only be write/read lockable if it has a hw or sw digest."
-                    )
-                    exit(1)
-
-            if check_int(part["offset"]) % 8:
-                log.error("Partition offset must be 64bit aligned")
-                exit(1)
-
-            if check_int(part["size"]) % 8:
-                log.error("Partition size must be 64bit aligned")
-                exit(1)
-
-            # Loop over items within a partition
-            for item in part["items"]:
-                item.setdefault("name", "unknown_name")
-                item.setdefault("size", "0")
-                item.setdefault("isdigest", "false")
-                item.setdefault("offset", offset)
-                # Generate random constant to be used when partition has
-                # not been initialized yet or when it is in error state.
-                if check_bool(item.setdefault("rand_inv_default", "false")):
-                    inv_default = random.getrandbits(
-                        check_int(item["size"]) * 8)
-                else:
-                    inv_default = 0
-                item.setdefault(
-                    "inv_default", "{}'h{:0X}".format(
-                        check_int(item["size"]) * 8, inv_default))
-                log.info("> Item {} at offset {} with size {}".format(
-                    item["name"], offset, item["size"]))
-                offset += check_int(item["size"])
-
-            # Place digest at the end of a partition.
-            if part["sw_digest"] or part["hw_digest"]:
-                part["items"].append({
-                    "name":
-                    part["name"] + DIGEST_SUFFIX,
-                    "size":
-                    DIGEST_SIZE,
-                    "offset":
-                    check_int(part["offset"]) + check_int(part["size"]) -
-                    DIGEST_SIZE,
-                    "isdigest":
-                    "True",
-                    "inv_default":
-                    "{256{1'b1}}"
-                })
-
-                log.info("> Adding digest {} at offset {} with size {}".format(
-                    part["name"] + DIGEST_SUFFIX, offset, DIGEST_SIZE))
-                offset += DIGEST_SIZE
-
-            if len(part["items"]) == 0:
-                log.warning("Partition does not contain any items.")
-
-            # check offsets and size
-            if offset > check_int(part["offset"]) + check_int(part["size"]):
-                log.error("Not enough space in partitition "
-                          "{} to accommodate all items. Bytes available "
-                          "= {}, bytes requested = {}".format(
-                              part["name"], part["size"],
-                              offset - part["offset"]))
-                exit(1)
-
-            offset = check_int(part["offset"]) + check_int(part["size"])
-
-        otp_size = check_int(config["otp"]["depth"]) * check_int(
-            config["otp"]["width"])
-        config["otp"]["size"] = otp_size
-        config["otp"]["addr_width"] = ceil(
-            log2(check_int(config["otp"]["depth"])))
-        config["otp"]["byte_addr_width"] = ceil(log2(check_int(otp_size)))
-
-        if offset > otp_size:
-            log.error(
-                "OTP is not big enough to store all partitions. "
-                "Bytes available {}, bytes required {}",
-                otp_size, offset)
+        if "otp" not in config:
+            log.error("Missing otp configuration.")
+            exit(1)
+        if "scrambling" not in config:
+            log.error("Missing scrambling configuration.")
+            exit(1)
+        if "partitions" not in config:
+            log.error("Missing partition configuration.")
             exit(1)
 
-        log.info("Total number of partitions: {}".format(num_part))
-        log.info("Bytes available in OTP: {}".format(otp_size))
-        log.info("Bytes required for partitions: {}".format(offset))
+        # Validate OTP info.
+        _validate_otp(config["otp"])
+        # Validate scrambling info.
+        _validate_scrambling(config["scrambling"])
+        # Validate memory map.
+        _validate_mmap(config)
 
         self.config = config
 
@@ -192,7 +259,6 @@
         log.info('Successfully parsed and translated OTP memory map.')
         log.info('')
 
-
     def create_partitions_table(self):
         header = [
             "Partition", "Secret", "Buffered", "WR Lockable", "RD Lockable",
diff --git a/util/design/lib/common.py b/util/design/lib/common.py
index 0d12e92..20e2ba9 100644
--- a/util/design/lib/common.py
+++ b/util/design/lib/common.py
@@ -203,4 +203,27 @@
             scatterword += bits[j]
             j += 1
 
-    return scatterword
\ No newline at end of file
+    return scatterword
+
+
+def random_or_hexvalue(dict_obj, key, num_bits):
+    '''Convert hex value at "key" to an integer or draw a random number.'''
+
+    # Initialize to default if this key does not exist.
+    dict_obj.setdefault(key, '0x0')
+
+    # Generate a random number of requested size in this case.
+    if dict_obj[key] == '<random>':
+        dict_obj[key] = random.getrandbits(num_bits)
+    # Otherwise attempt to convert this number to an int.
+    # Check that the range is correct.
+    else:
+        try:
+            dict_obj[key] = int(dict_obj[key], 16)
+            if dict_obj[key] >= 2**num_bits:
+                log.error('Value is out of range.')
+                exit(1)
+        except ValueError:
+            log.error('Invalid value "{}". Must be hex or "<random>".'
+                      .format(dict_obj[key]))
+            exit(1)